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 while app.config.ts evaluates if the token is empty or whitespace, so a deployment with an unset SCIM_TOKEN fails to start rather than coming up and answering the first anonymous request. This exists because scimPlugin({ 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_users is the SCIM-side mirror; map it to your real users table in onCreate/sync logic if they differ.

Permissions

store:write (the SCIM tables).