The Stack I'll Reuse: My 2026 Next.js Foundation, and the Rules That Keep It Current

The Stack I'll Reuse: My 2026 Next.js Foundation, and the Rules That Keep It Current
Topics:Next.jsFrontend ArchitectureDeveloper Tooling
Tech:React 19TypeScriptTailwind CSS

I've started enough Next.js projects to know exactly how the first day used to go: two hours of building, five hours of re-litigating decisions I'd already made on the last project. Which auth provider this time. Whether the styling setup from eight months ago still holds up. Which testing tools are worth wiring in before there's anything to test. Every project began with a fresh round of stack deliberation, and every deliberation produced a slightly different answer — which meant every project I maintained had a slightly different foundation, and none of the maintenance knowledge transferred cleanly.

This year I rebuilt my portfolio from the ground up, and I made a deliberate decision to treat the rebuild as something more than a redesign: it became the reference implementation. Every layer got auditioned under real load — real content migration, real auth, real AI features, real caching bugs, a real independent code review — and what survived is the stack I intend to carry into every Next.js project I start from here on.

This article is that stack, layer by layer, with the reasoning and the scars. But the inventory is the less important half. The second half covers what I've come to believe matters more than any individual tool choice: the rules that keep a stack current after the day you pick it. Steal whichever decisions fit your project. The discipline is the part I'd argue is universal.

How this stack earned its place

A stack list means nothing without its selection principle, so here's mine: boring majors, sharp edges only where they pay. Every foundational choice favors the widely-adopted, well-documented option, and the deliberately interesting choices (an embedded CMS, agent-operable content APIs) are confined to the layers where being interesting delivers something I can name.

The second principle: everything below was proven in production conditions, not assembled in a tutorial. The rebuild migrated roughly fifty articles, shipped server-side content gating, survived a two-blocker independent code review, and debugged three genuinely nasty caching and environment issues on live infrastructure. A stack that's only been greenfield-happy tells you nothing. This one has been yelled at.

The foundation: Next.js 16, React 19, TypeScript, pnpm

The framework layer is Next.js 16 on the App Router with React 19 and TypeScript 5.9, built with Turbopack, managed exclusively by pnpm.

The App Router's server-first model is the load-bearing decision. React Server Components let the content layer fetch directly in components-as-functions, which is what makes a repository-module architecture natural: pages call typed repo functions, repo functions call the data source, and the client bundle carries only what's actually interactive. On this site, that means the article renderer, the search index, and the SEO surfaces are all built server-side from typed content — the command palette's search payload is assembled on the server with access rules already applied, and the client's job is reduced to displaying it. The pages that are interactive (the palette, the AI chat, the animated hero) opt into the client explicitly, so interactivity is a decision you can point to in the code rather than the default everything pays for.

If your mental model of React is still client-components-plus-API-routes, this stack will feel inverted — that's a real adoption cost, and I'll come back to it in the who-shouldn't-copy-this section.

Version posture matters as much as version choice. The rebuild pinned every dependency major, and that pin is what made staying current cheap: when Next.js 16.3 shipped mid-project with Turbopack's build disk cache and faster server rendering, adopting it was a same-week, one-evening upgrade precisely because nothing else was drifting. The pattern I keep relearning: teams that float their versions dread upgrades; teams that pin them schedule upgrades.

Two mechanical details that pay for themselves on every project:

{
  "packageManager": "pnpm@11.21.0",
  "engines": { "node": ">=22 <25" },
  "scripts": { "preinstall": "npx only-allow pnpm" }
}

One package manager, enforced, so no contributor (human or AI) ever half-converts the lockfile. One Node range, stated, so the local/CI/production runtimes can't quietly diverge. These are one-line decisions that eliminate whole categories of "works on my machine."

Content: Payload, living inside the app

The CMS layer is Payload 3, embedded directly in the Next.js app — same repo, same deploy, content in Postgres, types generated from the schema. I wrote a full article on this decision and the migration that proved it — From Notion to Payload — so here I'll keep it to the one-sentence version: content became typed data I own, behind a repository seam that let me swap the entire CMS without breaking a single URL.

What "embedded" buys operationally, beyond the migration story: the admin panel ships with the site at /admin, draft preview renders unpublished content inside the real design system rather than an approximation of it, and schema changes travel through the same pull request as the code that consumes them — one deploy, one review, no coordination problem between a content platform and an app that consumes it. The CMS also exposes a permission-scoped API endpoint that AI agents can operate through, which quietly became one of the most-used features of the whole rebuild: content operations became something I can delegate without handing over the keys.

The stack-level lesson worth repeating even in summary: put a repo-module layer between your content source and your pages from day one, and enforce access rules at the data layer rather than in the pages that remember to check. Both of those choices were stress-tested by an independent review, and both held.

Styling and components: Tailwind v4 and shadcn/ui

Styling is Tailwind CSS v4 in its CSS-first configuration — theme tokens live in the stylesheet, there's no JavaScript config file to drift, and the design system's custom variants sit next to the custom properties they consume. The v4 model finally makes the stylesheet the single source of truth, which suits a design system that has to hold light/dark parity as an acceptance criterion rather than an afterthought.

Components start from shadcn/ui primitives — and the distinction that makes shadcn the right call is that it isn't a dependency, it's a starting point you own. The primitives land in the repo, get adapted to the design system, and evolve without waiting on an upstream. When a primitive needs to grow a variant or absorb a project-specific behavior, that's an edit to your own file, not a fork, a wrapper, or a feature request.

On top of that sits a house rule that's done more for quality than any individual component choice: every new UI component gets a Storybook story, and serious accessibility violations fail the story. Accessibility regressions become test failures, not audit findings. The same stories double as visual documentation and interaction tests, so the design system stays honest about what exists and how it behaves in both themes and under reduced motion.

Two design-system acceptance criteria ride along with every component, stated in the repo docs rather than carried in anyone's head: light/dark parity (both themes are first-class; audits cover both) and honored reduced motion (every animated surface — including the WebGL shader hero — degrades to a static, fully functional DOM when the user asks for less motion). Writing those down as invariants is what let the accessibility gate enforce them mechanically; unwritten standards can't fail a build.

Auth: Clerk, with enforcement below it

Auth is Clerk — hosted components for the session UX, webhooks (signature-verified, replay-protected) for syncing user lifecycle events, and a clean seam for gated content and future billing without building any of that from scratch. The pragmatic case is time-to-correct: session management, OAuth providers, and account UX are solved problems where custom implementations mostly manufacture novel security bugs, and a portfolio's differentiation budget belongs elsewhere. The webhook channel is the piece people skip and shouldn't: it's what turns sign-ups into data you own (an email capture list, a user record) instead of state that lives only in the provider's dashboard.

The rule that makes hosted auth safe rather than convenient: Clerk answers who the viewer is; my data layer decides what they can see. Gating on this site is enforced with field-level access rules at the CMS layer, so the auth provider's client components are conveniences, never the enforcement point. That separation was exactly what the code review pressure-tested, and it's the reason swapping auth providers someday would be a UX project, not a security project.

AI: Vercel AI SDK, wrapped in guardrails

The AI layer is the Vercel AI SDK powering Hermes, the site's chat assistant. The SDK earns its place by making the hard parts boring: streaming, tool calls, and provider abstraction are solved problems, so the engineering effort goes where it actually belongs: the guardrails.

That guardrail stack is the part I'd port to any AI feature: same-origin request validation, per-IP and daily rate limits backed by Upstash Redis, Zod validation at the boundary, per-message size and window caps, client-supplied system messages stripped server-side, and the system prompt enforced on the server where no client can rewrite it. An AI endpoint is an open wallet attached to your API; the SDK makes the feature possible, but the guardrails make it shippable. The same layered stack turned out to be reusable verbatim when the site's contact endpoint needed hardening — origin guard, rate limit, schema validation — which is the test of a good pattern: the second use costs a fraction of the first.

This layer also delivered the rebuild's freshest humility lesson. The independent review caught a subtle bug in my rate-limiter wiring: limiter instances were cached module-wide with whichever route's configuration happened to warm the serverless instance first, meaning one route's limits could silently apply to another's traffic. It passed every test, because the default configuration masked it. Which is the point of the next section.

The testing spine: Vitest, Playwright, Storybook

Testing is Vitest for unit and component tests, Playwright for end-to-end flows, and Storybook's interaction runner for component behavior with the accessibility gate described above. AI behavior gets its own evaluation suite, because "the chatbot seems fine" is not a regression strategy.

Each layer earns its slot by catching a different failure class. Vitest holds the pure logic and the data-layer contracts — including regression tests for every security fix, and an invariant test that pins the cache-tag vocabulary so a rename can't silently disconnect the publish pipeline from the caches it purges. Storybook's interaction tests catch the failures that only exist rendered: a contrast ratio that regressed in dark mode, a component that breaks without its theme context. Playwright holds the flows a user would actually notice breaking. The discipline that makes the pyramid work is assigning each bug you fix to the cheapest layer that could have caught it, then writing that test; most fixes in this rebuild shipped with a regression test at the Vitest layer for exactly that reason.

The spine works. It's also where I'll offer this article's most honest caveat: a green test suite measures your code against your assumptions, and your assumptions are the thing that's usually wrong. The rebuild's three worst bugs — a chat endpoint that rejected every request on staging, a revalidation webhook that silently 401'd, a draft-preview pane that silently 403'd — were all the same species: behavior coupled to environment configuration that no local test or CI run could ever observe, because CI doesn't run with production's environment topology. All three shipped through a fully green pipeline.

The countermeasure isn't more unit tests. It's treating live verification as a first-class step: after every deploy that touches an env-coupled surface, exercise that surface on the real deployment, and when something can't be verified live, write down that it wasn't. Which brings me to the discipline section.

The rules that keep it current

Everything above is replaceable. What I actually expect to carry across every future project is this list.

