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:
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: "ada@example.com", 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],
})
},
})| Variable | Holds |
|---|---|
CLERK_PUBLISHABLE_KEY, NEXT_PUBLIC_…, VITE_… | The publishable key, derived from the slug. |
CLERK_SECRET_KEY | The secret key your server sends. |
CLERK_JWT_KEY, CLERK_PEM_PUBLIC_KEY | The public key, so tokens verify without a network call. |
CLERK_WEBHOOK_SIGNING_SECRET | The webhook secret, when there's one endpoint. |
Seed it
| Field | What it seeds |
|---|---|
instance | The slug, the secret key, the app's origin, a signingKey of your own, and password: false for emailed codes only. |
users | Users with a password, names, username, externalId and metadata. |
organizations | Organizations and their members' roles. |
jwtTemplates | JWT templates by name, with claims and lifetime. |
webhooks | Endpoints, events, secret and extra headers. |
social | Social connections, such as Google, with the OAuth client the instance signs in to the provider with. |
announce | Send 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:
const id = stableClerkId("user", "ada@example.com")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:
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("ada@example.com")
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("ada@example.com")).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:
const google = googleService({
seed: {
users: [{ email: "lin@example.com", 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:
| Step | What 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 it | they land on redirectUrl with Clerk's ticket |
<SignUp/> there | signs them up, address verified, and adds them to the organization |
<SignIn/> there | does the same for someone who already has an account |
What's covered
| Area | Covered |
|---|---|
| Backend API | Users, sessions and tokens, organizations and invitations, JWT templates, keys. |
| Frontend API | The client, sign-up and sign-in by password, email code, Google or invitation ticket, token refresh, the handshake, the user's organizations. |
| ClerkJS | Served from the Frontend API, so <ClerkProvider> and Clerk's components load, with their images. |
| Account Portal | Sign-in with a password or a code, sign-up with a code. |
| Tokens | Session tokens with organization claims and your templates. |
Faults
| Call | Faults the matrix tries |
|---|---|
A Backend API write, such as POST /v1/users | 429, 500, 503, a 10-second delay, connection reset, answer lost after Clerk acted |
| A Backend API read | 429, 500, 503, a 10-second delay, connection reset |
| Every webhook delivery | arrives 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.
| Events | Sent 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
Auth0
Auth0's Universal Login with sign-up, MFA and social connections, the authentication API's grants, and the Management API v2, on your tenant's own domain.
S3 and Tigris
S3 object storage for apps on AWS S3 or Tigris, with the AWS SDK and the Tigris SDK, presigned URLs, multipart uploads and signature checks.