Skip to main content
Articles

Clerk vs AWS Cognito: Developer Experience vs Scale Economics - Part 2

Author: Roy Anger
Published: (last updated )

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.

DimensionClerkAWS Cognito
Developer experienceComponents-first: prebuilt UI, hooks, and a typed SDK get production auth running in hours.Infrastructure primitive: you assemble UI and flows on top of the SDK or Amplify.
Prebuilt UI componentsDrop-in <SignIn />, <UserButton />, <OrganizationSwitcher />, plus profile and MFA flows.Managed login (hosted UI) or build your own. No drop-in profile or org components.
Setup time (consumer auth)About 4-12 hours (estimate).About 40-80 hours (estimate).
Local dev and testingPaste pk_test_/sk_test_ keys, no infra. Deterministic test users via +clerk_test and OTP 424242.No official emulator. Use a real pool, LocalStack (paid), or cognito-local (feature gaps).
Server-side framework support (RSC, Server Actions, use cache)Call auth() directly in Server Components, Actions, and Route Handlers. Next.js 16-aware.Supported through Amplify's runWithAmplifyServerContext(...) wrapper. Works, but more ceremony.
B2B organizations / multi-tenancyNative organizations: members, roles, invites, org switching, and org-scoped SSO.No native org or tenant object. Emulate with pools, app clients, groups, or custom:tenantID.
Enterprise SSO (SAML/OIDC)Per-organization enterprise connections plus EASIE. One connection included on Pro, then tiered.SAML and OIDC federation configured per pool. Operationally heavy for per-tenant SSO.
SCIM / Directory SyncDirectory Sync (SCIM) is GA and bundled free with each enterprise connection. Deprovisioning revokes sessions.No native SCIM. Build a serverless provisioning stack yourself.
Webhooks / event syncBuilt-in signed, retrying webhooks across the user, org, and session lifecycle (user.created and more), with a first-party DB-sync guide.Synchronous Lambda triggers plus CloudTrail-fed EventBridge. No native per-mutation signed webhook.
RBAC / rolesRoles and permissions with default org:admin/org:member and up to 10 custom roles. Custom Role Sets need the B2B add-on.Groups mapped to IAM roles (coarse). Fine-grained app permissions need Amazon Verified Permissions.
MFA and passwordlessSMS, TOTP, and backup codes. Passkeys as a separate passwordless credential.Email, SMS, and TOTP MFA on Essentials. Passkeys on Essentials and up.
Security and complianceSOC 2 Type 2 and HIPAA since 2022, plus GDPR/CCPA. Bot protection, breached-password blocking, and brute-force lockout on every plan.Inherits AWS SOC/PCI, HIPAA eligibility, and FedRAMP. At-rest encryption by default (optional customer-managed KMS). Advanced threat protection is the paid Plus tier; edge filtering is the separately billed AWS WAF.
Customization / brandingThemeable components via appearance, plus fully headless hooks.Managed login handles visual styling only: no text/label edits, no custom HTML/CSS, no custom fields. Deeper branding means custom flows.
Framework SDK coverageFirst-class SDKs for Next.js, React, React Router, Astro, TanStack Start, Expo, Vue/Nuxt, iOS/Android, and Chrome Extension, plus backend SDKs.AWS SDK and Amplify (AWS-shaped, framework-agnostic).
AWS-ecosystem integrationStandards-based tokens that integrate with any backend, not AWS-coupled.Genuine win: native IAM, Lambda triggers, API Gateway, and AppSync. Identity pools issue temporary AWS credentials directly.
Data portability / lock-inCSV export includes hashed passwords, programmatic export via the Backend API, and standards-based JWTs.Does not export password hashes. User pools are largely immutable after creation.
Pricing modelPer Monthly Retained User (MRU), which excludes sign-up-and-bounce users. 50K free.Per Monthly Active User (MAU) across Lite, Essentials, and Plus. 10K free for new pools.
Pricing at scaleCheaper than Cognito Essentials below about 208K users, with features bundled in.Genuine consideration: the Lite tier is the lowest raw per-user price at very high volume, though feature-limited.
Support / ecosystemDedicated auth vendor. The docs and framework SDKs are the product.Backed by AWS support plans and the broad AWS ecosystem and community.

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

  1. Clerk vs AWS Cognito: Developer Experience vs Scale Economics
  2. Clerk vs AWS Cognito: Developer Experience vs Scale Economics - Part 2 (you are here)
  3. Clerk vs AWS Cognito: Developer Experience vs Scale Economics - Part 3