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)

SurfaceLocationNotes
Native build-time envapps/native/.env.development, .env.local, EAS secretsEXPO_PUBLIC_* only, and they are PUBLIC — baked into the JS bundle. Never put secrets here.
Native app configstatic 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 — localapps/web/supabase/config.toml[auth] site_url + additional_redirect_urls, [auth.external.*], [auth.rate_limit], [auth.mfa.*], email templates.
Supabase — hostedDashboard → AuthenticationMust 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 ConsoleOAuth clientsThe 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 DeveloperApp ID + Sign in with Apple capability, Services ID, signing keyNeeded for Sign in with Apple.
App Store Connect / Play ConsoleStore listing, privacy, reviewGuideline 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.tomladditional_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 iOSOffer 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 deletionRevoke 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_PROVIDERSThe 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 SupabaseDon'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_PROVIDERSEnable 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 templateVerify 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 projectConfigure 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.yamlcatalogs.expoBump 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

FeatureNative envapp.jsonconfig.tomlHosted dashboardProvider consoleStore
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 enabledReuse existing Web client
Additional OAuth providers (Facebook / GitHub / Microsoft…)✅ add ids to the comma list— until enabled✅ enable each providerper-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 / _OTPshipped — 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 policyEXPO_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-upEXPO_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_LINKSdoes not exist yet⚠️ none — no associatedDomains / intentFilters in app.json or app.config.tsApple Team ID; release SHA-256hosted .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:

SettingHosted defaultWhy it matters
[auth.email] otp_expiry24 hSizes the brute-force window. 1 h is 24× smaller. Local CLI default is already 3600.
[auth.rate_limit] token_verifications30 / 5 min / IPThe rate half of the bound on a 10⁶ keyspace.
[auth.email] otp_length6Native'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:

SettingCurrentWhy it matters
[auth.rate_limit] email_sent1000 / hour — upstream's default, untouched by this repoThe 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-turbo
docker restart $KONG $AUTH
docker exec $AUTH sh -c "wget -qO- http://$KONG:8088/email/confirmation.html | wc -c"
wc -c apps/web/supabase/templates/confirm-email.html

Password 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_digitslower_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)

RequirementTriggerNotes
Sign in with Apple (App Store 4.8)Offering Google (or any social) login on iOSiOS-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 presentApp 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-appMust be reachable in-app; cannot link out to web.
Privacy labels / Play Data SafetyOAuth collects email / nameDeclare 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-authentication is a dependency (the dormant code imports it), and Expo autolinks its config plugin at prebuild, so the applesignin entitlement is injected just by the package being installed — not by the plugins array. So app.config.ts strips that entitlement unless EXPO_PUBLIC_AUTH_OAUTH_PROVIDERS contains apple (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.