DEV Community

Cover image for Stop Reinventing React Native Auth — Buy a Template
Russel Dsouza
Russel Dsouza

Posted on

Stop Reinventing React Native Auth — Buy a Template

  • Building React Native auth from scratch routinely takes 60–120 hours and ships subtle bugs (the Apple Sign In fullName trap is the classic).
  • The real surface area is seven traps: AsyncStorage encryption, refresh-token mutex, three OAuth deep-link code paths, fullName persistence, mandatory account deletion, magic-link rate limiting, token hashing at rest.
  • A vetted template covers all seven for $99–$499. Break-even is ~3 hours of saved work.
  • Build it yourself only if you have enterprise IdP (SAML/Okta), HIPAA/FIDO2 constraints, or your company is the auth product.

Every React Native auth implementation starts with "it's just signInWithPassword" and ends 75 hours later debugging refresh-token rotation. This post is the field guide I wish I had before I built mobile auth from scratch three times.

The seven traps

If you build auth from scratch in React Native, here's what bites you in production. None of these are in the happy-path tutorial.

1. AsyncStorage is not encrypted

// 🚫 wrong
import AsyncStorage from "@react-native-async-storage/async-storage";
await AsyncStorage.setItem("access_token", token);

// ✅ right
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("access_token", token);
Enter fullscreen mode Exit fullscreen mode

On iOS, SecureStore uses Keychain. On Android, EncryptedSharedPreferences. AsyncStorage is a plaintext SQLite blob.

2. Refresh-token rotation needs a mutex

Supabase rotates refresh tokens on every use. Two concurrent API calls at the moment of expiry will both try to refresh, and one of them invalidates the other's freshly-minted token. You need a single-flight mutex around refresh:

let refreshPromise: Promise<Session> | null = null;

async function getValidSession() {
  if (refreshPromise) return refreshPromise;
  refreshPromise = supabase.auth.refreshSession()
    .finally(() => { refreshPromise = null; });
  return refreshPromise;
}
Enter fullscreen mode Exit fullscreen mode

3. OAuth deep links are three code paths

  • iOS native build: ASWebAuthenticationSession via expo-auth-session
  • Android native build: Chrome Custom Tabs via expo-auth-session
  • Expo Go: a polyfill that opens a system browser

Each one returns control through a different URL scheme. Universal Links on iOS, intent filters on Android, and an exp:// URL in Expo Go. Get any wrong and the user lands on a white screen.

4. Apple Sign In fullName only returns on first sign-in

const credential = await AppleAuthentication.signInAsync({
  requestedScopes: [
    AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
    AppleAuthentication.AppleAuthenticationScope.EMAIL,
  ],
});
// credential.fullName is null on EVERY subsequent sign-in.
// You MUST persist it the first time, server-side.
Enter fullscreen mode Exit fullscreen mode

This is the bug I've shipped on three separate projects.

5. Account deletion is mandatory

App Store guideline 5.1.1(v): you must offer in-app account deletion. That means a confirmation screen, a server endpoint that revokes all sessions, and a way to handle a GDPR data-export request. Skip it, fail review.

6. Rate limit your magic-link endpoint

Without per-email and per-IP throttling, anyone can blow up your transactional email bill. The minimum pattern:

const RATE_LIMIT_WINDOW_MS = 60_000;
const RATE_LIMIT_MAX = 3;

const recent = await supabaseServer
  .from("magic_link_tokens")
  .select("created_at", { count: "exact" })
  .eq("email", email)
  .gte("created_at", new Date(Date.now() - RATE_LIMIT_WINDOW_MS).toISOString());

if ((recent.count ?? 0) >= RATE_LIMIT_MAX) {
  return Response.json({ error: "rate_limited" }, { status: 429 });
}
Enter fullscreen mode Exit fullscreen mode

7. Hash your tokens at rest

const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
// store tokenHash, email the user `token`
Enter fullscreen mode Exit fullscreen mode

If your DB ever leaks, the rows are worthless.

The cost, with numbers

Workstream Hours from scratch Hours with a template
Email + password screens 6h 0h
Google OAuth (iOS + Android + Expo Go) 8h 1h
Apple Sign In + name persistence 6h 1h
Magic link + email delivery 10h 1h
Secure token storage + refresh mutex 8h 0h
Biometric unlock 5h 1h
Password reset + account deletion 6h 0h
Schema + RLS policies 8h 0h
Rate limiting + email DKIM 6h 1h
Testing 12h 4h
Total 75h 9h

At $75/hour, that's $5,625 vs $675. Templates range from $99 to $499. The math isn't subtle.

What a real template ships

Production-grade React Native templates wire all seven traps above and ship them as default behavior. The baseline:

  • expo-secure-store token storage
  • Refresh-token mutex
  • Google + Apple + magic link + email/password
  • Apple Sign In fullName persistence
  • Account deletion endpoint
  • Rate-limited magic-link API with SHA-256 token hashing
  • Supabase migrations with RLS policies on every user-scoped table
  • Biometric unlock via expo-local-authentication

If a template you're evaluating doesn't ship all of these, you're getting a UI kit with a database attached — not auth. Ask to see the migrations and the magic-link route before you spend any money.

When to build it yourself

  • Enterprise IdP (SAML / Okta) integration
  • Compliance constraints (HIPAA-eligible IdPs, FIDO2 step-up)
  • You're literally building an auth product

Otherwise, you're paying a tax with zero offsetting return. The hours you spend rebuilding OAuth callbacks are hours your competitors are spending on features users actually see.

Further reading


For the longer breakdown — the actual template I use, the migrations, and the full magic-link route — see Applighter.

What's the auth gotcha that ate the most hours on your last React Native project? Drop it in the comments — I'm collecting the failure modes that don't make it into the docs for a follow-up.

Top comments (0)