dánwòdánwò

Identity

Clerk

A Clerk development instance on its real hosts, with the Backend API, the Frontend API serving ClerkJS and its sign-in components, the Account Portal, stable ids and Svix-signed webhooks.

danwo's Clerk emulator is a development instance on Clerk's real hosts. Your server reaches the Backend API, and the browser loads ClerkJS and <SignIn/> from the Frontend API or signs in on the Account Portal. Clerk's SDKs work against it unchanged.

Set up

Add clerkService and hand the app the instance's keys. clerkInstance(…).env() gives every variable Clerk's SDKs read:

danwo.config.ts
import { appEnvironment, clerkInstance, clerkService, defineConfig } from "@danwo/sdk"

const url = "http://localhost:3000"

export default defineConfig({
  testDir: "tests",
  readiness: { mode: "page+backend", settle: true },
  environment: () => {
    const clerk = clerkService({
      seed: {
        instance: { slug: "my-app", origin: url },
        users: [{ email: "[email protected]", password: "correct-horse-battery", firstName: "Ada" }],
        webhooks: [{ endpoint: `${url}/api/webhooks/clerk`, events: ["user.created"] }],
      },
    })

    return appEnvironment({
      app: { command: "npm run dev", url, env: clerkInstance(clerk.emulator).env() },
      services: [clerk],
    })
  },
})
VariableHolds
CLERK_PUBLISHABLE_KEY, NEXT_PUBLIC_…, VITE_…The publishable key, derived from the slug.
CLERK_SECRET_KEYThe secret key your server sends.
CLERK_JWT_KEY, CLERK_PEM_PUBLIC_KEYThe public key, so tokens verify without a network call.
CLERK_WEBHOOK_SIGNING_SECRETThe webhook secret, when there's one endpoint.

Seed it

FieldWhat it seeds
instanceThe slug, the secret key, the app's origin, a signingKey of your own, and password: false for emailed codes only.
usersUsers with a password, names, username, externalId and metadata.
organizationsOrganizations and their members' roles.
jwtTemplatesJWT templates by name, with claims and lifetime.
webhooksEndpoints, events, secret and extra headers.
socialSocial connections, such as Google, with the OAuth client the instance signs in to the provider with.
announceSend user.created and the like for the seeded records, so an app that builds its rows from webhooks has them.

Stable ids

A seeded user's id is derived from their email, and an organization's from its slug or name, so user_… is the same on every run. Rows in your own database that point at a Clerk user keep pointing at the right one after a reset. Give an id to choose it yourself.

To seed your own database with the same ids, import stableClerkId from @danwo/sdk:

seed.ts · a user's row
const id = stableClerkId("user", "[email protected]")

A key that stays put

The emulator signs session tokens with its own key. If your app pins Clerk's public key in a file, seed instance.signingKey with a PKCS#8 PEM so tokens keep verifying across runs. clerkInstance(clerk).publicKeyPem() reads the public half.

Use it in a test

Sign in on the Account Portal, then check the session:

tests/sign-in.danwo.ts
import { type ClerkFixtures, clerkInstance, test as base } from "@danwo/sdk"

const test = base.extend<ClerkFixtures>()

test("signs in with a password", async ({ app, screen, expect, clerk }) => {
  await app.open("/dashboard")
  await screen.getByLabel("Email address").fill("[email protected]")
  await screen.getByLabel("Password").fill("correct-horse-battery")
  await screen.getByRole("button", { name: "Continue" }).click()

  await expect(screen.getByText("Welcome, Ada")).toBeVisible()
  expect(clerkInstance(clerk).sessions("[email protected]")).toHaveLength(1)
})

With emailed codes, read the code with clerkInstance(clerk).code(email) and type it into "Verification code". Addresses with +clerk_test take the code 424242 and get no email, as on a development instance.

clerkInstance(clerk)What it's for
user(email), sessions(email)A user, and their sessions.
users()Every user, with their linked logins.
memberships(orgId?)Members and their roles.
code(to), messages(to)The codes and emails Clerk sent.
organizations()The organizations.
revokeSessions(email)Sign a user out mid-test.
env(), publicKeyPem(), webhookSecret()The keys and secrets your app needs.

clerkMail(clerk) gives the codes to the mail fixture.

users() and memberships() let an invariant check your own rows against Clerk's. Each user lists the social accounts linked to it, such as Google, under external_accounts.

Sign in with Google

Seed a social connection and hand clerkService the Google service it signs in on. Clerk's sign-in and sign-up components then show "Continue with Google". The browser picks an account on the Google emulator's chooser, and Clerk signs that person in, links Google to the account with that email, or signs them up:

danwo.config.ts · Google
const google = googleService({
  seed: {
    users: [{ email: "[email protected]", name: "Lin Wu" }],
    oauth_clients: [
      {
        client_id: "my-app.apps.googleusercontent.com",
        client_secret: "my-app-google-secret",
        name: "My app",
        redirect_uris: ["https://my-app.clerk.accounts.dev/v1/oauth_callback"],
      },
    ],
  },
})
const clerk = clerkService({
  social: [google],
  seed: {
    instance: { slug: "my-app" },
    social: [
      {
        provider: "google",
        clientId: "my-app.apps.googleusercontent.com",
        clientSecret: "my-app-google-secret",
      },
    ],
  },
})

Put both in services. The redirect URI is the instance's Frontend API, as with a real Google app.

Organizations and invitations

<OrganizationSwitcher/> lists the signed-in user's organizations and creates new ones. An invitation goes out like this:

StepWhat happens
organizations.createOrganizationInvitation({ …, redirectUrl })Clerk emails the invitee a link to the Frontend API
mail.link(email, { match: "ticket" })the test reads that link
The invitee opens itthey land on redirectUrl with Clerk's ticket
<SignUp/> theresigns them up, address verified, and adds them to the organization
<SignIn/> theredoes the same for someone who already has an account

What's covered

AreaCovered
Backend APIUsers, sessions and tokens, organizations and invitations, JWT templates, keys.
Frontend APIThe client, sign-up and sign-in by password, email code, Google or invitation ticket, token refresh, the handshake, the user's organizations.
ClerkJSServed from the Frontend API, so <ClerkProvider> and Clerk's components load, with their images.
Account PortalSign-in with a password or a code, sign-up with a code.
TokensSession tokens with organization claims and your templates.

Faults

CallFaults the matrix tries
A Backend API write, such as POST /v1/users429, 500, 503, a 10-second delay, connection reset, answer lost after Clerk acted
A Backend API read429, 500, 503, a 10-second delay, connection reset
Every webhook deliveryarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

The Frontend API and the Account Portal are left alone, so the browser can always sign in.

Webhooks

Deliveries are signed the Svix way, so verifyWebhook from @clerk/nextjs/webhooks and the svix package accept them.

EventsSent when
user.*A user is created, updated or deleted.
session.*A session starts, ends, is removed or revoked.
organization.*An organization is created, updated or deleted.
organizationMembership.*A member joins, changes role or leaves.

Fidelity

Live. danwo fidelity clerk runs 5 scenarios against a real development instance: users, organizations, sessions and keys, errors, and the Frontend API's sign-up and sign-in with test addresses and 424242. All 5 match. Webhooks are checked offline.

Not covered yet

  • Social sign-in other than Google, and enterprise sign-in (SAML)
  • Phone numbers, SMS codes and passkeys
  • Production instances, satellite domains and the Clerk Dashboard
  • Billing

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI