The Stack Behind heathschweitzer.com: Every Decision Explained

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
If this post was useful, consider buying me a coffee ☕ with ₿itcoin — no account needed, any amount welcome.