All chapters Part IV · Building
Chapter 15

Choosing Your Tech Stack

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:

  1. What you already know. Familiarity beats theoretical superiority by a wide margin at this stage.
  2. How fast you can diagnose a problem. Fewer layers of abstraction means fewer places for a bug to hide.
  3. How much of your problem is already solved. Ecosystem maturity, not language elegance.

The decision axes

AxisQuestionWhy it matters
FamiliarityHave I shipped in this before?Learning + building at once doubles both
EcosystemDoes a library exist for my hard parts?Auth, payments, file handling are solved problems
DebuggabilityWhen it breaks at 2am, can I find it?The dominant hidden cost
HiringCan I find help later?Matters at team stage, not solo
Platform fitWeb, mobile, desktop, all three?Constrains everything
Operational loadWho 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 buildingReasonable defaultWhy
Web SaaSNext.js / Rails / Django + PostgresMature, batteries-included, huge ecosystem
MarketplaceSame, plus a payments layer (Stripe Connect)Payouts and disputes are the hard part
Mobile consumerReact Native / Flutter, or native if you're nativeCross-platform unless you need platform depth
Mobile from an existing web appCapacitor / Expo web wrapperDays, not months, of rework
AI productWhatever web stack + a hosted model APIThe model is a service call; don't overbuild
Dev tool / CLIGo / Rust / Node, single binaryDistribution matters more than framework
Internal / B2B dashboardAny web stack, or Retool / RefineSpeed of change beats craft
Content-heavyStatic generation (Astro, Next static)Fast, cheap, indexable

Backend: the three genuine options

OptionWhenCost of being wrong
BaaS (Supabase, Firebase)Standard CRUD + auth; small teamVendor lock-in; escaping is real work
Managed platform (Vercel, Railway, Fly)You want code control, not opsSlightly more cost, much less lock-in
Your own servers (VPS, AWS)Specific compliance or cost needsYou 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

ApproachRework from webNative feelBest for
Native (Swift/Kotlin)Total rewrite ×2PerfectCamera, AR, heavy gestures, background work
React Native / FlutterTotal rewrite ×1Very goodMost consumer apps built fresh
Capacitor / webview~NoneGood for content; poor for gesturesExisting web app; content/text products
PWA onlyNoneNo store presenceNo 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 buildUse instead
AuthenticationSupabase Auth, Clerk, Auth0, Firebase
Payments / subscriptionsStripe, RevenueCat, Paddle, Lemon Squeezy
Email deliveryResend, Postmark, Brevo, SendGrid
File storageS3, R2, Supabase Storage
SearchAlgolia, Typesense, Postgres FTS
Analytics / crashPostHog, 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

LayerOptionsCost at launch
Web frameworkNext.js, Remix, Astro, Rails, Django, Laravel$0
MobileReact Native, Expo, Flutter, Capacitor$0
Backend/BaaSSupabase, Firebase, PocketBase, Convex$0 tiers
HostingVercel, Netlify, Railway, Fly, Cloudflare$0-20/mo
DatabasePostgres (Supabase, Neon), SQLite/Turso$0 tiers
AuthSupabase Auth, Clerk, Auth0$0-25/mo
PaymentsStripe, RevenueCat, Paddle% of revenue
EmailResend, Postmark, Brevo$0 tiers
Errors/analyticsSentry, PostHog$0 tiers
Repo/CIGitHub + 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


📓 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?"

OptionReworkTime to first build
Capacitor~zerohours
React Nativetotal rewriteweeks
Fluttertotal rewrite in Dartweeks
Native Swift + Kotlintwo rewritesmonths

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.urgency from localStorage is interpolated unsanitized into an insertAdjacentHTML call, allowing stored XSS.

Lesson content is interpolated directly into innerHTML with 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

  1. Optimise for the distance between noticing a problem and fixing it. That's the real criterion.
  2. If you have a web app, a webview wrapper is hours to an app store. The rework math is not close.
  3. Vanilla is defensible for content-heavy products, and you must consciously re-add what the framework gave you. Escaping first.
  4. Write down what you gave up. Unwritten trade-offs become unfixed vulnerabilities.
  5. Your dependencies set your platform floor. Audit them on day one.
  6. When forced to upgrade, take the whole ecosystem.
  7. Read the package before engineering around it. Thirty minutes beats a day of workaround.
  8. Buy auth, payments, email, storage, analytics. Building them is spending your only asset on solved problems.
  9. A complete modern stack is $0/month at launch. Infrastructure is not your constraint.

Next: Chapter 16: Project Architecture and Working Practices →

Useful? Share this chapter