<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shipstack</title>
    <description>The latest articles on DEV Community by Shipstack (@shipstack_).</description>
    <link>https://dev.to/shipstack_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4005876%2F0a93f68a-e19c-4c21-a082-dd7e316144e5.png</url>
      <title>DEV Community: Shipstack</title>
      <link>https://dev.to/shipstack_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shipstack_"/>
    <language>en</language>
    <item>
      <title>How to share Supabase auth between Next.js and Expo (one client, both platforms)</title>
      <dc:creator>Shipstack</dc:creator>
      <pubDate>Sat, 27 Jun 2026 23:13:55 +0000</pubDate>
      <link>https://dev.to/shipstack_/how-to-share-supabase-auth-between-nextjs-and-expo-one-client-both-platforms-bp1</link>
      <guid>https://dev.to/shipstack_/how-to-share-supabase-auth-between-nextjs-and-expo-one-client-both-platforms-bp1</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep the Supabase client framework-agnostic
&lt;/h2&gt;

&lt;p&gt;Depend only on &lt;code&gt;@supabase/supabase-js&lt;/code&gt;. Expose a factory that takes the storage adapter as an argument:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export function createSupabaseClient({ url, anonKey, storage, detectSessionInUrl }) {
  return createClient(url, anonKey, {
    auth: { storage, autoRefreshToken: true, persistSession: true, detectSessionInUrl },
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Web uses default browser storage; mobile passes &lt;code&gt;AsyncStorage&lt;/code&gt;. Same client, same auth helpers, same session context everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reference env vars literally
&lt;/h2&gt;

&lt;p&gt;Next and Expo only inline &lt;strong&gt;literal&lt;/strong&gt; &lt;code&gt;process.env.NEXT_PUBLIC_X&lt;/code&gt; / &lt;code&gt;EXPO_PUBLIC_X&lt;/code&gt; accesses. A dynamic &lt;code&gt;process.env[key]&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; in the bundle. Pass the literals into the factory from each app.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Authenticate API routes with a Bearer token, not cookies
&lt;/h2&gt;

&lt;p&gt;Cookie sessions are awkward to share with a mobile app. Instead, send the access token from the client session and validate it server-side:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const token = req.headers.get('Authorization')?.replace('Bearer ', '');
const { data } = await admin.auth.getUser(token);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Identical from web &lt;code&gt;fetch&lt;/code&gt; and from the mobile app.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The monorepo gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One React version&lt;/strong&gt; across the workspace (Expo pins it) — mixing breaks shared components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;node-linker=hoisted&lt;/code&gt;&lt;/strong&gt; so pnpm's symlinks don't trip Metro.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;metro.config.js&lt;/code&gt;&lt;/strong&gt; that adds the workspace root to &lt;code&gt;watchFolders&lt;/code&gt; and &lt;code&gt;nodeModulesPaths&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Keep billing server-authoritative
&lt;/h2&gt;

&lt;p&gt;Let clients &lt;em&gt;read&lt;/em&gt; their subscription (RLS, select-own) but never write it. The Stripe webhook (service role) is the only writer. No trust placed in the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;Shipstack&lt;/strong&gt; if you'd rather not wire it yourself — &lt;a href="https://4426308762925.gumroad.com/l/shipstack" rel="noopener noreferrer"&gt;you can grab it here&lt;/a&gt;. But the patterns above are yours to use either way.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>reactnative</category>
      <category>supabase</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
