SuperAPI AI Directory

implementing-scim-provisioning

Security & auditing

View repository

Updated 10-16 04:32

About this skill

SCIM Provisioning with Scalekit

Adds automated user lifecycle management (create, update, deactivate) via Scalekit's Directory API and real-time webhooks.

Guardrails

  • MUST verify the SCIM webhook signature before processing any event; MUST NOT act on a payload that fails verification.
  • MUST make user provisioning idempotent — upsertUser MUST handle duplicate directory events without creating duplicate users.
  • MUST deactivate users on user_deleted events; MUST NOT hard-delete unless the codebase already does so explicitly.
  • MUST return a 2xx response quickly and offload heavy processing to a queue; MUST NOT block the handler long enough to trigger Scalekit's retry backoff.

Workflow

Copy and track progress:

SCIM Implementation Progress:
- [ ] Step 1: Detect stack and install SDK
- [ ] Step 2: Configure environment credentials
- [ ] Step 3: Initialize Scalekit client
- [ ] Step 4: Add Directory API sync (polling/on-demand)
- [ ] Step 5: Add webhook endpoint (real-time)
- [ ] Step 6: Register webhook in Scalekit dashboard
- [ ] Step 7: Map directory events to local user operations
- [ ] Step 8: Validate end-to-end

Step 1: Detect stack and install SDK

Detect the project's language/framework from existing files (package.json, requirements.txt, go.mod, pom.xml) and install accordingly:

Stack Install command
Node.js npm install @scalekit-sdk/node
Python pip install scalekit-sdk-python
Go go get github.com/scalekit-inc/scalekit-sdk-go/v2
Java Add com.scalekit:scalekit-sdk-java to pom.xml or build.gradle

Step 2: Environment credentials

Add to .env (never hardcode):

SCALEKIT_ENVIRONMENT_URL='https://<your-env>.scalekit.com'
SCALEKIT_CLIENT_ID='<CLIENT_ID>'
SCALEKIT_CLIENT_SECRET='<CLIENT_SECRET>'
SCALEKIT_WEBHOOK_SECRET='<WEBHOOK_SECRET>'

Credentials are found in Dashboard > Developers > Settings > API Credentials. Webhook secret is found in Dashboard > Webhooks after registering an endpoint.


Step 3: Initialize the Scalekit client

Insert initialization near the app's startup or service layer — match the project's existing patterns (singleton, DI, module export, etc.).

Node.js:

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
);

Python:

from scalekit import ScalekitClient
scalekit_client = ScalekitClient(
    env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
    client_id=os.getenv("SCALEKIT_CLIENT_ID"),
    client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
)

For Go and Java patterns, see the Scalekit SDK documentation.


Step 4: Directory API (on-demand sync)

Use for scheduled jobs, onboarding flows, or bulk imports. Integrate into existing user service/repository layer — do not create a parallel user management path.

Fetch users and sync:

orgId / org_id is the Scalekit organization id (e.g. org_…) for the customer tenant — take it from the app's org/tenant record or from the webhook event payload, not from the directory list response.

// Node.js
// orgId = Scalekit organization id for this tenant (from your DB / request context)
// Note: this takes the first directory — fine for single-directory orgs.
// For multi-directory orgs, select the target directory by ID from `directories` instead of indexing [0].
const { directories } = await scalekit.directory.listDirectories(orgId);
const directory = directories[0];
const { users } = await scalekit.directory.listDirectoryUsers(orgId, directory.id);

for (const user of users) {
  await upsertUser({ email: user.email, name: user.name, orgId });
}
# Python
# org_id = Scalekit organization id for this tenant (from your DB / request context)
# Note: this takes the first directory — fine for single-directory orgs.
# For multi-directory orgs, select the target directory by ID from `directories` instead of indexing [0].
directory = scalekit_client.directory.list_directories(organization_id=org_id).directories[0]
users = scalekit_client.directory.list_directory_users(org_id, directory.id)

for user in users:
    upsert_user(email=user.email, name=user.name, org_id=org_id)

Group sync for RBAC:

const { groups } = await scalekit.directory.listDirectoryGroups(orgId, directory.id);
for (const group of groups) {
  await syncGroupPermissions(group.id, group.name);
}

