SSO and SCIM
Packaging
SSO and SCIM are both on every paid tier. The "SSO tax" — gating single sign-on, or the directory sync that makes it operable, behind an Enterprise quote — is a documented churn trigger that comes up in procurement, and there is deliberately no separate "enterprise SSO" or "enterprise SCIM" flag anywhere in the codebase.
| Feature | Free | Pro | Team | Enterprise |
|---|---|---|---|---|
| SSO (SAML + OIDC) | — | ✅ | ✅ | ✅ |
| SCIM directory sync | — | ✅ | ✅ | ✅ |
This matches the code, not just the marketing.
billing/plans.tssets bothsso: trueandscim: trueon every paid tier (PRO,TEAM,ENTERPRISE) and both false onFREE— design §3.1 ("on all paid tiers (Pro and up), never enterprise-gated") is the binding commitment there, and the file notes that the §13 plan sketch listing SCIM under Enterprise-only is superseded by it.
SSO
One code path serves SAML and OIDC. Both reduce to (subject, email, name, groups),
and everything after that — find-or-create the user, find-or-create the org membership
at the mapped role, mint a session — is protocol-agnostic.
Attribute mapping
Well-known claim names are used by default: the SAML URNs Entra ID, Okta and Google Workspace emit, plus the short names OIDC and simpler IdPs emit. A connection can override any of them.
Two provisioning rules that matter
A JIT-provisioned user gets no password. passwordHash stays null, so a federated
account cannot be taken over through a password-reset flow against an address the IdP
controls.
An existing member's role is never silently downgraded by an unmapped group. The IdP is authoritative for membership, but a mapping typo that demotes every admin at once is the failure mode that makes teams distrust SSO. Role changes only ever move a membership up unless the connection maps the group explicitly.
Enforcing SSO
An org can require SSO. It is enforced by a global request hook on the routes where a password is exchanged for a session — which keeps the whole feature inside the SSO module, leaving the auth module unaware federation exists.
Owners are exempt, deliberately. An org whose IdP configuration breaks — expired certificate, rotated key, deleted app registration — must retain a way back in. Exempting owners is the standard break-glass; exempting nobody means one bad certificate rotation locks a customer out of their own account permanently.
SCIM 2.0
RFC 7644, at /scim/v2:
GET /scim/v2/ServiceProviderConfig
GET /scim/v2/ResourceTypes
/scim/v2/Users GET POST · /:id GET PUT PATCH DELETE
/scim/v2/Groups GET POST · /:id GET PUT PATCH DELETE
/scim/v2/Agents GET POST · /:id GET PUT PATCH DELETE
/scim/v2/Agents is the interesting one: non-human identities are provisioned through
the same directory as humans. Agents are principals on the same spine (see
agent-governance.md), so an org that offboards a person through
its IdP can offboard an agent the same way.
Tenancy and errors
The /scim/v2 tree lives in an encapsulated scope so it can do two things it must do:
- authenticate with a SCIM bearer token instead of a JWT. The token is the org
binding —
/scim/v2/Usershas no org in its path, because IdP connectors will not put one there — so the credential carries the tenant. - render errors in SCIM's own envelope without changing the API-wide error shape. IdP connectors parse the SCIM error body and will not follow a bespoke one.
Only the sha256 hash of a SCIM token is stored; a leaked database yields nothing replayable, and the raw value is shown exactly once, like every other opaque token here.
Token administration is a normal JWT-authenticated admin surface:
GET/POST/DELETE /orgs/:orgSlug/scim/tokens[/:tokenId]
Not built
- No SCIM
/Meendpoint. - No IdP-initiated deprovisioning webhooks beyond the SCIM DELETE path.
- No per-connection audit export; SSO and SCIM events land in the normal audit log.