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.
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.
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
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. |
| 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. |
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.
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. |
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.
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. |
| 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. |
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.
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.
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. |
— (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.