All pages

Clerk

Clerk is identity and nothing else. It answers who the caller is; billing is hand-rolled on Stripe, because Clerk Billing does neither one-time payments nor multiple concurrent paid plans.

You will leave with a publishable key, a secret key, and optionally a webhook signing secret. The publishable and secret keys each have an API and web environment entry, for four required key entries in total.

During the agent-run setup, the agent uses the current Clerk CLI to inventory or create the application, patch supported instance settings, and pull keys. It uses authenticated browser control for settings such as creating the webhook when the current CLI or API has no supported operation. You handle Clerk login, MFA, OAuth consent, tool-required approval, and an ambiguous application choice. The agent does not run clerk init, because that command would scaffold over the auth integration already in this repository.

  1. Create an application

    The agent first lists your applications and reuses one only after you confirm an ambiguous match. If Clerk authentication is missing, complete the browser login it opens. The dashboard fallback is dashboard.clerk.com, where Create application creates the same resource. The name is yours; nothing in this repo reads it.

    Clerk gives every application a development instance and a production instance, with separate keys. Development keys start pk_test_ and sk_test_, production keys start pk_live_ and sk_live_. Use the development pair until you deploy.

  2. Turn on the sign-in methods, and turn passwords off

    In the application's user and authentication settings, enable Email address as an identifier and Email verification code as the strategy, then turn Password off.

    No user of this product has a password. There is no route that sets, verifies, forgets or resets one, and no session can ever carry Clerk's reset-password task. Leaving passwords on gives readers a sign-in path the UI in this repo does not implement.

  3. Add Google and GitHub

    Add both under SSO connections and enable each for sign-up and sign-in. A development instance needs nothing more: Clerk's shared credentials sign you in, and Google's consent screen names Clerk rather than your product. A production instance requires your own Google credentials. For those, run pnpm setup:wizard google against the production keys, or follow the rest of this step: in the Google connection, turn on Use custom credentials, then copy Clerk's exact Authorized Redirect URI.

    In Google Cloud Console, open APIs & Services, then Credentials, create an OAuth client ID with application type Web application, add your web origins, and paste Clerk's redirect URI under Authorized redirect URIs. Put the resulting Google client ID and client secret into the Clerk connection. They belong in Clerk, not in either app's .env file. Test the connection through Clerk's Account Portal, then complete the real sign-in flow at /auth/login. An agent can perform the console steps through authenticated browser control after you complete Google login, consent, and any tool-required approval.

    Configure GitHub in the same Clerk SSO-connections area. Clerk's shared credentials are enough for a development instance. Production uses a buyer-owned GitHub OAuth application and the callback URL Clerk displays. The agent can use Clerk's supported configuration surface where available and hands over provider authentication or consent when required.

    GitHub is a sign-in option only. Nothing at checkout or after it reads the GitHub identity.

  4. Copy the two keys

    From the application's API keys page, copy the publishable key and the secret key.

    They become four environment entries, because both apps need both keys for their respective browser and server identity boundaries:

    # apps/api/.env
    CLERK_PUBLISHABLE_KEY=pk_test_...
    CLERK_SECRET_KEY=sk_test_...
    
    # apps/web/.env.development
    VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
    CLERK_SECRET_KEY=sk_test_...

    The publishable key is not a secret: the web app ships it to the browser through the VITE_ prefix. It is still required on the api, because clerkMiddleware() throws once per request without it rather than failing once at boot, which turns one missing value into a 500 on every route.

  5. Point the webhook at the api

    Add an endpoint under the application's webhooks section, targeting https://<your-api-host>/clerk/webhook, and subscribe it to user.created, user.updated and user.deleted. Copy the signing secret, which starts whsec_, into apps/api/.env:

    CLERK_WEBHOOK_SECRET=whsec_...

    You can skip this locally. requireUser creates the users row the first time it sees an account, so the app works without a tunnel; the webhook keeps that mirror fresh rather than bringing it into existence. That is why the variable sits in the optional tier, and why an unconfigured deployment answers 501 on that route instead of refusing to boot.

    user.created and user.updated keep the stored email and name current. A buyer signs in before paying, so their purchase already knows its owner, and these events link nothing.

What the app expects back

Set the sign-in and sign-up URLs. They are already in apps/web/.env.example:

VITE_CLERK_SIGN_IN_URL=/auth/login
VITE_CLERK_SIGN_UP_URL=/auth/signup

Leave them out and Clerk falls back to its hosted Account Portal, which sends a reader to a different domain in the middle of signing in. The sign-in and sign-up screens in this repo are its own components on Clerk's headless Signal API, and that redirect is the thing owning them was meant to avoid.

Do not set VITE_CLERK_AFTER_SIGN_IN_URL. The app does not read that name. The pair it reads is ..._FALLBACK_REDIRECT_URL, and FALLBACK rather than FORCE is what preserves the page a reader was trying to reach when the sign-in interrupted them.

Warning

setup-mfa is on by default in a new Clerk application, and Clerk treats a session with a pending task as signed out everywhere, including getAuth() on the api. The symptom is signed in but sees nothing, with no error and nothing in a log.

apps/web/src/routes/__root.tsx handles it by passing taskUrls={{ 'setup-mfa': '/auth/tasks/setup-mfa' }} to <ClerkProvider>. If you add a task Clerk supports, add its URL there too.

Making yourself an admin

Admin is an environment allowlist and nothing else. Find your own Clerk user id in the dashboard's user list, and put it in the api's environment:

ADMIN_CLERK_IDS=user_2abc...,user_2def...

Empty means this deployment has no admins, which is a valid state and the one a fresh clone is in. There is no table, no column and no UI behind it, so there is no way for an admin to grant admin.

Keys you now hold

Value Goes in As
Publishable key apps/api/.env CLERK_PUBLISHABLE_KEY
Publishable key apps/web/.env.development VITE_CLERK_PUBLISHABLE_KEY
Secret key apps/api/.env CLERK_SECRET_KEY
Secret key apps/web/.env.development CLERK_SECRET_KEY
Webhook signing secret apps/api/.env CLERK_WEBHOOK_SECRET
  • docs/adr/0001-clerk-for-identity-not-clerk-billing.md, why billing is not Clerk's
  • docs/adr/0021-no-passwords-email-code-and-oauth.md, why there is no password
  • docs/adr/0022-own-auth-ui-on-headless-clerk.md, why the auth screens are in this repo
  • Clerk CLI
  • Google social connection

Updated