Heath SchweitzerHeath Schweitzer
← All posts

The Stack Behind heathschweitzer.com: Every Decision Explained

April 29, 2026|Heath Schweitzer|5 min read|22 views|Last Updated June 15, 2026

Technology
Modern web stack workflow diagram showing frontend, database, CI/CD, server, decisions, and results flow

I've written about individual pieces of this stack throughout April — Next.js, Prisma, CI/CD, Nginx, Cloudflare. This post is the complete picture: every technology decision, why I made it, and what I'd do differently.

The Starting Point

In early 2026 I decided to migrate heathschweitzer.com from WordPress on a LAMP stack to something modern. The goals were: learn the current JavaScript ecosystem properly, build something I'd actually use and maintain, and end up with infrastructure I understood completely.

I had 20+ years of LAMP experience — Linux, Apache/Nginx, MySQL, PHP. The new stack needed to make use of that foundation rather than throw it away.

Frontend: Next.js 16 + React 19

Decision: Next.js 16 with the App Router, React 19, TypeScript throughout.

Why: Next.js is the dominant React framework for production applications. The App Router's Server Components model — where components that fetch data run on the server and never ship JavaScript to the client — clicked with my server-side background. It's conceptually similar to PHP rendering HTML, with modern tooling and TypeScript's safety net on top.

React 19 was current when I scaffolded the project. The main improvement I use is use() for async data in server components, which simplifies some data fetching patterns.

What I'd do differently: Start with Next.js 15 documentation when it was the stable release rather than scaffolding with the latest and discovering breaking changes mid-project. Pinning to a specific version in your create-next-app call avoids this: npx create-next-app@15.

Styling: Tailwind v4 + shadcn/ui (Base UI)

Decision: Tailwind CSS v4 for utility styling, shadcn/ui for components — but using the Base UI variant rather than Radix.

Why: Tailwind's utility-first approach trades CSS file management for longer class strings in JSX. For a solo developer it's a good tradeoff — no context switching between files, no naming things, no dead CSS to clean up. shadcn gives you polished accessible components as code you own rather than a dependency.

The Base UI gotcha: When shadcn asked which component primitive library to use, I chose Base UI (Anthropic's primitive library) over Radix. This turns out to be a meaningful difference: Base UI doesn't support the asChild prop that a lot of shadcn component patterns rely on. Anywhere you'd use <Button asChild><Link>...</Link></Button>, you instead need to use buttonVariants — a CVA class string — applied directly to the Link element.

What I'd do differently: Choose Radix unless you have a specific reason to use Base UI. The ecosystem assumes Radix and most tutorials, examples, and third-party components are written for it.

Database: MySQL + Prisma 6

Decision: MySQL (my existing database server in CloudPanel) with Prisma 6 as the ORM.

Why: I already knew MySQL. Keeping the database layer familiar let me focus learning energy on the application layer. Prisma adds TypeScript type safety to database access — the generated client gives you autocomplete on table names and columns, and type errors at compile time rather than runtime.

The Prisma version decision: Prisma 7 was current when I started but introduced breaking changes that conflicted with my setup. I pinned to Prisma 6, which has been stable and problem-free.

What I'd do differently: Nothing major here. MySQL + Prisma 6 has been solid. If starting fresh today I might consider PostgreSQL for better JSON column support and more advanced query features, but MySQL works fine for this use case.

Authentication: NextAuth v5 Beta

Decision: NextAuth v5 (beta) with the credentials provider and JWT strategy.

Why: NextAuth is the standard auth library for Next.js. v5 was a significant rewrite that aligns with the App Router's server-first model. The credentials provider handles username/password auth against my user table using SHA-256 password hashing.

Key configuration notes: AUTH_TRUST_HOST=true is required in production when running behind a reverse proxy (Nginx → Node.js). Without it, NextAuth rejects requests because the host header doesn't match the expected origin. This was a non-obvious debugging session.

What I'd do differently: Use a more robust password hashing algorithm — bcrypt or Argon2 — rather than SHA-256. SHA-256 is fast, which is a security weakness for password hashing (fast to brute force). For a personal site with one admin user it's acceptable, but I'd use bcrypt in any real application.

Infrastructure: CloudPanel + PM2 + Nginx + Cloudflare

Decision: DigitalOcean VPS managed with CloudPanel, Next.js process managed by PM2, Nginx as reverse proxy, Cloudflare in front of everything.

Why: I already ran a CloudPanel server. Adding a Node.js site to existing infrastructure was zero marginal cost. Cloudflare provides DDoS protection, edge caching, and DNS management for free.

The maintenance mode addition: The deploy script creates a flag file that Nginx detects to serve a static maintenance page during deploys. Users see a brief message instead of a 502 error during the ~10 seconds of restart. Simple but polished.

What I'd do differently: Nothing significant. This stack handles a personal site with room to grow. If traffic scaled dramatically, moving the database to a managed service (PlanetScale, Neon) and adding read replicas would be the first infrastructure changes.

CI/CD: GitHub Actions

Decision: GitHub Actions workflow that SSHes into the server and runs the deploy script on every push to main.

Why: Already on GitHub, already free, 15 lines of YAML, works reliably. Average deploy time is 40 seconds.

What I'd do differently: Add a TypeScript type check and lint step before deployment to catch errors in the GitHub Actions runner rather than on the server. A failed build on the server still works correctly (the deploy script aborts on error), but catching it earlier is faster feedback.

The Custom CMS

Unplanned but worth calling out: I built a full CMS on top of this stack — post editor, category/tag management, scheduled publishing, draft preview, contact form management, AI-assisted content generation, and a token-gated REST API. None of this was in scope when I started.

Building it was the best learning investment of the whole project. Every feature required understanding how Next.js, Prisma, and React actually work — not from a tutorial, but from solving a real problem.

The Stack in Summary

Frontend:      Next.js 16 / React 19 / TypeScript
Styling:       Tailwind v4 / shadcn (Base UI)
Database:      MySQL / Prisma 6
Auth:          NextAuth v5 beta / JWT
Email:         Resend
AI:            Anthropic API (claude-sonnet)
Server:        DigitalOcean / CloudPanel / PM2
Proxy:         Nginx
CDN/DNS:       Cloudflare
CI/CD:         GitHub Actions

Every piece is chosen deliberately, documented in the codebase, and understood end to end. That last part — understood end to end — was the whole point of the exercise.

Tagged

next.jsweb developmentdevopsstack

If this post was useful, consider buying me a coffee ☕ with ₿itcoin — no account needed, any amount welcome.

Bitcoin tip QR code
⚡ Open in Wallet