{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreiglsqd3ltu6gpkuc3oswsj4tnzv6ztcw7q37n36yo4ycflfs3mpoi",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpcnpzkuiym2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreigy5r2qrr3c4ui3qipsbjdjv3zslbf5a3by4rfvko6shtgbeltcvy"
    },
    "mimeType": "image/webp",
    "size": 74594
  },
  "path": "/shipstack_/how-to-share-supabase-auth-between-nextjs-and-expo-one-client-both-platforms-bp1",
  "publishedAt": "2026-06-27T23:13:55.000Z",
  "site": "https://dev.to",
  "tags": [
    "nestjs",
    "reactnative",
    "supabase",
    "typescript",
    "you can grab it here",
    "@supabase"
  ],
  "textContent": "Most teams building a web + mobile product end up with two auth integrations that slowly drift apart. You don't need that. Here's how to run a single Supabase auth layer across a Next.js web app and an Expo mobile app in a monorepo — including the gotchas nobody warns you about.\n\n##  1. Keep the Supabase client framework-agnostic\n\nDepend only on `@supabase/supabase-js`. Expose a factory that takes the storage adapter as an argument:\n\n\n    export function createSupabaseClient({ url, anonKey, storage, detectSessionInUrl }) {\n      return createClient(url, anonKey, {\n        auth: { storage, autoRefreshToken: true, persistSession: true, detectSessionInUrl },\n      });\n    }\n\n\nWeb uses default browser storage; mobile passes `AsyncStorage`. Same client, same auth helpers, same session context everywhere.\n\n##  2. Reference env vars literally\n\nNext and Expo only inline **literal** `process.env.NEXT_PUBLIC_X` / `EXPO_PUBLIC_X` accesses. A dynamic `process.env[key]` is `undefined` in the bundle. Pass the literals into the factory from each app.\n\n##  3. Authenticate API routes with a Bearer token, not cookies\n\nCookie sessions are awkward to share with a mobile app. Instead, send the access token from the client session and validate it server-side:\n\n\n    const token = req.headers.get('Authorization')?.replace('Bearer ', '');\n    const { data } = await admin.auth.getUser(token);\n\n\nIdentical from web `fetch` and from the mobile app.\n\n##  4. The monorepo gotchas\n\n  * **One React version** across the workspace (Expo pins it) — mixing breaks shared components.\n  * **`node-linker=hoisted`** so pnpm's symlinks don't trip Metro.\n  * A **`metro.config.js`** that adds the workspace root to `watchFolders` and `nodeModulesPaths`.\n\n\n\n##  5. Keep billing server-authoritative\n\nLet clients _read_ their subscription (RLS, select-own) but never write it. The Stripe webhook (service role) is the only writer. No trust placed in the client.\n\n##  Close\n\nThat's the whole pattern: a portable client, token-based route auth, and a monorepo that respects Metro's quirks. I packaged it (plus Stripe, push, RLS, docs) into a starter kit called **Shipstack** if you'd rather not wire it yourself — you can grab it here. But the patterns above are yours to use either way.",
  "title": "How to share Supabase auth between Next.js and Expo (one client, both platforms)"
}