# Native app feature coverage | Expo Supabase SaaS Kit

> Which Makerkit features the Expo app covers, which deliberately stay on web, and which are planned. Includes the flag, platform and store constraint for every feature.

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

---

What the Expo app in `apps/native` covers, feature by feature, against the web kit it ships alongside.

**As of kit 4.0.1, Expo SDK 57 (React Native 0.86).** Statuses describe the code in this repository, not a roadmap commitment.

{% alert type="info" title="Where each kind of detail lives" %}
This page owns status: whether a feature works natively. It carries no effort estimates or sequencing.

- **Configuration**, and what a change forces elsewhere: `docs/native/config-requirements.mdoc`
- **Message catalogue**, and how native inherits web's strings: `docs/native/i18n.mdoc`
{% /alert %}

## Legend

| Status | Meaning |
| --- | --- |
| **Parity** | Works natively and behaves like web. |
| **Adapted** | Works natively, deliberately different from web. The difference is always stated. |
| **Planned** | Not built. Intended. |
| **Web-only** | Exists in the product, deliberately stays on web. Native users are sent to the web app. |
| **Declined** | Evaluated and rejected. The reason is stated, so the decision isn't re-litigated. |

A † on a *Planned* row means it also needs credentials or hosting you must provide: an Apple Services ID, a real HTTPS domain, `.well-known` files. Nobody can finish or test those locally.

Unless a row says otherwise, a feature behaves the same on iOS and Android.

---

