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

*Canonical: https://makerkit.dev/docs/react-native-supabase/introduction*

---

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](#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`

| Concern | Shared with `apps/web`? | How |
| --- | --- | --- |
| Supabase project and database | Yes | Both 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 access | Almost entirely indirect | Native 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`. |
| Authorization | Yes | RLS in the database, plus the same services the web Server Actions call, invoked from the `/api/v1` handlers. |
| Authentication | Yes, same GoTrue | Native calls `@supabase/supabase-js` directly for sign-in, MFA, and session refresh. The session lives in the Keychain / Keystore, not a cookie. |
| Storage | Yes | Avatar and team-logo uploads go straight to Supabase Storage from the device. |
| Validation schemas | Yes | Imported from the `@kit/*/shared` subpaths, so a rule tightened on web tightens on native. |
| UI components | No | Web uses `@kit/ui` (Radix + Tailwind for the DOM); native uses `@kit/mobile-ui` (React Native Reusables + Uniwind). |
| Feature flags | Mirrored, not shared | Each `NEXT_PUBLIC_*` flag has an `EXPO_PUBLIC_*` twin that you set separately. |
| Routing config | No | Web'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`:

```ts
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.

| Concern | Choice | Version |
| --- | --- | --- |
| Runtime | Expo SDK, React Native, React | `57.0.8` / `0.86.0` / `19.2.3` |
| Routing | `expo-router`, file-based, typed routes on | `57.0.8` |
| Styling | Uniwind — Tailwind v4, CSS-first, for React Native | `1.10.0` |
| Components | `@kit/mobile-ui`, React Native Reusables primitives | workspace |
| Backend SDK | `@supabase/supabase-js` with an encrypted `expo-secure-store` adapter | `2.110.8` |
| Server state | TanStack Query | `5.101.4` |
| Forms | `react-hook-form` + `zod` resolvers | `7.83.0` / `4.4.3` |
| Translations | `use-intl`, against a generated catalogue | `4.13.4` |

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

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

| Package | What native takes from it |
| --- | --- |
| `@kit/mobile-ui` | Every UI primitive — `text`, `button`, `input`, `form`, `sonner`, `icon`, and the rest. |
| `@kit/auth/shared` | Sign-in, sign-up and password schemas, shared with web's forms. |
| `@kit/accounts/shared` | Personal-account schemas and query keys. |
| `@kit/team-accounts/shared` | Team, member and invitation schemas and types. |
| `@kit/supabase` | The generated `Database` type, so the Supabase client is typed. |
| `@kit/shared` | Declared 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.

{% alert type="info" title="The feature matrix is the authority" %}
`docs/native/covered-features.mdoc` lists every feature with a status (Parity, Adapted, Planned, Web-only, Declined), the flag that gates it, and the store constraint attached to it. When this section and that page disagree about whether something is built, that page wins.
{% /alert %}

## What is deliberately not there

| Surface | Why it stays on web |
| --- | --- |
| Billing checkout, plans, customer portal | Selling 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-admin | An operator tool. Shipping it in a store-distributed binary widens the attack surface with no user benefit. |
| Marketing pages, blog, CMS content | Acquisition 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 to | Read |
| --- | --- |
| Get the app — a fresh clone, or added to a project you already have | `docs/native/installation.mdoc` |
| Run the app on a simulator or device | `docs/native/running-locally.mdoc` |
| Understand the folder layers and the import rule | `docs/native/project-structure.mdoc` |
| Add a screen, or understand the route guards | `docs/native/routing-navigation.mdoc` |
| Work on sign-in, MFA, OAuth or session storage | `docs/native/authentication.mdoc` |
| Understand personal vs team accounts and the switcher | `docs/native/workspaces.mdoc` |
| Add an endpoint and a hook that calls it | `docs/native/data-fetching.mdoc` |
| Change theme tokens or add a component | `docs/native/styling-theming.mdoc` |
| Build a form | `docs/native/forms.mdoc` |
| Produce a build, or ship to the stores | `docs/native/building-shipping.mdoc` |
| Look up a setting, or what changing one forces elsewhere | `docs/native/config-requirements.mdoc` |
| Change a string, or add a locale | `docs/native/i18n.mdoc` |
| Check whether a feature is built | `docs/native/covered-features.mdoc` |
| Fix a build, cache or typecheck problem | `docs/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.
