PROTOCOL FOUR// field manual
FieldField Manual
SystemsSystems
AssetsAssets
Systems/Authentication
01 / 06

Authentication

Clerk + Convex, one identity

ClerkJWTConvex Auth

Overview

We picked Clerk because we didn't want to own a password reset story or maintain three OAuth integrations ourselves. Convex receives Clerk's JWT via ConvexProviderWithClerk, validates it server-side, and exposes the verified identity to every query, mutation, and action.

How it works

1

The user signs in via Clerk's SDK. Clerk returns a session and issues a JWT signed with its public key.

2

`ConvexProviderWithClerk` wraps the React tree at `app/_layout.tsx`. It listens to Clerk's session and forwards the current JWT to the Convex client on every change.

3

Convex validates the JWT against `auth.config.ts` — which lists Clerk's domain — on every request. Invalid or expired tokens get rejected before any function runs.

4

Inside any query / mutation, `ctx.auth.getUserIdentity()` returns the verified Clerk identity (sub = clerkId, email, name, etc.) or null.

5

We map Clerk identities to internal `users` table rows via the `by_clerk_id` index. The first time a Clerk user signs in, `users.upsertCurrent` creates the row and stamps an `agentCode`. Subsequent signins just look it up.

6

Session persistence on device uses Clerk's `tokenCache` adapter with `expo-secure-store`, so the agent stays signed in across cold starts without exposing the token to JS-accessible storage.

Key decisions

Clerk over rolling our own

Rolling auth means owning password reset, session refresh, OAuth callbacks, MFA, account merging, and rate-limiting. Clerk's free tier covers everything we need for an indie launch, and the SDK was the only one that played cleanly with expo-router's Stack.Protected guard.

Verified identity, not trust-the-client user IDs

Every Convex function calls `ctx.auth.getUserIdentity()`. The clerkId comes from a server-validated JWT, not from a request body. Even if the client lies about who they are, the server reads the signed identity.

Agent code generated on first upsert

Every user gets a stable 8-character `agentCode` like '0086BBUL'. It's generated in `users.upsertCurrent` on the first auth, indexed for search, and never changes. The code is the public-facing handle — usable on receipts, business cards, anywhere.

NextDatabase