# SCIM

> SCIM 2.0 provisioning — Users + Groups REST endpoints so an enterprise IdP (Okta, Entra, OneLogin) can create / update / deactivate users in your app.



---

<!-- source: en/plugins/scim.md -->
## 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

```ts
// 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).
