Skip to content
Writing
Build-in-public·8 min read

The Stack Behind 6 Production Apps I Built Solo

A practical walkthrough of the opinionated scaffold I reuse across every personal project: Next.js 16, SST 4, Aurora Serverless v2, Drizzle ORM, Auth.js v5, and a deploy pipeline that fits in a single config file.

Over the past year I shipped six production web apps — five personal projects running on AWS and one contracted AI platform. They cover wildly different domains: a 3D-printing storefront for houses, a fence-estimating SaaS, an FAA airspace screener, a language-learning platform, a Gemini-powered yard redesign tool, and an AI proposal assistant for government contractors. Underneath all of them is the same opinionated scaffold. Here is what it looks like and why I landed on each piece.

The core quartet

Every project starts with four packages locked at specific major versions:

  • Next.js 16 — App Router, React Server Components, and next/og for dynamic social cards. The output: "standalone" setting produces a self-contained Node server that OpenNext can lift straight into Lambda.
  • SST 4 — the control plane that wires the Next.js server, Aurora, SES, CloudWatch, and every other AWS resource into a single declarative config. One sst deploy pushes the whole stack; sst dev tunnels live AWS resources into local hot-reload.
  • Drizzle ORM — SQL-first, TypeScript-first, zero magic. Schema migrations live next to the app code, and every query is a typed expression with no runtime surprises.
  • Aurora Serverless v2 — scales to zero between traffic bursts (important when five of these apps are pre-revenue), and I never have to think about connection pools thanks to the RDS Proxy SST provisions automatically.

The combination means that a new project goes from npx create-next-app to a live, auto-scaling AWS deployment in an afternoon. The sst.config.ts is the single file that describes the entire infra.

Auth: Auth.js v5 + Cognito

Four of the five personal projects need user accounts. My default setup layers Auth.js v5 (formerly NextAuth) on top of AWS Cognito:

  • Cognito handles the identity plane — MFA, password resets, token rotation — so I never touch credentials myself.
  • Auth.js provides the session abstraction that Next.js middleware and server components consume. The session cookie is short-lived; token refresh is automatic.
  • For role-based access control (FenceFlow has four roles: Owner, Sales, Foreman, Customer) I store roles in the Drizzle schema and check them in server-component data-fetching, never on the client.

AI integrations

Three of the apps are AI-native:

  • Your House in 3D uses Claude Vision for a multi-pass elevation analysis that reads geometry straight from phone photos. Claude runs in a Python FastAPI sidecar on Lambda; the result feeds a manifold3d/trimesh CSG pipeline.
  • Landscape AI uses the Vercel AI SDK streaming API backed by Gemini. A two-stage pipeline preserves the real yard structure and restyles only the landscaping.
  • BreezeRFP (contracted) uses a LangChain + FAISS retrieval pipeline grounding Claude 3.5 Sonnet and GPT-4o in a company's own knowledge base.

In every case the pattern is the same: the AI is invoked server-side (either in a Next.js Route Handler or a Lambda function), the result is stored in Aurora and streamed or polled back to the client, and usage is metered through Stripe.

Stripe billing

Three projects charge users directly. The pattern is always:

  1. Create a Stripe Customer on first sign-in and store the ID in Aurora.
  2. Present a Stripe Checkout session (hosted, not embedded) for simplicity and PCI scope reduction.
  3. After checkout.session.completed fires the webhook, credit the account in Aurora and unlock the feature.

For token-based usage (Landscape AI packs, LOS Analyzer per-report) I write a credits column; for subscription access (FenceFlow) I write a plan column and check it in middleware.

The deploy pipeline

Each app has a GitHub Actions workflow with two jobs: ci (lint, typecheck, Vitest) and deploy (SST deploy on merge to main). The SST state lock means I can only deploy one app at a time on a shared AWS account — I merge PRs serially to avoid the race.

Because output: "standalone" is set, OpenNext can package the Next.js server into a Lambda function alongside a CloudFront distribution for edge caching. Static assets go to S3; API routes and RSC payloads go to Lambda. Cold start on a warmed function averages under 400 ms for these apps.

What I'd change

A few things I'd do differently on a greenfield project today:

  • Skip the RDS Proxy for purely personal apps with low concurrency — the cost savings don't justify the added latency for tiny traffic.
  • Use Vercel for Next.js if the project doesn't need direct AWS service access (SES, CloudWatch, custom Lambda) — the DX is smoother and Image Optimization works out of the box.
  • Drizzle migrations in CI — I currently run drizzle-kit migrate manually before deploying. Automating this in the deploy job would remove the one manual step remaining.

If you want to dig into any of these apps in more depth, the case studies are on the projects page.