- Published on
Startup Tech Stack Selection Guide 2025: Technical Decisions from 0→1 to Series B
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. Tech Stack Philosophy
- 2. Stage 0: MVP ($0-50/month)
- 3. Stage 1: Product-Market Fit ($200-500/month)
- 4. Stage 2: Growth ($2K-10K/month)
- 5. Stage 3: Scale ($10K+/month)
- 6. Frontend Stack
- 7. Backend Stack
- 8. Database
- 9. Infrastructure
- 10. Hiring and Tech Stack
- 11. Common Mistakes
- 12. Real Examples
- 13. Quiz
- References
1. Tech Stack Philosophy
1.1 The Boring Technology Principle
Dan McKinley's "Choose Boring Technology" essay is the bible of startup technology selection. The core argument:
Innovation Tokens: Every team has a limited number of "innovation tokens." Each time you adopt a new, unproven technology, you spend a token. Since your business itself is already one innovation, there is no need to take risks in your technology stack as well.
┌─────────────────────────────────────────────────────┐
│ Innovation Token Allocation │
├─────────────────────────────────────────────────────┤
│ │
│ Available Innovation Tokens: 3 │
│ │
│ Good Example: │
│ ├── Business model (innovation) ... 1 token used │
│ ├── PostgreSQL (boring) ........... 0 tokens │
│ ├── Next.js (proven) ............. 0 tokens │
│ ├── Custom ML pipeline (innovation) 1 token used │
│ └── Remaining: 1 token (reserve) │
│ │
│ Bad Example: │
│ ├── Business model (innovation) ... 1 token used │
│ ├── CockroachDB (novel) .......... 1 token used │
│ ├── Bun Runtime (novel) .......... 1 token used │
│ ├── Custom ML pipeline (innovation) no tokens! │
│ └── Remaining: 0 tokens (risky) │
│ │
└─────────────────────────────────────────────────────┘
1.2 Monolith First
Martin Fowler's "Monolith First" principle: Do not start with microservices. Start with a monolith and split when necessary.
Why monolith first:
- It is nearly impossible to draw service boundaries correctly up front
- Development speed is faster (no inter-service communication overhead)
- Debugging is easier (single process)
- Transaction management is simple (no distributed transactions)
- Deployment is straightforward (single artifact)
┌──────────────────────────────────────────────────┐
│ Architecture Evolution Path │
│ │
│ Stage 0: Monolith (MVP) │
│ ├── Single Next.js app │
│ ├── Single DB (Supabase) │
│ └── Single deployment (Vercel) │
│ │ │
│ v (PMF achieved) │
│ Stage 1: Modular Monolith │
│ ├── Domain-based module separation │
│ ├── Internal API boundaries defined │
│ └── DB schema separation begins │
│ │ │
│ v (10+ team, traffic surge) │
│ Stage 2: Selective Service Extraction │
│ ├── Extract only bottleneck services │
│ ├── Event-driven async processing │
│ └── Caching layer added │
│ │ │
│ v (30+ team, global expansion) │
│ Stage 3: MSA (only when needed) │
│ ├── Independently deployable services │
│ ├── Service mesh │
│ └── Centralized observability │
└──────────────────────────────────────────────────┘
1.3 PaaS-First Strategy
Do not spend time managing infrastructure. Start with PaaS and move to IaaS when needed.
| Approach | Infra Management Time | Flexibility | Cost (Early) | Cost (Growth) |
|---|---|---|---|---|
| PaaS (Vercel, Railway) | Minimal | Low | Free-$50 | Can get expensive |
| Managed (AWS ECS, GKE) | Medium | Medium | $100-500 | Medium |
| Self-managed (K8s on EC2) | High | High | $200-1000 | Optimizable |
Core Principle: In the early stages, development speed matters more than cost optimization. Spending time on infrastructure optimization before finding PMF is wasted effort.
2. Stage 0: MVP ($0-50/month)
2.1 Recommended Stack
┌───────────────────────────────────────────────┐
│ MVP Tech Stack (2025) │
├───────────────────────────────────────────────┤
│ │
│ Frontend: Next.js 15 (App Router) │
│ Styling: Tailwind CSS + shadcn/ui │
│ Backend: Next.js API Routes / Server Actions│
│ Database: Supabase (PostgreSQL) │
│ Auth: Supabase Auth / NextAuth.js │
│ Storage: Supabase Storage / Cloudflare R2 │
│ Deploy: Vercel │
│ Analytics: PostHog (self-hosted) / Plausible │
│ Payments: Stripe │
│ Email: Resend │
│ Monitoring: Vercel Analytics + Sentry │
│ │
│ Total Monthly Cost: $0 ~ $50 │
│ (Vercel Free + Supabase Free Tier) │
│ │
└───────────────────────────────────────────────┘
2.2 Next.js + Supabase Project Setup
# Create project
npx create-next-app@latest my-startup --typescript --tailwind --app --src-dir
cd my-startup
# Core dependencies
npm install @supabase/supabase-js @supabase/ssr
npm install stripe @stripe/stripe-js
npm install resend
npm install zod react-hook-form @hookform/resolvers
# UI library
npx shadcn@latest init
npx shadcn@latest add button card input form dialog toast
# Dev tools
npm install -D prettier eslint-config-prettier
npm install -D @types/node
2.3 Supabase Configuration
// src/lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Cannot set in Server Components - ignore
}
},
},
}
)
}
// src/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
2.4 Authentication Implementation
// src/app/auth/login/page.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
alert(error.message)
} else {
router.push('/dashboard')
router.refresh()
}
setLoading(false)
}
const handleGoogleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
})
}
return (
<div className="flex min-h-screen items-center justify-center">
<form onSubmit={handleLogin} className="w-full max-w-md space-y-4 p-8">
<h1 className="text-2xl font-bold">Sign In</h1>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded border p-2"
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded border p-2"
required
/>
<button
type="submit"
disabled={loading}
className="w-full rounded bg-blue-600 p-2 text-white"
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
<button
type="button"
onClick={handleGoogleLogin}
className="w-full rounded border p-2"
>
Sign in with Google
</button>
</form>
</div>
)
}
2.5 Stripe Payment Integration
// src/app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { createClient } from '@/lib/supabase/server'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: NextRequest) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { priceId } = await req.json()
const session = await stripe.checkout.sessions.create({
customer_email: user.email,
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
success_url: `${req.nextUrl.origin}/dashboard?success=true`,
cancel_url: `${req.nextUrl.origin}/pricing?canceled=true`,
metadata: {
userId: user.id,
},
})
return NextResponse.json({ url: session.url })
}
// src/app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
export async function POST(req: NextRequest) {
const body = await req.text()
const sig = req.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
const userId = session.metadata?.userId
if (userId) {
await supabaseAdmin
.from('subscriptions')
.upsert({
user_id: userId,
stripe_customer_id: session.customer as string,
stripe_subscription_id: session.subscription as string,
status: 'active',
plan: 'pro',
})
}
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
await supabaseAdmin
.from('subscriptions')
.update({ status: 'canceled' })
.eq('stripe_subscription_id', subscription.id)
break
}
}
return NextResponse.json({ received: true })
}
2.6 MVP Cost Analysis
| Service | Free Tier | Paid Trigger | Cost |
|---|---|---|---|
| Vercel | 100GB bandwidth, unlimited deploys | Traffic increase | Pro: $20/mo |
| Supabase | 500MB DB, 1GB storage | Usage increase | Pro: $25/mo |
| Stripe | Usage-based | Revenue starts | 2.9% + $0.30/txn |
| Resend | 100 emails/day | Conversion testing | $20/mo |
| Sentry | 5K events/mo | Error volume | $26/mo |
| PostHog | 1M events/mo | Advanced analytics | $0 (self-hosted) |
Total MVP Cost: $0-50/month (essentially free until paid conversion)
3. Stage 1: Product-Market Fit ($200-500/month)
3.1 Technology Changes After PMF
After finding PMF, upgrades are needed in these areas:
┌───────────────────────────────────────────────┐
│ Stage 1 Tech Stack Upgrades │
├───────────────────────────────────────────────┤
│ │
│ Database: Supabase Pro -> larger instance │
│ Cache: + Redis (Upstash) │
│ Queue: + BullMQ (Redis-based) │
│ Search: + Meilisearch / Typesense │
│ CDN: + Cloudflare │
│ Monitoring: + Better Stack / Grafana Cloud │
│ CI/CD: GitHub Actions enhanced │
│ Error: Sentry Pro │
│ │
│ Monthly Cost: $200 ~ $500 │
└───────────────────────────────────────────────┘
3.2 Adding Redis Cache
// src/lib/cache.ts
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
export async function getCached<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 3600 // 1 hour
): Promise<T> {
const cached = await redis.get<T>(key)
if (cached !== null) {
return cached
}
const data = await fetcher()
await redis.set(key, data, { ex: ttl })
return data
}
export async function invalidateCache(pattern: string) {
const keys = await redis.keys(pattern)
if (keys.length > 0) {
await redis.del(...keys)
}
}
// Usage
// const user = await getCached(
// `user:${userId}`,
// () => db.user.findUnique({ where: { id: userId } }),
// 1800 // 30 minutes
// )
3.3 Background Job Queue
// src/lib/queue.ts
import { Queue, Worker, Job } from 'bullmq'
import { Redis } from 'ioredis'
const connection = new Redis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
})
export const emailQueue = new Queue('email', { connection })
export const analyticsQueue = new Queue('analytics', { connection })
const emailWorker = new Worker(
'email',
async (job: Job) => {
const { to, subject, template, data } = job.data
console.log(`Sending email to ${to}: ${subject}`)
// await resend.emails.send(...)
},
{
connection,
concurrency: 5,
limiter: {
max: 100,
duration: 60000, // 100 per minute
},
}
)
emailWorker.on('completed', (job) => {
console.log(`Email job ${job.id} completed`)
})
emailWorker.on('failed', (job, err) => {
console.error(`Email job ${job?.id} failed:`, err)
})
3.4 Database Optimization
-- Index optimization
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_orders_user_created
ON orders(user_id, created_at DESC);
CREATE INDEX CONCURRENTLY idx_products_category_status
ON products(category_id, status) WHERE status = 'active';
-- Partitioning (when event tables grow large)
CREATE TABLE events (
id BIGSERIAL,
user_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2025_01 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE events_2025_02 PARTITION OF events
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
4. Stage 2: Growth ($2K-10K/month)
4.1 Growth Stage Tech Stack
┌───────────────────────────────────────────────┐
│ Stage 2 Tech Stack │
├───────────────────────────────────────────────┤
│ │
│ Compute: AWS ECS Fargate / GKE │
│ Database: RDS PostgreSQL Multi-AZ │
│ Cache: ElastiCache Redis Cluster │
│ CDN: CloudFront + S3 │
│ Queue: SQS / RabbitMQ │
│ Search: ElasticSearch / OpenSearch │
│ CI/CD: GitHub Actions + ArgoCD │
│ Monitoring: Datadog / Grafana Stack │
│ Log: CloudWatch / Loki │
│ IaC: Terraform │
│ │
│ Team Size: 10-30 │
│ Monthly Cost: $2,000 ~ $10,000 │
└───────────────────────────────────────────────┘
4.2 Kubernetes Adoption
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
labels:
app: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: my-registry/api-server:latest
ports:
- containerPort: 3000
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
4.3 Terraform Infrastructure as Code
# terraform/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-startup-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_db_instance" "main" {
identifier = "my-startup-production"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.r6g.large"
allocated_storage = 100
max_allocated_storage = 500
storage_encrypted = true
multi_az = true
backup_retention_period = 14
performance_insights_enabled = true
monitoring_interval = 60
tags = {
Environment = "production"
Service = "database"
}
}
resource "aws_elasticache_replication_group" "main" {
replication_group_id = "my-startup-cache"
description = "Redis cache cluster"
node_type = "cache.r6g.large"
num_cache_clusters = 2
automatic_failover_enabled = true
multi_az_enabled = true
engine_version = "7.0"
port = 6379
at_rest_encryption_enabled = true
transit_encryption_enabled = true
}
5. Stage 3: Scale ($10K+/month)
5.1 Scale Stage Architecture
┌─────────────────────────────────────────────────────────┐
│ Stage 3 Architecture │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │CloudFront│───>│ ALB/NLB │───>│ K8s │ │
│ │ + WAF │ │ │ │ Cluster │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ┌─────────────────┼──────────┐ │
│ │ │ │ │
│ v v v │
│ ┌───────────┐ ┌───────────┐ ┌────────┐ │
│ │API Gateway│ │ Worker │ │ Cron │ │
│ │ Service │ │ Service │ │Service │ │
│ └─────┬─────┘ └─────┬─────┘ └───┬────┘ │
│ │ │ │ │
│ ┌─────────────┼───────────────┼────────────┤ │
│ │ │ │ │ │
│ v v v v │
│ ┌────────┐ ┌─────────┐ ┌──────────┐ ┌────────┐ │
│ │RDS │ │Redis │ │ Kafka │ │ S3 │ │
│ │Multi-AZ│ │Cluster │ │ Cluster │ │ │ │
│ └────────┘ └─────────┘ └──────────┘ └────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ElasticSearch│ │
│ │ Cluster │ │
│ └───────────┘ │
│ │
│ Team Size: 30+ │
│ Monthly Cost: $10,000+ │
└─────────────────────────────────────────────────────────┘
5.2 Event-Driven Architecture (Kafka)
// src/events/producer.ts
import { Kafka, Partitioners } from 'kafkajs'
const kafka = new Kafka({
clientId: 'my-startup',
brokers: (process.env.KAFKA_BROKERS || '').split(','),
ssl: true,
sasl: {
mechanism: 'scram-sha-256',
username: process.env.KAFKA_USERNAME!,
password: process.env.KAFKA_PASSWORD!,
},
})
const producer = kafka.producer({
createPartitioner: Partitioners.DefaultPartitioner,
idempotent: true,
})
export async function publishEvent(topic: string, event: {
type: string
payload: Record<string, unknown>
}) {
await producer.connect()
const message = {
key: event.payload.id as string || crypto.randomUUID(),
value: JSON.stringify({
...event,
timestamp: new Date().toISOString(),
version: '1.0',
}),
headers: {
'event-type': event.type,
'content-type': 'application/json',
},
}
await producer.send({
topic,
messages: [message],
})
}
5.3 ElasticSearch Integration
// src/lib/search.ts
import { Client } from '@elastic/elasticsearch'
const esClient = new Client({
node: process.env.ELASTICSEARCH_URL!,
auth: {
apiKey: process.env.ELASTICSEARCH_API_KEY!,
},
})
export async function searchProducts(query: string, filters?: {
category?: string
minPrice?: number
maxPrice?: number
page?: number
size?: number
}) {
const must: Record<string, unknown>[] = []
const filter: Record<string, unknown>[] = []
if (query) {
must.push({
multi_match: {
query,
fields: ['name^3', 'description', 'tags^2'],
type: 'best_fields',
fuzziness: 'AUTO',
},
})
}
if (filters?.category) {
filter.push({ term: { category: filters.category } })
}
if (filters?.minPrice || filters?.maxPrice) {
const range: Record<string, number> = {}
if (filters.minPrice) range.gte = filters.minPrice
if (filters.maxPrice) range.lte = filters.maxPrice
filter.push({ range: { price: range } })
}
const result = await esClient.search({
index: 'products',
body: {
query: {
bool: { must, filter },
},
from: ((filters?.page || 1) - 1) * (filters?.size || 20),
size: filters?.size || 20,
highlight: {
fields: { name: {}, description: {} },
},
},
})
return {
total: (result.hits.total as { value: number }).value,
hits: result.hits.hits.map((hit) => ({
...hit._source,
score: hit._score,
highlights: hit.highlight,
})),
}
}
6. Frontend Stack
6.1 Framework Comparison
| Criterion | Next.js 15 | Remix | SvelteKit |
|---|---|---|---|
| Rendering | SSR/SSG/ISR/RSC | SSR/CSR | SSR/SSG/CSR |
| Server Components | Yes (RSC) | No | No (uses runes) |
| Routing | File-based (App Router) | File-based | File-based |
| Data Fetching | Server Components, fetch | Loader/Action | Load functions |
| Deployment | Vercel-optimized, anywhere | Anywhere | Anywhere |
| Ecosystem | Very large | Medium | Growing |
| Learning Curve | Medium-High | Medium | Low-Medium |
| Startup Rec | Strongly recommended | Recommended | Small teams |
Startup conclusion: As of 2025, Next.js is the safest choice. It leads in ecosystem, hiring market, and deployment infrastructure.
6.2 UI Component Strategy
Recommended Stack:
├── Tailwind CSS ........... Utility-first CSS
├── shadcn/ui .............. Copy-paste components (ownership)
├── Radix UI ............... Accessible headless components
├── Framer Motion .......... Animations
└── Lucide Icons ........... Icon set
Alternatives:
├── Mantine ................ All-in-one UI library
├── Ark UI ................. Headless from Chakra UI team
└── Park UI ................ Ark UI + Tailwind
6.3 State Management
| Library | Complexity | Use Case | Startup Rec |
|---|---|---|---|
| React useState/useContext | Minimal | Local state | Default |
| Zustand | Low | Global state | Strongly recommended |
| Jotai | Low | Atomic state | Recommended |
| TanStack Query | Medium | Server state | Essential |
| Redux Toolkit | High | Complex state | Not recommended (early) |
// Zustand example - simple store
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface CartStore {
items: Array<{ id: string; name: string; price: number; quantity: number }>
addItem: (item: { id: string; name: string; price: number }) => void
removeItem: (id: string) => void
clearCart: () => void
total: () => number
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id)
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
}
}
return { items: [...state.items, { ...item, quantity: 1 }] }
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
clearCart: () => set({ items: [] }),
total: () =>
get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}),
{ name: 'cart-storage' }
)
)
7. Backend Stack
7.1 Language Comparison (Startup Context)
| Criterion | Node.js (TS) | Go | Python | Java/Kotlin |
|---|---|---|---|---|
| Dev Speed | Fast | Medium | Fast | Slow |
| Performance | Medium | Very High | Low | High |
| Hiring Difficulty | Easy | Medium | Easy | Medium |
| Full-stack Possible | Yes (Next.js) | No | Limited | No |
| Ecosystem | Very Large | Large | Very Large (ML) | Very Large |
| Startup Rec | Best | When performance matters | ML/Data | Enterprise |
7.2 Recommendations by Startup Type
B2C SaaS -> Node.js (TypeScript) + Next.js
├── One full-stack dev can build everything
├── Vercel deployment minimizes infra concerns
└── Frontend-backend type sharing
B2B SaaS -> Node.js or Go
├── Go if API performance is critical
├── Node.js if rapid feature development matters
└── Both are good choices
AI/ML Startup -> Python + TypeScript
├── ML pipelines in Python
├── API server in FastAPI (Python) or Next.js (TS)
└── Frontend in Next.js
Fintech -> Go or Java/Kotlin
├── High performance and reliability needed
├── Static type system safety
└── Strong ecosystem for financial regulation
Gaming/Realtime -> Go or Rust
├── High concurrency handling
├── WebSocket server performance
└── Low latency
7.3 API Design Patterns
// src/app/api/v1/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createClient } from '@/lib/supabase/server'
const createProductSchema = z.object({
name: z.string().min(1).max(200),
description: z.string().max(5000).optional(),
price: z.number().positive(),
category: z.string(),
tags: z.array(z.string()).max(10).optional(),
})
export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams
const page = parseInt(searchParams.get('page') || '1')
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100)
const category = searchParams.get('category')
const supabase = await createClient()
let query = supabase
.from('products')
.select('*', { count: 'exact' })
.eq('status', 'active')
.range((page - 1) * limit, page * limit - 1)
.order('created_at', { ascending: false })
if (category) {
query = query.eq('category', category)
}
const { data, count, error } = await query
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({
data,
pagination: {
page,
limit,
total: count || 0,
totalPages: Math.ceil((count || 0) / limit),
},
})
}
export async function POST(req: NextRequest) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await req.json()
const parsed = createProductSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.flatten() },
{ status: 400 }
)
}
const { data, error } = await supabase
.from('products')
.insert({ ...parsed.data, user_id: user.id })
.select()
.single()
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ data }, { status: 201 })
}
8. Database
8.1 PostgreSQL vs MySQL
| Criterion | PostgreSQL | MySQL |
|---|---|---|
| JSON Support | JSONB (indexable) | JSON (limited) |
| Full-text Search | Built-in (tsvector) | Built-in (FULLTEXT) |
| Extensions | Rich (PostGIS, etc.) | Limited |
| Replication | Logical/Physical | Physical (default) |
| Performance | Read/write balanced | Read optimized |
| Startup Rec | Strongly recommended | OK if team knows MySQL |
8.2 BaaS Comparison
| Service | DB Engine | Starting Price | Features | Startup Rec |
|---|---|---|---|---|
| Supabase | PostgreSQL | Free | Auth, Storage, Realtime included | Best for MVP |
| Neon | PostgreSQL | Free | Serverless, branching | Good |
| PlanetScale | MySQL (Vitess) | Free | Branching, zero-downtime schema | Good |
| Turso | SQLite (libSQL) | Free | Edge, very fast | Small apps |
| CockroachDB | PostgreSQL compat | Paid | Distributed SQL, global | Large apps |
8.3 When to Add Redis
When to introduce Redis:
├── Same query runs 100+ times per second
├── API response time exceeds 500ms
├── Session management needed
├── Real-time features (leaderboard, counters) needed
├── Rate limiting required
└── Distributed locking needed
When you do NOT need Redis yet:
├── DAU under 1,000
├── DB queries under 20ms
├── Next.js built-in cache is sufficient
└── Cost is a constraint
9. Infrastructure
9.1 When to Move from PaaS to IaaS
When to move from Vercel/Railway to AWS/GCP:
1. Cost Threshold
├── Vercel Pro costs exceed $500/month
├── Bandwidth costs more than compute
└── Function execution time limits are hit
2. Technical Requirements
├── Long-running processes needed (5+ minutes)
├── WebSocket is a core feature
├── GPU computing needed
└── Custom network configuration needed
3. Regulatory/Compliance
├── Data residency regulations
├── SOC 2, HIPAA certification needed
└── VPC isolation required
When NOT to move:
├── Monthly cost under $500
├── Traffic is predictable and stable
├── Serverless architecture is sufficient
└── No dedicated DevOps staff
9.2 Cloud Comparison
| Criterion | AWS | GCP | Azure | Vercel |
|---|---|---|---|---|
| Startup Credits | $10K-100K | $200K (Google for Startups) | $150K | None (free tier) |
| Service Breadth | Best | High | High | Frontend focused |
| Learning Curve | High | Medium | High | Low |
| Startup Rec | Series A+ | ML-focused | Enterprise | MVP-PMF |
9.3 Cost Optimization Checklist
- Spot/Preemptible Instances (up to 90% savings)
- Reserved Instances 1-year commitment (up to 40% savings)
- Right-sizing: Adjust instance sizes based on actual usage
- Auto-scaling: Scale based on traffic
- S3 Lifecycle Policies: Move old data to Glacier
- CloudFront Caching: Minimize origin requests
- NAT Gateway Cost Awareness: Data transfer costs add up
- Dev/Staging Environments: Auto-shutdown nights/weekends
10. Hiring and Tech Stack
10.1 How Tech Stack Affects Hiring
Technology choices directly determine your hiring pool:
| Technology | Developer Pool | Junior Ratio | Senior Hiring Difficulty |
|---|---|---|---|
| React/Next.js | Very large | High | Medium |
| TypeScript | Large | Medium | Medium |
| Python | Very large | High | Hard (ML field) |
| Go | Small | Low | High |
| Rust | Very small | Very low | Very high |
| Java/Kotlin | Very large | High | Medium |
| PostgreSQL | Large | Medium | Medium |
| Kubernetes | Medium | Low | High |
10.2 Hiring-Friendly Tech Stacks
Easy to hire for:
├── Next.js + TypeScript (frontend)
├── Node.js + Express/Nest.js (backend)
├── PostgreSQL / MySQL (database)
├── Redis (cache)
├── Docker + GitHub Actions (DevOps)
└── AWS (infrastructure)
Hard to hire for:
├── Svelte / SolidJS (frontend)
├── Rust / Elixir (backend)
├── CockroachDB / ScyllaDB (database)
├── Nomad / Consul (orchestration)
└── Pulumi / CDK (IaC)
11. Common Mistakes
11.1 Resume-Driven Development
Bad decision patterns:
├── "I want K8s experience" -> K8s adoption (DAU 100)
├── "Rust is trending" -> Rust API server (team of 2)
├── "MSA is the standard" -> 3 people managing 10 services
├── "GraphQL is great" -> GraphQL for simple CRUD
└── "Google uses monorepos" -> Turborepo for team of 3
Good decisions:
├── "What is the simplest way to solve this?"
├── "Can we maintain this in 6 months?"
├── "Is the team already familiar with this?"
├── "Is it easy to hire for?"
└── "Is the migration cost manageable?"
11.2 Premature Optimization
- Creating 40 DB indexes upfront (without knowing query patterns)
- Building 3-layer caching (100 users/day traffic)
- Setting up global CDN (all users in one country)
- Load balancer + auto scaling (5% CPU utilization)
11.3 Premature Microservices
When a team of 3 adopts microservices:
- Entire days spent debugging inter-service communication
- Data inconsistency from distributed transaction issues
- Managing 10 deployment pipelines
- 2 weeks just to set up monitoring dashboards
- Result: 50% slower development, 300% higher infrastructure costs
11.4 Ignoring Technical Debt
On the other hand, accumulating too much technical debt is also problematic:
- 6 months of development without tests makes refactoring impossible
- Tens of thousands of lines of untyped JavaScript
- Hard-coded configuration values scattered everywhere
- No logging/monitoring makes incident response impossible
Balance: Apply the 80/20 rule at the MVP stage. Invest 80% of quality in the 20% of core code.
12. Real Examples
12.1 Vercel Stack
Vercel (Next.js creator):
├── Frontend: Next.js (own product)
├── Backend: Go + Node.js
├── Database: PlanetScale (MySQL) -> Neon (PostgreSQL)
├── Cache: Redis (Upstash)
├── Monorepo: Turborepo-based
├── Deploy: Own platform
├── Monitoring: Custom + Datadog
└── Note: Heavy dogfooding of own products
12.2 Linear Stack
Linear (project management tool):
├── Frontend: React + TypeScript
├── Backend: Node.js + TypeScript
├── Database: PostgreSQL
├── Cache: Redis
├── Sync: CRDT-based real-time synchronization
├── Deploy: Google Cloud
├── Monitoring: Custom built
└── Note: Offline-first, local data priority
12.3 Cal.com Stack
Cal.com (open-source scheduling):
├── Frontend: Next.js + TypeScript
├── Backend: tRPC + Prisma
├── Database: PostgreSQL
├── Auth: NextAuth.js
├── Email: Multiple provider support
├── Deploy: Vercel
├── Monorepo: Turborepo
└── Note: Open source, monorepo, type-safe with tRPC
12.4 Key Takeaways
Common patterns across these examples:
- Proven technologies dominate (PostgreSQL, Redis, TypeScript)
- Monolith or modular monolith as starting architecture
- TypeScript as the default language choice
- PaaS-first approach (Vercel, Google Cloud managed)
- Innovation investment only in core differentiators
13. Quiz
Q1: What are "Innovation Tokens" in the Boring Technology principle, and why should you conserve them?
A: Innovation Tokens is a concept proposed by Dan McKinley, representing the number of new/unproven technologies a team can handle. Every team has a limited number (typically around 3), and each unproven technology adoption consumes one.
Why conserve them: The startup's business model itself is already one innovation, using up 1 token. If you spend all remaining tokens on the tech stack, you have no capacity to handle problems when they arise. Proven (boring) technologies produce only predictable issues with well-known solutions, reducing operational burden.
Q2: Why is the Next.js + Supabase + Vercel combination recommended for the MVP stage?
A: This combination is recommended because:
- Development speed: Next.js full-stack capabilities enable development without frontend/backend separation
- Cost: All three services have generous free tiers ($0-50/month)
- Zero infrastructure management: Vercel handles deployment/CDN/SSL; Supabase handles DB/Auth/Storage
- Type safety: Full-stack type sharing with TypeScript
- Scalability: Supabase runs on PostgreSQL, making future migration straightforward
- Ecosystem: Easy integration with shadcn/ui, Stripe, Resend, etc.
- Hiring: React/Next.js has the largest developer pool
The key is not spending time on infrastructure before finding PMF.
Q3: When should you transition from PaaS (Vercel) to IaaS (AWS)?
A: Transition criteria:
- Cost: Vercel costs exceed $500/month (equivalent workload is cheaper on AWS)
- Technical constraints: Long-running processes (5+ min), WebSocket-based real-time features, GPU computing needed
- Regulation: Data residency requirements, SOC 2/HIPAA certification, VPC isolation needed
- Team: Dedicated DevOps/infrastructure staff has been hired
When NOT to transition: Monthly cost under $500, serverless architecture is sufficient, no dedicated DevOps staff. The key is transitioning "when needed," not preparing in advance.
Q4: Why should startups NOT adopt microservices from the start?
A: Reasons not to start with microservices:
- Service boundary failure: Before PMF, domains keep changing, making correct service boundaries impossible
- Operational overhead: Inter-service communication, distributed transactions, service discovery complexity explodes
- Debugging difficulty: Distributed system debugging is 10x harder than monolith
- Infrastructure cost: Each service needs its own deployment pipeline, monitoring, logging (3x+ cost)
- Team size mismatch: A 3-5 person team managing 10 services is unrealistic
Correct approach: Start monolith, extract services selectively only after sufficient domain understanding and at bottleneck points.
Q5: How does tech stack choice impact hiring?
A: Tech stack directly impacts hiring in several ways:
- Pool size: React/TypeScript developers are very numerous; Rust/Elixir developers are extremely scarce
- Hiring speed: Popular stacks attract more applicants, enabling faster hiring
- Hiring cost: Rare technologies command premium salaries (e.g., senior Go devs cost 20-30% more than senior Node.js devs)
- Onboarding: Known technologies mean onboarding in under a week; new technologies take a month or more
- Retention: Unpopular technologies create career anxiety, increasing turnover
Recommendation: TypeScript + React + PostgreSQL + AWS is the most hiring-friendly combination for most markets.
References
- McKinley, D. "Choose Boring Technology." https://boringtechnology.club/
- Fowler, M. "Monolith First." Martin Fowler's blog.
- Next.js Documentation (2025). https://nextjs.org/docs
- Supabase Documentation. https://supabase.com/docs
- Vercel Documentation. https://vercel.com/docs
- Stripe Documentation. https://stripe.com/docs
- Terraform AWS Provider Documentation.
- "The Twelve-Factor App." https://12factor.net/
- Kleppmann, M. "Designing Data-Intensive Applications." O'Reilly.
- Newman, S. "Building Microservices, 2nd Edition." O'Reilly.
- Basecamp. "Getting Real." https://basecamp.com/gettingreal
- Linear Engineering Blog. https://linear.app/blog
- Cal.com GitHub Repository. https://github.com/calcom/cal.com
- AWS Startup Guides. https://aws.amazon.com/startups/
- Google for Startups Cloud Program. https://cloud.google.com/startup
- Y Combinator Library - Technical Decisions. https://www.ycombinator.com/library