SCIM
SCIM 2.0 provisioning — Users + Groups REST endpoints so an enterprise IdP (Okta, Entra, OneLogin) can create / update / deactivate users in your app.
@voltro/plugin-scim exposes the SCIM 2.0 endpoints enterprise IdPs use to provision users into your app. When an admin adds someone to your app in Okta/Entra, the IdP POSTs to your /scim/v2/Users endpoint; when they remove them, it PATCHes active: false. This is a hard requirement in most enterprise procurement.
Wiring
// app.config.ts
import { scimPlugin } from '@voltro/plugin-scim'
export default {
type: 'api' as const, name: 'api',
plugins: [scimPlugin({ token: process.env.SCIM_TOKEN! })], // basePath defaults to /scim/v2
}Mounts /scim/v2/Users + /scim/v2/Groups, bearer-gated by token (paste it into the IdP's SCIM config). Contributes the _voltro_scim_users + _voltro_scim_groups tables.
Endpoints
| Method + path | SCIM operation |
|---|---|
GET /scim/v2/Users?filter=userName eq "x" |
list / find a user (userName / externalId eq filters, startIndex/count pagination) |
GET /scim/v2/Users/<id> |
read a user |
POST /scim/v2/Users |
create (provision) — duplicate userName answers 409 uniqueness (DB-unique, IdP-retry-safe) |
PUT /scim/v2/Users/<id> |
replace |
PATCH /scim/v2/Users/<id> |
partial update — incl. the active:false deactivation IdPs send |
DELETE /scim/v2/Users/<id> |
delete |
GET/POST/DELETE /scim/v2/Groups |
list (displayName / externalId eq filters, paginated) / create / delete groups |
PUT/PATCH /scim/v2/Groups/<id> |
replace / membership add-remove-replace (how Okta/Entra push membership changes) |
GET /scim/v2/ServiceProviderConfig · /Schemas · /ResourceTypes |
the RFC 7644 §4 discovery trio IdPs probe at connector setup |
Requests + responses use the SCIM JSON envelope (schemas, id, meta, ListResponse); an unsupported filter is answered 400 invalidFilter (never silently unfiltered). The pure mapping (toScimUser, fromScimUser, applyScimPatch, applyScimGroupPatch, scimList, parseScimFilter) is exported + unit-tested.
Notes
- The bearer token is the trust boundary — keep it secret + rotate it via your IdP.
- An empty token is refused at boot.
scimPlugin({ token })throws whileapp.config.tsevaluates if the token is empty or whitespace, so a deployment with an unsetSCIM_TOKENfails to start rather than coming up and answering the first anonymous request. This exists becausescimPlugin({ token: process.env.SCIM_TOKEN ?? '' })— the shape everyone writes — used to turn the gate off silently, leaving SCIM Users and Groups (a full directory dump, plus the endpoints that deactivate accounts) readable with no credentials. The miss is likeliest in a preview environment or a fresh cluster where the variable was never copied over, which is exactly where a boot failure is cheap and an open directory is not. _voltro_scim_usersis the SCIM-side mirror; map it to your realuserstable inonCreate/sync logic if they differ.
Permissions
store:write (the SCIM tables).