Founders lose weeks to this decision and it barely affects the outcome. The one criterion that matters: minimise the distance between noticing a problem and having it fixed. Everything else, performance, elegance, hireability, "scalability", is secondary for a product with no users.
The concept
Stack choice feels consequential because it's the first technical decision and it's irreversible-feeling. It's neither. Most successful products could have been built on several stacks; most failed products would have failed on all of them.
What actually varies is iteration speed, and iteration speed is dominated by three things:
- What you already know. Familiarity beats theoretical superiority by a wide margin at this stage.
- How fast you can diagnose a problem. Fewer layers of abstraction means fewer places for a bug to hide.
- How much of your problem is already solved. Ecosystem maturity, not language elegance.
The decision axes
| Axis | Question | Why it matters |
|---|---|---|
| Familiarity | Have I shipped in this before? | Learning + building at once doubles both |
| Ecosystem | Does a library exist for my hard parts? | Auth, payments, file handling are solved problems |
| Debuggability | When it breaks at 2am, can I find it? | The dominant hidden cost |
| Hiring | Can I find help later? | Matters at team stage, not solo |
| Platform fit | Web, mobile, desktop, all three? | Constrains everything |
| Operational load | Who patches, backs up, monitors? | Servers are a second job |
"Boring technology" is the correct default. Every genuinely novel technology in your stack spends a unit of your limited capacity for dealing with the unknown. Spend that on your product, not your framework.
Stack shapes by product type
Not prescriptions, starting points that are hard to regret:
| You're building | Reasonable default | Why |
|---|---|---|
| Web SaaS | Next.js / Rails / Django + Postgres | Mature, batteries-included, huge ecosystem |
| Marketplace | Same, plus a payments layer (Stripe Connect) | Payouts and disputes are the hard part |
| Mobile consumer | React Native / Flutter, or native if you're native | Cross-platform unless you need platform depth |
| Mobile from an existing web app | Capacitor / Expo web wrapper | Days, not months, of rework |
| AI product | Whatever web stack + a hosted model API | The model is a service call; don't overbuild |
| Dev tool / CLI | Go / Rust / Node, single binary | Distribution matters more than framework |
| Internal / B2B dashboard | Any web stack, or Retool / Refine | Speed of change beats craft |
| Content-heavy | Static generation (Astro, Next static) | Fast, cheap, indexable |
Backend: the three genuine options
| Option | When | Cost of being wrong |
|---|---|---|
| BaaS (Supabase, Firebase) | Standard CRUD + auth; small team | Vendor lock-in; escaping is real work |
| Managed platform (Vercel, Railway, Fly) | You want code control, not ops | Slightly more cost, much less lock-in |
| Your own servers (VPS, AWS) | Specific compliance or cost needs | You now run a server: patching, backups, monitoring, uptime |
Default to the highest level of abstraction that fits. Running your own infrastructure is a second job with no product upside until you're large enough that the bill hurts.
Mobile: the real trade-off
| Approach | Rework from web | Native feel | Best for |
|---|---|---|---|
| Native (Swift/Kotlin) | Total rewrite ×2 | Perfect | Camera, AR, heavy gestures, background work |
| React Native / Flutter | Total rewrite ×1 | Very good | Most consumer apps built fresh |
| Capacitor / webview | ~None | Good for content; poor for gestures | Existing web app; content/text products |
| PWA only | None | No store presence | No IAP, no discovery, usually disqualifying |
Prototype your hardest interaction first. If your product lives on a gesture-heavy canvas, build that in a webview before committing, you'll know within a day.
Build vs buy
Buy (or rent) anything that isn't your differentiation:
| Never build | Use instead |
|---|---|
| Authentication | Supabase Auth, Clerk, Auth0, Firebase |
| Payments / subscriptions | Stripe, RevenueCat, Paddle, Lemon Squeezy |
| Email delivery | Resend, Postmark, Brevo, SendGrid |
| File storage | S3, R2, Supabase Storage |
| Search | Algolia, Typesense, Postgres FTS |
| Analytics / crash | PostHog, Sentry |
Each of these represents years of edge cases. Building them is not engineering discipline; it's a decision to spend your only asset, time, on a solved problem.
📐 Best practice
Use what you already know, unless it genuinely can't do the job.
Pick one novel thing at most. Learning a new language and a new framework and a new database while validating a product is three unknowns too many.
Prototype the hardest interaction before committing.
Audit your dependencies' requirements early. Your plugins and SDKs, not the framework docs, set your real minimum platform version.
Prefer managed over self-hosted until the bill or a compliance requirement forces otherwise.
Choose a stack with an obvious deployment story. If deploying is a project, you'll deploy less, and shipping less is the actual risk.
Write down what you gave up. If you skip a framework, you skip what it gave you for free, escaping, routing, type safety. Consciously re-add the important parts (see the case study).
💀 Common mistakes
Framework paralysis. Two weeks of comparison, no code. Pick, and move.
Choosing for a scale you don't have. Kubernetes for zero users is a hobby.
Choosing for a team you don't have. "Hireability" matters at team stage.
Wrong wrapper for the interaction model. A webview for a gesture-heavy product will be janky and you'll discover it late.
Not checking plugin requirements. One SDK can force a major framework upgrade and a minimum-OS bump mid-project.
Building auth or payments yourself. Both are security surfaces with no product upside.
Adding a bundler/toolchain you don't need, and then losing days to build configuration instead of product.
Forgetting that skipping a framework skips its safety features. Auto-escaping is the classic one.
The professional workflow
1. LIST HARD REQUIREMENTS
platforms · offline? · realtime? · heavy media? · compliance?
2. LIST WHAT YOU ALREADY KNOW
3. IDENTIFY THE HARDEST INTERACTION → prototype it first
4. CHOOSE THE WRAPPER (mobile) by rework cost
5. CHOOSE THE BACKEND by operational load you're willing to carry
6. BUY EVERYTHING THAT ISN'T YOUR DIFFERENTIATION
7. AUDIT DEPENDENCY REQUIREMENTS
minimum OS · framework version · bundler assumptions ·
what each SDK ships in your binary
8. WRITE DOWN WHAT YOU GAVE UP — and re-add the safety-critical parts
9. CONFIRM THE DEPLOY STORY is one command
Tools, websites & costs
| Layer | Options | Cost at launch |
|---|---|---|
| Web framework | Next.js, Remix, Astro, Rails, Django, Laravel | $0 |
| Mobile | React Native, Expo, Flutter, Capacitor | $0 |
| Backend/BaaS | Supabase, Firebase, PocketBase, Convex | $0 tiers |
| Hosting | Vercel, Netlify, Railway, Fly, Cloudflare | $0-20/mo |
| Database | Postgres (Supabase, Neon), SQLite/Turso | $0 tiers |
| Auth | Supabase Auth, Clerk, Auth0 | $0-25/mo |
| Payments | Stripe, RevenueCat, Paddle | % of revenue |
| Resend, Postmark, Brevo | $0 tiers | |
| Errors/analytics | Sentry, PostHog | $0 tiers |
| Repo/CI | GitHub + Actions | $0 |
A complete modern stack costs $0/month at launch scale, with a clear paid step at each layer once revenue exists.
Alternatives & trade-offs
Monolith vs microservices. Monolith, always, at this stage. Microservices solve organisational problems you don't have and add distributed-systems problems you don't want.
Typed vs untyped. TypeScript costs setup and pays back at ~10k lines and again when you return after a gap. For anything you'll maintain, take the types.
Framework vs vanilla. Frameworks give components, routing, state, and, critically, automatic escaping. Vanilla gives zero build tooling and total transparency. Vanilla is defensible for content-heavy, low-interactivity products with one developer; you must consciously re-add what you gave up.
No-code vs code. No-code reaches a working product faster and hits ceilings on cost, performance, and customisation. Excellent for validating demand; painful once you have traction.
Cross-platform vs native. Cross-platform saves roughly half the work and costs some platform polish. Native wins when the product is the platform integration.
Checklist
- I chose primarily on familiarity and iteration speed
- At most one novel technology
- I prototyped the hardest interaction first
- The wrapper choice was made on rework cost
- I'm buying auth, payments, email, storage, analytics
- I audited dependency requirements, minimum OS, framework version, what each SDK ships
- Operational load is one I'm willing to carry
- Deploy is one command
- I wrote down what I gave up by skipping a framework, and re-added the safety-critical parts
- Every free tier's ceiling and paid step is known
📓 Case Study: a contrarian stack, and its honest bill
Project: SOLIS, an iOS content app, one developer, no build step.
APP Vanilla JavaScript. No framework, no bundler.
One index.html (~3,200 lines) + content in plain .js files
NATIVE Capacitor → iOS WKWebView
BACKEND Supabase (Postgres, Auth, Edge Functions)
PAYMENTS RevenueCat → StoreKit
WEB Static HTML on Vercel; Cloudflare DNS/email; Brevo
The wrapper decision was straightforward and correct. A working web prototype already existed, so the real question was "how do I reach an app store with the least rework?"
| Option | Rework | Time to first build |
|---|---|---|
| Capacitor | ~zero | hours ✅ |
| React Native | total rewrite | weeks |
| Flutter | total rewrite in Dart | weeks |
| Native Swift + Kotlin | two rewrites | months |
Total native code written across the project: zero lines, haptics, notifications, sign-in and purchases were all JS calls to plugins.
The no-framework decision is the contrarian one, and the case for it was real: the app is mostly content (450 lessons), so it isn't a state-management problem; no build step means no build failures; everything is greppable; and offline is trivial with no hydration or chunk loading.
⚠️ And here is the honest bill, which is the point of this case study.
Going frameworkless means opting out of what a framework gives you for free, most importantly automatic HTML escaping. That wasn't consciously re-added, and it produced real findings in a day-one security review:
answers.urgencyfrom localStorage is interpolated unsanitized into aninsertAdjacentHTMLcall, allowing stored XSS.Lesson content is interpolated directly into
innerHTMLwith no escaping. Low exploitability today, but if lesson data ever moves to a CMS or third-party API, a natural growth step, any crafted entry executes JavaScript in the app's origin.
React would have prevented both, for free. The fix is one esc() helper used at every interpolation, about fifteen lines, written on day one. It wasn't, and the finding was still open 24 days later (Chapter 30).
That's the trade-off stated plainly: the stack bought genuine iteration speed and cost a class of vulnerability that a framework eliminates by default. Both halves are true.
⚠️ Deviation 2: dependency requirements weren't audited, and one feature became a framework upgrade.
On day 21 the task was "add Apple and Google sign-in." An afternoon's work. What actually happened:
"Add Apple sign-in"
└─ need a social-login plugin
└─ the maintained one requires Capacitor 8 (project was on 6)
└─ upgrade Capacitor 6 → 8 (framework-wide)
└─ every @capacitor/* plugin bumps
└─ RevenueCat 9 → 13 (two major versions)
└─ iOS deployment target 13 → 15
└─ drops support for older iPhones
The alternative, a Capacitor-6 plugin that was still a release candidate, would have meant shipping unfinished code in an auth flow adjacent to payments. Worse option.
Your plugins set your real platform floor, not the framework docs. A ten-minute audit on day one would have surfaced this; instead it landed on day 21 with a working app to keep working.
It did end well: landing on current RevenueCat before writing any payment code meant that work started on supported libraries. When forced to upgrade, take the whole ecosystem with you, half-upgraded is the worst state.
One thing done well: reading the package before engineering around it. A no-bundler project can't import ESM-only packages. Supabase was solved by vendoring a UMD build. For RevenueCat, the assumption was that the same vendoring was needed, until thirty minutes of reading the source found that its export is just a proxy over window.Capacitor.Plugins.Purchases, which Capacitor attaches automatically. No vendoring needed. The documented UMD artifact didn't even exist.
Thirty minutes of reading source saved a vendoring step, a sync step, and a stale-copy risk.
Lessons
- Optimise for the distance between noticing a problem and fixing it. That's the real criterion.
- If you have a web app, a webview wrapper is hours to an app store. The rework math is not close.
- Vanilla is defensible for content-heavy products, and you must consciously re-add what the framework gave you. Escaping first.
- Write down what you gave up. Unwritten trade-offs become unfixed vulnerabilities.
- Your dependencies set your platform floor. Audit them on day one.
- When forced to upgrade, take the whole ecosystem.
- Read the package before engineering around it. Thirty minutes beats a day of workaround.
- Buy auth, payments, email, storage, analytics. Building them is spending your only asset on solved problems.
- A complete modern stack is $0/month at launch. Infrastructure is not your constraint.
Next: Chapter 16: Project Architecture and Working Practices →