tation
1. Stack Selection: The Boring Foundation
Select a stack where the developer has high proficiency and the ecosystem offers mature, managed integrations. Avoid novelty.
- Runtime: Node.js or Deno (TypeScript).
- Framework: Next.js or SvelteKit (Full-stack capabilities reduce context switching).
- Database: PostgreSQL via a managed provider (Supabase, Neon, or AWS RDS).
- ORM: Drizzle ORM (Lightweight, type-safe, no runtime overhead).
- Deployment: Vercel, Cloudflare Pages, or Railway.
2. Vertical Slice Architecture
Organize code by feature, not by layer. This reduces cognitive load when implementing changes.
src/
features/
auth/
auth.service.ts
auth.schema.ts
auth.routes.ts
billing/
billing.service.ts
stripe.webhook.ts
dashboard/
dashboard.component.tsx
shared/
db.ts
utils.ts
3. AI-Augmented Development Workflow
Integrate AI tools directly into the development loop to handle boilerplate, testing, and documentation.
- Pattern: Use AI to generate Zod schemas from database definitions and vice versa.
- Pattern: Use AI to write unit tests for critical business logic immediately after implementation.
4. Implementation Code Examples
Database Schema with Drizzle and Zod Validation:
This pattern ensures type safety from database to frontend and leverages AI-friendly generation.
// src/features/users/user.schema.ts
import { pgTable, varchar, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { z } from 'zod';
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// AI can generate these schemas or validate them against the table definition
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
export type InsertUser = z.infer<typeof insertUserSchema>;
export type User = z.infer<typeof selectUserSchema>;
Robust Webhook Handler for Payments:
Indie hackers cannot afford payment discrepancies. This pattern uses idempotency and atomic transactions to ensure data integrity.
// src/features/billing/stripe.webhook.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { eq } from 'drizzle-orm';
import { db } from '@/shared/db';
import { subscriptions } from '@/features/billing/billing.schema';
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return new Response(`Webhook Error: ${err}`, { status: 400 });
}
// Use a switch map for extensibility without modifying core logic
const handlers: Record<string, (data: any) => Promise<void>> = {
'invoice.payment_succeeded': handlePaymentSucceeded,
'invoice.payment_failed': handlePaymentFailed,
'customer.subscription.deleted': handleSubscriptionDeleted,
};
const handler = handlers[event.type];
if (handler) {
try {
await handler(event.data.object);
} catch (error) {
// Log to external service (e.g., Sentry) but return 200 to prevent retries
console.error(`Handler failed for ${event.type}:`, error);
return new Response('Error processing webhook', { status: 200 });
}
}
return new Response(null, { status: 200 });
}
async function handlePaymentSucceeded(invoice: any) {
const subscriptionId = invoice.subscription;
const customerId = invoice.customer;
// Atomic update to prevent race conditions
await db.update(subscriptions)
.set({ status: 'active', currentPeriodEnd: new Date(invoice.lines.data[0].period.end * 1000) })
.where(eq(subscriptions.stripeId, subscriptionId));
}
AI Feature Toggle Pattern:
Use feature flags to roll out AI features safely without complex infrastructure.
// src/features/ai/ai-feature.service.ts
import { env } from '@/env.mjs';
export async function generateContent(prompt: string) {
// Check feature flag via simple env var or lightweight DB flag
if (!env.AI_FEATURE_ENABLED) {
throw new Error('Feature disabled');
}
// Direct integration with provider, no abstraction layer needed initially
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` },
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] }),
});
if (!response.ok) {
// Fail fast and log
throw new Error(`AI API Error: ${response.statusText}`);
}
return response.json();
}
Architecture Decisions and Rationale
- Monolith over Microservices: A single codebase allows the developer to navigate the entire application without context switching. Refactoring is trivial. Deployment is atomic.
- Managed Services: Database, Auth, and Payments are outsourced. The indie hacker should never manage a database server or build an auth system. This eliminates security vulnerabilities and scaling headaches.
- TypeScript End-to-End: Using TypeScript across the stack (frontend, backend, database) allows for shared types and schema validation, reducing runtime errors and development time.
- Serverless/Edge Deployment: Costs scale with usage. A static site or serverless function costs nothing when idle, preserving capital during the validation phase.
Pitfall Guide
1. Building Custom Auth
Mistake: Implementing JWT rotation, password hashing, and session management from scratch.
Consequence: Security vulnerabilities, password reset flows, and email deliverability issues consume weeks.
Best Practice: Use Clerk, Auth0, or Supabase Auth. The cost is negligible compared to the engineering time saved.
2. Premature Database Optimization
Mistake: Sharding databases or implementing read replicas before hitting 1,000 concurrent users.
Consequence: Complexity spikes, and debugging becomes difficult.
Best Practice: PostgreSQL handles millions of rows efficiently. Optimize queries and add indexes only when metrics indicate latency issues.
3. Ignoring Backup Strategies
Mistake: Relying solely on the managed provider's default retention policy without testing restores.
Consequence: Data loss due to accidental deletion or corruption.
Best Practice: Enable automated daily backups with point-in-time recovery. Write a script to verify backups weekly.
4. Over-Reliance on Free Tiers
Mistake: Building the entire stack on free tiers of multiple services (Vercel, Supabase, Stripe, etc.).
Consequence: Sudden cost spikes or service limits when traffic grows, leading to downtime.
Best Practice: Monitor usage metrics. Budget for costs at scale. Free tiers are for validation, not production stability.
5. Feature Creep Without Metrics
Mistake: Adding features based on personal interest rather than user data.
Consequence: Codebase bloats, maintenance increases, and core value proposition dilutes.
Best Practice: Implement analytics early. Only build features that correlate with retention or conversion metrics.
6. Neglecting Error Monitoring
Mistake: Relying on user reports for bug detection.
Consequence: Silent failures erode trust. Users churn without complaining.
Best Practice: Integrate Sentry or LogRocket immediately. Set up alerts for critical errors.
7. Tech Debt Accumulation via "Quick Hacks"
Mistake: Hardcoding values or bypassing validation to ship faster.
Consequence: Technical debt compounds, making future changes risky and slow.
Best Practice: Use the "Boy Scout Rule": leave the code cleaner than you found it. Automated linting and formatting enforce consistency.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Solo Founder, <1k Users | Next.js + Supabase + Vercel | Lowest ops overhead, generous free tiers, rapid dev. | <$20/mo |
| High Traffic, Low Margin | SvelteKit + Cloudflare Workers + D1 | Edge performance, pay-per-request pricing, minimal idle cost. | Scales with usage |
| Complex Data Relationships | PostgreSQL + Drizzle ORM | Relational integrity, ACID compliance, flexible querying. | Fixed DB cost |
| AI-Heavy Product | OpenAI API + Vector DB | Specialized tooling for embeddings and context management. | Token-based cost |
| Strict Compliance (HIPAA/GDPR) | AWS/GCP + Managed DB + Audit Logs | Enterprise-grade controls, data residency options. | High infrastructure cost |
Configuration Template
biome.json
Optimized for speed and consistency, replacing ESLint/Prettier for faster CI/CD.
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error",
"useExhaustiveDependencies": "warn"
},
"suspicious": {
"noExplicitAny": "error"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
}
}
docker-compose.yml
For local development consistency, even with managed services.
version: '3.8'
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: indie
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Quick Start Guide
-
Initialize Project:
npx create-next-app@latest my-indie-app --typescript --tailwind --app
cd my-indie-app
-
Add Core Dependencies:
npm i drizzle-orm drizzle-zod zod @t3-oss/env-nextjs
npm i -D drizzle-kit biomejs
-
Setup Environment:
cp .env.example .env.local
# Add DATABASE_URL, STRIPE_SECRET_KEY, OPENAI_API_KEY
-
Run Database Migration:
npx drizzle-kit push
-
Start Development Server:
npm run dev
# App running at http://localhost:3000
This architecture and workflow enable a single developer to build, launch, and maintain a profitable product with minimal operational drag, mirroring the technical strategies of successful indie hackers. Focus on the business logic; let the stack handle the rest.