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 expoto add the native app to a Makerkit 4.xnext-supabaseproject 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 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 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_imagebucket, 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:
| 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 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.
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 expoThe 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:resetpnpm run start:nativeThere 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?
Can I add the native app to my existing Makerkit project?
Does it work with the Drizzle, Prisma, React Router or TanStack kits?
Why is billing not available in the app?
Can I run the app in Expo Go?
Why do sign-up and password reset use codes instead of email links?
Which versions does the kit target?
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.