Expo + Convex + Auth

PreviousNext

Scaffold an Expo app with BNA UI, a Convex backend and authentication — Google, Apple, password and email OTP sign-in, pre-wired.

Everything from the Convex starter, plus @convex-dev/auth with sign-in screens already built. This is what npx bna-ui convex gives you by default.

Create the app

pnpm dlx bna-ui convex my-app

The CLI scaffolds the project, installs dependencies, then runs four steps for you. The first two are interactive — follow the prompts:

npx convex dev --once

A browser window opens so you can log in or sign up, then pick or create a Convex project. Your deployment URL is written to .env.local as EXPO_PUBLIC_CONVEX_URL.

npx @convex-dev/auth

Generates the signing keys your deployment needs to issue JWTs and sets them as deployment environment variables.

npx convex env set EXPO_URL my-app://

Your app's deep-link scheme, so OAuth can redirect back into the app after sign-in.

npx convex env set SITE_URL http://localhost:3000/

The web redirect target. See production — this one needs changing before you ship.

Pass --skip-convex to skip all four and run them yourself later.

Run it

Two processes, as with any Convex app. In one terminal:

pnpm dlx convex dev

In another:

pnpm dlx expo start

You land on the sign-in screen. Log in anonymously works immediately — everything else needs credentials, below.

What you get

On top of the Expo starter:

app/_layout.tsx           ConvexAuthProvider (SecureStore on iOS/Android)
                          + AuthLoading / Unauthenticated / Authenticated
components/auth/
├── auth.tsx              The sign-in screen: Password / OAuth / OTP tabs
├── password.tsx          Sign in, sign up, forgot and reset password
├── email-otp.tsx         Passwordless email codes
├── google.tsx            Google sign-in button
├── apple.tsx             Apple sign-in button
└── singout.tsx           Sign-out button (used in the settings tab)
convex/
├── auth.ts               Provider configuration and the redirect allow-list
├── auth.config.ts        JWT issuer
├── schema.ts             authTables + a users table
├── users.ts              get, getAll, update, …
├── http.ts               Mounts the auth HTTP routes
├── resendOTP.ts          Email OTP delivery via Resend
├── passwordReset.ts      Password-reset codes via Resend
└── resendPasswordOTP.ts

The root layout swaps on auth state: a spinner while the session resolves, the sign-in screen when signed out, your tabs when signed in. Tokens are stored in the platform keychain via expo-secure-store.

Configure the providers

Every provider except anonymous needs credentials set on your Convex deployment. Set them with npx convex env set — they live on the deployment, not in .env.local, because they are read by server-side functions.

Email — required for OTP and password reset

Both the email OTP tab and the forgot-password flow send mail through Resend. Without this key they fail silently:

pnpm dlx convex env set AUTH_RESEND_KEY re_your_key_here

Google

pnpm dlx convex env set AUTH_GOOGLE_ID your_google_client_id
npx convex env set AUTH_GOOGLE_SECRET your_google_client_secret

Full walkthrough: Google OAuth setup.

Apple

pnpm dlx convex env set AUTH_APPLE_ID your_apple_service_id
npx convex env set AUTH_APPLE_SECRET your_generated_jwt

Full walkthrough: Apple Sign-In setup.

GitHub

convex/auth.ts also configures GitHub, but the sign-in screen ships no button for it. Add AUTH_GITHUB_ID / AUTH_GITHUB_SECRET and a button modelled on components/auth/google.tsx if you want it.

Environment reference

VariableWhere it livesSet byUsed for
EXPO_PUBLIC_CONVEX_URL.env.localnpx convex devBuilding the client in app/_layout.tsx
EXPO_URLConvex deploymentthe CLIAllow-listing your app scheme for OAuth redirects
SITE_URLConvex deploymentthe CLIAllow-listing the web redirect target
AUTH_RESEND_KEYConvex deploymentyouEmail OTP and password reset
AUTH_GOOGLE_ID/SECRETConvex deploymentyouGoogle sign-in
AUTH_APPLE_ID/SECRETConvex deploymentyouApple sign-in
CONVEX_SITE_URLConvex deploymentConvex, automaticallyThe JWT issuer in auth.config.ts

Check what is set at any time:

pnpm dlx convex env list

Password rules

convex/auth.ts enforces a minimum of 8 characters with at least one digit, one lowercase and one uppercase letter. Edit validatePasswordRequirements to change that — it throws, and the message surfaces in the sign-up form.

Production

Two things bite here, both in convex/auth.ts's redirect callback, which rejects any target that is not an exp:// dev URL, EXPO_URL, or SITE_URL:

Point SITE_URL at your real site

The CLI sets it to http://localhost:3000/, which is fine for development and wrong everywhere else.

pnpm dlx convex env set SITE_URL https://your-site.com --prod

Make sure EXPO_URL matches your production scheme

It is set from your project name. If you change scheme in app.json, change this to match, or OAuth redirects will be rejected.

pnpm dlx convex env set EXPO_URL your-scheme:// --prod

Then set your provider credentials against the production deployment too, and deploy:

pnpm dlx convex env set AUTH_RESEND_KEY re_your_prod_key --prod
npx convex deploy

Before you ship

Confirm SITE_URL and EXPO_URL are set against --prod

Not just dev. Both gate the redirect callback above, and the CLI's defaults (http://localhost:3000/, your dev scheme) are wrong in production.

Set every provider's credentials against --prod too

AUTH_RESEND_KEY, AUTH_GOOGLE_ID/SECRET, AUTH_APPLE_ID/SECRET — each one, again, with --prod.

Re-read every query and mutation as an attacker

There is no RLS backstop here. convex/users.ts is your entire access-control surface — confirm every function that returns or changes user data checks getAuthUserId(ctx) against the right owner.

Confirm the production EAS profile points at your prod deployment

EXPO_PUBLIC_CONVEX_URL in the production build profile has to be the prod deployment's URL, not the dev one you have been testing against.

Full checklist, including EAS and CI: deployment.

Next