Introducing the Expo (React Native) Supabase SaaS Kit

The Makerkit Expo kit ships an iOS and Android app alongside your Next.js web app: one monorepo, one Supabase project, shared packages. Start from the kit, or install the native app into an existing Next.js Supabase project with a single CLI command.

Your Makerkit SaaS can now ship an iOS and Android app that shares its database, its API and its packages with the web app. The Expo Supabase SaaS Kit is available today, built with the Senvio team as the main contributor.

TL;DR

  • What it is: a complete Makerkit kit containing the Next.js web app and an Expo (React Native) app in apps/native, wired to the same Supabase project.
  • Two ways in: clone the kit for a new project, or run npx @makerkit/cli@latest plugins add expo to add the native app to a Makerkit 4.x next-supabase project you already have.
  • What works natively: email and password sign-in, Google OAuth, Sign in with Apple, TOTP multi-factor authentication, personal accounts, team accounts, members, invitations, and role-based permissions.
  • What stays on web on purpose: plan selection and checkout, because in-app purchase means a 15% to 30% platform commission and anti-steering rules.
  • Stack: Expo SDK 57, React Native 0.86, React 19.2.3, expo-router, React Query, Uniwind (Tailwind v4 compiled for React Native), Supabase.
  • Who built it: the Senvio team did the bulk of the development.
  • Where it is in its life: this is the initial release, and updates land frequently from here. Expanded documentation, bug fixes and the planned features are all in flight.

Two ways to get it

The kit is a variant of the Next.js Supabase kit rather than a separate product, so you can adopt it at either end of a project's life.

Start from this kitAdd as a plugin
You haveNothing yetA Makerkit Next.js + Supabase project
You getapps/web and apps/native together, already wiredThe same apps/native, copied into your repo and wired to it
Howgit clonenpx @makerkit/cli@latest plugins add expo
Your existing workNot applicableUntouched, the plugin only adds files and edits config
Kit variantsThis kit onlynext-supabase only

The plugin path is the one that matters if you already shipped. It targets the v4 line and the next-supabase variant, and the CLI refuses to install it on the Drizzle, Prisma, React Router or TanStack variants, because the plugin declares no configuration for them. Commit your tree first, since the install edits many files at once and you want to read the result as one diff.

What the native app does today

Authentication, personal accounts, team accounts and member management work natively. The feature coverage page tracks every row with its status and its feature flag, but the short version is:

  • Auth: email and password sign-in, sign-up, password reset, Google OAuth, Sign in with Apple (iOS, off by default), TOTP challenge with auto-submit on the sixth digit, and team invitation acceptance.
  • Personal account: display name, avatar upload to the account_image bucket, password change, MFA enrollment and unenrollment, and OTP-confirmed account deletion.
  • Team accounts: create a team, rename it, change the slug, upload a logo, and leave a team, with the primary owner refused by RLS.
  • Members and roles: the members list, invitations (invite, list, change role, revoke, renew), member role changes, and removal. The role list is hierarchy-filtered server-side, so the app never receives roles the current user may not assign.
  • Billing: a read-only subscription status badge in team settings. Removing a member still decrements your per-seat count correctly, because the shared service owns that logic.

Everything is gated by the same RLS policies as the web app. There is no second authorization model to keep in sync.

Three decisions worth knowing before you open the code

The app is not the web app rendered in React Native. Three choices differ deliberately, and each one will look wrong until you know why.

Data goes through a versioned REST API, not the Supabase client

