Clerk + Convex Auth
This is the most important setup page in the starter.
If Clerk and Convex do not agree on the convex JWT template and issuer, the app can sign users in while protected server-side reads still fail.
Required env values
Make sure apps/web/.env.local has these values:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/start
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/start
CLERK_SECRET_KEY=
CLERK_FRONTEND_API_URL=
CLERK_AUTHORIZED_PARTIES=http://localhost:3000
NEXT_PUBLIC_CONVEX_URL=NEXT_PUBLIC_CONVEX_URL comes from the Convex tooling once you run bun run dev:convex. That command also writes CONVEX_DEPLOYMENT into apps/web/.env.local so the repo scripts and convex run commands target your hosted dev deployment explicitly.
Clerk setup
- Create a Clerk app.
- Copy
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,CLERK_SECRET_KEY, andCLERK_FRONTEND_API_URL. - If you keep the starter’s custom login page, set
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-inso Clerk OAuth callbacks resolve to the right route. - Set the Clerk fallback redirect URLs to
/startso direct visits to auth routes land in the packet-first protected shell. - Keep the Clerk frontend API domain consistent everywhere you wire auth.
- Set
CLERK_AUTHORIZED_PARTIESto the trusted web origins that should be allowed to present Clerk session cookies.
The starter-owned auth routes are:
/sign-in: the branded Ship by Sunday sign-in surface/session-tasks/reset-password: the Clerk reset-password task inside the same identity shell/sso-callback: the OAuth callback completion route inside the same identity shell
/sign-up is not a custom starter sign-up UI in this repo. It redirects to pricing so new-account creation still starts from checkout.
The convex template is not optional
Inside Clerk:
- Open your app.
- Go to
JWT Templates. - Create or confirm a template named
convex. - Use
convexas the application ID. - Make sure the issuer/domain matches your Clerk frontend API URL.
This starter is hard-wired to that identifier.
Convex setup
Connect the repo to your hosted Convex dev deployment with:
bun run dev:convexThat flow should give you the NEXT_PUBLIC_CONVEX_URL value the app expects and set CONVEX_DEPLOYMENT for CLI-driven QA helpers. This repo does not support Convex’s local backend mode as a first-class workflow, so keep the Clerk app, frontend API domain, and token-template assumptions aligned with your hosted Convex dev deployment.
Run the auth doctor any time you switch Clerk instances or Convex dev deployments:
bun run auth:dev:doctorLocal bun run dev startup now runs that same check automatically before next dev starts.
What success looks like
You are correctly wired when:
- sign-in works
- sign-in from a deep link returns the user to that exact page after auth or after any Clerk task
/sign-in,/session-tasks/reset-password, and/sso-callbackall render cleanly in local smoke runs- protected routes can read server-side state
- protected pages do not throw
Unauthorizedduring the first client hydration pass - the workspace gate can tell the difference between signed-out and signed-in users
- product-access checks do not fail with a token or issuer mismatch
Why the starter now seeds initial data on the server
Protected routes and /checkout/success now render from a server-fetched Convex snapshot first, then let Clerk-gated live queries take over in the browser.
That is intentional. It protects the first render from a short Clerk/Convex auth sync gap during hydration, where the browser can briefly lack a Convex identity even though the server already validated the session.
If this breaks
Check these in order:
- the Clerk JWT template is named exactly
convex - the issuer/domain in Clerk matches the same Clerk frontend API domain used by the app
NEXT_PUBLIC_CLERK_SIGN_IN_URLis still/sign-inCLERK_AUTHORIZED_PARTIESstill includes the current app originNEXT_PUBLIC_CONVEX_URLandCONVEX_DEPLOYMENTare not still pointed at an old or different Convex dev deployment- you restarted the app after changing auth env values
For local browser smoke, remember that the Playwright local-server flow intentionally sets DISABLE_CLERK_MIDDLEWARE=1. In that mode, the sign-in route must respect shouldBypassClerkProtection() before calling auth() during server render. If smoke fails before any auth-route assertions run, check the sign-in route entrypoint and the Clerk bypass helper before treating it as a Clerk misconfiguration.
If the app is using the right local Clerk keys but hosted Convex still trusts the wrong issuer, push the current local frontend API URL into the active hosted dev deployment and rerun the doctor:
cd apps/web
./node_modules/.bin/convex env set CLERK_FRONTEND_API_URL "$CLERK_FRONTEND_API_URL"
bun run auth:dev:doctorIf checkout succeeds but access still fails, go straight to Troubleshooting Stripe and Convex.