Examples
Login with Clerk
Signing in with Clerk on Next.js: Clerk's own components for sign-up and sign-in by emailed code or Google, organizations and invitations, and the app's users in Postgres from Clerk's webhooks.
Signing in with Clerk on Next.js: Clerk's own components for sign-up and sign-in by emailed code or Google, organizations and invitations, and the app's users in Postgres from Clerk's webhooks. See it on GitHub.
What it shows
- The real
<SignIn/>,<SignUp/>,<UserButton/>and<OrganizationSwitcher/>against danwo's Clerk emulator: emailed codes, Google's consent screen, invitations - A second person in a second browser: Bob opens the invitation email and joins Ada's organization
- Invariants that read Clerk and Postgres: one app user per Clerk user, the same memberships on both sides, every unsigned webhook refused
danwo matrixfaulting Clerk's Backend API, its webhooks (duplicate, late, forged, stale) and the app's SQL, withCLERK_LOGIN_BUGswitches that turn a safeguard off
Stack
Next.js App Router@clerk/nextjsPostgresTailwind CSSEmulates: Clerk, Google. Runs on: A Next.js app and the real ClerkJS in Chromium, with Postgres in Docker.
Run it
Copy examples/clerk-login anywhere, then from its folder:
npm install
npx @danwo/cli testIts scripts:
npm run dev # next dev
npm run build # next build
npm run start # next start
npm run typecheck # tsc --noEmit
npm run test # danwo test
npm run test:agentic # danwo test tests/agentic.danwo.ts
npm run matrix # danwo matrix tests/deterministic.danwo.ts --grep "signs up with an emailed code"
npm run matrix:plan # danwo matrix tests/deterministic.danwo.ts --grep "signs up with an emailed code" --plan
npm run matrix:invite # danwo matrix tests/deterministic.danwo.ts --grep "invites Bob"
npm run serve # danwo serveIts README says what else it needs and what the last full matrix found.
Until the danwo version it pins is on npm, install it from a local build instead: from the repository's root, bun run release <version> --pack, then node examples/link.ts clerk-login, which installs a copy and prints where it is.
Clerk explains what this example leans on.
The test
import { clerkInstance } from "@danwo/sdk"
import {
accountsAgree,
invitedOnce,
ADA,
ADA_IDS,
ADAS_ORG,
LIN,
oneAppUserPerClerkUser,
signIn,
test,
} from "./lumen.ts"
/** Someone new to Lumen, who signs up in the first test. */
const GRACE = { email: "grace@lumen.test", firstName: "Grace", lastName: "Hopper" } as const
/** Who Ada invites. */
const BOB = "bob@lumen.test"
test("signs up with an emailed code", async ({ app, screen, mail, expect }) => {
await app.open("/sign-up")
await screen.getByLabel("First name").fill(GRACE.firstName)
await screen.getByLabel("Last name").fill(GRACE.lastName)
await screen.getByLabel("Email address").fill(GRACE.email)
await screen.getByRole("button", { name: "Continue", exact: true }).click()
/* Clerk emails a six-digit code; typing the last digit sends it. */
await screen.getByLabel("Enter verification code").fill(await mail.code(GRACE.email))
await expect(screen.getByRole("heading", { name: "Welcome, Grace" })).toBeVisible()
/* The app's own row comes from Clerk's `user.created` webhook, before or after the redirect. */
await expect(screen.getByRole("status")).toContainText(
`Lumen has your account for ${GRACE.email}`,
)
}).invariant(accountsAgree)
test("signs in with an emailed code", async ({ app, screen, mail, expect }) => {
await signIn({ app, screen, mail, expect }, ADA.email)
await expect(screen.getByRole("status")).toContainText(`Lumen has your account for ${ADA.email}`)
await screen
.getByRole("navigation", { name: "Main" })
.getByRole("link", { name: "Profile" })
.click()
await expect(screen.getByTestId("profile-name")).toHaveText("Ada Lovelace")
await expect(screen.getByTestId("profile-clerk-user-id")).toHaveText(ADA_IDS.user)
}).invariant(accountsAgree)
test("signs in with Google on Google's own page", async ({ app, screen, expect }) => {
await app.open("/sign-in")
await screen.getByRole("button", { name: /Continue with Google/ }).click()
/* Google's account chooser, served by the Google emulator: no Google app, no real account. */
await expect(screen.getByText("Choose an account")).toBeVisible()
await screen.getByRole("button", { name: `Continue as ${LIN.name}` }).click()
await expect(screen.getByRole("heading", { name: "Welcome, Lin" })).toBeVisible()
await screen
.getByRole("navigation", { name: "Main" })
.getByRole("link", { name: "Profile" })
.click()
await expect(screen.getByTestId("profile-signs-in-with")).toHaveText("Google, or an emailed code")
}).invariant(async (fixtures) => {
await accountsAgree(fixtures)
fixtures.expect(clerkInstance(fixtures.clerk).user(LIN.email)?.first_name).toBe("Lin")
})
test("sends a signed-out visitor from the dashboard to sign in", async ({
app,
screen,
expect,
}) => {
await app.open("/dashboard")
await expect(screen.getByRole("heading", { name: /Sign in to/ })).toBeVisible()
await app.open("/organization")
await expect(screen.getByRole("heading", { name: /Sign in to/ })).toBeVisible()
})
test("signs out", { session: "signed-in-ada" }, async ({ app, screen, expect }) => {
await app.open("/dashboard")
await expect(screen.getByRole("heading", { name: "Welcome, Ada" })).toBeVisible()
await screen.getByRole("button", { name: "Open user menu" }).click()
await screen
.getByRole("dialog", { name: "Account panel" })
.getByRole("button", { name: "Sign out" })
.click()
await expect(screen.getByRole("heading", { name: /Sign in to/ })).toBeVisible()
/* The server sees no one, and the browser kept no session cookie for it to see. */
await app.open("/api/me")
await expect(screen.getByText(/"userId":null/)).toBeVisible()
await expect(screen.getByText(/"sessionCookie":false/)).toBeVisible()
}).invariant(async ({ clerk, expect }) => {
const live = clerkInstance(clerk)
.sessions(ADA.email)
.filter((session) => session.status === "active")
expect(live, "Ada has no session left in Clerk").toEqual([])
})
test(
"creates an organization and invites Bob, who joins from the email",
{ session: "signed-in-ada" },
async ({ app, screen, expect, mail, users }) => {
await app.open("/organization")
await screen.getByLabel("Organization name").fill("Lumen Labs")
await screen.getByRole("button", { name: "Create organization" }).click()
await expect(screen.getByRole("heading", { name: "Lumen Labs" })).toBeVisible()
await screen.getByLabel("Email address").fill(BOB)
await screen.getByRole("button", { name: "Send invitation" }).click()
await expect(screen.getByRole("status")).toHaveText(`Invitation sent to ${BOB}.`)
await expect(screen.getByRole("list", { name: "Pending invitations" })).toContainText(BOB)
/*
* Bob, in a browser of his own, opens the link in Clerk's email. The ticket in it vouches for
* his address, so Clerk's <SignUp/> signs him up and into Lumen Labs with no code to type.
*/
const bob = await users.open("bob")
await bob.app.open(await mail.link(BOB, { match: "ticket" }))
await bob.expect(bob.screen.getByRole("heading", { name: `Welcome, ${BOB}` })).toBeVisible()
/* Back in Ada's browser, Bob is a member, in Clerk and in Lumen's own records. */
await app.open("/organization")
await screen
.getByRole("navigation", { name: "Your organizations" })
.getByRole("link", { name: "Lumen Labs" })
.click()
const row = screen.getByRole("row", { name: new RegExp(BOB) })
await expect(row).toContainText("Member")
await expect(row).toContainText("Yes")
},
).invariant(async (fixtures) => {
await accountsAgree(fixtures)
await invitedOnce(fixtures, { organization: "Lumen Labs", invitee: BOB })
})
test("refuses a wrong code", async ({ app, screen, expect }) => {
await app.open("/sign-in")
await screen.getByLabel("Email address").fill(ADA.email)
await screen.getByRole("button", { name: "Continue", exact: true }).click()
await screen.getByLabel("Enter verification code").fill("000000")
await expect(screen.getByRole("paragraph").filter({ hasText: "Incorrect code" })).toBeVisible()
await expect(screen.getByRole("heading", { name: /Check your email/ })).toBeVisible()
})
test("refuses a code once it has expired", async ({ app, screen, mail, expect, time }) => {
await app.open("/sign-in")
await screen.getByLabel("Email address").fill(ADA.email)
await screen.getByRole("button", { name: "Continue", exact: true }).click()
const code = await mail.code(ADA.email)
/* Clerk's codes last ten minutes. */
await time.travel({ minutes: 11 })
await screen.getByLabel("Enter verification code").fill(code)
await expect(screen.getByRole("paragraph").filter({ hasText: /expired/i })).toBeVisible()
await expect(screen.getByRole("heading", { name: /Check your email/ })).toBeVisible()
}).invariant(oneAppUserPerClerkUser)
/** The ids the seed gave Ada's organization, which the app's database was seeded with too. */
test(
"shows Ada's seeded organization",
{ session: "ada-org-owner" },
async ({ app, screen, expect }) => {
await app.open("/organization")
await expect(screen.getByRole("heading", { name: ADAS_ORG.name })).toBeVisible()
await expect(screen.getByRole("row", { name: new RegExp(ADA.email) })).toContainText("Admin")
},
).invariant(accountsAgree)Login with Better Auth
Self-hosted sign-in with Better Auth: a Vite and React app on a Hono API and Postgres, with emailed codes through Resend, Google and GitHub sign-in, a password with a confirmed email, linked accounts and signed-in devices.
Commerce
A small shop selling mugs, notebooks and prints: a Next.js storefront and admin, a Hono API on Postgres, and Stripe Checkout, Resend receipts and S3 pictures.