Stack comparison
One option per category - what each one costs you (Expo Go, keys, native code) and what it requires
Every option of every category side by side. bun run setup asks one question per category; every answer is a module under modules/<category>/<option>/. Non-interactive: bun run setup --preset default --ui tamagui --auth supabase --yes. This page is generated from the same module.json files setup reads, so it can't drift from what ships. For how to choose, read Pick your stack first.
Presets
| Category | minimal | default | saas |
|---|---|---|---|
ui | nativewind4 | nativewind4 | nativewind4 |
data | none | react-query | react-query |
state | none | zustand | zustand |
storage | kv-store | kv-store | kv-store |
forms | none | react-hook-form | react-hook-form |
i18n | none | i18next | i18next |
auth | none | none | supabase |
payments | none | none | revenuecat |
analytics | none | none | posthog |
crash | none | none | sentry |
push | none | none | expo-notifications |
backend | none | none | api-routes |
onboarding | off | on | on |
location | none | expo-location | expo-location |
widgets | none | none | expo-widgets |
consent | none | none | consent |
testing | none | jest | jest |
The committed tree equals setup --preset default; saas is default plus every service category turned on.
UI (ui)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
nativewind4 | NativeWind 4 (default) | yes | Stable · Tailwind 3 classes + RNR components behind @/ui · Expo Go OK |
nativewind5 | NativeWind 5 | yes | RC · Tailwind 4 classes behind @/ui · Expo Go OK |
tamagui | Tamagui | yes | Tamagui 2 (v5 config, optimising compiler) behind @/ui · Expo Go OK |
unistyles | Unistyles 3 | no - dev build required | C++ StyleSheet with themes/breakpoints behind @/ui · dev build required (no Expo Go) |
stylesheet | StyleSheet (plain RN) | yes | no style engine, tokens + StyleSheet.create · Expo Go OK |
Data (data)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
react-query | TanStack Query (default) (most people pick this) | yes | server state + cache · Expo Go OK |
apollo | Apollo Client | yes | GraphQL client + normalized cache · Expo Go OK |
swr | SWR | yes | stale-while-revalidate hooks · Expo Go OK |
none | No data layer | yes | plain fetch; add TanStack Query/Apollo/SWR later |
State (state)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
zustand | Zustand (default) (most people pick this) | yes | tiny stores, persisted on the storage adapter · Expo Go OK |
jotai | Jotai | yes | atomic state, persisted on the storage adapter · Expo Go OK |
none | No state library | yes | built-in useSyncExternalStore stores only |
zustand: requiresstorage∈kv-store|mmkv|async-storagejotai: requiresstorage∈kv-store|mmkv|async-storage
Storage (storage)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
kv-store | expo-sqlite/kv-store (default) (most people pick this) | yes | sync + async key/value on SQLite · Expo Go OK |
mmkv | MMKV | no - dev build required | fastest sync key/value (Nitro) · dev build required (no Expo Go) |
async-storage | AsyncStorage | yes | async-only key/value, the community standard · Expo Go OK |
Forms (forms)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
react-hook-form | React Hook Form (default) (most people pick this) | yes | Form + Field on @/ui Input, zod resolver · Expo Go OK |
none | No forms library | yes | controlled @/ui Inputs with useState |
i18n (i18n)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
i18next | i18next (default) (most people pick this) | yes | react-i18next + expo-localization, 6 languages bundled incl. Arabic (RTL) · Expo Go OK |
lingui | Lingui | yes | @lingui/core + @lingui/react runtime API, 6 languages bundled incl. Arabic (RTL) · Expo Go OK |
none | No i18n | yes | i18n.t/useT return the English key |
i18next: requiresstorage∈kv-store|mmkv|async-storagelingui: requiresstorage∈kv-store|mmkv|async-storage
Auth (auth)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
supabase | Supabase Auth (default) (most people pick this) | yes | email+password, Apple (native), Google (OAuth) · session on the storage adapter · Expo Go OK |
clerk | Clerk | yes | email+code verification, Apple (native), Google (SSO) · @clerk/expo · Expo Go OK for JS flows |
better-auth | Better Auth | yes | self-hosted on the API routes · email/password + Apple/Google · Expo Go OK |
none | No auth | yes | auth.useSession() shim returns signed-out, signOut resolves |
supabase: requiresstorage∈kv-store|mmkv|async-storage; needsEXPO_PUBLIC_SUPABASE_URL,EXPO_PUBLIC_SUPABASE_ANON_KEYclerk: requiresstorage∈kv-store|mmkv|async-storage; needsEXPO_PUBLIC_CLERK_PUBLISHABLE_KEYbetter-auth: requiresbackend∈api-routes; needsBETTER_AUTH_SECRET,BETTER_AUTH_URL
Payments (payments)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
revenuecat | RevenueCat (default) (most people pick this) | no - dev build required | in-app subscriptions + paywall · dev build required (no Expo Go) |
adapty | Adapty | no - dev build required | in-app subscriptions + Paywall Builder · dev build required (no Expo Go) |
stripe | Stripe Checkout | yes | web checkout via API routes (no native SDK) · Expo Go OK |
none | No payments | yes | payments.useEntitlements() shim returns none |
revenuecat: needsEXPO_PUBLIC_REVENUECAT_IOS_KEY,EXPO_PUBLIC_REVENUECAT_ANDROID_KEYadapty: needsEXPO_PUBLIC_ADAPTY_PUBLIC_KEYstripe: requiresbackend∈api-routes; needsEXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_PRICE_ID
Analytics (analytics)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
posthog | PostHog (default) (most people pick this) | yes | product analytics + feature flags (analytics.isFeatureEnabled / useFeatureFlag) · Expo Go OK |
amplitude | Amplitude | yes | product analytics · Expo Go OK (JS SDK, storage adapter) |
none | No analytics | yes | analytics.track/identify/screen are no-ops, feature flags read undefined |
posthog: requiresstorage∈kv-store|mmkv|async-storage; needsEXPO_PUBLIC_POSTHOG_KEYamplitude: requiresstorage∈kv-store|mmkv|async-storage; needsEXPO_PUBLIC_AMPLITUDE_KEY
Crash reporting (crash)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
sentry | Sentry (default) (most people pick this) | yes | JS + native crash reporting · Expo Go OK (JS errors only) |
none | No crash reporting | yes | crash.capture is a no-op |
sentry: needsEXPO_PUBLIC_SENTRY_DSN
Push notifications (push)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
expo-notifications | Expo Notifications (default) (most people pick this) | no - dev build required | Expo push service + local notifications · dev build required for remote push (Expo Go: local only) |
none | No push notifications | yes | nothing registered |
expo-notifications: requiresstorage∈kv-store|mmkv|async-storage
Backend (backend)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
api-routes | API routes (default) (most people pick this) | yes | Expo Router +api.ts routes · src/server helpers · EAS Hosting · Expo Go OK |
none | No API routes | yes | no src/app/api |
Onboarding (onboarding)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
on | Onboarding flow (default) | yes | 3-slide intro at /onboarding, redirect until completed |
off | No onboarding | yes | app opens straight into the tabs |
Location (location)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
expo-location | Expo Location (default) (most people pick this) | yes | foreground position + reverse geocoding, permission states handled · Expo Go OK |
none | No location | yes | no permission prompt, no expo-location |
Home-screen widgets (widgets)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
expo-widgets | Expo Widgets (default) (most people pick this) | no - dev build required | iOS home-screen widget + Live Activity (Lock Screen, Dynamic Island) in SwiftUI (@expo/ui), fed from the app · dev build required (no Expo Go) |
none | No widgets | yes | no app extension, widgets.update() is a no-op |
Privacy consent (consent)
| Option | Label | Expo Go | Hint |
|---|---|---|---|
consent | Consent sheet (GDPR / CCPA) (default) (most people pick this) | yes | asks before analytics + crash start (EU/EEA, UK, Switzerland, Canada, Brazil or everywhere); Settings toggles · Expo Go OK |
none | No consent sheet | yes | consent.get() reads all-true; analytics and crash SDKs start as before |