The Expo React Native app | Expo Supabase SaaS Kit

Understand what the Expo app in apps/native is, how it shares a database, an API and packages with the web app, and which page to read next.

The kit ships a second application alongside the Next.js one: an Expo (React Native) app in apps/native, building for iOS and Android from the same monorepo, against the same Supabase project as apps/web. This page explains what that app is, what it is not, and where the rest of this section takes you. Read it before you open the code — the app makes a few deliberate architectural choices that look surprising if you approach it as "the web app, in React Native".

What apps/native is

apps/native is an Expo app that covers a subset of the web kit: the surfaces a signed-in customer uses on a phone. Authentication, personal account settings, team accounts, members and invitations, and the app shell around them.

It is not a port of apps/web. Nothing in the kit tries to keep the two screen-for-screen identical, and several web surfaces are intentionally absent — see What is deliberately not there. Nor is it a WebView wrapper: every screen is a real React Native screen.

The two apps are siblings, not layers. apps/native does not import from apps/web, and apps/web does not import from apps/native. What they share is the database, a set of @kit/* packages, and an HTTP contract.

How it relates to apps/web

ConcernShared with apps/web?How
Supabase project and databaseYesBoth point at the same project. Locally that is the stack started by pnpm run supabase:web:start; apps/native/.env.development sets EXPO_PUBLIC_SUPABASE_URL to the same 127.0.0.1:54321.
Database accessAlmost entirely indirectNative reads and writes through web's versioned /api/v1/* route handlers, so authorization and business rules live in one place. The one exception is usePersonalAccount, which calls fetchPersonalAccount() from @kit/accounts/shared — a direct RLS-protected table read. See docs/native/data-fetching.mdoc.
AuthorizationYesRLS in the database, plus the same services the web Server Actions call, invoked from the /api/v1 handlers.
AuthenticationYes, same GoTrueNative calls @supabase/supabase-js directly for sign-in, MFA, and session refresh. The session lives in the Keychain / Keystore, not a cookie.
StorageYesAvatar and team-logo uploads go straight to Supabase Storage from the device.
Validation schemasYesImported from the @kit/*/shared subpaths, so a rule tightened on web tightens on native.
UI componentsNoWeb uses @kit/ui (Radix + Tailwind for the DOM); native uses @kit/mobile-ui (React Native Reusables + Uniwind).
Feature flagsMirrored, not sharedEach NEXT_PUBLIC_* flag has an EXPO_PUBLIC_* twin that you set separately.
Routing configNoWeb's navigation config files have no native counterpart; native declares its tabs in the route layouts.

Why the database goes through /api/v1

Supabase's client SDK can query the database from React Native perfectly well. The kit deliberately does not do that.

