Aden McCusker
03/04/2026, 4:26 AMpermit support automation2
03/04/2026, 4:26 AMAsk Inkeep
03/04/2026, 4:26 AMAsk Inkeep
03/04/2026, 4:26 AM(object_type, key) tuple within that namespace.
*Rationale:*
The key serves as a unique identifier that Permit uses to identify the resource instance for permission checks—you pass this identifier to the permit.check() API. (2)
A key can be anything (a database ID, URL slug, UUID, etc.) as long as it's unique on your end and URL-friendly.
This means you *can* have:
• Two resource instances with the same key in *different* Environments
• A Resource keyed "john" and a Role keyed "john" in the same Environment (different object types)
But you *cannot* have:
• Two resource instances of the same resource type with the same key in the same Environment (1)
The design allows you to use your own system's identifiers as keys while ensuring Permit can unambiguously resolve which resource instance you're referring to during permission checks.Ask Inkeep
03/04/2026, 4:26 AMAden McCusker
03/04/2026, 4:29 AMAsk Inkeep
03/04/2026, 4:29 AMAsk Inkeep
03/04/2026, 4:29 AMkey that uniquely identifies that instance within the environment. (1)
For example, if you have a repository resource with auto-incrementing IDs, you could structure your keys like:
await permit.api.resource_instances.create({
key: "react",
resource: "repository",
tenant: "default",
attributes: {
private: "false",
owner: "Company C",
},
}) ;
In your multi-tenant scenario with auto-incrementing PKs, you'd adapt this pattern to something like {tenant_key}:{pk} or {tenant_key}-{pk} (e.g., acme-corp:123 or stripeinc-456) . This ensures uniqueness across tenants while still allowing you to derive the key programmatically from your existing data.
The key just needs to be URL-friendly and unique within the environment—your own system's identifiers work fine as long as you make them unique across tenants.Ask Inkeep
03/04/2026, 4:29 AMAden McCusker
03/04/2026, 4:50 AMAsk Inkeep
03/04/2026, 4:50 AMAsk Inkeep
03/04/2026, 4:50 AMawait permit.api.assignRole({
role: "AccountOwner",
tenant: cleanedEmail,
user: email,
}) ;
(3)
So the behavior you're seeing isn't replacing tenant X with tenant Y—it's *adding* the user to tenant Y while they remain in tenant X. Each user can have different roles in different tenants. (2)
From the documentation: "Users are assigned to tenants (each user can belong to more than one tenant) and can then be assigned roles and permissions." (4)
The same user can exist in multiple tenants with different roles—for example, an Admin in one tenant and a Manager in another. (5) This is handled at the API level, not just the frontend.
To explicitly add a user to a new tenant with a role:
const user = await permit.tenants.assignUser({
key: "user|123456789",
email: "<mailto:john@smith.com|john@smith.com>",
first_name: "John",
last_name: "Smith",
role_assignments: [
{
role: "Manager",
tenant: "Marketing",
},
],
}) ;
This adds the user to the "Marketing" tenant without removing them from any existing tenants.Ask Inkeep
03/04/2026, 4:50 AMAden McCusker
03/04/2026, 4:52 AMAsk Inkeep
03/04/2026, 4:52 AMAsk Inkeep
03/04/2026, 4:53 AMsyncUser function (and the underlying API) is designed to create the user if they don't exist, or update them if they do. (1)
When you call the create/sync user endpoint with a user key that already exists, it won't fail or replace the user—it will update that user's information and add any new role assignments you specify. (2) , (3)
For example, if you sync a user with role assignments to a new tenant:
const user = await permit.tenants.assignUser({
key: "user|123456789",
email: "<mailto:john@smith.com|john@smith.com>",
first_name: "John",
last_name: "Smith",
role_assignments: [
{
role: "Manager",
tenant: "Marketing",
},
],
}) ;
(2)
This will add the user to the "Marketing" tenant with the "Manager" role while preserving their existing tenant memberships.
The key point is that users are assigned to tenants via roles, and a single user can belong to multiple tenants simultaneously. (4) So the API call doesn't "move" the user—it extends their tenant memberships.
If you want to assign a role to an existing user in a new tenant without re-syncing their profile data, you can use the role assignment endpoint directly:
const result = await permit.roles.assign(userId, roleData.role, roleData.tenant) ;
(2) Ask Inkeep
03/04/2026, 4:53 AMAden McCusker
03/06/2026, 10:54 PMpermit support automation2
03/06/2026, 10:54 PMAsk Inkeep
03/06/2026, 10:54 PMAsk Inkeep
03/06/2026, 10:55 PMrole_assignments that include both the role and tenant. (2)
const user = await permit.tenants.assignUser({
key: "user|123456789",
email: "<mailto:john@smith.com|john@smith.com>",
first_name: "John",
last_name: "Smith",
role_assignments: [
{
role: "Manager",
tenant: "Marketing",
},
],
}) ;
The API examples consistently show tenant assignment happening through role assignments. , (3) , (4)
However, I don't have explicit documentation on whether the frontend allows adding a user to a tenant without a role, or what specific API endpoint enables that behavior. If you're seeing this capability in the frontend but can't find the corresponding API call, this might be worth clarifying withAsk Inkeep
03/06/2026, 10:55 PMAden McCusker
03/06/2026, 10:59 PM{
"email": "j.martinez@example.com",
"first_name": "Jessica",
"last_name": "Martinez"
}
Response:
{
"key": "j.martinez@example.com",
"id": "...",
"organization_id": "...",
"project_id": "...",
"environment_id": "...",
"associated_tenants": [
{
"tenant": "qa-17",
"roles": [],
"status": "pending",
"resource_instance_roles": []
}
],
"roles": [],
"created_at": "2026-03-01T09:14:53+00:00",
"updated_at": "2026-03-01T09:14:53+00:00",
"email": "j.martinez@example.com",
"first_name": "Jessica",
"last_name": "Martinez",
}Ask Inkeep
03/06/2026, 10:59 PMAsk Inkeep
03/06/2026, 10:59 PMassociated_tenants entry with "roles": [] (empty array) and "status": "pending". (1)
The documentation consistently states that "Users are assigned to tenants via a role" and shows tenant assignment happening through role assignments. , (2)
However, the API response you captured shows something interesting: the user _is_ associated with tenant qa-17 but has no roles assigned ("roles": []) . This suggests there may be a mechanism to associate a user with a tenant without immediately assigning a role—though the "status": "pending" might indicate this is an intermediate state.
I don't have explicit documentation covering this specific behavior—where a user can be added to a tenant's associated_tenants list without a role assignment via the PATCH endpoint. The documented approach for tenant assignment is through role assignments:
await permit.api.assignRole({
role: "AccountOwner",
tenant: cleanedEmail,
user: email,
}) ;
This looks like it could be undocumented API behavior or a frontend-specific workflow. To help clarify this and ensure our engineers can investigate:
1. Which Workspace / Project / Env are you using?
2. What's the exact sequence of UI actions you performed (which dropdown/field did you use to "switch their tenant") ?
3. Which PDP version are you running?
4. Could you share any relevant PDP logs if you've tested permit.check() with this user in the qa-17 tenant?
This would help determine whether this is intended behavior and how it interacts with permission checks.Ask Inkeep
03/06/2026, 10:59 PMAsk Inkeep
03/06/2026, 10:59 PMAsk Inkeep
03/06/2026, 10:59 PMassociated_tenants entry with "roles": [] and "status": "pending". (1)
The documented approach consistently shows users being assigned to tenants _via roles_:
await permit.api.assignRole({
role: "AccountOwner",
tenant: cleanedEmail,
user: email,
}) ;
(2)
And the documentation states: "Users are assigned to tenants via a role." (1)
The "status": "pending" in your response is notable—this typically indicates the assignment hasn't fully completed or synced.
I don't have explicit documentation covering this specific UI workflow where a user gets added to associated_tenants without a role via the PATCH user endpoint. The body you showed only contains profile fields (email, first_name, last_name) , yet the response shows a new tenant association—suggesting the frontend may be making additional calls or there's undocumented behavior in how the UI handles tenant switching.
To remove a user from a tenant entirely, there is a dedicated endpoint:
DELETE /v2/facts/{proj_id}/{env_id}/tenants/{tenant_id}/users/{user_id} (3)
This might be worth raising with the team to clarify the intended API behavior for adding a user tAsk Inkeep
03/06/2026, 11:00 PMNatalia Edelson
03/06/2026, 11:00 PMAden McCusker
03/06/2026, 11:15 PMAden McCusker
03/06/2026, 11:18 PM/v2/facts/.../.../role_assignments
{
"role": "__role_tenant_association",
"tenant": "tenant1",
"user": "test"
}
Response
{
"id": "...",
"user": "test",
"role": "tenant-association",
"tenant": "tenant1",
"resource_instance": null,
"resource_instance_id": null,
"user_id": "...",
"role_id": "...",
"tenant_id": "...",
"organization_id": "...",
"project_id": "...",
"environment_id": "...",
"created_at": "2026-03-06T23:09:21+00:00"
}Aden McCusker
03/06/2026, 11:22 PM409 DUPLICATE_ENTITY, and the other one is the above hacky request which associates the new tenant with the pre-existing user via the role_assignments endpoint, by setting role to __role_tenant_associationdavid
03/08/2026, 9:37 AM