## Authentication and session

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| Email + password sign-in | Parity | `EXPO_PUBLIC_AUTH_PASSWORD` (on) | Shares web's `PasswordSignInSchema`. |
| Sign-out | Parity | — | Clears the query cache and the stored session. |
| Session persistence | Adapted | — | JWT in Keychain / Keystore via `expo-secure-store`, not an httpOnly cookie. Restored on cold start. |
| MFA challenge (TOTP) | Parity | — | Factor picker, auto-submit on the sixth digit. |
| Sign-up | Adapted | `EXPO_PUBLIC_AUTH_PASSWORD` | Confirmed with a 6-digit emailed code, not a confirmation link. |
| Password reset | Adapted | — | Three steps: request, enter the emailed code, set the new password. No link round-trip. |
| Google OAuth | Parity | `EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS` | PKCE in the system browser, then `exchangeCodeForSession`. Reuses your existing Supabase Web OAuth client. |
| Other OAuth providers | Parity | `EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS` | Provider-generic: enabling one is configuration plus a logo asset, not code. |
| Sign in with Apple | Adapted | `EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS` (off) | iOS only, since Android filters it out. A native `signInWithIdToken` sub-flow, not the browser flow. **Off by default**, because the entitlement makes even simulator builds require a paid Apple signing team. Required by App Store review once you offer Google — see [store requirements](#platform-and-store-requirements). |
| Accept a team invitation | Adapted | — | Matched to your verified email inside the app, not by opening an emailed link. |
| Passwordless / magic link | Planned | `EXPO_PUBLIC_AUTH_MAGIC_LINK`, `EXPO_PUBLIC_AUTH_OTP` | The flags exist and are read; no screens consume them yet. |
| Passkeys: list, rename, delete | Planned | — | Web's passkey code is browser-only, but `supabase-js` exposes a headless `auth.passkey.*` API that works in React Native. Management needs no new infrastructure. |
| Passkeys: register and sign in | Planned † | — | Needs relying-party domain association: a real HTTPS domain, Apple Team ID, Android release SHA-256. A `localhost` RP cannot be bound, so this is not locally testable. |
| Captcha (Turnstile) | Web-only | — | Native carries no Turnstile token. Bot protection is rate-limiting instead. **Never enable the global GoTrue captcha toggle.** It gates sign-in, OTP and reset for every client and breaks native auth outright. |
| Recovery codes | Web-only | — | Not surfaced on either platform today. |

{% alert type="warning" title="Email flows use typed codes by design" %}
Cookie-style email-link parity is impossible on a native app: the browser your mail client opens has no shared cookie jar with the app. Every email flow therefore uses a typed code, so sign-up, password reset and invitation acceptance need no deep-link infrastructure.

Printing a code alongside the link also makes three backend settings load-bearing: code length, expiry and verification rate limit. They are shared web/native policy, so they are yours to choose; `docs/native/config-requirements.mdoc` lists the defaults and why they matter.
{% /alert %}

## Personal account

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| Display name | Parity | — | |
| Avatar upload | Parity | — | Uploads to the `account_image` bucket. |
| Change password | Parity | — | |
| MFA enroll (TOTP) | Adapted | — | No QR scanner. Opens an `otpauth://` link into your authenticator, with a copyable secret as fallback. |
| MFA factor list / unenroll | Parity | — | |
| Delete account | Parity | `EXPO_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION` (off) | OTP-confirmed. Both stores require in-app deletion, so this cannot be a link out to web. |
| Change email | Planned | — | Read-only today, with a "manage on web" notice. Needs a double confirmation, old address and new. |
| Identity linking | Planned | `EXPO_PUBLIC_AUTH_IDENTITY_LINKING` (off) | Unblocked now that OAuth ships; it reuses the same flow. |
| Language preference | Planned | — | The row exists but is disabled: the app ships English only. |

## Team accounts

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| Create a team | Parity | `EXPO_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION` (on) | |
| Rename, change slug | Parity | — | Slug regenerates from the name. Primary-owner-only, enforced by RLS. |
| Team logo | Parity | — | |
| Leave a team | Parity | — | Typed confirmation. The primary owner is refused. |
| Delete a team | Planned | `EXPO_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION` (off) | Shows a "manage on web" notice today. The OTP-confirm pattern it needs already ships with personal deletion, so this is a port rather than new infrastructure. |
| Transfer ownership | Planned | — | Visible to the primary owner as a disabled row today, mirroring web's visibility. Reuses the same OTP-confirm pattern. |

## Members, invitations and roles

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| Members list | Parity | — | |
| Invite members | Parity | — | |
| Pending invitations list | Parity | — | |
| Change an invitation's role | Parity | — | |
| Revoke an invitation | Parity | — | |
| Renew an invitation | Parity | — | |
| Change a member's role | Parity | — | |
| Remove a member | Parity | — | Per-seat billing decrements server-side, same service as web. |
| Roles and permission gating | Parity | — | The role list is hierarchy-filtered server-side, so the app never sees roles it may not assign. |

## Billing

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| Subscription status | Adapted | — | Read-only badge in team settings. No personal-account view. |
| Plan selection, checkout, customer portal | Web-only | — | A commercial decision rather than a technical gap. See the note below. |
| Per-seat and metered billing | Web-only | — | Seat counts still update correctly from native member changes, because the shared service owns them. |
| Billing webhooks | Web-only | — | Server infrastructure; no client involvement. |

{% alert type="info" title="Why billing stays on web" %}
Selling digital subscriptions inside a mobile app means platform in-app purchase: a 15–30% commission, and anti-steering rules on how you may mention other payment options. It also shares no code with the kit's Stripe or Lemon Squeezy integration: IAP brings its own purchase flow, receipt model and reconciliation path.
{% /alert %}

## Notifications

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| In-app list, bell, unread badge | Planned | — | No native surface. Portable: it reads Supabase directly, so no new infrastructure. Web's `notifications` strings are already in the inherited catalogue. |
| Mark as read / dismiss | Planned | — | |
| Realtime delivery | Planned | — | `supabase.channel()` works from native as-is. |
| Push notifications | Planned † | — | A separate track from the in-app bell: APNs and FCM credentials, per-device token storage, an OS permission prompt, and server-side sending. Delivered by the OS even when the app is closed. |

## App shell and cross-cutting

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| Workspace switcher | Parity | — | Personal plus teams, with create. |
| Workspace context | Parity | — | Selection persisted in secure storage. |
| Navigation | Adapted | — | Native tab bars (Home, Settings, plus Members for a team) instead of web's sidebar. |
| User menu | Parity | — | |
| Theme: light / dark / system | Parity | — | Web's `enableThemeToggle` has no native counterpart; the toggle always ships. |
| Membership and route guards | Parity | — | Enforced in native layouts, same rules as web's. |
| i18n infrastructure | Adapted | — | Inherits web's catalogue and keeps only a local delta, so a string fixed on web propagates. Ships English only. |
| Home dashboard content | Planned | — | Thin placeholders on both personal and team contexts, as on web. |
| App-level language switcher | Planned | — | Only worth wiring once you ship a second locale. |
| Version updater | Web-only | — | Native has no over-the-air update channel configured (`expo-updates` is not installed); app updates go through the stores. |

## Admin and super-admin

| Feature | Status | Flag | Notes |
| --- | --- | --- | --- |
| Admin dashboard | Web-only | — | |
| Browse and inspect accounts | Web-only | — | |
| Ban, reactivate, delete a user | Web-only | — | |
| Delete an account | Web-only | — | |
| Impersonate a user | Web-only | — | |
| Reset a user's password | Web-only | — | |

Administration is an operator tool. `is_super_admin()` returns false unless the session is AAL2, so super-admin plus MFA is enforced in the database rather than only in the app. Shipping the surface in a store-distributed binary would widen the attack surface for no user benefit, so the whole domain stays on web.

## Marketing and content

| Feature | Status | Notes |
| --- | --- | --- |
| Landing page, pricing, FAQ, contact | Web-only | Acquisition surfaces, aimed at visitors who have not installed the app. |
| Blog, changelog | Web-only | |
| Legal pages (terms, privacy) | Adapted | Only the sign-up terms checkbox, which links out to your deployed web app, so `EXPO_PUBLIC_SITE_URL` must point at a real host in a release build. |
| Documentation site | Web-only | |
| CMS integration (Keystatic, WordPress) | Web-only | Content rendering is a web concern; native imports no CMS client. |

The app's catalogue never inherits the `marketing` message namespace, so those strings stay out of the bundle.

## Native-only features

Capabilities with no web counterpart, most of them required by the app stores.

| Feature | Status | Notes |
| --- | --- | --- |
| Secure session storage | Parity | Keychain / Keystore via `expo-secure-store`. |
| Device-locale detection | Parity | Picks the initial language from the OS. |
| Device info diagnostics | Planned | `features/device-info/device-info-card.tsx` ships as a component, but nothing renders it — there is no route and no entry point. Mount it on a screen of your own to use it. |
| In-app account deletion | Parity | Web offers it too, but on native it is store-mandated rather than optional. |
| Universal Links | Planned † | Not implemented: no flag, no `associatedDomains`, no `intentFilters`. Needs hosted `.well-known` files, an Apple Team ID and an Android release SHA-256. Kept off the auth critical path, since typed codes made it unnecessary. |
| Biometric sign-in | Planned † | Needs the same domain association as passkey registration. |

## Integrations

| Integration | Status | Notes |
| --- | --- | --- |
| Supabase Auth | Parity | Called directly with the JS SDK. |
| Supabase Database | Adapted | Reached over the kit's versioned `/api/v1/*` endpoints rather than directly, so authorization and business rules stay in one place. |
| Supabase Storage | Parity | Direct uploads for avatars and logos. |
| Supabase Realtime | Planned | Works from native; nothing consumes it yet. |
| Stripe / Lemon Squeezy | Web-only | See [Billing](#billing). |
| Resend / Nodemailer | Web-only | Server-side senders. Native triggers emails through the API, never sends them. |
| Sentry / monitoring | Planned | Web's provider is wired for a Next.js runtime; native error reporting is not configured. |
| Analytics providers | Planned | No native analytics client. |
| Cloudflare Turnstile | Declined | No native SDK exists. Wrapping the widget in a WebView was rejected as a poor trade for a sign-up form. |
| Keystatic / WordPress CMS | Web-only | |

## Platform and store requirements

These gate shipping rather than adding features. `docs/native/config-requirements.mdoc` has the configuration detail.

| Requirement | Trigger | Status |
| --- | --- | --- |
| **Sign in with Apple** (App Store 4.8) | Offering Google or any social login on iOS | Implemented, off by default. Turn it on before an iOS submission. |
| **Apple token revocation** (App Store 5.1.1(v)) | Sign in with Apple **and** account deletion both present | **Planned †. A hard blocker for iOS submission.** Apple requires revoking the token on deletion, and Supabase does not do it for you: it needs an Apple Services ID and `.p8` key, a server-side authorization-code exchange at sign-in to capture a refresh token, and a revoke call during deletion. |
| **In-app account deletion** (App Store 5.1.1(v), Play) | Any in-app account creation | Implemented. Must stay reachable in-app; a link to web is not accepted. |
| **Privacy labels / Play Data Safety** | OAuth collecting email or name | Declare at submission. Revisit when adding a provider. |

## Rejected approaches

Recorded so they aren't proposed again.

| Approach | Why not |
| --- | --- |
| A WebView wrapper around the web app | The point of the native app is real native screens against a shared API. A WebView inherits web's cookie and navigation model and fails the "minimum functionality" bar at review. May still suit an isolated advanced surface later. |
| `@expo/ui` native components | Looks more native, but most components would need separate iOS and Android implementations, and its styling model diverges from the rest of the repo. Staying on cross-platform primitives keeps the component API matched to web's. |
| Origin-aware emailing (different email for app vs web) | There is no reliable platform signal at send time, and the requesting device is often not the opening device. |
| A QR code for MFA enrollment | Needs a camera permission and a scanner dependency to solve a problem an `otpauth://` link already solves, on the device that holds the authenticator. |
| Turnstile in a WebView | See [Integrations](#integrations). |

## Configuration parity

Every public setting web exposes, and whether native honours it. Where a setting means the same thing on both, native reuses web's name with an `EXPO_PUBLIC_` prefix, so an existing `.env` mostly transfers by search-and-replace.

Statuses read the same as the feature tables: **Parity** mirrored, **Adapted** honoured differently, **Planned** no native setting yet, **Web-only** / **Declined** never will be. Build-tooling variables (`NEXT_PUBLIC_CI`) are omitted.

{% alert type="warning" title="Every EXPO_PUBLIC_ value is baked into the bundle" %}
They are public and build-time. Changing one needs a new build to reach an installed app, and a mismatch with web is silent: a password rule set on web but not native means native accepts passwords web rejects.
{% /alert %}

### Feature flags

| Web | Native | Status | Notes |
| --- | --- | --- | --- |
| `ENABLE_TEAM_ACCOUNTS` | `EXPO_PUBLIC_ENABLE_TEAM_ACCOUNTS` (on) | Parity | |
| `ENABLE_TEAM_ACCOUNTS_ONLY` | `EXPO_PUBLIC_ENABLE_TEAM_ACCOUNTS_ONLY` (off) | Parity | |
| `ENABLE_TEAM_ACCOUNTS_CREATION` | `EXPO_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION` (on) | Parity | |
| `ENABLE_TEAM_ACCOUNTS_DELETION` | `EXPO_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION` (off) | Parity | Gates the "manage on web" notice, since the native flow is still Planned. The committed `.env.development` sets it to `true`, so it is on in local dev. |
| `ENABLE_PERSONAL_ACCOUNT_DELETION` | `EXPO_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION` (off) | Parity | The committed `.env.development` sets it to `true`, so it is on in local dev. |
| `LANGUAGE_PRIORITY` | — | Adapted | Native always resolves from the device locale, which is web's `user` setting. Web defaults to `application`. Not configurable natively. |
| `ENABLE_THEME_TOGGLE` | — | Planned | The toggle always ships; hiding it is a small addition nobody has needed yet. |
| `ENABLE_NOTIFICATIONS` | — | Planned | Arrives with the notifications surface. |
| `REALTIME_NOTIFICATIONS` | — | Planned | Same. |
| `ENABLE_PERSONAL_ACCOUNT_BILLING` | — | Web-only | |
| `ENABLE_TEAM_ACCOUNTS_BILLING` | — | Web-only | |
| `ENABLE_VERSION_UPDATER`, `VERSION_UPDATER_REFETCH_INTERVAL_SECONDS` | — | Declined | Store-distributed builds, no OTA channel. |

### Authentication

| Web | Native | Status | Notes |
| --- | --- | --- | --- |
| `AUTH_PASSWORD` | `EXPO_PUBLIC_AUTH_PASSWORD` (on) | Parity | |
| `DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX` | `EXPO_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX` (off) | Parity | Turning it on also requires `EXPO_PUBLIC_SITE_URL` to point at your deployed web app, since the two links open there. |
| `PASSWORD_REQUIRE_SPECIAL_CHARS`, `_NUMBERS`, `_UPPERCASE` | `EXPO_PUBLIC_PASSWORD_REQUIRE_*` (all off) | Parity | Mirror these or native accepts what web rejects. Both platforms validate client-side only, and the settings change-password screen honours neither. |
| `AUTH_MAGIC_LINK` | `EXPO_PUBLIC_AUTH_MAGIC_LINK` (off) | Planned | The flag is read; no screen consumes it. |
| `AUTH_OTP` | `EXPO_PUBLIC_AUTH_OTP` (off) | Planned | Same. |
| `AUTH_IDENTITY_LINKING` | `EXPO_PUBLIC_AUTH_IDENTITY_LINKING` (off) | Planned | Same. |
| `AUTH_PASSKEY` | — | Planned | |
| `CAPTCHA_SITE_KEY`, `CAPTCHA_WIDGET_SIZE` | — | Declined | No native Turnstile SDK. See [Authentication](#authentication-and-session). |
| — (web hardcodes `oAuth: ['google']`) | `EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS` | Adapted | Native made the provider list configurable; web still edits the array in `auth.config.ts`. |

### Branding and appearance

| Web | Native | Status | Notes |
| --- | --- | --- | --- |
| `PRODUCT_NAME` | `EXPO_PUBLIC_PRODUCT_NAME` | Parity | |
| `SITE_URL` | `EXPO_PUBLIC_SITE_URL` | Adapted | On web it is the app's own origin; on native it is where outbound links go. A loopback default is dead in a release build. |
| `DEFAULT_THEME_MODE` | — | Planned | Native defaults to following the OS and persists the user's choice; the default is not yet configurable. |
| `THEME_COLOR`, `THEME_COLOR_DARK` | — | Declined | Web writes these into the document head for browser chrome. Native colours its navigation chrome from the theme tokens through React Navigation, and `app.json` sets the static launch background. No env override. |
| `DEFAULT_LOCALE` | — | Adapted | Resolved from the device locale. The app ships English only. |
| `SITE_TITLE`, `SITE_DESCRIPTION` | — | Declined | Document-head metadata for search engines and social previews. An app has no document head; the store listing plays that role. |

### Navigation and layout

| Web | Native | Status | Notes |
| --- | --- | --- | --- |
| `USER_NAVIGATION_STYLE`, `TEAM_NAVIGATION_STYLE` | — | Declined | Web picks sidebar or header. Native uses platform tab bars, so there is no equivalent setting to expose. |
| `HOME_SIDEBAR_COLLAPSED`, `TEAM_SIDEBAR_COLLAPSED`, `SIDEBAR_COLLAPSIBLE_STYLE` | — | Declined | No sidebar exists to collapse. |

Web's navigation *items* come from `personal-account-navigation.config.tsx` and `team-account-navigation.config.tsx`. Native declares its tabs in the route layouts instead, so adding a screen to one platform does not add it to the other.

### Billing

| Web | Native | Status |
| --- | --- | --- |
| `BILLING_PROVIDER` | — | Web-only |

### Native-only settings

No web counterpart, because web has no equivalent need.

| Setting | Purpose |
| --- | --- |
| `EXPO_PUBLIC_API_BASE_URL` | Which web deployment serves `/api/v1/*`. The one knob for pointing the app at a different backend. |
| `EXPO_PUBLIC_SUPABASE_URL`, `EXPO_PUBLIC_SUPABASE_PUBLIC_KEY` | Direct SDK access for auth, MFA and storage. |

The mirrored ones are `enableTeamAccounts`, `enableTeamsOnly`, `enableTeamCreation`, `enableTeamDeletion` and `enableAccountDeletion`, each with an `EXPO_PUBLIC_*` twin. Native also adds auth and password-policy flags of its own. Both sets, and what a change to one forces elsewhere, are in `docs/native/config-requirements.mdoc`.
