# 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.

*Published: 2026-08-13*
*Canonical: https://makerkit.dev/blog/changelog/introducing-expo-react-native-saas-kit*

---

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](https://www.senvio.com) as the main contributor.

{% img src="/images/docs/native-sign-in.webp" width="402" height="874" alt="The Expo app's sign-in screen on iOS: email and password fields, a forgot-password link, and a Sign in with Google button" /%}

Every screenshot in this post is the kit as it ships, running on a simulator against a local Supabase stack. No design work was done for the post.

**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](https://www.senvio.com) 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 kit | Add as a plugin |
| --- | --- | --- |
| **You have** | Nothing yet | A Makerkit Next.js + Supabase project |
| **You get** | `apps/web` and `apps/native` together, already wired | The same `apps/native`, copied into your repo and wired to it |
| **How** | `git clone` | `npx @makerkit/cli@latest plugins add expo` |
| **Your existing work** | Not applicable | Untouched, the plugin only adds files and edits config |
| **Kit variants** | This kit only | `next-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](/docs/react-native-supabase/covered-features) 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.

Teams are the part people assume gets cut on mobile, so it is worth showing. Creating one is a form sheet over whatever you were doing:

{% img src="/images/docs/native-create-team.webp" width="402" height="874" alt="The Create Team form sheet in the Expo app, with a Team Name field and Create Team and Cancel buttons" /%}

The slug field is missing on purpose. The server derives the slug from the name, and the field only appears if the name uses non-Latin characters or the derived slug collides with an existing one.

Managing the team afterwards is the full surface, not a summary screen:

{% img src="/images/docs/native-team-members.webp" width="402" height="874" alt="The Expo app's Members tab: an Invite Members button, a member search field, and member rows badged Owner, Member, Custom-Role and Primary Owner, with Pending Invites below" /%}

Two details in that screenshot are the whole authorization argument. `Custom-Role` is a row someone added to the `roles` table, rendered without an app change, because roles are runtime values rather than an enum baked into the binary. And the row-level menus are absent exactly where they should be: on your own row, on the primary owner's, and on anyone you do not outrank. The role list behind the picker is filtered by hierarchy level on the server, so the app never receives the name of a role the current user may not assign.

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:

| Half | Where it lives | What it is |
| --- | --- | --- |
| Encryption key | `expo-secure-store` (Keychain / Keystore) | A fresh AES-256 key, generated per write |
| Encrypted blob | `@react-native-async-storage/async-storage` | The 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](/docs/react-native-supabase/authentication) covers the full flow.

### Email flows use typed codes, not links

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.

{% img src="/images/docs/native-sign-up.webp" width="402" height="874" alt="The Expo app's sign-up screen: email, password and repeat-password fields above a Sign up with Email button" /%}

Submitting this swaps the same route to the code step rather than pushing a new screen, which is why hardware back and a swipe both return to the filled form instead of dropping you out to sign-in.

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](/docs/react-native-supabase/config-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.

Light and dark ship from the same token set, and the user preference is tri-state — light, dark or follow the system:

{% img src="/images/docs/native-home.webp" width="402" height="874" alt="The Expo app's home screen in light mode, with the workspace switcher pill in the header and a Home, Settings and Members tab bar" /%}

{% img src="/images/docs/native-home-dark.webp" width="402" height="874" alt="The same Expo app home screen in dark mode, with the header, tab bar and status bar all following the theme" /%}

The header, the tab bar and the status bar flip too, and those are not styled by class names at all: they are painted by React Navigation and `expo-status-bar`, which take colour props. The kit bridges the tokens across that boundary, so editing `global.css` still moves the chrome along with the content.

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](/docs/react-native-supabase/styling-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

```bash
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](/docs/react-native-supabase/installation) 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:

```bash
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](/docs/react-native-supabase/building-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.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "What is the Makerkit Expo Supabase SaaS Kit?", "answer": "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."},
     {"question": "Can I add the native app to my existing Makerkit project?", "answer": "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."},
     {"question": "Does it work with the Drizzle, Prisma, React Router or TanStack kits?", "answer": "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."},
     {"question": "Why is billing not available in the app?", "answer": "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."},
     {"question": "Can I run the app in Expo Go?", "answer": "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`."},
     {"question": "Why do sign-up and password reset use codes instead of email links?", "answer": "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."},
     {"question": "Which versions does the kit target?", "answer": "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](https://www.senvio.com) 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](/react-native-supabase) page for the full pitch and pricing. Read the [introduction to the Expo app](/docs/react-native-supabase/introduction) for the architecture in full, then [feature coverage](/docs/react-native-supabase/covered-features) to see exactly what ships. If you are adding it to an existing project, the [Next.js Supabase kit docs](/docs/next-supabase-turbo) cover the v4 baseline the plugin expects.
