Implement SCIM
Provision users and groups from a directory. Then stop.
Guardrails
- MUST call
verifyWebhookPayloadbefore reading the event. Invalid signature → 400. Do not process it. - MUST upsert. The same
user_createdcan arrive twice. - MUST deactivate on
organization.directory.user_deleted. MUST NOT hard-delete unless the repo already does. - MUST NOT generate the admin portal. Name
implement-ssoinstead.
Gotchas
- Default language is Node. Same client as
implement-saaskit. - Read
SCALEKIT_ENVIRONMENT_URL,SCALEKIT_CLIENT_ID,SCALEKIT_CLIENT_SECRET, andSCALEKIT_WEBHOOK_SECRET. NeverSCALEKIT_ENV_URL. Do not prependhttps://. - The signing secret exists after the user registers the endpoint. Write the route first with that env name.
- Mount
express.raw({ type: 'application/json' })on/webhooks/scalekitbeforeexpress.json(). The signature is the exact request bytes. verifyWebhookPayload(secret, headers, payload)takes a string. It throws. Catch → 400.- Headers are
webhook-id,webhook-timestamp, andwebhook-signature. - Webhook user fields:
email,name,organization_id. Group fields:id,display_name,organization_id. - Directory API users have
email,givenName,familyName— noname. Groups haveid,displayName. getPrimaryDirectoryByOrganizationId(orgId)returns the directory object, not{ directory }.- Copy
org_…from the dashboard or the event. Do not invent IDs. - The endpoint URL must be public HTTPS. Print dashboard checklists. Wait. Do not click the dashboard.
Step 1 — Confirm env
If the user wants the admin portal so IT can turn on the directory, name implement-sso. Stay here for the webhook.
Print this checklist. Wait for the user. Then continue.
- app.scalekit.com → Developers → Settings → API Credentials. Copy the three env names.
- The customer org already has a directory, or IT will add one after the webhook exists.
Put the three env names in the project env file. Leave SCALEKIT_WEBHOOK_SECRET for Step 5.
Done when: the three env names exist.
Step 2 — Init the SDK
Install @scalekit-sdk/node only when the repo has no Scalekit SDK yet.
import { ScalekitClient } from '@scalekit-sdk/node';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
Done when: the client initializes from those env vars.
Step 3 — Webhook route
app.post('/webhooks/scalekit', express.raw({ type: 'application/json' }), async (req, res) => {
const payload = req.body.toString('utf8');
try {
scalekit.verifyWebhookPayload(
process.env.SCALEKIT_WEBHOOK_SECRET,
req.headers,
payload
);
} catch {
return res.status(400).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
await handleDirectoryEvent(event.type, event.data);
return res.status(201).json({ received: true });
});
Respond 201 within 10 seconds. If the map is slow, enqueue and still return 201.
Done when: POST /webhooks/scalekit verifies the signature and returns 201.
Step 4 — Map users and groups
Call the repo's existing create, update, and deactivate helpers. Search for those names. Do not invent a second user store.
async function handleDirectoryEvent(type, data) {
switch (type) {
case 'organization.directory.user_created':
case 'organization.directory.user_updated':
return upsertUser({ email: data.email, name: data.name, orgId: data.organization_id });
case 'organization.directory.user_deleted':
return deactivateUser(data.email);
case 'organization.directory.group_created':
case 'organization.directory.group_updated':
return upsertGroup({ id: data.id, name: data.display_name, orgId: data.organization_id });
case 'organization.directory.group_deleted':
return removeGroup(data.id);
default:
return;
}
}
Done when: those six events map to local users and groups.
Step 5 — Register the webhook
Print this checklist. Wait for the user. Then continue.
- Dashboard → Webhooks → +Add Endpoint
- URL: the public
https://…/webhooks/scalekit - Subscribe:
organization.directory.user_createdorganization.directory.user_updatedorganization.directory.user_deletedorganization.directory.group_createdorganization.directory.group_updatedorganization.directory.group_deleted
- Copy the signing secret into
SCALEKIT_WEBHOOK_SECRET - Send Test Event
- Share the SCIM setup guide with the customer's IT admin
Done when: the secret is in env and a test event created or updated a local user.
Step 6 — On-demand directory map
Use this for a first sync or a backfill. orgId is the customer's org_… from the dashboard or the last event.
const directory = await scalekit.directory.getPrimaryDirectoryByOrganizationId(orgId);
const { users } = await scalekit.directory.listDirectoryUsers(orgId, directory.id);
for (const user of users) {
await upsertUser({
email: user.email,
name: [user.givenName, user.familyName].filter(Boolean).join(' '),
orgId,
});
}
const { groups } = await scalekit.directory.listDirectoryGroups(orgId, directory.id);
for (const group of groups) {
await upsertGroup({ id: group.id, name: group.displayName, orgId });
}
Done when: a list of directory users and groups upserts into the same store as Step 4.
Step 7 — Stop
Do not embed the admin portal here.
Done when: the webhook maps users and groups, a test event landed, and this skill has stopped.
Reach for
implement-ssofor the admin portal so IT can turn on the directoryimplement-saaskitif the app also needs SaaSKit loginimplement-access-controlfor roles and permissions at a route
Live lookups
- Docs index: https://docs.scalekit.com/llms.txt
- Modular SCIM: https://docs.scalekit.com/directory/scim/quickstart/
- Directory events: https://docs.scalekit.com/directory/reference/directory-events/
- MCP: https://mcp.scalekit.com