rn-backend — provider-agnostic backend integration for RN/Expo
For the current Expo API and per-version details, verify against the Expo docs / MCP
mcp.expo.dev/expo/skills(see rn-fundamentals → Source of truth).
The 5 rules (non-negotiable, regardless of provider)
- Tokens in
expo-secure-store, neverAsyncStorageor plain in-memory. Tokens are bearer credentials — they go in Keychain (iOS) / Keystore (Android) encrypted at rest. - Auth state in a Zustand store + replicated in TanStack Query with key
["auth", "session"]. Zustand for synchronous reads in components and_layout.tsxredirect logic; TanStack Query for refetch-on-focus and revalidation. api()wrapper owns auth headers + refresh-on-401. Every fetch goes through it. Refresh flow is centralized — no per-call retry logic.- Auth gate via Expo Router groups:
(auth)/for public screens,(app)/for protected; the(app)/_layout.tsxchecks the store and<Redirect />if no user. - Sign-out clears EVERYTHING: secure-store token, Zustand store, TanStack Query cache (
queryClient.clear()). No "user signed out but still see old data" bugs.
Quick decision tree
- "Which provider? Supabase / Firebase / custom REST / tRPC?" →
references/decision-tree.md - "What's the shared client-auth architecture?" →
references/concepts.md - "How do I wire it up — store, middleware, gate?" →
references/patterns.md - "I picked Supabase. Specifics?" →
references/supabase.md - "I picked Firebase." →
references/firebase.md - "I'm bringing my own REST/JSON backend." →
references/custom-rest.md - "I want tRPC end-to-end typed." →
references/trpc.md
Common anti-patterns (NEVER do)
- ❌
AsyncStorage.setItem("token", token)— useSecureStore.setItemAsync("token", token). - ❌ Reading the token inside every
api()call from secure-store synchronously — cache it in the Zustand store, refresh from secure-store only on app start. - ❌ Stored "logged in" boolean in the client that drifts from the server token's actual validity — single source of truth is the token + a
/mequery. - ❌ Calling
signOut()and forgetting toqueryClient.clear()— next user sees previous user's posts in cache. - ❌ Hardcoding
Authorization: Bearer xxxin a single screen — wrap inapi(). - ❌ Vendor lock-in code scattered (
supabase.auth.signIn(...)in 12 components) — wrap inlib/auth.tsso swapping providers touches one file.
Sources
- Course: codewithbeto.dev/rnCourse — "Backend Basics" + "Supabase" modules (paid).
- Official Supabase: https://supabase.com/docs/reference/javascript/auth-signinwithpassword
- Official Firebase: https://firebase.google.com/docs/auth/web/start
- Official tRPC: https://trpc.io/docs/client/react/server-components
- Official secure-store: https://docs.expo.dev/versions/latest/sdk/securestore/