thomabig
09/01/2025, 1:47 PMthomabig
09/01/2025, 2:54 PMthomabig
09/01/2025, 3:38 PMalbert_wong
09/01/2025, 9:06 PMthomabig
09/01/2025, 9:11 PMfinnbergquist
09/02/2025, 9:48 PMforceNewMembership param set to True.
I would proceed with caution here because generally this can become complicated with the way that you authenticate, and it is a faily new feature.
By default, the least recently used ProjectMembership will be used on login. Unfortunately, there was not an easy way to pass a ProjectMembership to specify which ProjectMembership to login with, but you can do this with the MedplumClient after authenticating. See how Medplum app skeleton does that [here](https://github.com/medplum/medplum/blob/b132fea7f9c732d541e05e58e1f1db4396aacffd/packages/react/src/AppShell/HeaderDropdown.tsx#L44-L49) with setActiveLogin(). I think this is your best bet.
Alternatively, you could also just use the [on-behalf-of](https://www.medplum.com/docs/auth/methods/on-behalf-of) header with your Patient or Practitioner ProjectMembership depending on who you are logged in with.
Generally we recommend creating the ProjectMeberships with the medplum.invite() because it is accessible to admins.thomabig
09/03/2025, 7:45 AMfinnbergquist
09/03/2025, 3:52 PMthomabig
09/03/2025, 9:38 PMcurl https://a**ai/admin/projects/019**51/invite \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Practitioner",
"firstName": "**",
"lastName": "**",
"email": "dr@ex.gov",
"scope": "project"
}'
Second invitation (same email, with forceNewMembership):
curl https://a**ai/admin/projects/019**51/invite \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Practitioner",
"firstName": "George",
"lastName": "Washington",
"email": "dr@ex.gov",
"forceNewMembership": true,
"scope": "project"
}'
Error returned:
{
"resourceType":"OperationOutcome",
....
"text":"**Key (project, email)=(Project/0197bff0-7b52-772f-84fb-66f1d6a9f251, dr.gw@example.gov) already exists**."
We looked a bit deeper into the invite implementation. The problem seems to happen at the conditionalCreate step:
After the first invite, we expected conditionalCreate to find the existing User.
Instead, matches comes back as an empty array, so Medplum tries to create a new User → and that fails with the duplicate conflict.
We tried several variations (inviting the same Patient twice, the same Practitioner twice, or a Patient + Practitioner with the same email). The result is always the same conflict.
👉 Do you have any idea why matches would return empty even though the User already exists with that email?
👉 Is there a recommended way to make forceNewMembership work in this scenario?
Thanks a lot for your support!finnbergquist
09/03/2025, 9:45 PMUser.meta.project field, then it is project scoped. Otherwise, it is server scoped.finnbergquist
09/03/2025, 9:46 PMforceNewMembership , I don't think that the server would not find the existing one.thomabig
09/03/2025, 9:49 PMcurl https://api.ai/admin/projects/01**66f1d6a9f251/invite \
-d '{
"resourceType": "Patient",
"firstName": "George",
"lastName": "Washington",
"email": "patient@example.gov",
"scope": "project",
"forceNewMembership": true
}'
The user has a meta.project
{
"resourceType": "User",
"meta": {
"project": "0197bff0-7b52-772f-84fb-66f1d6a9f251",
"versionId": "69c0b2a3-6e45-4d30-8ebf-a44a400a0e25",
"lastUpdated": "2025-09-03T21:27:38.277Z",
"author": {
"reference": "system"
},
"compartment": [
{
"reference": "Project/0197bff0-7b52-772f-84fb-66f1d6a9f251"
}
]
},
"firstName": "George",
"lastName": "Washington",
"email": "patient@example.gov",
"project": {
"reference": "Project/0197bff0-7b52-772f-84fb-66f1d6a9f251",
"display": "Apneal"
},
"id": "4c513862-878f-4fa9-9fd8-6a5d77880352"
}
If I look for the patient using the following curl
curl https://api.uat.apneal.ai/fhir/R4/Patient\?email\=patient@example.gov \
-H "Authorization: Bearer A"
{"resourceType":"Bundle","type":"searchset","entry":[{"fullUrl":"https://api.uat.apneal.ai/fhir/R4/Patient/ff32b6e1-bb29-483c-8ee4-a7060562a1ad","search":{"mode":"match"},"resource":{"resourceType":"Patient","meta":{"versionId":"48d77df7-520a-4842-bff2-fde62294ad1a","lastUpdated":"2025-09-03T21:27:38.307Z"},"name":[{"given":["George"],"family":"Washington"}],"telecom":[{"system":"email","use":"work","value":"patient@example.gov"}],"id":"ff32b6e1-bb29-483c-8ee4-a7060562a1ad"}}],"link":[{"relation":"self","url":"https://api.uat.apneal.ai/fhir/R4/Patient?_count=20&email=patient%40example.gov"},{"relation":"first","url":"https://api.uat.apneal.ai/fhir/R4/Patient?_count=20&email=patient%40example.gov"}]}%
I do retrieve the patient, but my understanding is that I cannot search for the user from the API.thomabig
09/03/2025, 9:51 PMif (email) {
const searchRequest: SearchRequest<User> = {
resourceType: 'User',
filters: [
{
code: 'email',
operator: Operator.EXACT,
value: email,
},
request.resourceType === 'Patient'
? { code: 'project', operator: Operator.EQUALS, value: project.id }
: { code: 'project', operator: Operator.MISSING, value: 'true' },
],
};
Indeed if the type is Practitioner it will only search for server scoped User even if the invitation specifies "scoped"finnbergquist
09/03/2025, 10:01 PMfinnbergquist
09/03/2025, 10:02 PMthomabig
09/03/2025, 10:04 PMthomabig
09/03/2025, 10:04 PMthomabig
09/03/2025, 10:04 PMfinnbergquist
09/03/2025, 10:05 PMthomabig
09/03/2025, 10:06 PMthomabig
09/03/2025, 10:07 PMthomabig
09/03/2025, 10:09 PMfinnbergquist
09/03/2025, 10:10 PMfinnbergquist
09/03/2025, 10:10 PM.../User?email=<your-email>finnbergquist
09/03/2025, 10:12 PMthomabig
09/03/2025, 10:12 PMcurl https://api.al.ai/admin/projects/0197bb-66f1d6a9f251/invite \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NNiA" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Patient",
"firstName": "Test",
"lastName": "ForDiscor",
"email": "testForDiscord@example.com"
}'
{"resourceType":"ProjectMembership","project":{"reference":"Project/0197bff0-7b52-772f-84fb-66f1d6a9f251","display":"Apneal"},"user":{"reference":"User/a082e252-0295-4999-b76d-d774dcc39cc2","display":"testfordiscord@example.com"},"profile":{"reference":"Patient/fbdb8e4c-12d4-4b55-96fd-c58182d40058","display":"Test ForDiscor"},"id":"721f80b1-00e3-4de7-8846-4b3619c7331f","meta":{"versionId":"a735a820-b539-4da6-8597-6adc4cfd0f7e","lastUpdated":"2025-09-03T22:11:33.859Z","author":{"reference":"system"},"project":"0197bff0-7b52-772f-84fb-66f1d6a9f251","compartment":[{"reference":"Project/0197bff0-7b52-772f-84fb-66f1d6a9f251"}]}}thomabig
09/03/2025, 10:12 PM{
"resourceType": "User",
"meta": {
"project": "0197bff0-7b52-772f-84fb-66f1d6a9f251",
"versionId": "6c0f8e69-e963-4a57-a904-2b8799065c20",
"lastUpdated": "2025-09-03T22:11:33.805Z",
"author": {
"reference": "system"
},
"compartment": [
{
"reference": "Project/0197bff0-7b52-772f-84fb-66f1d6a9f251"
}
]
},
"firstName": "Test",
"lastName": "ForDiscor",
"email": "testfordiscord@example.com",
"project": {
"reference": "Project/0197bff0-7b52-772f-84fb-66f1d6a9f251",
"display": "Apneal"
},
"id": "a082e252-0295-4999-b76d-d774dcc39cc2"
}thomabig
09/03/2025, 10:16 PMcurl https://api.uat.apneal.ai/admin/projects/0197bff0-7b52-772f-84fb-66f1d6a9f251/invite \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjAxOTc5Y2U1LTJhNTUtNzM4YS05NzY4LWNjNTU3YjY1OWUxNSIsInR5cCI6IkpXVCJ9.eyJsb2dpbl9pZCI6IjkxYjgzYzQ0LTI0NTgtNDYyNy1hNzVhLWYwMDRiODBkODMzYSIsInN1YiI6IjAxOTdjNjVkLTYxMjktNzUzNC1iMjgzLThlOTlhMDZkMDU0ZCIsInVzZXJuYW1lIjoiMDE5N2M2NWQtNjEyOS03NTM0LWIyODMtOGU5OWEwNmQwNTRkIiwic2NvcGUiOiJvcGVuaWQiLCJwcm9maWxlIjoiUHJhY3RpdGlvbmVyLzAxOTdjNjVkLTYxNDMtNzIwOC1iMzM3LTQyYzJkNTBmOTVmZSIsImlhdCI6MTc1NjkzNDM5MSwiaXNzIjoiaHR0cHM6Ly9hcGkudWF0LmFwbmVhbC5haS8iLCJleHAiOjE3NTY5Mzc5OTF9.TmUwAzJ4oJ0J-D9lIoDcjXJFONPcTUUiLvmxM3LcvBF8iJsqElpMFXrvhRR48TwVCsxMKHsV33w0q_5bnlB-qtWaC10HK_eUuKSwMThxhsnQnTQrbrv3kcQxR4v5th6ogzYJEg1PADOhP9QD4xRn8JrgwD4FZTDC2IJ65EI6M78dNu3b-XhEq-sevmHBQ23ltLdiPxpW-DjMm3u-W6UOEDHpZVd0WUH2hNEMfcK-IlOGsGqGNB1KCZinfuzMa4NrNVRdymoiatN6_oB0RARF4bAHDyCX8ua1PDErpX6JGS9HETFf-3wOCLhhATlB5v_s8BlBTiHZXjSxxDou-EWNiA" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Practitioner",
"firstName": "Test",
"lastName": "ForDiscor Practi",
"email": "testForDiscord@example.com",
"forceNewMembership": true
}'
{"resourceType":"ProjectMembership","project":{"reference":"Project/0197bff0-7b52-772f-84fb-66f1d6a9f251","display":"Apneal"},"user":{"reference":"User/818fa4bd-c6d3-4845-8a52-0d9870336a72","display":"testfordiscord@example.com"},"profile":{"reference":"Practitioner/d9d22b97-83e8-4a3b-98fc-bcf735b97d01","display":"Test ForDiscor Practi"},"id":"0babccbb-14db-48ab-8eb0-780f9bc9aa4f","meta":{"versionId":"0631f550-e879-47fd-a505-f45e64451acf","lastUpdated":"2025-09-03T22:15:53.231Z","author":{"reference":"system"},"project":"0197bff0-7b52-772f-84fb-66f1d6a9f251","compartment":[{"reference":"Project/0197bff0-7b52-772f-84fb-66f1d6a9f251"}]}}%thomabig
09/03/2025, 10:16 PMthomabig
09/03/2025, 10:17 PMthomabig
09/03/2025, 10:17 PMthomabig
09/03/2025, 10:17 PMthomabig
09/03/2025, 10:20 PMcurl https://api.ai/admin/projects/0197bff0--84fb-66f1d6a9f251/invite \
-H "Authorization: Bearer eyJ-EWNiA" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Patient",
"firstName": "Test",
"lastName": "ForDiscor Patient",
"email": "testForDiscord2@example.com"
}'
{"resourceType":"ProjectMembership","project":{"reference":"Project/0197bff0-7b52-772f-84fb-66f1d6a9f251","display":"Apneal"},"user":{"reference":"User/ff29005f-ba6a-4f35-ad03-1f7397e2796a","display":"testfordiscord2@example.com"},"profile":{"reference":"Patient/7cb0da67-b168-4601-a1f3-0946928b0c74","display":"Test ForDiscor Patient"},"id":"7567a178-a770-4163-b594-d316bc291acc","meta":{"versionId":"5d8a00f6-9f8b-4759-afeb-0a4f516205c4","lastUpdated":"2025-09-03T22:18:19.473Z","author":{"reference":"system"},"project":"0197bff0-7b52-772f-84fb-66f1d6a9f251","compartment":[{"reference":"Project/0197bff0-7b52-772f-84fb-66f1d6a9f251"}]}}%
➜ ~ curl https://api.uat.apneal.ai/admin/projects/0197bff0-7b52-772f-84fb-66f1d6a9f251/invite \
-H "Authorization: Bearer eyJWNiA" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Practitioner",
"firstName": "Test",
"lastName": "ForDiscor Practi",
"email": "testForDiscord2@example.com",
"forceNewMembership": true,
"scope": "project"
}'
{"resourceType":"OperationOutcome","id":"conflict","issue":[{"severity":"error","code":"conflict","details":{"text":"Key (project, email)=(Project/0197bff0-7b52-772f-84fb-66f1d6a9f251, testfordiscord2@example.com) already exists."}}],"extension":[{"url":"https://medplum.com/fhir/StructureDefinition/tracing","extension":[{"url":"requestId","valueId":"46ebe6b7-245f-4d6a-8c66-73811e22db56"},{"url":"traceId","valueId":"583c5323-e092-4a15-bcda-682603495200"}]}]}%thomabig
09/03/2025, 10:21 PMfinnbergquist
09/03/2025, 10:24 PMthomabig
09/03/2025, 10:26 PMthomabig
09/03/2025, 10:26 PMfinnbergquist
09/03/2025, 11:30 PMrequest.resourceType === 'Patient'
? { code: 'project', operator: Operator.EQUALS, value: project.id }
: { code: 'project', operator: Operator.MISSING, value: 'true' },
should be
request.resourceType === 'Patient'
? { code: 'project', operator: Operator.EQUALS, value: `Project/${project.id } //should compare to project ref not just id
: { code: 'project', operator: Operator.MISSING, value: 'true' },finnbergquist
09/03/2025, 11:32 PMfinnbergquist
09/03/2025, 11:45 PMforceNewMembership on both Patients and project scoped Practitionersthomabig
09/04/2025, 7:07 AMProject/${project.id } //should compare to project ref not just id
: { code: 'project', operator: Operator.MISSING, value: 'true' },thomabig
09/04/2025, 7:08 AMthomabig
09/04/2025, 7:08 AMthomabig
09/04/2025, 8:35 PMthomabig
09/04/2025, 8:35 PMthomabig
09/04/2025, 8:35 PMthomabig
09/04/2025, 8:35 PMthomabig
09/04/2025, 8:36 PMfinnbergquist
09/04/2025, 9:50 PMfinnbergquist
09/05/2025, 2:24 PMthomabig
09/05/2025, 3:25 PM