- Auth is a commodity surface in 2026. Don't rebuild it.
- 60–120 hours of senior-dev work for $59–$299 of template = obvious trade.
- Vet templates for: RLS in versioned migrations, separated Supabase clients (anon vs. service_role), a working Stripe webhook, and ≤1k lines of auth code.
- Roll your own only if you're regulated, weird, or learning.
You sat down to ship a side project. You opened a fresh Expo app. You typed "weekend plan: build auth."
Three weeks later you're still here.
This post is the argument for git cloneing somebody else's auth and getting back to writing the thing that's actually your app.
The full auth surface in React Native (2026)
It's never just a login screen. Real auth for a mobile app shipping on the App Store today includes:
- Email/password (optional, but users ask)
- Magic links (token hashing, single-use, expiry, rate limits)
- Google OAuth (iOS + Android URL schemes)
- Apple Sign-In (mandatory if you ship Google on iOS)
- Session persistence + refresh
- Deep links for verification emails
- RLS on every user-touching table
- Account deletion (App Store rule)
- Password reset
- Tests for at least magic link + OAuth happy paths
A focused senior dev spends 60–120 hours building this correctly. At $120/hr that's $7K–$14K of opportunity cost. And the failure mode for skipping a step isn't "ugly UI" — it's a leaked credential.
What a good template gives you
Skip ahead. The stack you want is roughly:
- NextAuth for the session orchestration (provider-agnostic)
- Supabase for the database, with RLS turned on
- A magic-link endpoint with hashed tokens
- A Stripe webhook that converts checkout → license grant
- Two Supabase clients: anon for the app, service role for trusted server code
Here's what the magic-link route looks like in a real template (app/api/auth/magic-link/route.ts):
// Pseudocode showing the shape, not the literal source
const token = crypto.randomBytes(32).toString('hex')
const tokenHash = createHash('sha256').update(token).digest('hex')
await rateLimit(email, { window: 60, max: 3 })
await supabase.from('magic_link_tokens').insert({
email,
token_hash: tokenHash,
expires_at: new Date(Date.now() + 15 * 60 * 1000),
})
await sendEmail({ to: email, link: `/verify?token=${token}` })
Three details to notice:
- The raw token never touches the database. Only the SHA-256 hash is stored.
- There's a rate limit (3 requests / 60s per email).
- The expiry is 15 minutes, not "we'll check it later."
If your homegrown magic-link route is missing any one of those, you've already shipped a vulnerability.
Two Supabase clients, not one
The single most common security mistake in React Native + Supabase code is one shared client. Don't.
// modules/db/supabaseClient.ts — browser/app code
export const supabase = createClient(URL, ANON_KEY)
// modules/db/supabaseServer.ts — server-only, never imported from app/
export const supabaseAdmin = createClient(URL, SERVICE_ROLE_KEY)
The service_role key bypasses RLS. If it appears in any file that gets bundled into your client (anything under your Expo app/), you've shipped the keys to your entire database. A good template separates these two clients structurally so the bad import is visibly wrong in code review.
Build vs. buy
| Capability | Roll your own | Auth-as-a-service | Template |
|---|---|---|---|
| Magic link | Build | Included | Included |
| Google + Apple OAuth | 1–2d each | Included | Included |
| RLS policies | Hand-written | N/A | Versioned migrations |
| Stripe license grants | 3–5d | DIY | Wired |
| Token hashing + rate limits | DIY | Included | Included |
| Source you own | Yes | No | Yes |
| Recurring cost | $0 | $25–$500+/mo | One-time |
| Time to first signed-in user | 2–6 weeks | 1–3 days | 1 day |
Auth-as-a-service (Clerk, Auth0) is fine if your buyer is an enterprise security team and your pricing supports it. For an indie consumer app it's almost always overkill and locks you into someone's opinions about identity.
When you SHOULD roll your own
There are real cases:
- Regulated industries (healthcare, fintech) where security needs a small bespoke surface to audit
- Strange tenancy models (per-device identity, supervised iPad fleets)
- You're learning OAuth/JWT/RLS and the project IS the learning
- You have an internal IdP to integrate with
For everyone else: don't.
Vetting a template before you buy
Most templates are zombies. Use this checklist:
[ ] Auth code is under ~1,000 lines
[ ] RLS policies live in versioned migrations, not "enable it in the dashboard"
[ ] Stripe webhook writes to the same DB as auth
[ ] Anon and service_role clients are in separate files
[ ] Dependencies updated in the last 90 days
[ ] You can read the magic-link route in one sitting
If a template passes those six, you're buying real engineering, not a UI kit with a database attached.
If you want to see the actual files — lib/auth.ts, app/api/auth/magic-link/route.ts, app/api/webhook/stripe/route.ts — the longer breakdown lives on the Applighter blog, including a deeper walkthrough of how the Stripe webhook binds a checkout to a Supabase identity row.
Further reading:
What was the auth gotcha that ate the most time on your last project? Drop it in the comments — I'm collecting the patterns for a follow-up on the failures that don't make it into the documentation.











