
Clerk vs AWS Cognito: Developer Experience vs Scale Economics - Part 2
Part 2 of 3. Start with Clerk vs AWS Cognito: Developer Experience vs Scale Economics.
This is the second part of a three-part series comparing Clerk and AWS Cognito. Part 1 covered the core platform differences and developer experience. In this part, we focus on the architectural comparison and enterprise capabilities like B2B multi-tenancy, SSO, RBAC, and Directory Sync. Part 3 will cover scale economics and vendor lock-in.
Architecture and feature comparison
Clerk is a developer-experience-first auth platform built around drop-in components and a typed SDK; AWS Cognito is an identity primitive inside the AWS ecosystem that you assemble into a full auth experience. The table below credits Cognito where it genuinely leads: native AWS integration, identity pools that issue temporary AWS credentials, and the lowest raw per-MAU price at very high volume. Every factual cell here is auditable to a primary source cited inline in the sections that follow, with the full pricing breakdown in Part 3.
A deeper treatment of the enterprise feature gaps follows in the sections below.
Enterprise capabilities: where Cognito gets hard
Enterprise auth is where the build-it-yourself cost on Cognito compounds. Organizations, per-tenant SSO, SCIM provisioning, and database sync are first-class and bundled in Clerk, while on Cognito each is a system you design, build, and maintain on primitives. Cognito can do all of it. The question is how much of it you want to own.
B2B organizations and multi-tenancy
This is the biggest gap. Cognito has no native organization or tenant primitive. Teams emulate multi-tenancy with one of several documented patterns: pool-per-tenant, app-client-per-tenant, group-based, or custom-attribute (custom:tenantID), and each one carries trade-offs.
The group-based approach runs into hard caps: 10,000 groups per pool, 100 groups per user, and no nesting. The shared-pool patterns carry an isolation gap, where a valid session can reach other tenant apps that trust the same pool. And per-tenant MFA or password policy isn't possible in a shared pool, since those settings live at the pool level.
Clerk ships organizations as first-class objects: members, roles, invitations, org switching, and org-scoped SSO. Users can belong to multiple organizations, with an Active Organization per browser tab. Drop in <OrganizationSwitcher /> and the multi-tenant model is there.
Enterprise SSO (SAML/OIDC)
Cognito supports SAML and OIDC federation. Credit where it's due: both protocols work, and the IdP catalog is broad. The friction is operational. Configuration is per pool, the SP entity ID is pool-scoped (urn:amazon:cognito:sp:<pool-id>, the same value for every tenant), and there's no per-tenant ACS URL. SAML metadata is cached for up to 6 hours, which becomes a cert-rotation risk when an IdP rolls its signing certificate. The default quota is 300 identity providers per pool, adjustable to a published maximum of 1,000 — a hard ceiling if you dedicate one SAML connection per enterprise tenant.
Clerk offers per-organization enterprise connections plus the EASIE pattern for Google Workspace and Microsoft Entra ID. One honest packaging note: a plain enterprise connection is included on Pro (1 free, then tiered), but org-linked connections require the B2B Authentication add-on ($100/mo, or $85/mo billed annually).
RBAC, roles, and permissions
Cognito provides groups mapped to IAM roles (cognito:groups, cognito:preferred_role), which is coarse. Native fine-grained application permissions require Amazon Verified Permissions, which now supports Cognito group-based RBAC through Cedar policies. That's real capability, but you configure the entity type and author the policies yourself. It's a policy engine, not turnkey app permissions.
Clerk provides roles and permissions in the org:<feature>:<permission> format, with default org:admin and org:member roles and up to 10 custom roles per instance. Custom Role Sets require the B2B Authentication add-on.
SCIM and Directory Sync
Cognito has no native SCIM. Provisioning and deprovisioning means building a serverless stack yourself. The representative shape is API Gateway plus Lambda — the same architecture as AWS's own SCIM-for-Cognito sample, which builds SCIM /Users endpoints on exactly that stack. A full build exposes /Users (and often /Groups) and drives AdminCreateUser, AdminDisableUser, and AdminDeleteUser under the hood; heavier implementations add Step Functions to orchestrate longer-running syncs. Then you maintain it across the quirks of every IdP that connects.
Clerk offers Directory Sync (SCIM), generally available (core GA April 16 2026; group-to-role and custom-attribute mapping GA May 21 2026), bundled free with each enterprise connection. Deprovisioning immediately revokes all of a user's sessions, consistent with Clerk's session model, and fires a standard user.updated webhook. SCIM changes flow through the same handler as everything else.
Keeping your database in sync
Clerk ships built-in, signed, automatically-retried webhooks for the full user, organization, and session lifecycle: user.created, user.updated, user.deleted, plus org and session events. They're delivered over Svix to any endpoint and verified with verifyWebhook() from @clerk/nextjs/webhooks (or @clerk/backend/webhooks for non-Next.js backends, same signature). There's a first-party guide for syncing Clerk data to your database. Consumers stay idempotent on the svix-id header.
Directory Sync reuses that same path. An IdP provision fires user.created, an attribute change fires user.updated, and a deprovision deactivates the user, revokes sessions, and fires user.updated. One handler covers both self-serve and enterprise-provisioned changes. (Note that scim.* events are dashboard-only diagnostics, not your sync feed.)
Cognito, to be fair, does have events. It offers synchronous in-flow Lambda triggers (post-confirmation, post-authentication, pre-token-generation, and so on) that run inside auth flows and can block or modify them. It also has partial EventBridge coverage delivered via CloudTrail (source: aws.cognito-idp, which requires a trail and is best-effort). What's missing is a native, durable, signed webhook for every directory mutation. And there's a sharp named gap: an out-of-band AdminUpdateUserAttributes fires no DB-sync trigger at all — at most the email/phone verification-message sender runs, and only when a change needs re-verification — so a backend attribute write to a custom field can silently skip your sync.
Standing up per-tenant SSO on Cognito
Here's the heavy-code contrast. To onboard one enterprise customer's SAML IdP on a shared pool, you orchestrate several calls and protect them with a lock.
import {
CognitoIdentityProviderClient,
CreateIdentityProviderCommand,
DescribeUserPoolClientCommand,
UpdateUserPoolClientCommand,
} from '@aws-sdk/client-cognito-identity-provider'
const client = new CognitoIdentityProviderClient({ region: 'us-east-1' })
async function onboardTenantSaml(opts: {
poolId: string
appClientId: string
providerName: string // max 32 chars
metadataUrl: string
emailDomain: string
callbackUrl: string
}) {
// UpdateUserPoolClient is capped at 5 RPS per pool and is a destructive
// full-replace. Two onboardings racing throw ConcurrentModificationException,
// so serialize per pool.
await acquirePerPoolLock(opts.poolId)
try {
// 1. Register the tenant's IdP.
await client.send(
new CreateIdentityProviderCommand({
UserPoolId: opts.poolId,
ProviderName: opts.providerName,
ProviderType: 'SAML',
ProviderDetails: { MetadataURL: opts.metadataUrl },
AttributeMapping: { email: 'emailAddress' },
IdpIdentifiers: [opts.emailDomain],
}),
)
// 2. Read the current app client BEFORE writing.
const { UserPoolClient: current } = await client.send(
new DescribeUserPoolClientCommand({
UserPoolId: opts.poolId,
ClientId: opts.appClientId,
}),
)
// 3. Echo back EVERY field or Cognito resets it to default.
await client.send(
new UpdateUserPoolClientCommand({
UserPoolId: opts.poolId,
ClientId: opts.appClientId,
SupportedIdentityProviders: [
...(current?.SupportedIdentityProviders ?? []),
opts.providerName,
],
CallbackURLs: Array.from(new Set([...(current?.CallbackURLs ?? []), opts.callbackUrl])),
// You must also re-send AllowedOAuthFlows, AllowedOAuthScopes,
// ExplicitAuthFlows, token validities, and every other setting,
// or they silently revert to defaults.
}),
)
} finally {
await releasePerPoolLock(opts.poolId)
}
// 4. And you still need a Pre-Token-Generation Lambda to stamp tenant_id,
// a tenant registry mapping email domain to provider, and a sign-in
// routing layer in front of all of it.
await registerTenant(opts.emailDomain, opts.providerName)
}That's the work for one tenant, and you own it forever. In Clerk you add one enterprise connection in the Dashboard, on the SSO connections → Enterprise page, and select the organization. Users from that organization's email domain then authenticate through it automatically with strategy: 'enterprise_sso'.
No per-IdP pool surgery, no destructive full-replace, no serialization lock.
In this second part, we compared the enterprise capabilities of Clerk and AWS Cognito, showing how Clerk bundles features like B2B multi-tenancy and Directory Sync, whereas Cognito requires you to assemble them. In Part 3, we will break down scale economics, the cost of ownership, and how to handle vendor lock-in and migrations.
Frequently asked questions
Does AWS Cognito support B2B multi-tenancy?
Cognito does not have a native organization or tenant primitive. Teams emulate multi-tenancy using patterns like a pool-per-tenant, app-client-per-tenant, group-based isolation, or custom attributes, each of which carries trade-offs in isolation and scale.
How do I sync AWS Cognito users to my database?
Cognito offers synchronous Lambda triggers and EventBridge coverage via CloudTrail, but it lacks a native, durable webhook for every directory mutation. Out-of-band attribute updates may not fire a trigger, so keeping a database in sync often requires custom serverless architecture.
Does Clerk support SCIM provisioning?
Yes. Clerk offers Directory Sync (SCIM) as a generally available feature bundled with each enterprise connection. Deprovisioning a user through SCIM immediately revokes their sessions and fires a standard user.updated webhook.
In this series
- Clerk vs AWS Cognito: Developer Experience vs Scale Economics
- Clerk vs AWS Cognito: Developer Experience vs Scale Economics - Part 2 (you are here)
- Clerk vs AWS Cognito: Developer Experience vs Scale Economics - Part 3