Clerk production setup — actual walkthrough (hermes-wiki)
Companion to clerk_based_auth.md — that doc is the architecture
plan, this one is what actually happened setting up hermes-wiki in Clerk's dashboard, since
a few steps differed from what the plan assumed. Reuse this when setting up Courses and Localz.
1. Creating the production instance
Clerk's "Create production instance" dialog asks for an Application domain and whether it's the primary or secondary application for the root domain.
- Application domain = where your app itself lives (e.g.
wiki.fullstackfusions.com) — not the Clerk auth subdomain. Clerk auto-prependsclerk.to whatever you enter here, so typingclerk.wiki.fullstackfusions.comproduces a broken double-prefixed domain (clerk.clerk.wiki.fullstackfusions.com) and an error that "the clerk subdomain is reserved." - Primary vs secondary is about the root domain (
fullstackfusions.com), not the subdomain: Primary would host Clerk's API atclerk.fullstackfusions.com(the bare root). Since the root domain is the separate static portfolio site with no Clerk involvement, every subdomain project (wiki, courses, localz) should pick Secondary application — this hosts Clerk's API atclerk.<subdomain>.fullstackfusions.com, matching the architecture plan.
2. DNS records — it's 5, not 1
The plan doc says "a CNAME record" (singular) for the custom domain. In practice Clerk's production custom domain requires 5 CNAME records, in two groups:
| Purpose | Name | Points to |
|---|---|---|
| Frontend API (auth traffic) | clerk.<subdomain> |
frontend-api.clerk.services |
| Account portal (hosted sign-in/up pages) | accounts.<subdomain> |
accounts.clerk.services |
| Email sending | clkmail.<subdomain> |
mail.<instance-id>.clerk.services |
| Email DKIM 1 | clk._domainkey.<subdomain> |
dkim1.<instance-id>.clerk.services |
| Email DKIM 2 | clk2._domainkey.<subdomain> |
dkim2.<instance-id>.clerk.services |
The <instance-id> (e.g. c3gdgrxd28e9) is unique per Clerk app — don't reuse hermes-wiki's
values for Courses/Localz, get them fresh from that app's Configure → Developers → Domains
page (use the "Copy DNS instructions" button rather than typing by hand).
Cloudflare specifics:
- All 5 as CNAME, proxy status set to DNS only (grey cloud) — Clerk terminates its own TLS and does domain verification directly; Cloudflare's proxy in front breaks both.
- Enter just the left-hand label (e.g.
clk._domainkey.wiki) as the record name — Cloudflare auto-appends the zone (fullstackfusions.com), adding the full FQDN yourself doubles it up. - All 5 must verify before Clerk issues SSL certs for the Frontend API and Account portal. Verification + cert issuance took a few minutes end to end for hermes-wiki (Clerk's own estimate is "usually minutes, up to 24h").
3. Google + GitHub custom OAuth credentials
Production instances need your own OAuth app credentials per provider — Clerk's shared dev keys don't carry over. Same pattern for both providers: get a redirect URI from Clerk first, then create the OAuth app on the provider's side, then paste the resulting client ID/secret back into Clerk.
Google (console.cloud.google.com):
- OAuth consent screen: External user type, add
fullstackfusions.comas authorized domain, default scopes (email/profile/openid) are enough — must publish the app, otherwise it's capped at 100 test users and shows an "unverified app" warning to everyone signing in. - Credentials → OAuth client ID → Web application → paste Clerk's redirect URI as the authorized redirect URI.
GitHub (github.com/settings/developers → OAuth Apps → New OAuth App):
- Homepage URL:
https://<subdomain>.fullstackfusions.com - Authorization callback URL: Clerk's redirect URI from the GitHub SSO connection config.
4. Before going live
Dev and production instances have independent settings in Clerk — configuring the dev instance doesn't carry over. On the production instance:
- Turn off email/password sign-up and sign-in (on by default) under User & authentication.
- Restrictions → Allowlist is a paid-plan feature, not available on Hobby — despite what the
original architecture plan assumed. Use the app-level workaround instead, see
clerk_free_tier_access_control.md.
5. CLI / local dev setup gotchas (from setting up Localz)
Everything above is about the production dashboard flow. These are from wiring Clerk into
Localz's Next.js app locally with the Clerk CLI (clerk init) and Docker Compose.
CLI and browser can be signed into different Clerk accounts — and it fails silently wrong.
clerk init --app <id> and clerk apps list --json both scope to whichever account the CLI
is authenticated as (clerk auth login). If you create an app while your browser dashboard
session is on a different account, both commands just return a plain 404/empty list — not an
"access denied" or "wrong account" error. Run clerk whoami first and confirm it matches the
account shown in the dashboard's account switcher before troubleshooting an app-ID/linking
issue any further. (This cost real time on Localz: [email protected] was logged into
the CLI while the app lived under [email protected]. clerk auth logout +
clerk auth login as the right account fixed it immediately.)
Missing CLERK_SECRET_KEY fails loud, immediately, on every route — not just protected
ones. clerkMiddleware() performs a handshake/token-verification check on every request. If
CLERK_SECRET_KEY is a placeholder or invalid, this throws a bare "Internal Server Error" —
even on the public homepage, even in incognito with no session at all. Fast diagnostic for any
Clerk + Next.js app: if you see an unstyled "Internal Server Error" immediately after adding
Clerk, check the secret key is a real value before looking anywhere else.
clerk init is safe to re-run on an already-wired project. It diffs against what's already
there and skips files that already match Clerk's expected patterns (existing ClerkProvider,
existing proxy.ts/middleware.ts, existing sign-in/sign-up pages) rather than overwriting
them — it only added the missing sign-in/sign-up redirect env vars for Localz. No need to
hesitate about running it on a non-empty project.
Next.js + Docker: NEXT_PUBLIC_* vars need to be build args, not just runtime env. Not
Clerk-specific, but bites you the moment NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is involved:
Next.js inlines NEXT_PUBLIC_* vars into the client bundle at next build time. Docker
Compose's environment: only sets container env at runtime, after the image is already
built — too late. They need to be passed as build.args in docker-compose.yml and declared
with ARG in the Dockerfile's builder stage, e.g.:
frontend:
build:
context: ./frontend
args:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
Only the secret key (CLERK_SECRET_KEY, server-only, never inlined into the bundle) stays as a
regular runtime environment:/env_file: value.