Switching between tenants
Sessions & devicesA user who belongs to more than one tenant changes which one their session acts in.
- GET /v1/me/memberships → tenants the user belongs to (default flagged)
- POST /v1/auth/switch-tenant → new accessToken scoped to the target tenant's roles; refresh rotated
/v1/me/membershipsauthList my membershipsEach row carries the tenant's name so a picker can show a human label instead of the opaque tenantId, plus isDefault and status.
Field guide — what each value means & where it comes fromno inputs
No request body, path params, or query params. Just a bearer-authenticated GET that lists the tenants the caller belongs to (the tenant picker); the tester supplies the access token and the gateway injects x-gateway-context, so you set nothing yourself. Returns 403 forbidden if the gateway context is missing or invalid.
GET https://api.kerja.team/v1/me/membershipsResponse guide — what comes back & what each value means4 fields
200 OK. Body is the standard { data } envelope where data is an ARRAY of membership objects (one per tenant the user belongs to — the tenant picker). No notable response headers (no Set-Cookie / no Cache-Control beyond defaults).
data[].tenantIdUUIDalwaysdata[].namestringalwaysdata[].isDefaultbooleanalwaystrue— This is the user's default tenant — pre-select it in the tenant picker.false— Not the default tenant.
data[].statusenumalwaysactive— Membership is active — the user may operate within this tenant.
/v1/auth/switch-tenantauthSwitch active tenantNeeds a sid in the gateway context. The new accessToken (scoped to the TARGET tenant's roles, which may differ — admin in one, member in another) is auto-captured and the refresh token is rotated. 403 without an active membership in the target. Like refresh, this is a re-mint and never triggers a device challenge. THE SWITCH IS SESSION-WIDE, SO EVERY OPEN APP FOLLOWS IT (scenario 19): one session holds one tenant, so switching in app A repoints EVERY live chain under that session before rotating the caller's own — app B's next request carries a stale tenant_id and is refused, and B lands in the new tenant on its next renewal. Repointing all of them is not an optimisation: a chain left on the old tenant cannot see the repointed session under RLS and its app reports the session gone. THAT CONSEQUENCE MUST BE VISIBLE IN THE UI, NOT DISCOVERED AS A BUG — “switch tenant” moves every tab, so it is a deliberate, announced act; it is also why an app the caller holds only in ANOTHER tenant is absent from GET /v1/me/apps rather than silently switching the tenant for them when picked. The app the caller is IN survives the switch: the re-mint stays in the chain it just retired, so azp is carried across and the user is not bounced out of the product they were using.
Field guide — what each value means & where it comes from1 field
Switches the active tenant for the current session and re-mints tokens scoped to the target tenant; rotates the refresh_token cookie. Requires an authenticated session (the tester supplies Authorization and the gateway injects x-gateway-context with the session id); the caller must already hold an active membership in the target tenant. Body has one field.
targetTenantIdbodystringrequiredfrom a responsePOST https://api.kerja.team/v1/auth/switch-tenantResponse guide — what comes back & what each value means3 fields
200 OK with the standard { data } envelope; Cache-Control: no-store, and Set-Cookie: refresh_token (rotated) sets the new refresh cookie. The body re-mints an access token scoped to the target tenant's roles.
data.accessTokenJWTalwaysdata.tokenTypeenumalwaysBearer— Send the access token as 'Authorization: Bearer <accessToken>'.
data.expiresInintegeralways