Pin majors; schedule upgrades. Floating versions turn every install into a small gamble and every upgrade into an archaeology project. Pinned majors make minor-version adoption routine (the 16.3 upgrade cost one evening) and make major-version adoption a deliberate, planned event with a diff you can actually read.

Upgrade version-locked families as one set. Payload and its plugin packages move together, always. Mixed versions within a framework family are a bug factory with a delayed fuse: the failures show up two weeks later in a module you didn't touch.

Attempt the upgrade; respect the blocker; document the retreat. Mid-rebuild, TypeScript 7 shipped its native compiler. I tried it the same week: the compiler itself worked, but the lint toolchain's peer dependencies hard-rejected it, and there was no clean dual-version path. So I retreated — but the productive kind of retreat: the config changes TS7 needs landed anyway, the blocker is documented in the repo's maintenance doc, and when the ecosystem catches up the upgrade is a one-line bump. Staying current isn't about adopting everything immediately. It's about knowing precisely what you're waiting for.

Let platform warnings schedule work, not panic. Mid-rebuild, my deployment platform started flagging projects for a Node runtime upgrade. The reflex is to bump immediately; the discipline is to check the actual constraint first. This project's runtime wasn't affected by the deadline, the shared project settings served an older production branch too, and the honest answer was: schedule the flip for the promotion moment, when it's one deliberate change in a checklist instead of a mid-flight variable. A warning is an input to planning, not an interrupt.

Verify live; log what was measured versus assumed. The maintenance doc keeps a running distinction between behavior that was measured on real infrastructure and behavior that was assumed from docs or local runs. When live measurement contradicted assumptions — as it did, repeatedly, with cache invalidation — the honest move was documenting the measured behavior and its bounds, not patching the docs to match the theory. A maintenance doc that admits what's unsolved is worth ten that don't.

Keep one canonical instruction file for AI collaborators. This stack is maintained with AI agents, not just by me, and that stopped being a novelty and became an architecture concern. The repo keeps one canonical agent-instructions file (symlinked to the filenames each tool ecosystem expects) plus a docs directory where each file states auditable rules: which layers own which decisions, what the invariants are, what must never regress. Those docs did double duty in a way I didn't fully anticipate: when an independent review agent audited the rebuild, the written standards were what made review possible: the reviewer checked the code against the repo's own stated rules, and the gaps it found were real gaps. Documentation-as-standards turns review from opinion into verification, for humans and agents alike.

Get fresh eyes before the merge — and let them be adversarial. The rebuild's endgame was an independent review with no prior context, instructed to verify claims rather than trust them. It found two real security gaps my own layered defenses had missed, and its second pass caught the rate-limiter bug described above. The pattern generalizes: the author's test suite encodes the author's blind spots, and only an outside pass — human or agent — audits the assumptions themselves.

Who shouldn't copy this stack

Decision guidance means naming who this is wrong for, so:

  • Mostly-static sites with no content operation. If you publish rarely and your content is prose, an embedded CMS plus Postgres is infrastructure you'll resent. A static-first framework, or plain Next.js with MDX, serves you better.
  • Teams not deploying to Vercel or an equivalent serverless platform. The stack runs elsewhere, but real decisions here — caching semantics, rate-limiter storage, the serverless-instance behavior that produced the limiter bug — are tuned to that topology. Self-hosting changes the trade-offs; re-derive them, don't inherit mine.
  • Teams that don't want the server-first model. RSC is this stack's spine. If your team's expertise and preference is client-heavy SPA architecture, adopting this means retraining, not just installing.
  • Organizations with hard requirements against hosted auth. Clerk is a dependency on someone else's uptime and data handling. The enforcement-below-auth pattern still applies, but the provider slot needs your own answer.
  • Anyone who doesn't need the AI layer. It's cleanly separable. Don't carry a guardrail stack for a feature you don't ship.

What survives even if you reject every tool above: the selection principle (boring majors, sharp edges only where they pay), the seams (repo layer between content and pages, enforcement at the data layer), and every rule in the discipline section. Those are stack-agnostic.

Assembling your own

If you're standing at the start of a new project, here's the compressed version of what this rebuild taught me to do:

  • Pick boring majors and pin them; spend your novelty budget on at most one or two layers where being interesting pays something specific.
  • Enforce the environment mechanically — one package manager, one Node range, stated in the repo, not in the wiki.
  • Put a seam at every boundary you might someday want to change: content behind a repo layer, auth above data-layer enforcement, AI behind guardrails.
  • Wire the testing spine before the features, and give accessibility a failing gate, not a checklist.
  • Write your rules down where your collaborators — including the AI ones — will actually read them, and keep an honest ledger of measured versus assumed.
  • Before you ship the foundation, buy one adversarial review from fresh eyes.

The next time you start a project and feel the stack deliberation beginning again, notice what it's costing you. Settling the debate once — with a reference implementation that's been yelled at in production, and rules for keeping it current — is the highest-leverage project you can run. Mine took a rebuild. Yours can start with a checklist.