Clerk
Set up Clerk for auth in an Expo app with ReadyNative: email+code verification, Apple (native), Google (SSO) · @clerk/expo · Expo Go OK for JS flows
Expo Go: yes
Runs in Expo Go; no dev build needed for this module.
This wires Clerk into the app: email/password with a 6-digit code step, Sign in with Apple, Google SSO, and a hosted user database you never have to run. You get src/app/(auth)/{sign-in,sign-up,reset} with src/screens/auth/*, the redirect hook src/hooks/use-auth-redirect.ts (signed-out users land on /(auth)/sign-in, signed-in users leave (auth), onboarding wins first), and, with --with-examples, an example at src/app/examples/auth.tsx. @/lib/auth keeps the same API whichever option you pick - pick Clerk when you want user management, organizations and a polished dashboard without owning a database.
Setup
bun run setup --auth clerk- Create an application at clerk.com, then open [Dashboard] → [API keys]. Copy the publishable key (
pk_test_…) into.envasEXPO_PUBLIC_CLERK_PUBLISHABLE_KEY. Without itClerkProvideris never mounted:useSession()staysunauthenticated, there's no redirect, and sign-in shows "Configure Clerk". - In Clerk, open [User & Authentication] → [Email, phone, username] and enable Email address and Password, with verification set to Email verification code - the shipped sign-up and reset screens expect the code step.
- Open [SSO connections] → [Apple] and enable it. Add your iOS bundle id under Clerk's "Apple → Native" section so the native sheet is accepted.
- Turn on the Sign in with Apple capability for that bundle id in the [Apple Developer] portal → [Certificates, Identifiers & Profiles] → [Identifiers] → your app id.
- Open [SSO connections] → [Google] and enable it. Development instances work with Clerk's shared credentials, so there's nothing to paste yet.
- Open [Native applications] and add the iOS bundle id and the Android package from
readynative.config.ts, so Clerk accepts native requests from your app. - Screens call
useT()with English keys. If you selected an i18n module and haven't addedruentries, they render in English - keys fall back to themselves.
Going to production?
Swap the key for the production instance's pk_live_…, and give Google your own OAuth client: create one in [Google Cloud Console] → [APIs & Services] → [Credentials] and paste the client id and secret into Clerk's Google connection. Clerk's shared credentials are development-only.
- If you use API routes that need the caller (payments/stripe), copy the Secret key from [Dashboard] → [API keys] into
.envasCLERK_SECRET_KEY, under the# server (never EXPO_PUBLIC)heading - never with anEXPO_PUBLIC_prefix. Alternatively setCLERK_JWT_KEYto the JWT public key (PEM) from the same page to verify tokens without a network call. With neither set,getRequestUserthrows. For EAS Hosting add it witheas env:set --environment production --visibility sensitive(secretvariables don't deploy to EAS Hosting). - Run
bun run doctor- every row for this module should be green.
The keys, in one place:
| Key | Where |
|---|---|
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY | Dashboard → API keys (pk_test_…) |
CLERK_SECRET_KEY (server) | Dashboard → API keys (sk_test_…); needed by API routes that verify the caller |
CLERK_JWT_KEY (server, optional) | Dashboard → API keys → JWT public key (PEM); networkless verification instead of the secret key |
CLERK_WEBHOOK_SIGNING_SECRET (server, optional) | Dashboard → Webhooks → your /api/webhooks/clerk endpoint → Signing secret (whsec_…); with backend/api-routes, for user.deleted |
Deps: @clerk/expo ^4.6.6 (the maintained package; @clerk/clerk-expo is deprecated), expo-secure-store, expo-auth-session, expo-apple-authentication, expo-crypto (expo-web-browser is a core dep). Config plugins: @clerk/expo, expo-apple-authentication, expo-secure-store.
Usage
Read the session anywhere:
import { auth } from "@/lib/auth";
const session = auth.useSession();
if (session.status === "authenticated") console.log(session.user.email);Sign up in two steps, because Clerk mails a code:
import { signUpWithEmail, verifySignUpCode } from "@/lib/auth";
const result = await signUpWithEmail(email, password);
if (result.status === "needs_verification") await verifySignUpCode(code);Social sign-in and sign-out:
import { auth, signInWithApple, canSignInWithApple, signInWithGoogle } from "@/lib/auth";
if (canSignInWithApple) await signInWithApple();
await signInWithGoogle();
await auth.signOut();auth.openSignIn() opens /(auth)/sign-in from anywhere; Settings → Developer tools uses it. A signed-in user is sent straight back, so sign out first.
Server routes
API routes never trust a user id from the client. The app attaches credentials with auth.getAuthHeaders() (authorization: Bearer <session token>; {} while signed out), and the route resolves the caller with serverAuth.getRequestUser(request) from src/server/session.ts (verifyToken from @clerk/backend, keyed by CLERK_SECRET_KEY, or networkless by CLERK_JWT_KEY). It returns null for a missing or invalid credential, and throws when neither CLERK_SECRET_KEY nor CLERK_JWT_KEY is set - answer 503 then.
import { auth } from "@/lib/auth";
const res = await fetch(url, { headers: await auth.getAuthHeaders() });import { serverAuth } from "@/server/session";
const user = await serverAuth.getRequestUser(request);
if (!user) return new Response("Unauthorized", { status: 401 });Delete account
Settings ships a confirm-guarded "Delete account" row (App Store Review 5.1.1(v), Play "Account deletion") that calls auth.deleteAccount() → clerk.user.delete(). Turn on User & Authentication → Settings → Allow users to delete their accounts in the Clerk dashboard first - with it off (user.deleteSelfEnabled is false) deleteAccount() throws a clear error saying so. Clerk ends the session itself once the user is gone; useAuthRedirect sends them to sign-in. Data you keep outside Clerk needs a user.deleted webhook: with backend/api-routes, POST /api/webhooks/clerk handles user.deleted and erases that user in the configured vendors. In the Clerk dashboard → [Webhooks], add the endpoint https://<your-api>/api/webhooks/clerk subscribed to user.deleted, and put its signing secret (whsec_…) in .env as CLERK_WEBHOOK_SIGNING_SECRET (server, never EXPO_PUBLIC_).
Gotchas
- Clerk's iOS SDK requires iOS 17; the config plugin raises the deployment target for dev builds.
- Sign-in steps that need a second factor (MFA) or extra fields surface as an error toast. If you enable those in the dashboard, add screens for them.
- Expo Go is fine for the flows shipped here: Clerk's native module is optional (
requireOptionalNativeModule) and only backs Clerk's native views and biometrics, which this module doesn't use.
Check it works (the Examples steps need a tree set up with --with-examples):
- Put the key in
.env, runnpx expo start(or a dev build), and open a fresh install: onboarding →/(auth)/sign-in. - Sign up with a new email → code step → enter the mailed code → you land on
/and Settings shows the Account card with the email. - Settings → Sign out → back on sign-in.
- Sign in with a wrong password → error toast with Clerk's message, no navigation.
- On iOS, tap Continue with Apple → native sheet → signed in (the first time creates the user, the second signs in).
- Tap Continue with Google → system browser → back in the app signed in.
- Tap Forgot password → code step → code plus a new password → signed in with the new password.
- Kill and reopen the app → still signed in, token read from secure store.
- Examples → Auth session → the JSON shows
status: "authenticated".
Remove it
While modules/ exists (a tree set up with --keep-modules), setup does all of it:
bun run setup --auth none --yes --keep-modulesIn a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:
- Delete the files that are still there (the demo screens are gone already unless you set up with
--with-examples):src/app/(auth)/_layout.tsx,src/app/(auth)/reset.tsx,src/app/(auth)/sign-in.tsx,src/app/(auth)/sign-up.tsx,src/app/examples/auth.tsx,src/lib/__tests__/auth-clerk.test.tsx,src/screens/auth/auth-form.ts,src/screens/auth/reset-screen.tsx,src/screens/auth/sign-in-screen.tsx,src/screens/auth/sign-up-screen.tsx,src/screens/examples/auth-example-screen.tsx,src/server/__tests__/session-clerk.test.ts. - Replace, don't delete
src/hooks/use-auth-redirect.ts,src/lib/auth.ts,src/server/session.ts: core code imports them, so swap in the no-op version frommodules/auth/none/files/of a fresh clone of your tier repo - same exports, nothing behind them. - Uninstall the dependencies:
bun remove @clerk/backend @clerk/expo expo-apple-authentication expo-auth-session expo-crypto expo-secure-store. - Drop the config plugins
@clerk/expo,expo-apple-authentication,expo-secure-storefrom.readynative.json→modules.app.expo.plugins(that is whereapp.config.tsreads them from), then rebuild the dev build. - Unwrap the provider: delete
<AuthProvider>and its import fromsrc/providers.tsx. - Remove the env keys
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY,CLERK_SECRET_KEY,CLERK_JWT_KEYfrom.env,.env.exampleand your EAS environment, andEXPO_PUBLIC_CLERK_PUBLISHABLE_KEYfromsrc/lib/env.ts. - Update the privacy declarations: remove this module's entries from
.readynative.json→modules.app.expo.ios.privacyManifests, then re-runbun run gen:privacyand revise your store privacy answers. - Check it:
bun run typecheckandbun run lintpoint at anything that still imports the removed files;bun run gen:graphrefreshesdocs/ARCHITECTURE.md.
Reference
Everything below is generated from modules/auth/clerk/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --auth clerkModule id: auth/clerk.
Dependencies
| Package | Version | Kind |
|---|---|---|
@clerk/backend | ^3.19.0 | dependency |
@clerk/expo | ^4.6.8 | dependency |
expo-apple-authentication | ~57.0.2 | dependency (expo install) |
expo-auth-session | ~57.0.12 | dependency (expo install) |
expo-crypto | ~57.0.3 | dependency (expo install) |
expo-secure-store | ~57.0.4 | dependency (expo install) |
Config plugins
Merged into app.config.ts through .readynative.json (modules.app):
@clerk/expoexpo-apple-authenticationexpo-secure-store
Environment keys
| Key | Required | Server-only | Example | Docs |
|---|---|---|---|---|
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY | yes | no | pk_test_... | dashboard |
CLERK_SECRET_KEY | no | yes | sk_test_... | dashboard |
CLERK_JWT_KEY | no | yes | -----BEGIN PUBLIC KEY-----... | dashboard |
Keys go in .env (see .env.example). Required keys are checked by bun run doctor; Server-only keys have no EXPO_PUBLIC_ prefix, are read only by API routes and never reach the bundle.
Privacy
Play Data safety draft: collects Email address, phone, name (account), Session tokens, Device identifiers; shared with Clerk (processor). Source of truth: vendor disclosure.
Apple privacy manifest data types (composed into ios.privacyManifests by setup):
| Type | Linked to user | Tracking | Purposes |
|---|---|---|---|
EmailAddress | yes | no | AppFunctionality |
PhoneNumber | yes | no | AppFunctionality |
Name | yes | no | AppFunctionality |
UserID | yes | no | AppFunctionality |
DeviceID | yes | no | AppFunctionality |
Providers
Rendered in src/providers.tsx (lower order = outermost):
| Order | Provider | From |
|---|---|---|
| 30 | AuthProvider | @/lib/auth |
Compatibility
- Requires
storage=kv-store,mmkv,async-storage
Doctor checks
- env:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
After setup
- Clerk: create an application, copy the Publishable key (API keys) into .env as EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY.
- Clerk: API routes that need the caller (payments/stripe) verify the session token with @clerk/backend - copy the Secret key into .env as CLERK_SECRET_KEY (server-only; for EAS Hosting also
eas env:set --environment production --visibility sensitive). - Clerk: User & Authentication → Email → enable Email address + Password + Email verification code.
- Clerk: User & Authentication → Settings → enable "Allow users to delete their accounts" (Settings → Delete account calls user.delete()).
- Clerk: SSO connections → Apple: add the iOS bundle id (native Sign in with Apple); Google: add your own OAuth client for production.
- Clerk: Native applications → add the iOS bundle id / Android package so native flows are allowed.
- Apple: enable the Sign in with Apple capability for the bundle id (EAS does it on the first build).
- Dev builds: the @clerk/expo plugin raises the iOS deployment target to 17.0.
Files
15 files copied to the project root
src/app/(auth)/_layout.tsxsrc/app/(auth)/reset.tsxsrc/app/(auth)/sign-in.tsxsrc/app/(auth)/sign-up.tsxsrc/app/examples/auth.tsxsrc/hooks/use-auth-redirect.tssrc/lib/__tests__/auth-clerk.test.tsxsrc/lib/auth.tssrc/screens/auth/auth-form.tssrc/screens/auth/reset-screen.tsxsrc/screens/auth/sign-in-screen.tsxsrc/screens/auth/sign-up-screen.tsxsrc/screens/examples/auth-example-screen.tsxsrc/server/__tests__/session-clerk.test.tssrc/server/session.ts
Supabase Auth
Set up Supabase Auth for auth in an Expo app with ReadyNative: email+password, Apple (native), Google (OAuth) · session on the storage adapter · Expo Go OK
Better Auth
Set up Better Auth for auth in an Expo app with ReadyNative: self-hosted on the API routes · email/password + Apple/Google · Expo Go OK