Plug upsertUser / syncGroupPermissions into the project's existing user/role management functions: search the codebase for functions named createUser, updateUser, deactivateUser (or the project's equivalent naming) and call those directly instead of writing new persistence logic.


Step 5: Webhook endpoint (real-time provisioning)

Add a new route to the existing HTTP server/router. Match the framework pattern already in use (Express, FastAPI, Spring Boot, net/http, etc.).

ALWAYS verify the signature before processing. Return 401 Unauthorized on signature failure (do not process the payload).

Node.js (Express): mount the route with express.raw({ type: 'application/json' }) so req.body is the raw Buffer — signature verification must run on the exact bytes that were signed.

Verify the signature before any work, then acknowledge with 2xx and process off the request path (queue / background task). Do not await full user provisioning inside the HTTP handler in production.

app.post('/webhooks/scalekit', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await scalekit.verifyWebhookPayload(
    process.env.SCALEKIT_WEBHOOK_SECRET,
    req.headers,
    req.body
  );
  if (!ok) return res.status(401).end(); // invalid signature

  const { type, data } = JSON.parse(req.body.toString('utf8'));
  // Ack fast; process asynchronously (replace with your queue: Bull, SQS, etc.)
  setImmediate(() => {
    handleDirectoryEvent(type, data).catch((err) => console.error('SCIM handler failed', err));
  });
  return res.status(202).json({ status: 'accepted' });
});

Python (FastAPI): read the raw body BEFORE parsing JSON so the bytes match exactly what Scalekit signed.

@app.post("/webhooks/scalekit")
async def scalekit_webhook(request: Request, background_tasks: BackgroundTasks):
    raw_body = await request.body()
    valid = scalekit_client.verify_webhook_payload(
        secret=os.getenv("SCALEKIT_WEBHOOK_SECRET"),
        headers=dict(request.headers),
        payload=raw_body,
    )
    if not valid:
        raise HTTPException(status_code=401, detail="Invalid signature")

    body = json.loads(raw_body)
    # Ack fast; process in background (or enqueue to Celery/RQ/etc.)
    background_tasks.add_task(handle_directory_event, body.get("type"), body.get("data", {}))
    return JSONResponse(status_code=202, content={"status": "accepted"})

For Go and Java, see the Scalekit SDK documentation.


Step 6: Event handler

Create a single dispatcher that routes to existing user operations. Map events to the project's existing create/update/deactivate functions:

async function handleDirectoryEvent(type, data) {
  switch (type) {
    case 'organization.directory.user_created':
      return createUser(data.email, data.name, data.organization_id);
    case 'organization.directory.user_updated':
      return updateUser(data.email, data.name);
    case 'organization.directory.user_deleted':
      return deactivateUser(data.email); // prefer deactivate over hard delete
    case 'organization.directory.group_created':
    case 'organization.directory.group_updated':
      return syncGroup(data);
    default:
      console.log(`Unhandled event: ${type}`);
  }
}

Prefer deactivation over deletion for user_deleted events unless the project explicitly hard-deletes users.


Step 7: Dashboard registration checklist

After deploying the webhook endpoint:

  1. Go to Dashboard > Webhooks > +Add Endpoint
  2. Enter the public HTTPS URL: https://your-app.com/webhooks/scalekit
  3. Subscribe to events:
    • organization.directory.user_created
    • organization.directory.user_updated
    • organization.directory.user_deleted
    • organization.directory.group_created
    • organization.directory.group_updated
  4. Copy the webhook secret into SCALEKIT_WEBHOOK_SECRET
  5. Share the SCIM setup guide with the customer's IT admin for their IdP-specific directory sync steps.

Customer self-serve SCIM setup (admin portal)

Let customers configure directory sync via an embedded admin portal. Generate a single-use portal link server-side, embed it in an iframe, and handle SCIM_CONFIGURED and SESSION_EXPIRED postMessage events.

// Server: generate link (single-use, regenerate on each page load)
const { location } = await scalekit.organization.generatePortalLink(organizationId);

// Client: embed in iframe
// <iframe src="{{ portalLink }}" width="100%" height="600px" allow="clipboard-write"></iframe>

Register your app domain in Dashboard > Developers > API Configuration > Redirect URIs or the iframe will be blocked.

For no-code onboarding: Dashboard > Organizations → select org → Generate link → share URL directly. Also share SCIM setup guides for IdP-specific steps.


Reference