Every screen that shows server data calls a React Query hook, which calls api(), which calls a route handler under apps/web/app/api/v1/* with a Supabase bearer token. The Supabase JS SDK is used directly in exactly four places: sign-in and session refresh, MFA enrollment and challenge, storage uploads, and nothing else.

The reason is code sharing. Your product's rules live in services under packages/features, and a route handler can call them. A supabase.from(...) call in a screen cannot, so the rule gets reimplemented in the app and the two copies drift. Pushing an avatar through a JSON endpoint would also mean base64 in a request body, which is why uploads keep the SDK.

The session lives in the keychain, split in two

Web keeps the session in an httpOnly cookie. A native app has no cookie jar, so apps/native/features/core/supabase.ts hands Supabase a custom storage adapter called LargeSecureStore.

It splits the session because iOS Keychain items are practically limited to around 2 KB, and a Supabase session (a JWT plus a refresh token) is larger than that:

HalfWhere it livesWhat it is
Encryption keyexpo-secure-store (Keychain / Keystore)A fresh AES-256 key, generated per write
Encrypted blob@react-native-async-storage/async-storageThe session, AES-CTR encrypted with that key

The two halves can desync after an OS keychain reset, a restore from backup, or a write that did not finish. getItem treats any failure as exactly that and clears both halves rather than throwing, so the user lands on the sign-in screen instead of in a broken session. Authentication covers the full flow.

Sign-up confirmation, password reset and invitation acceptance all use a six-digit emailed code. This is not a shortcut. The browser your mail client opens shares no cookie jar with your app, so link-based parity is not available without deep-link infrastructure, universal links, and a real HTTPS domain. Typed codes need none of that and work on the simulator on day one.

One consequence: code length, code expiry and the verification rate limit become load-bearing settings, and they are shared web and native policy. Configuration and requirements lists the defaults.

Styling: Tailwind, compiled for React Native

The app is styled with Uniwind, which compiles Tailwind v4 for React Native through a Metro transformer. You write className on React Native components, theme tokens live in apps/native/global.css, and there is no tailwind.config.js in either apps/native or packages/mobile-ui. Rebranding the app is one file.

The gotcha that costs people an afternoon: there is no cascade and no CSS box model, so only utilities Uniwind can map to a React Native style property do anything. A class with no React Native equivalent is resolved and then dropped silently, with no build warning. Hand-porting a web component is where this bites. Styling and theming documents the mapping and the resync-safe place to add your own components.

Why billing stays on web

Plan selection, checkout and the customer portal are web-only, and that is a commercial decision rather than a missing feature. Selling digital subscriptions inside a mobile app means platform in-app purchase: a 15% to 30% commission, plus anti-steering rules constraining how you may even mention other payment options. IAP also shares no code with the kit's Stripe or Lemon Squeezy integration, since it brings its own purchase flow, receipt model and reconciliation path.

Seat counts still update correctly from native member changes, because the shared service owns them.

What is not there yet

This is the initial release, so treat the gaps as a schedule rather than a boundary. Expect a faster cadence than the web kits over the next few weeks: documentation gets expanded as real projects hit real questions, bugs get fixed as you report them, and the planned rows below get built. The kit documents those gaps instead of hiding them. Notifications (in-app list, bell, unread badge, realtime delivery) are planned and portable, since supabase.channel() works from React Native as-is. Passkey management is planned; passkey registration additionally needs a relying-party domain, an Apple Team ID and an Android release SHA-256, so it cannot be tested against localhost. Team deletion, ownership transfer and email changes show a "manage on web" notice today.

One warning worth repeating: never enable the global GoTrue captcha toggle. Native carries no Turnstile token, and that switch gates sign-in, OTP and reset for every client, which breaks native auth outright.

Installing into an existing project

npx @makerkit/cli@latest plugins add expo

The CLI detects your kit variant, then hands off to a codemod you can also run directly with npx codemod @makerkit/next-supabase-expo. The installation guide lists every file it touches. It copies apps/native, packages/mobile-ui, the /api/v1 routes and the docs, then applies AST transforms to files you already own: the workspace catalog gains the Expo and React Native versions, package.json gains ten *:native scripts, supabase/config.toml allows the mkkit:// OAuth callback, and oxlint learns to ban web's password schemas inside apps/native.

Every step is idempotent, the file fetch is skipped when apps/native already exists, and an interrupted copy rolls back rather than leaving a half-installed app. After the copy it runs pnpm install, regenerates the message catalogue from your web strings (otherwise your app renders our product name and our copy), then typechecks and formats.

Then restart Supabase so it picks up the new redirect URL:

pnpm run supabase:web:reset
pnpm run start:native

There is no Expo Go path. The app depends on native modules that ship config plugins (expo-secure-store, expo-image-picker, expo-apple-authentication, expo-dev-client), so every device and simulator runs a build you produced. expo prebuild generates ios/ and android/ from app.json, and neither directory is committed. Building and shipping covers the store gates, and you should read them before you promise anyone a release date.

Quick Recommendation

The Expo Supabase SaaS Kit is best for:

  • Teams already on the Makerkit Next.js Supabase kit who need an iOS and Android app without a second backend.
  • New SaaS products where mobile is part of the plan rather than a later port.
  • Products whose mobile surface is accounts, teams and your own data, with purchasing on the web.

Skip it if:

  • You are on the Drizzle, Prisma, React Router or TanStack variants. The plugin declares no configuration for them today.
  • Your app must sell subscriptions inside the app. You would be building the in-app purchase flow yourself.
  • You need in-app notifications or passkey sign-in on day one. Both are planned, neither ships today.

Our pick: if you are already on the Next.js Supabase kit at v4, install the plugin into a branch and run it against your local Supabase stack. It is one command, it is idempotent, and it tells you within an hour whether mobile is a week of work or a quarter.

Frequently Asked Questions

What is the Makerkit Expo Supabase SaaS Kit?
It is a Makerkit starter kit containing a Next.js web app and an Expo (React Native) app in the same monorepo, sharing one Supabase project, one set of RLS policies and the `@kit/*` packages. The native app covers authentication, personal accounts, team accounts, members, invitations and roles on iOS and Android.
Can I add the native app to my existing Makerkit project?
Yes, if you are on Makerkit 4.x and the `next-supabase` variant. Run `npx @makerkit/cli@latest plugins add expo` from the root of your repository. It copies `apps/native`, `packages/mobile-ui` and the `/api/v1` routes into your repo and edits your config to wire them up. Every step is idempotent, so re-running is safe.
Does it work with the Drizzle, Prisma, React Router or TanStack kits?
Not today. The plugin declares configuration for the `next-supabase` variant only, and the CLI refuses to install it elsewhere. The Expo kit itself ships the Next.js Supabase web app.
Why is billing not available in the app?
Selling digital subscriptions inside a mobile app requires platform in-app purchase, which carries a 15% to 30% commission and anti-steering rules, and shares no code with the kit's Stripe or Lemon Squeezy integration. Plan selection and checkout stay on web. The app shows a read-only subscription badge, and per-seat counts still update from native member changes.
Can I run the app in Expo Go?
No. The app depends on native modules that ship config plugins, including `expo-secure-store` and `expo-apple-authentication`, and Expo Go carries a fixed set of native modules that cannot load them. You build a development client instead, and `expo prebuild` generates the `ios/` and `android/` projects from `app.json`.
Why do sign-up and password reset use codes instead of email links?
The browser your mail client opens shares no cookie jar with a native app, so link-based flows would need deep links, universal links and a real HTTPS domain before they work at all. A six-digit emailed code needs none of that and behaves identically on a simulator and a physical device.
Which versions does the kit target?
As of kit 4.0.1: Expo SDK 57, React Native 0.86, React 19.2.3, Tailwind 4.3.3 through Uniwind 1.10, with expo-router for navigation and React Query for data fetching.

Credits

The Expo kit was built with the Senvio team as the main contributor to its development. The architecture decisions above, the secure session storage, the code-based email flows and the /api/v1 boundary came out of that work.

Next steps

See the React Native SaaS Starter Kit page for the full pitch and pricing. Read the introduction to the Expo app for the architecture in full, then feature coverage to see exactly what ships. If you are adding it to an existing project, the Next.js Supabase kit docs cover the v4 baseline the plugin expects.