Skip to content
Supastack
EN
Theme
Sign inGet lifetime access
All articles

Sharing Authentication & 8 Payment Gateways Across Next.js, Expo Mobile, and Tauri v2

The two hardest seams in cross-platform SaaS: deep-link auth callbacks on mobile, and keeping web payments, RevenueCat IAP, and desktop checkout on one entitlement. How one monorepo solves both.

Noah Zixin Zhang3 min read

How do you share auth and payments across Next.js, Expo, and Tauri?

It depends on keeping one identity model and one entitlement store. Web uses HttpOnly cookie sessions, Expo stores a bearer token in expo-secure-store after a deep-link OAuth callback, and Tauri reuses the web session inside its webview — all validated by the same Better Auth instance. Payments follow the same rule: eight web gateways and RevenueCat mobile IAP feed one idempotent webhook ledger that writes workspace-scoped entitlements every surface reads.

The Two Seams Where Cross-Platform SaaS Breaks

Ask any team that bolted a mobile app onto a web SaaS what actually hurt, and it is never the UI. It is identity and money: an OAuth flow that returns cleanly to a browser has to return into an app through a deep link, and a customer who pays on the web expects their entitlement to exist on their phone — instantly.

Teams usually discover this after shipping two half-solutions: cookie logic that silently fails inside a WKWebView, and a RevenueCat entitlement that fights the Stripe subscription in the web database. The fix is architectural, and it is the same fix twice — one identity model, one entitlement store.

One Better Auth Instance, Three Session Transports

supastack keeps a single Better Auth configuration in @supastack/auth with distinct entry points per surface. The web apps (Next.js 16 and TanStack Start) use HttpOnly, SameSite=Lax cookie sessions — CSRF-safe by default and native to Server Components. The Expo app exchanges OAuth or magic-link completion for a bearer token at the end of a deep-link callback, then persists it in expo-secure-store (iOS Keychain / Android Keystore) and sends it as an Authorization header the same auth middleware validates.

Tauri's desktop shell is a single webview pointed at the web app, so it inherits the web session outright — no third auth flow to maintain. The mobile authentication guide and deep links guide document the callback contract; our web + mobile monorepo guide walks the shared-client code.

// packages/auth/src/client.ts — one client, two storages
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";

export function createCrossPlatformAuthClient(baseURL: string, storage?: any) {
  return createAuthClient({
    baseURL,
    plugins: storage ? [expoClient({ scheme: "myapp", storage })] : [],
  });
}

// Web: cookie session.  Mobile: bearer token in SecureStore.
// Desktop: the web app's own session inside the Tauri webview.

Eight Payment Gateways Behind One Contract

Every web payment provider — Stripe, Lemon Squeezy, Polar, Creem, Dodo, Paddle, Whop and PayPal — plugs into the same @supastack/billing contract, and every signed webhook lands in one idempotent webhook_event ledger before it can touch an entitlement. Provider retries and out-of-order delivery become no-ops at the database boundary, not support tickets.

Mobile in-app purchases join the same ledger through RevenueCat: the store receipt verifies server-side, the entitlement writes against the workspace — not the device — and the user who paid on iOS is instantly paid-up on web and desktop. The RevenueCat guide covers the App Store / Play wiring; idempotent billing webhooks explains why the ledger is the load-bearing piece.

The Entitlement Model That Makes Sync Boring

The reason this stays boring is where entitlements live: on the workspace, resolved server-side. No client ever stores 'is paid' — web asks the session, mobile asks the API, desktop asks the web app — and all three get the same answer because there is one record to answer from.

Restore-purchase on a new phone, a Stripe renewal on the web, an Agency seat added in a dashboard — each is a write to the same entitlement rows, and each surface reads them the next time it asks. There is no sync engine to build because there is nothing to sync.

SurfaceSession transportPayment pathReads entitlements from
Next.js 16 webHttpOnly cookieAny of 8 gateways → webhook ledgerShared Postgres entitlement rows
TanStack Start webHttpOnly cookieSame adapters, same ledgerShared Postgres entitlement rows
Expo iOS / AndroidBearer token in expo-secure-storeRevenueCat IAP → server-verified receiptSame rows via typed API
Tauri 2 desktopWeb session in webviewWeb checkout inside the shellSame rows via web app
MV3 extensionWorkspace API key— (reads workspace data)Same rows via /api/v1

What You Skip Building

Priced in evenings, this is the difference between a foundation and a feature list: deep-link auth returns, SecureStore token persistence, eight webhook integrations with replay safety, App Store receipt verification, and an entitlement model that never contradicts itself across devices. Each is shipped, tested (693+ tests run without credentials), and documented.

The Full Bundle at $299 includes every surface in one repo — see pricing for seat counts and the agency tier.

FAQ: Shared Auth and Payments Across Platforms

The questions teams ask before committing to a unified stack:

  • Do mobile users sign in separately from the web app?, No. The OAuth or magic-link flow returns into the app through a deep link and mints one session; the same Better Auth instance issued it, so web and mobile agree on who you are.
  • What happens when someone pays on iOS but uses the web app?, RevenueCat delivers the purchase server-side; the entitlement writes to the same workspace rows web reads, so the web app shows them paid on next request.
  • Can I start web-only and add the app later?, Yes — auth and billing are in shared packages, so the mobile and desktop apps inherit them whenever you turn those surfaces on.
  • Does the Tauri app need its own checkout?, No — the desktop shell reuses the web app's session and checkout inside its webview; there is no second payment surface to secure.

Your next product starts here.

Pay once. Keep building profitable products.