Every week, first-time React Native founders ask us the same question: what stack should I use?
Here's our copy-pasted answer, with the receipts.
TL;DR
Client: Expo SDK 54 (managed) + Expo Router + TypeScript
Styling: NativeWind v4
Data: TanStack Query v5 + React Hook Form
Motion: Reanimated 4.1
Backend: Supabase (Postgres + Auth + Storage + Edge Functions + RLS)
Payments: Stripe (payment mode, one-time)
Builds: EAS Build + EAS Update
No Redux. No Firebase. No custom auth. No Fastlane. No bare workflow.
Why "boring"
You have zero product-market fit information on day zero. Every stack decision you make on day zero is a bet without data. The correct bet is the one that:
- Has the most Stack Overflow answers
- Has the shortest "hello world → first user" path
- Has the lowest switching cost when you learn you were wrong
That's a boring stack, definitionally.
The client
Expo SDK 54 (managed workflow)
npx create-expo-app@latest my-app
cd my-app
npx expo start
Don't expo prebuild. Don't eject. The managed workflow gives you EAS Build (cloud iOS builds without a Mac), expo-updates for OTA JS pushes, and pre-wrapped native modules for 90% of what you'll need.
Expo Router
File-based routing. If you know Next.js App Router, you know Expo Router.
app/
├── (tabs)/
│ ├── _layout.tsx
│ ├── index.tsx
│ └── profile.tsx
├── notes/
│ └── [id].tsx
└── _layout.tsx
Deep links, universal links, and typed routes come for free. Docs:
NativeWind v4
Tailwind for React Native.
<View className="flex-1 bg-white dark:bg-neutral-900 px-4">
<Text className="text-2xl font-semibold text-neutral-900 dark:text-white">
Hello
</Text>
</View>
Your team already knows Tailwind. Move on.
Data layer
TanStack Query v5 for anything that touches the network
const { data, isPending, error } = useQuery({
queryKey: ['notes', userId],
queryFn: () => supabase.from('notes').select('*').eq('user_id', userId),
})
Cache invalidation, background refetch, optimistic updates, offline persistence — all included. You do not need Redux.
React Hook Form for forms
const { control, handleSubmit } = useForm<Note>()
<Controller
control={control}
name="title"
render={({ field }) => (
<TextInput onChangeText={field.onChange} value={field.value} />
)}
/>
Boring. Fast. Good TypeScript types. The end.
Motion: Reanimated 4.1
const offset = useSharedValue(0)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}))
const gesture = Gesture.Pan().onUpdate((e) => {
offset.value = e.translationX
})
Worklets run on the UI thread. 60fps by default. No bridging.
The backend: Supabase, all of it
The reason Supabase is boring in the good way: it's Postgres. When your query is slow at 11pm, you open the SQL editor and read the plan.
Row-Level Security (non-negotiable)
Enable RLS on every table before the table has data:
alter table notes enable row level security;
create policy "notes_owner_select"
on notes for select
using (auth.uid() = user_id);
create policy "notes_owner_insert"
on notes for insert
with check (auth.uid() = user_id);
Now no client bug, no leaked JWT, no misconfigured API route can leak user A's rows to user B. The database refuses. This is the single highest-leverage security decision you make.
Edge Functions for secrets
Every third-party API key (OpenAI, Anthropic, Stripe, Deepgram) lives in an Edge Function. Zero secrets in the client bundle:
Deno.serve(async (req) => {
const apiKey = Deno.env.get('OPENAI_API_KEY')!
// ... call OpenAI, stream response back via SSE
})
Payments: Stripe (one-time, not subscriptions)
First-time founders default to subscriptions because that's what YC tells them to want. Ship one-time first — simpler dunning, faster checkout, no churn number to obsess over on Monday mornings.
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/cancel`,
})
Webhook → product_licenses table → grant unlocked. Applighter itself ships in this exact pattern — every template in the catalog uses this one-time-purchase flow end-to-end.
Builds: EAS
eas build --platform ios --profile production
eas submit --platform ios --latest
eas update --branch production --message "fix: crash on launch"
Cloud iOS builds. No Mac required. OTA updates ship in minutes instead of days.
Comparison table
| Piece | Boring pick | Trendy alt | Why v1 skips trendy |
|---|---|---|---|
| Framework | Expo managed | Bare RN | 2 weeks re-wrapping native libs |
| Styling | NativeWind | Tamagui | Your team knows Tailwind |
| State | TanStack Query + useState | Redux / Zustand | 95% of state is server state |
| Backend | Supabase | Firebase | Firestore's query model is a trap |
| Auth | Supabase Auth | Clerk | It's already bundled with the DB |
| Payments | Stripe one-time | RevenueCat subs | Subs are v2 |
| Builds | EAS | Fastlane | Cloud-native, no Mac needed |
What we deliberately omit
- Redux / Zustand — see above
- GraphQL — Postgres + PostgREST is enough
- Sentry — recommended, but not day one
- Monorepo — great for 3+ apps, overkill for one
- Detox / Maestro — unit tests first, E2E after 6 months
The AI stack question
"But my app is an AI app — does the stack change?"
Almost not at all:
+ Supabase Edge Function (holds OPENAI_API_KEY)
+ Postgres table for chat history (RLS on)
+ SSE streaming back to client (rendered with Reanimated)
That's it. AI features are just features.
The playbook
-
npx create-expo-appon latest stable SDK - Add NativeWind + Expo Router + Reanimated + TanStack Query + RHF
- Supabase project → schema in migrations → RLS before any rows
- Every third-party key in Edge Functions
- Stripe
paymentmode → webhook →product_licensestable - EAS → TestFlight in the first two weeks
- Ship features. Reconsider stack in 18 months.
Or: skip steps 1–5
Our templates are this exact stack, wired end-to-end, with real Stripe flow and RLS on every table — the catalog is linked up in the payments section. If you'd rather start on hour one with a working app instead of hour thirty, that's what they're for.
Either way — pick a boring stack and ship. Reading yet another stack post is the enemy.
What's the stack you actually shipped with — and what would you cut from this list? Drop a comment.
Top comments (0)