Native configuration and requirements matrix | Expo Supabase SaaS Kit
Look up what each native feature needs configured — across app env, app.json, Supabase, provider consoles and the app stores — and what a change to one setting forces you to change elsewhere.
Native features fan configuration out across many surfaces (app env, app.json, Supabase local and hosted, provider consoles, the app stores), and one change frequently forces another — the native URL scheme dictates a Supabase redirect-URL entry, for instance. This page is the single place to look up what a feature needs configured and what a config change triggers elsewhere. It tracks operational reality, not roadmap.
1. Config surfaces (where config lives)
| Surface | Location | Notes |
|---|---|---|
| Native build-time env | apps/native/.env.development, .env.local, EAS secrets | EXPO_PUBLIC_* only, and they are PUBLIC — baked into the JS bundle. Never put secrets here. |
| Native app config | static apps/native/app.json + dynamic apps/native/app.config.ts (extends app.json for computed values — currently the conditional Sign-in-with-Apple plugin; later Universal Links) | scheme, ios.bundleIdentifier, android.package, plugins (config plugins → native entitlements), associatedDomains/intentFilters (Universal Links). |
| Supabase — local | apps/web/supabase/config.toml | [auth] site_url + additional_redirect_urls, [auth.external.*], [auth.rate_limit], [auth.mfa.*], email templates. |
| Supabase — hosted | Dashboard → Authentication | Must mirror config.toml. Hosted is the source of truth in prod; the local stack can't do real Google/Apple OAuth (test OAuth against a hosted dev project). |
| Google Cloud Console | OAuth clients | The existing Web client (Supabase callback) is reused by the web-browser flow — no new client needed. A native Google SDK flow (not used) would need iOS/Android clients. |
| Apple Developer | App ID + Sign in with Apple capability, Services ID, signing key | Needed for Sign in with Apple. |
| App Store Connect / Play Console | Store listing, privacy, review | Guideline 4.8 (Sign in with Apple), account deletion, data-safety/privacy labels. |
2. Cross-config coupling / triggers ⚠️ (the important part)
When you change the left column, you must also do the right column.
| If you change / add… | …you must also… | Type |
|---|---|---|
Native app.json scheme (currently mkkit) | Update apps/web/supabase/config.toml → additional_redirect_urls (<scheme>://**) and the hosted dashboard → Auth → URL Configuration → Redirect URLs. The native OAuth redirectTo = makeRedirectUri({ scheme }), so the Supabase allow-list is derived from the native scheme. | URL coupling |
| Enable any OAuth provider (Google, etc.) on iOS | Offer Sign in with Apple (App Store Guideline 4.8). Google-only on iOS risks rejection. | STORE |
| Offer Sign in with Apple and in-app account deletion | Revoke the Apple token on deletion via /auth/revoke (Guideline 5.1.1(v)). Needs an Apple Services ID + .p8 key + a captured refresh token (authcode exchange at sign-in). Supabase does not do this — the app must. | STORE |
Add a native module that ships a config plugin (e.g. expo-apple-authentication) | Add it to plugins (via app.json or app.config.ts), run npx expo prebuild, ship a new dev build (Expo Go can't load it). | BUILD |
Enable Sign in with Apple — add apple to EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS | The applesignin entitlement makes even simulator builds require a paid Apple Developer signing team (expo run:ios → "No code signing certificates" without one). Expo autolinks expo-apple-authentication's plugin from the dependency, so the entitlement is injected unless app.config.ts strips it — which it does whenever the flag lacks apple. After toggling, do a clean prebuild (rm -rf apps/native/ios first). iOS works fine without Apple (Google + password) — Apple is a store-submission gate (4.8), not functional. | BUILD / STORE |
| Enable the global GoTrue captcha toggle in Supabase | ❌ Don't. It gates sign-in/OTP/reset for all clients and breaks token-less native auth (native has no Turnstile token). Rate-limiting is the substitute — but see the ⚠️ note below: it is not yet tightened, so today neither guard is really in place for native sign-up. | ANTI-TRIGGER |
Add a provider to EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS | Enable that provider in Supabase (local and hosted) + ensure a logo asset exists in the native provider-logo registry. | feature |
Change ios.bundleIdentifier / android.package (dev.makerkit.app) | Re-do: Apple App ID, Google/Apple console redirect URIs, Universal-Links AASA/assetlinks.json, store identity. | identity (high blast radius) |
Add {{ .Token }} to an email template | Verify the per-flow template mapping in config.toml; the edit is additive (web link + native code coexist) but affects both web and native rendering. | shared-template |
| Point a native email flow (sign-up / reset) at a hosted project | Configure custom SMTP in the dashboard first. On the built-in provider, auth emails are capped at a handful per hour project-wide and the cap is raisable only via custom SMTP, so the flows can't be exercised. Makerkit's going-to-production guide covers provider choice and the SPF/DKIM/DMARC records. | BLOCKER |
Turn on EXPO_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX (mirrors NEXT_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX) | Point EXPO_PUBLIC_SITE_URL at the deployed web app — the checkbox's two links open <site>/terms-of-service and /privacy-policy in a device browser, so a loopback default is dead in a release build. Dev is already handled (the links go through resolveDevHost, so the Android emulator rewrite applies); a physical device still needs the LAN IP. Both values are build-time inlined, so a new native build is needed. | feature (silent failure) |
Set / change any NEXT_PUBLIC_PASSWORD_REQUIRE_{SPECIAL_CHARS,NUMBERS,UPPERCASE} | Mirror it to EXPO_PUBLIC_PASSWORD_REQUIRE_* in apps/native/.env.* and ship a new native build — the value is inlined into the JS bundle, so an env change alone does nothing to installed apps. A mismatch is silent: native accepts passwords web rejects. Both apps validate client-side only (see the note below). | feature (silent failure) |
Bump the Expo SDK in pnpm-workspace.yaml → catalogs.expo | Bump the overrides: block in the same file in lockstep (currently expo-constants, react-native-svg). pnpm overrides win over catalog entries, so a stale entry silently holds a package on the previous SDK while everything else moves — pnpm install accepts it, only expo-doctor notices. Also: do not run npx expo install --fix — every native dep is catalog:expo (incl. packages/mobile-ui's peerDependencies), and --fix rewrites them to literal versions, destroying the convention. Hand-edit the catalog from the SDK's bundledNativeModules.json, then verify with expo install --check. | BUILD (silent failure) |
3. Per-feature config matrix
Legend: ✅ required · ➕ additive/no-behavior-change · — n/a
| Feature | Native env | app.json | config.toml | Hosted dashboard | Provider console | Store |
|---|---|---|---|---|---|---|
| Google OAuth (web-browser flow) | ✅ EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS=google | — (scheme already set; no plugin) | ➕ additional_redirect_urls += "mkkit://**" | ✅ mirror redirect URL; ✅ Google provider enabled | Reuse existing Web client | — |
| Additional OAuth providers (Facebook / GitHub / Microsoft…) | ✅ add ids to the comma list | — | — until enabled | ✅ enable each provider | per-provider app (Facebook / GitHub…) | — |
| Sign in with Apple (iOS-only) | ✅ add apple to the list (filtered out on Android; also gates the entitlement) | ✅ entitlement autolinked from the dep; app.config.ts strips it unless the flag has apple (+ clean prebuild) — needs a paid signing team even on sim | ✅ [auth.external.apple] enabled, client_id = "<bundle id>" | ✅ Apple provider Client IDs = bundle id (no secret for sign-in) | ✅ Apple Developer: App ID + Sign in with Apple capability (paid account) | ✅ Guideline 4.8 (sign-in) |
| Apple token revocation (required with Apple + account deletion) | — | — | — | ✅ Apple provider secret (Services ID + .p8 key) | ✅ Services ID, Key ID, .p8 signing key, Team ID | ✅ Guideline 5.1.1(v) — /auth/revoke on delete (app must call; Supabase doesn't) |
| Email OTP flows (magic link / OTP / sign-up / reset) | ✅ EXPO_PUBLIC_AUTH_MAGIC_LINK / _OTP | — | ✅ shipped — additive {{ .Token }} panel in confirm-email.html + reset-password.html; magic-link.html planned with passwordless sign-in; no [auth.rate_limit] / [auth.email] change — shared backend policy, see below | ✅ mirror both templates; read the config push warning below first | — | — |
| Password policy | ✅ EXPO_PUBLIC_PASSWORD_REQUIRE_{SPECIAL_CHARS,NUMBERS,UPPERCASE} (all false, mirroring web) | — | ➕ optional [auth] minimum_password_length / password_requirements (both unset) | ➕ mirror if set locally | — | — |
| Terms/privacy checkbox on sign-up | ➕ EXPO_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX (off, mirroring web) + EXPO_PUBLIC_SITE_URL for the two links | — | — | — | — | ➕ often expected at review when the app collects accounts |
| Universal Links (not implemented) | ⚠️ planned EXPO_PUBLIC_ENABLE_UNIVERSAL_LINKS — does not exist yet | ⚠️ none — no associatedDomains / intentFilters in app.json or app.config.ts | — | — | Apple Team ID; release SHA-256 | hosted .well-known/* |
(Extend rows as features are added.)
Mirroring templates to a hosted project
⚠️ config push is the dangerous one, not the dashboard. The CLI reads each [auth.email.template.*].content_path off disk and uploads the file body as mailer_templates_<flow>_content — verified in the pinned CLI 2.109.1. So pushing config for an unrelated reason (a redirect URL, a provider toggle, a rate limit — all of which the rows above tell you to mirror) ships every template in the same command, while you have honoured a "don't paste into the dashboard" instruction exactly.
After any mirror, re-check by firing every template against the hosted project.
⚠️ reset-password.html is load-bearing, not cosmetic. It is the one template that never interpolates {{ .RedirectTo }} — it hard-codes {{ .SiteURL }}/auth/confirm?…&type=recovery&next={{ .SiteURL }}/update-password. Replacing it with Supabase's stock recovery template therefore drops the user at the site root with a recovery session instead of on the password form, and no client can compensate: native deliberately sends no redirectTo because this template would ignore it, and web's own redirectTo is already dead weight for the same reason. Keep the hard-coded next, or change both the template and every caller together.
Typed-code preconditions — an operator decision, not something this repo forces
Printing {{ .Token }} turns a URL-only secret into a typed 6-digit one, so review these on the hosted project:
| Setting | Hosted default | Why it matters |
|---|---|---|
[auth.email] otp_expiry | 24 h | Sizes the brute-force window. 1 h is 24× smaller. Local CLI default is already 3600. |
[auth.rate_limit] token_verifications | 30 / 5 min / IP | The rate half of the bound on a 10⁶ keyspace. |
[auth.email] otp_length | 6 | Native's code entry is a fixed six-box input — an 8-digit code silently cannot be entered. |
They are deliberately left unset in config.toml: shared backend policy affecting web equally, so choosing values is the deploying operator's call. Left at hosted defaults, the code is a 6-digit secret valid for 24 hours.
⚠️ Sign-up abuse — the captcha substitute is not yet in place
Native sign-up deliberately ships no captcha (see the anti-trigger row above): the global GoTrue toggle would break every token-less native auth call, and no native Turnstile SDK exists. Rate-limiting is the intended substitute — but config.toml still carries upstream's permissive default:
| Setting | Current | Why it matters |
|---|---|---|
[auth.rate_limit] email_sent | 1000 / hour — upstream's default, untouched by this repo | The one knob bounding sign-up / OTP email flooding. At 1000 it bounds almost nothing. |
No config.toml value is changed here, by the same principle as the table above: [auth.rate_limit] is shared backend policy that throttles web's transactional auth email too, so picking a number is the deploying operator's call rather than something the native app decides unilaterally. Two consequences worth stating outright:
- Tighten it before exposing native sign-up publicly. Anything in the low tens per hour is a materially different posture from 1000.
- It throttles web as well. Check web's own flows (invitations, password reset, email confirmation) still fit under whatever ceiling you pick — a value tuned for native sign-up alone can silently starve a burst of team invitations.
Local template editing — kong bind-mount caveat
The CLI bind-mounts templates into kong, which can serve a size-changed file truncated to its old length — GoTrue then fails with 500 … ends in a non-text context, which looks like a syntax error but isn't. After any template edit, restart kong and auth and confirm the served length matches on-disk:
KONG=supabase_kong_next-supabase-saas-kit-turbo; AUTH=supabase_auth_next-supabase-saas-kit-turbodocker restart $KONG $AUTHdocker exec $AUTH sh -c "wget -qO- http://$KONG:8088/email/confirmation.html | wc -c"wc -c apps/web/supabase/templates/confirm-email.htmlPassword policy — where it is (and isn't) enforced
Both apps validate client-side only — sign-up calls auth.signUp straight from the browser or device, with no server action in between — so the zod schema is the whole gate. GoTrue's own minimum_password_length / password_requirements are left at their defaults deliberately: they are shared backend policy, not native config.
⚠️ The flags only reach the screens that use a policy-derived schema. Sign-up and the recovery new-password screen do; the settings change-password screen does not — it uses PasswordUpdateSchema from @kit/accounts/shared, a flat min(8) that ignores the requirement flags entirely. Web has the same gap on the same screen, so this is upstream's shape rather than a native regression, and turning the flags on leaves that one screen accepting passwords the others reject. Setting the requirements server-side closes it for every screen at once, which is the argument for doing so if you enable them at all.
Setting them server-side is safe and needs no client change — GoTrue rejects with weak_password + reasons[] and both apps render those reasons. One caveat if you do: password_requirements is a 4-level ladder ("" → letters_digits → lower_upper_letters_digits → lower_upper_letters_digits_symbols), so the kit's three independent booleans cannot be mirrored exactly — anything without digits has no server equivalent.
4. Store / account requirements (cross-cutting)
| Requirement | Trigger | Notes |
|---|---|---|
| Sign in with Apple (App Store 4.8) | Offering Google (or any social) login on iOS | iOS-only, native signInWithIdToken + hex-nonce flow. Sign-in config = bundle id in Apple Client IDs (no secret). Required before iOS submission. |
| Apple token revocation (App Store 5.1.1(v)) | Sign in with Apple and account deletion both present | App must call Apple /auth/revoke on delete; needs Services ID + .p8 key + a stored refresh token (authcode exchange at sign-in). Supabase doesn't handle it. |
| In-app account deletion (App Store 5.1.1(v) / Play) | Any account creation in-app | Must be reachable in-app; cannot link out to web. |
| Privacy labels / Play Data Safety | OAuth collects email / name | Declare on submission; revisit when adding providers. |
5. Conventions
EXPO_PUBLIC_*are public (bundled) — config flags, not secrets.- Local
config.toml↔ hosted dashboard must stay in sync; hosted wins in prod. - OAuth (Google + generic) is provider-generic in code — enabling a new provider is config + a logo, not code (except Apple-on-iOS, which is a native sub-flow).
- Sign in with Apple is OFF by default and build-gated. Note the Expo gotcha:
expo-apple-authenticationis a dependency (the dormant code imports it), and Expo autolinks its config plugin at prebuild, so theapplesigninentitlement is injected just by the package being installed — not by thepluginsarray. Soapp.config.tsstrips that entitlement unlessEXPO_PUBLIC_AUTH_OAUTH_PROVIDERScontainsapple(one flag drives both the button and the entitlement). Toggling requires a clean re-prebuild (rm -rf apps/native/ios— a merge prebuild keeps the stale entitlement); enabling needs a paid Apple Developer account or the build fails signing. The iOS app is fully usable without Apple (Google + password) — Apple is only required before public App Store submission.