Docs
Open the console →
Guides

Directory sync (pull) — Keycloak

Have Cushy read your identity provider on a schedule so users created, disabled or moved between groups upstream appear in your organization without signing in first — the answer for Keycloak, which has no outbound SCIM client.

There are three ways a user can reach Cushy from your identity provider, and they are not interchangeable. OIDC and SAML provision a person at sign-in only — create an account in your IdP and it stays invisible in Cushy until that person personally signs in. SCIM 2.0 fixes that by having the IdP push changes. Directory sync is the third: Cushy pulls your directory on a schedule.

Why pull exists — the Keycloak case

Keycloak 26 ships a SCIM *server* (inbound, preview); it has no built-in outbound SCIM client. So a Keycloak tenant cannot push users to Cushy without a third-party extension. Directory sync reads Keycloak's Admin API instead, using a read-only service account. If your IdP is Okta, Entra ID or Google Workspace, use SCIM push instead — all three ship a real SCIM client and pushing is immediate rather than polled.

Which channel should I use?

Identity providerRecommended channelWhat you configure
KeycloakDirectory sync (pull) — this pageA service-account client with the realm-managementview-users role.
OktaSCIM 2.0 pushOkta's SCIM provisioning app pointed at /api/scim/v2 with a Cushy bearer token.
Microsoft Entra IDSCIM 2.0 pushEntra's enterprise-app provisioning pointed at /api/scim/v2.
Google WorkspaceSCIM 2.0 pushA supported SCIM connector pointed at /api/scim/v2.
Any IdP, sign-in onlyOIDC or SAML JITProvisions each person the first time they sign in — no directory sync at all.
Pull is implemented for Keycloak only

Okta, Entra ID and Google Workspace appear in the provider list but are not implemented as pull connectors, and Cushy refuses to save a configuration for them rather than storing one that silently never runs. That is deliberate: those three can push over SCIM, which is better than polling.

Step 1 — create the Keycloak service-account client

In the Keycloak admin console, inside the realm you want Cushy to read:

  • Clients → Create client → Client type OpenID Connect, Client ID e.g. cushy-directory-sync.
  • Capability config: turn Client authentication ON (this makes it a confidential client) and enable Service accounts roles. Turn OFF Standard flow and Direct access grants — this client never logs a human in.
  • Credentials tab → copy the Client secret.
  • Service accounts roles tab → Assign role → switch the filter to Filter by clients → grant realm-management`view-users`.
`view-users` is exactly enough — do not grant more

In Keycloak, view-users is a composite role that already includes query-users and query-groups, which is everything this connector needs. Cushy only ever reads your directory: it never creates, edits or deletes anything in Keycloak. Do not grant manage-users or realm-admin.

Step 2 — configure it in Cushy

On the Team screen (/users), an Org Admin opens Directory sync (pull) → Configure and enters:

FieldValue
ProviderKeycloak
Base URLYour Keycloak origin, e.g. https://idp.example.com — no path, no trailing slash.
RealmThe realm holding your users, e.g. acme.
Service-account client IDcushy-directory-sync (from step 1).
Client secretThe secret from the Credentials tab. Encrypted at rest (AES-256-GCM) and never returned by any API.
Default role for new usersThe console role a pulled user gets when none of their groups is mapped.
Deactivate users removed upstreamOn by default — see the deactivation rules below.

Press Save, then Sync now. The card reports what changed: created, reactivated, deactivated, role changed and skipped counts, plus the time of the last run.

What a sync does

  • Creates a Cushy user for every enabled directory account that has an email address and is not already a member. They can sign in immediately through SSO — no password is ever set.
  • Links group membership: every Keycloak group a user belongs to is recorded, and the same group→role mappings that SAML and SCIM use resolve their console role (highest-privilege mapped group wins).
  • Reactivates a user who returns upstream, keeping the same account.
  • Deactivates a user removed from the directory or disabled in it — their sessions are revoked immediately and they can no longer sign in. This is never a delete: the account remains and active flips back if they return.
  • Skips any directory entry with no email address. A Cushy identity *is* an email address, so provisioning one without it would create an account nobody could sign in to.
Safety rules that cannot be overridden

The organization Owner and any platform admin are never deactivated and never role-changed by the directory — an org must not be able to lock itself out because someone left a group upstream. An existing user's role changes only when they are in a group you have actually mapped, so a first sync never rewrites roles you assigned by hand. And if the directory is larger than one pass can read, the pass is marked truncated and performs no deactivation at all — a partial view is never treated as a deletion.

When it runs

  • Sync now on the Team screen — immediate, org-admin only.
  • On a schedule — every 30 minutes on a self-hosted deployment, and daily via cron on the managed (Vercel) deployment. A sync is idempotent, so running it more often changes nothing.

When something is wrong, it says so

A failed pull never reports "synced 0 users" — nothing is applied, and the exact reason is stored and shown on the card:

What you seeWhat it means
Keycloak rejected the service-account credentialWrong client ID or secret, or the client doesn't have Client authentication + Service accounts roles enabled.
…not allowed to read users (403)The service account is missing realm-managementview-users. The message names it.
Keycloak has no realm "x" at that base URLWrong realm name, or the base URL points at something other than Keycloak's root.
The identity provider is unreachable / timed outNetwork, DNS or firewall between this deployment and your IdP.

Audit trail

Every configuration change and every pass is audited in your organization's audit log: org.directory_sync_configured, org.directory_sync_enabled / _disabled / _removed, org.directory_synced (with the counts and the accounts created or deactivated) and org.directory_sync_failed. The service-account secret never appears in an audit entry, a log line or an API response.

Limitations (honest)

  • Keycloak only. Okta, Entra ID and Google Workspace are declared seams that refuse rather than pretend — use SCIM push for those.
  • Polled, not instant. A change upstream appears at the next scheduled pass (or when you press Sync now). SCIM push is immediate; that is the trade you make when your IdP can't push.
  • Group names, not hierarchy. Keycloak groups are matched on their leaf name (the same string SAML/OIDC group claims and SCIM Groups carry), so one mapping serves all three channels. Nested-group inheritance is not expanded.
  • Attributes synced: email, display name, enabled state and group membership. Nothing else is read, and nothing at all is written back to your IdP.
  • One directory per organization, and a pass reads up to 2,000 users; beyond that it is marked truncated and skips deactivation.