ReadyNative

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

Pro

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
  1. Create an application at clerk.com, then open [Dashboard] → [API keys]. Copy the publishable key (pk_test_…) into .env as EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY. Without it ClerkProvider is never mounted: useSession() stays unauthenticated, there's no redirect, and sign-in shows "Configure Clerk".
  2. 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.
  3. Open [SSO connections] → [Apple] and enable it. Add your iOS bundle id under Clerk's "Apple → Native" section so the native sheet is accepted.
  4. Turn on the Sign in with Apple capability for that bundle id in the [Apple Developer] portal → [Certificates, Identifiers & Profiles] → [Identifiers] → your app id.
  5. Open [SSO connections] → [Google] and enable it. Development instances work with Clerk's shared credentials, so there's nothing to paste yet.
  6. 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.
  7. Screens call useT() with English keys. If you selected an i18n module and haven't added ru entries, 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.

  1. If you use API routes that need the caller (payments/stripe), copy the Secret key from [Dashboard] → [API keys] into .env as CLERK_SECRET_KEY, under the # server (never EXPO_PUBLIC) heading - never with an EXPO_PUBLIC_ prefix. Alternatively set CLERK_JWT_KEY to the JWT public key (PEM) from the same page to verify tokens without a network call. With neither set, getRequestUser throws. For EAS Hosting add it with eas env:set --environment production --visibility sensitive (secret variables don't deploy to EAS Hosting).
  2. Run bun run doctor - every row for this module should be green.

The keys, in one place:

KeyWhere
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEYDashboard → 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):

  1. Put the key in .env, run npx expo start (or a dev build), and open a fresh install: onboarding → /(auth)/sign-in.
  2. Sign up with a new email → code step → enter the mailed code → you land on / and Settings shows the Account card with the email.
  3. Settings → Sign out → back on sign-in.
  4. Sign in with a wrong password → error toast with Clerk's message, no navigation.
  5. On iOS, tap Continue with Apple → native sheet → signed in (the first time creates the user, the second signs in).
  6. Tap Continue with Google → system browser → back in the app signed in.
  7. Tap Forgot password → code step → code plus a new password → signed in with the new password.
  8. Kill and reopen the app → still signed in, token read from secure store.
  9. 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-modules

In a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:

  1. 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.
  2. 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 from modules/auth/none/files/ of a fresh clone of your tier repo - same exports, nothing behind them.
  3. Uninstall the dependencies: bun remove @clerk/backend @clerk/expo expo-apple-authentication expo-auth-session expo-crypto expo-secure-store.
  4. Drop the config plugins @clerk/expo, expo-apple-authentication, expo-secure-store from .readynative.json → modules.app.expo.plugins (that is where app.config.ts reads them from), then rebuild the dev build.
  5. Unwrap the provider: delete <AuthProvider> and its import from src/providers.tsx.
  6. Remove the env keys EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, CLERK_JWT_KEY from .env, .env.example and your EAS environment, and EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY from src/lib/env.ts.
  7. Update the privacy declarations: remove this module's entries from .readynative.json → modules.app.expo.ios.privacyManifests, then re-run bun run gen:privacy and revise your store privacy answers.
  8. Check it: bun run typecheck and bun run lint point at anything that still imports the removed files; bun run gen:graph refreshes docs/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 clerk

Module id: auth/clerk.

Dependencies

PackageVersionKind
@clerk/backend^3.19.0dependency
@clerk/expo^4.6.8dependency
expo-apple-authentication~57.0.2dependency (expo install)
expo-auth-session~57.0.12dependency (expo install)
expo-crypto~57.0.3dependency (expo install)
expo-secure-store~57.0.4dependency (expo install)

Config plugins

Merged into app.config.ts through .readynative.json (modules.app):

  • @clerk/expo
  • expo-apple-authentication
  • expo-secure-store

Environment keys

KeyRequiredServer-onlyExampleDocs
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEYyesnopk_test_...dashboard
CLERK_SECRET_KEYnoyessk_test_...dashboard
CLERK_JWT_KEYnoyes-----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):

TypeLinked to userTrackingPurposes
EmailAddressyesnoAppFunctionality
PhoneNumberyesnoAppFunctionality
NameyesnoAppFunctionality
UserIDyesnoAppFunctionality
DeviceIDyesnoAppFunctionality

Providers

Rendered in src/providers.tsx (lower order = outermost):

OrderProviderFrom
30AuthProvider@/lib/auth

Compatibility

Doctor checks

  • env: EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY

After setup

  1. Clerk: create an application, copy the Publishable key (API keys) into .env as EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY.
  2. 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).
  3. Clerk: User & Authentication → Email → enable Email address + Password + Email verification code.
  4. Clerk: User & Authentication → Settings → enable "Allow users to delete their accounts" (Settings → Delete account calls user.delete()).
  5. Clerk: SSO connections → Apple: add the iOS bundle id (native Sign in with Apple); Google: add your own OAuth client for production.
  6. Clerk: Native applications → add the iOS bundle id / Android package so native flows are allowed.
  7. Apple: enable the Sign in with Apple capability for the bundle id (EAS does it on the first build).
  8. 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.tsx
  • src/app/(auth)/reset.tsx
  • src/app/(auth)/sign-in.tsx
  • src/app/(auth)/sign-up.tsx
  • src/app/examples/auth.tsx
  • src/hooks/use-auth-redirect.ts
  • src/lib/__tests__/auth-clerk.test.tsx
  • src/lib/auth.ts
  • 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
  • src/server/session.ts

On this page

Get ReadyNative