A mobile binary is distributed through the app stores and updated on the user's schedule — you cannot assume every installed copy is running this week's code. If business rules lived in the app (which roles may be assigned, when a seat count changes, whether team creation is allowed), an old binary would keep enforcing an old version of them, and every rule would exist twice. Routing data through apps/web/app/api/v1/* keeps one implementation of each rule on a server you can redeploy, and gives the mobile client a versioned contract: fields get added within v1, and removals or renames go to a new version.

The wrapper that native calls those endpoints with is api() in apps/native/features/core/api-client.ts. It attaches the current Supabase JWT, prefixes /api/v1, applies a timeout, and normalizes failures into a typed ApiError carrying the handler's status and machine-readable code:

import type { UserWorkspaceShape } from '@kit/accounts/shared';
import { api } from '~/features/core/api-client';
const workspace = await api<UserWorkspaceShape>('/me/workspace');

On the server side, enhanceRouteHandler from @kit/next/routes detects the Authorization: Bearer <jwt> header and builds an RLS-bound client for that user, so a handler is written once and serves both the cookie-bearing browser and the token-bearing app. The endpoint list and the shape of each response are documented in apps/web/app/api/v1/AGENTS.md; adding an endpoint and the React Query hook that consumes it is covered in docs/native/data-fetching.mdoc.

Supabase's JS SDK is still used directly, but only for the three things an HTTP wrapper would make worse: auth (the session must live in the client to be refreshed), MFA (challenge and enrollment are GoTrue APIs), and storage (uploading an image through a JSON endpoint would mean base64 in a request body).

Tech stack

Versions come from pnpm-workspace.yaml — every native dependency is pinned there under the expo named catalog rather than in apps/native/package.json, so an SDK bump is one file.

ConcernChoiceVersion
RuntimeExpo SDK, React Native, React57.0.8 / 0.86.0 / 19.2.3
Routingexpo-router, file-based, typed routes on57.0.8
StylingUniwind — Tailwind v4, CSS-first, for React Native1.10.0
Components@kit/mobile-ui, React Native Reusables primitivesworkspace
Backend SDK@supabase/supabase-js with an encrypted expo-secure-store adapter2.110.8
Server stateTanStack Query5.101.4
Formsreact-hook-form + zod resolvers7.83.0 / 4.4.3
Translationsuse-intl, against a generated catalogue4.13.4

The React Compiler is enabled through babel-preset-expo in apps/native/babel.config.js.

The @kit/* packages apps/native depends on:

PackageWhat native takes from it
@kit/mobile-uiEvery UI primitive — text, button, input, form, sonner, icon, and the rest.
@kit/auth/sharedSign-in, sign-up and password schemas, shared with web's forms.
@kit/accounts/sharedPersonal-account schemas and query keys.
@kit/team-accounts/sharedTeam, member and invitation schemas and types.
@kit/supabaseThe generated Database type, so the Supabase client is typed.
@kit/sharedDeclared as a dependency; reached in practice through @kit/mobile-ui.

Only the /shared subpaths are imported. Those entrypoints are free of React DOM and Next.js imports, which is what makes them safe to pull into a Metro bundle.

What ships in the app today

At a high level: password sign-in and sign-up, password reset, MFA challenge and TOTP enrollment, Google (and other) OAuth, personal account settings including avatar and account deletion, team creation and settings, the full members/invitations/roles surface, a workspace switcher across personal and team accounts, theming, and a device-info diagnostics screen.

Every email flow uses a typed 6-digit code rather than a link, because the browser your mail client opens shares no cookie jar with the app — so sign-up confirmation, password reset and invitation acceptance need no deep-link infrastructure at all. docs/native/authentication.mdoc covers the flows.

What is deliberately not there

SurfaceWhy it stays on web
Billing checkout, plans, customer portalSelling subscriptions inside a mobile app means platform in-app purchase — a commission and a separate purchase, receipt and reconciliation model that shares no code with the kit's Stripe or Lemon Squeezy integration. Team subscription status is still shown read-only, and seat counts still update from native member changes.
Admin and super-adminAn operator tool. Shipping it in a store-distributed binary widens the attack surface with no user benefit.
Marketing pages, blog, CMS contentAcquisition surfaces, aimed at people who have not installed the app. The marketing message namespace is never even inherited into the native catalogue.

"Not there" is not the same as "unreachable": some settings rows render as a disabled row with a "manage on web" notice rather than disappearing, so the app never silently loses a capability. Which ones, and the Planned features that are simply not built yet, are in docs/native/covered-features.mdoc.

Where to go next

If you want toRead
Get the app — a fresh clone, or added to a project you already havedocs/native/installation.mdoc
Run the app on a simulator or devicedocs/native/running-locally.mdoc
Understand the folder layers and the import ruledocs/native/project-structure.mdoc
Add a screen, or understand the route guardsdocs/native/routing-navigation.mdoc
Work on sign-in, MFA, OAuth or session storagedocs/native/authentication.mdoc
Understand personal vs team accounts and the switcherdocs/native/workspaces.mdoc
Add an endpoint and a hook that calls itdocs/native/data-fetching.mdoc
Change theme tokens or add a componentdocs/native/styling-theming.mdoc
Build a formdocs/native/forms.mdoc
Produce a build, or ship to the storesdocs/native/building-shipping.mdoc
Look up a setting, or what changing one forces elsewheredocs/native/config-requirements.mdoc
Change a string, or add a localedocs/native/i18n.mdoc
Check whether a feature is builtdocs/native/covered-features.mdoc
Fix a build, cache or typecheck problemdocs/native/troubleshooting.mdoc

If you already have this repository checked out, start at docs/native/running-locally.mdoc — the app reads its Supabase URL, API base URL and feature flags from apps/native/.env.development, and seeing it run against your local stack makes the rest of this section concrete. If you are adding the Expo app to a Makerkit project you already have, start at docs/native/installation.mdoc instead.