devto 2026-06-28 원문 보기 ↗
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.
Depend only on @supabase/supabase-js. Expose a factory that takes the storage adapter as an argument:
export function createSupabaseClient({ url, anonKey, storage, detectSessionInUrl }) {
return createClient(url, anonKey, {
auth: { storage, autoRefreshToken: true, persistSession: true, detectSessionInUrl },
});
}
Web uses default browser storage; mobile passes AsyncStorage. Same client, same auth helpers, same session context everywhere.
Next 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.
Cookie sessions are awkward to share with a mobile app. Instead, send the access token from the client session and validate it server-side:
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
const { data } = await admin.auth.getUser(token);
Identical from web fetch and from the mobile app.
node-linker=hoisted so pnpm's symlinks don't trip Metro.metro.config.js that adds the workspace root to watchFolders and nodeModulesPaths.Let 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.
That'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.