<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>next-starters Blog</title>
        <link>https://next-starters.catesworks.dev/blog</link>
        <description>next-starters Blog</description>
        <lastBuildDate>Fri, 14 Aug 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Three verified bugs that only showed up when I actually ran the code]]></title>
            <link>https://next-starters.catesworks.dev/blog/skill-distribution-rollout</link>
            <guid>https://next-starters.catesworks.dev/blog/skill-distribution-rollout</guid>
            <pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Adversarial code review that runs the code beats adversarial code review that reads the code.]]></description>
            <content:encoded><![CDATA[<p><em>Adversarial code review that runs the code beats adversarial code review that reads the code.</em></p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="the-problem">The problem<a href="https://next-starters.catesworks.dev/blog/skill-distribution-rollout#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h2>
<p>I had a working plan for distributing small content packages (call them "skills" — self-contained bundles of docs + reference files an AI coding assistant can load) three ways at once: an existing symlink-based install, a plugin marketplace, and versioned npm packages. The plan went through several rounds of review before any code got written — a planner drafts, an architect and a critic both try to find holes, and the planner revises until nobody can find another one.</p>
<p>Three of those review rounds each found a real bug. Not a style nit, not a "have you considered" — an actual, reproducible defect that would have shipped broken artifacts to a public package registry. What made them worth trusting wasn't that the reviewers were thorough on paper. It's that they stopped taking the plan's word for anything and started <em>running it</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="what-i-tried">What I tried<a href="https://next-starters.catesworks.dev/blog/skill-distribution-rollout#what-i-tried" class="hash-link" aria-label="Direct link to What I tried" title="Direct link to What I tried" translate="no">​</a></h2>
<p>The review discipline was simple to state and expensive to follow: every claim about how a third-party tool behaves gets verified against the real tool, in a scratch environment, before it's allowed into the plan.</p>
<p>Round one: the plan assumed a version-bump-and-publish tool would shell out to one specific packaging command regardless of context. A reviewer built a real workspace, ran the real publish path, and watched it call a <em>different</em> command than the plan assumed — one with different file-exclusion rules. The plan's own validation step would have silently passed while the real artifact shipped broken.</p>
<p>Round two: the plan piped a multi-command shell chain into a CI action's input string. A reviewer traced the action's source, found it spawns processes without a shell, and reproduced the exact failure — the chain silently truncated after the first command and the job reported success anyway.</p>
<div class="language-text codeBlockContainer_ZOQN theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_wr3q"><pre tabindex="0" class="prism-code language-text codeBlock_m2zE thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_fAaQ"><div class="token-line" style="color:#393A34"><span class="token plain"># what the plan assumed (works in a terminal):</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">"version &amp;&amp; sync-metadata &amp;&amp; install --lockfile-only"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># what actually happens with no shell — everything after &amp;&amp; is dropped,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># and the tool that received it still exits 0</span><br></div></code></pre></div></div>
<p>Round three: a version-consistency rule was supposed to stop private packages from silently drifting ahead of what's actually published. It worked — until a reviewer noticed the <em>fix itself</em> had a timing bug: it wrote the corrected version to source control one step <em>after</em> the artifact was already packed and pushed to the registry. Structurally correct, sequenced wrong.</p>
<p>None of these were things you'd catch by reading the diff carefully. All three needed someone to actually spin up the dependency, feed it the exact input the plan specified, and watch what came back.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="what-i-learned">What I learned<a href="https://next-starters.catesworks.dev/blog/skill-distribution-rollout#what-i-learned" class="hash-link" aria-label="Direct link to What I learned" title="Direct link to What I learned" translate="no">​</a></h2>
<ul>
<li class=""><strong>"I read the source and I'm confident" is a different claim from "I ran it and confirmed."</strong> The gap between them is exactly where these three bugs lived.</li>
<li class=""><strong>A review that only critiques the plan's <em>logic</em> will pass a plan whose logic is sound and whose premises are wrong.</strong> All three bugs were premise bugs — "this tool behaves like X" — not reasoning bugs.</li>
<li class=""><strong>Give reviewers permission to be adversarial, and give them a scratch environment to be adversarial <em>in</em>.</strong> A review that can only read text argues about what's plausible. A review that can spin up the real dependency argues about what's true.</li>
<li class=""><strong>The fix for a caught bug deserves the same scrutiny as the original plan.</strong> The version-timing bug above was found on the <em>second</em> review pass, in a fix for something the <em>first</em> pass had already caught — verification has to be recursive, not one-and-done.</li>
<li class=""><strong>Rolling the same pattern out to a second, third, and fourth real environment surfaces the assumptions a single environment let you get away with.</strong> The first repo this pattern shipped to was clean. The next several each had at least one thing the first one never exercised — a different tool version, an existing pipeline with conflicting conventions, content that had never been validated before. None of those were bugs in the pattern; they were gaps in how much of the real world the first pass had touched.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="takeaways">Takeaways<a href="https://next-starters.catesworks.dev/blog/skill-distribution-rollout#takeaways" class="hash-link" aria-label="Direct link to Takeaways" title="Direct link to Takeaways" translate="no">​</a></h2>
<ul>
<li class="">Treat "I'm confident this is how the tool works" as a hypothesis, not a fact — verify it against the real tool before it becomes a load-bearing assumption in a plan.</li>
<li class="">When you can afford it, give a review pass a disposable environment and explicit permission to try to break the plan for real, not just to critique it on paper.</li>
<li class="">A bug found in round N doesn't retire the need for round N+1 to re-verify — fixes have premises too.</li>
<li class="">The cheapest time to find a wrong assumption about a third-party tool is before you've built five things on top of it, not after.</li>
</ul>]]></content:encoded>
            <category>code-review</category>
            <category>testing</category>
            <category>ci-cd</category>
            <category>ai-agents</category>
        </item>
        <item>
            <title><![CDATA[What four rounds of adversarial AI review caught in auth infrastructure code]]></title>
            <link>https://next-starters.catesworks.dev/blog/auth-email-skills</link>
            <guid>https://next-starters.catesworks.dev/blog/auth-email-skills</guid>
            <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Each round found something real. The findings got smaller every time — until they stopped.]]></description>
            <content:encoded><![CDATA[<p><em>Each round found something real. The findings got smaller every time — until they stopped.</em></p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="the-problem">The problem<a href="https://next-starters.catesworks.dev/blog/auth-email-skills#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h2>
<p>We had a proven pattern: AWS email infrastructure, provisioned by Terraform,
synced into a hosted auth provider's config, with branded transactional
emails compiled from templates and pushed the same way. It worked, in
production, across more than a dozen internal projects. It had just never
been turned into something a <em>new</em> project could pick up and use — it was
tribal knowledge, copy-pasted and drifting.</p>
<p>The task: package it as a reusable, documented pattern. Simple enough to
describe in a sentence. Nowhere near simple enough to get right on the
first pass — because the pattern touches Terraform-provisioned AWS
credentials, a hosted auth provider's SMTP/captcha config, and an
HMAC-signed internal email-sending endpoint. Get any of those subtly wrong
and you don't get a build failure. You get a working-looking scaffold that
quietly mishandles a secret.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="what-i-tried">What I tried<a href="https://next-starters.catesworks.dev/blog/auth-email-skills#what-i-tried" class="hash-link" aria-label="Direct link to What I tried" title="Direct link to What I tried" translate="no">​</a></h2>
<p>The approach that mattered wasn't the architecture — it was the review
loop around it. A plan went through four rounds of an adversarial AI critic
before implementation even started, each round explicitly told to verify
claims against real files on disk, not trust the plan's prose. Then, after
a team of parallel agents implemented it, the <em>implementation</em> went through
four more rounds of the same critic, re-reading source files and re-running
the test suite independently each time.</p>
<p>The rounds narrowed in a way worth noticing:</p>
<ul>
<li class=""><strong>Round 1</strong> found an architectural flaw: the plan split the system along
the wrong axis. Two "clean" halves shared no real connection — one
produced data the other needed, but nothing wired them together. The fix
wasn't a patch, it was re-cutting the boundary around the actual data
flow between them.</li>
<li class=""><strong>Round 2</strong>, after the re-cut, found execution-level gaps: a drift-check
test that would always fail against its own supporting files, and
generated documentation that pointed at the wrong tool for one specific
step.</li>
<li class=""><strong>Round 3</strong> found something sharper: an idempotency rule meant to keep a
secret-sync script from re-sending credentials on every run was <em>too</em>
effective — it also silently swallowed a legitimate credential rotation.
The script would report success while quietly not rotating a secret that
had actually changed upstream.</li>
<li class=""><strong>Round 4</strong> found one narrow leftover: a comment in generated output
still named the internal tool the new script had replaced.</li>
</ul>
<p>By round 4, the reviewer's own words were "no architectural replanning is
otherwise required." That's the signal that the loop had converged — not a
fixed number of rounds, but the point where findings stop being about the
design and start being about a single stale string.</p>
<div class="language-text codeBlockContainer_ZOQN theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_wr3q"><pre tabindex="0" class="prism-code language-text codeBlock_m2zE thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_fAaQ"><div class="token-line" style="color:#393A34"><span class="token plain">Round 1: wrong system boundary          (architecture)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Round 2: test scope, doc accuracy       (contract completeness)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Round 3: idempotency vs. secret rotation (correctness)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Round 4: one stale reference             (polish)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">→ APPROVE</span><br></div></code></pre></div></div>
<p>The same shape repeated after implementation: four more rounds, same
narrowing pattern, same stop condition.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="what-i-learned">What I learned<a href="https://next-starters.catesworks.dev/blog/auth-email-skills#what-i-learned" class="hash-link" aria-label="Direct link to What I learned" title="Direct link to What I learned" translate="no">​</a></h2>
<ul>
<li class=""><strong>A single review pass is not enough for secret-handling code.</strong> The
rotation bug in round 3 is exactly the kind of thing that passes every
functional test — the script behaves <em>correctly</em> on every input except
"the secret changed and nothing else did," which is precisely the input
a real credential rotation produces. It took a reviewer specifically
looking for it.</li>
<li class=""><strong>Split systems along the data dependency, not along the technology.</strong>
The instinct to divide "infrastructure code" from "application code"
felt clean and was wrong — it ignored that infrastructure output was the
literal input the application half needed. The dependency should decide
the boundary, and the boundary should carry a concrete, versioned
artifact across it — not an assumption.</li>
<li class=""><strong>Track when a review loop is actually converging.</strong> Round-over-round,
each finding got narrower and less architectural. That's the signal to
keep going one more round, not stop — and the signal to <em>actually</em> stop
is an explicit "no architectural replanning needed," not a fixed budget.</li>
<li class=""><strong>Multi-agent implementation surfaces its own race conditions.</strong> With
several agents editing a shared codebase in parallel, a test failure
during active concurrent work isn't automatically a bug — it can be a
read caught mid-write by another agent. Re-run before diagnosing; don't
let a transient read become a false story about what broke.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="takeaways">Takeaways<a href="https://next-starters.catesworks.dev/blog/auth-email-skills#takeaways" class="hash-link" aria-label="Direct link to Takeaways" title="Direct link to Takeaways" translate="no">​</a></h2>
<ul>
<li class="">For anything touching credentials, secrets, or auth config: budget for
3+ independent review rounds, not 1. The rounds get cheaper and narrower
as they go — that's the tell they're working, not a sign to stop early.</li>
<li class="">When splitting a system into independently-usable pieces, ask what data
actually has to cross the boundary before deciding where the boundary
goes.</li>
<li class="">The single most valuable instruction to give a review pass isn't "check
this" — it's "verify this against the real files, don't trust the
description." Every round that mattered here found something because it
re-read source, not because it re-read the plan.</li>
</ul>]]></content:encoded>
            <category>ai-agents</category>
            <category>code-review</category>
            <category>infrastructure-as-code</category>
            <category>security</category>
        </item>
        <item>
            <title><![CDATA[I asked an AI agent to build a feature. It found I'd already built it — badly, three times.]]></title>
            <link>https://next-starters.catesworks.dev/blog/mcp-tooling-harvest</link>
            <guid>https://next-starters.catesworks.dev/blog/mcp-tooling-harvest</guid>
            <pubDate>Sun, 09 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[*A tutorial-shaped feature request turned into a lesson about checking for prior]]></description>
            <content:encoded><![CDATA[<p><em>A tutorial-shaped feature request turned into a lesson about checking for prior
art before designing, and about what adversarial code review actually catches
that a careful first pass doesn't.</em></p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="the-problem">The problem<a href="https://next-starters.catesworks.dev/blog/mcp-tooling-harvest#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h2>
<p>I gave an AI coding agent a fairly open brief: read this tutorial about wiring
up MCP servers (the protocol coding agents use to call tools) for an app, and
turn it into a reusable pattern I could stamp into any of my projects — a CLI,
a local server for my coding agent, a remote server for hosted AI tools, and a
"skill" file teaching an agent how to use all of it, all sharing one service-
account auth story.</p>
<p>The agent did what I asked. It designed a new shared package, a three-way
pluggable auth adapter, and a distribution plan, all cleanly argued from the
tutorial. It looked good. It was also about to duplicate a system I'd already
built, tested, and shipped to production in a completely different project —
a fact the agent's own plan should have surfaced and didn't, because it only
searched the one repository the request landed in.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="what-i-tried">What I tried<a href="https://next-starters.catesworks.dev/blog/mcp-tooling-harvest#what-i-tried" class="hash-link" aria-label="Direct link to What I tried" title="Direct link to What I tried" translate="no">​</a></h2>
<p>Before letting execution start, I ran the plan through an adversarial review
step — a second AI pass whose entire job is to try to break the plan, not
approve it. That review did something the design pass hadn't: it searched
across my <em>whole</em> project family, not just the one repo, for anything that
already did what the plan proposed.</p>
<p>It found three independent, hand-rolled implementations of the same "mint a
secret token, hash it, verify it later" pattern — in three different projects,
written at different times, with entropy quality that had already drifted
between them. One project even had a working admin UI for managing these
tokens. The plan got scrapped and rebuilt from scratch around harvesting the
best of that existing code instead of reinventing it.</p>
<div class="language-text codeBlockContainer_ZOQN theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_wr3q"><pre tabindex="0" class="prism-code language-text codeBlock_m2zE thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_fAaQ"><div class="token-line" style="color:#393A34"><span class="token plain">Before: design from the tutorial</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  tutorial → new abstraction → 3-way adapter matrix → build</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">After: search for prior art first</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  tutorial → grep the whole project family → found 3 drifting</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  hand-rolled versions of the same thing → harvest the best one →</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  generalize it → build</span><br></div></code></pre></div></div>
<p>That reframing changed everything downstream: instead of inventing a new
auth-provider abstraction speculatively, the work became "take the pattern
that's already proven in production and make it reusable" — a much narrower,
much safer scope.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="what-i-learned">What I learned<a href="https://next-starters.catesworks.dev/blog/mcp-tooling-harvest#what-i-learned" class="hash-link" aria-label="Direct link to What I learned" title="Direct link to What I learned" translate="no">​</a></h2>
<p>The harvest-first plan still wasn't done. It went through four rounds of
adversarial review before the <em>implementation</em> was approved, and the findings
in round one were not nitpicks:</p>
<ul>
<li class="">A module that was supposed to be "off" by default only hid its UI — the
underlying API routes it managed stayed fully reachable. A "disabled"
feature that isn't actually disabled is a real security gap, and it's
exactly the kind of bug that passes every unit test (each route worked
correctly in isolation) while failing the property that actually matters
(does <em>every</em> surface this feature owns respect the same flag?).</li>
<li class="">A "this operation is safe to retry" guarantee was implemented as an
in-memory flag on one process — which means it silently stopped being true
the moment there were two processes, or one restart. The fix moved the
guarantee to the one place it could actually hold: the database write
itself, made idempotent at that layer instead of tracked in memory above it.</li>
<li class="">A function returned an object that shared memory with what had just been
saved to the database — meaning a caller could mutate the <em>result</em> and
retroactively change what was persisted, without going through any
authorized path.</li>
</ul>
<p>None of these are the kind of bug a single careful author tends to catch in
their own code, because the same mental model that wrote the implementation
also tends to review it, and it doesn't reliably interrogate its own
assumptions. What actually caught them was a genuinely adversarial second
pass — instructed explicitly to find reasons to reject, not to confirm the
work looks reasonable — repeated until it ran out of real findings, with every
fix re-verified against a fresh test run rather than trusted on the fixing
agent's own word.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="takeaways">Takeaways<a href="https://next-starters.catesworks.dev/blog/mcp-tooling-harvest#takeaways" class="hash-link" aria-label="Direct link to Takeaways" title="Direct link to Takeaways" translate="no">​</a></h2>
<ul>
<li class="">Before designing anything from an external tutorial or article, search your
<em>whole</em> project family for prior art — not just the repository the request
landed in. The right question often isn't "how do I build this" but "have I
already built this, and is it good enough to generalize?"</li>
<li class="">"It passed all its tests" and "it's actually safe" are different claims.
Property-level checks (does <em>every</em> surface a flag governs actually respect
it?) catch a different class of bug than unit tests do.</li>
<li class="">An idempotence or safety guarantee needs to live at the layer where the
state actually is. If two people can each think they're "the first" to do
something, the guarantee is in the wrong place.</li>
<li class="">Adversarial review earns its cost in proportion to how security-adjacent the
change is. For anything touching auth, access control, or "this should be
disabled," budget for a real adversarial pass, and re-verify each fix
independently before calling it done — not just once, but until the
reviewer runs out of real findings.</li>
</ul>]]></content:encoded>
            <category>ai-agents</category>
            <category>code-review</category>
            <category>software-architecture</category>
            <category>security</category>
        </item>
        <item>
            <title><![CDATA[Standardizing runtime env across a fleet of Next.js + Supabase apps]]></title>
            <link>https://next-starters.catesworks.dev/blog/env-stack-migration</link>
            <guid>https://next-starters.catesworks.dev/blog/env-stack-migration</guid>
            <pubDate>Sun, 02 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Sanitized write-up. Names/hosts/keys generalized. Not for a specific client.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Sanitized write-up. Names/hosts/keys generalized. Not for a specific client.</p>
</blockquote>
<p>I had a dozen Next.js + Supabase apps that each reinvented environment
configuration — the same layered-dotenv loader forked under a handful of package
scopes, inlined into a preload script in some repos, absent in others. Config
that varies by environment leaked into the build as <code>NEXT_PUBLIC_</code> constants, so
rotating a URL meant a rebuild. I set out to make one pattern and migrate the
first app end-to-end as the template.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="the-model">The model<a href="https://next-starters.catesworks.dev/blog/env-stack-migration#the-model" class="hash-link" aria-label="Direct link to The model" title="Direct link to The model" translate="no">​</a></h2>
<p>Environment variables are <strong>runtime config, not build constants</strong>. A single
variable — <code>ENV</code> — selects the stage (<code>dev</code>/<code>test</code>/<code>staging</code>/<code>prod</code>), and a small
loader merges dotenv files in precedence order:</p>
<div class="language-text codeBlockContainer_ZOQN theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_wr3q"><pre tabindex="0" class="prism-code language-text codeBlock_m2zE thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_fAaQ"><div class="token-line" style="color:#393A34"><span class="token plain">.env &lt; .env.local &lt; .env.${ENV} &lt; secretsPath &lt; .env.${ENV}.local</span><br></div></code></pre></div></div>
<p>The server reads <code>process.env</code> at request time and forwards only the
explicitly-public values to the client through a provider — no <code>NEXT_PUBLIC_</code>,
nothing baked into the bundle. Change a value, restart, done. One image promotes
across every stage.</p>
<p>Two shared packages carry it: a framework-agnostic <strong>loader</strong> and a <strong>typed
registry</strong> where each variable declares its type (<code>public</code>/<code>secret</code>/<code>connection</code>)
— which also drives what's safe to commit.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="the-gotchas-that-actually-cost-time">The gotchas that actually cost time<a href="https://next-starters.catesworks.dev/blog/env-stack-migration#the-gotchas-that-actually-cost-time" class="hash-link" aria-label="Direct link to The gotchas that actually cost time" title="Direct link to The gotchas that actually cost time" translate="no">​</a></h2>
<p><strong>ESM-only broke CommonJS consumers.</strong> I shipped the loader ESM-only. Wrong:
tracing agents, <code>--require</code> hooks, and a CJS preload all need to <code>require()</code> it —
and Next compiles <code>next.config.ts</code> to CJS and <code>require()</code>s externals, throwing
<code>ERR_PACKAGE_PATH_NOT_EXPORTED</code>. Dual ESM+CJS, with CJS as a first-class citizen,
not a shim.</p>
<p><strong>A build-time env preload clobbered <code>NODE_ENV</code>.</strong> Wiring the preload into the
build script overwrote the framework's <code>NODE_ENV=production</code> with <code>development</code>,
producing a dev/prod React mismatch that crashed prerendering. Rule: the env
preload belongs on <code>dev</code>/<code>start</code>, never on <code>build</code>.</p>
<p><strong>Local database bring-up ran migrations before the schema existed.</strong> The local
Supabase stack runs its own migrations at boot — but the ORM owns the schema, so
any migration enabling row-level security or an auth hook that reads app tables
failed. The fix: relocate that table-dependent SQL into a post-schema step that
runs <em>after</em> the ORM creates tables — the same ordered step locally and in prod.</p>
<p><strong>Row-level security with zero policies silently broke every login.</strong> The auth
token hook runs as a role that does <em>not</em> bypass RLS, so it read zero rows and
every token came back with no org/role. A <code>GRANT</code> isn't enough under RLS — you
need an explicit permissive policy for that role. Caught only by decoding a JWT.</p>
<p><strong>Seeded users couldn't log in.</strong> Identity is the auth provider's user id; a
plain database seed writes app rows with random ids that map to no auth user. The
fix: create real auth users via the admin API with email-confirm on (skips
signup, captcha, and the email round-trip), then key the app rows to the returned
id. Now automated tests log in with known credentials.</p>
<p><strong><code>.env.prod</code> never reaches the platform.</strong> The container build ignores <code>.env.*</code>,
and the managed host doesn't run the preload — so on both platforms <em>every</em>
variable, secret and non-secret, has to be entered in the platform's settings. A
push does not magically configure prod; secrets, migrations, and the auth
dashboard config are one-time manual (or scripted) setup.</p>
<h2 class="anchor anchorTargetStickyNavbar_R8aP" id="what-shipped">What shipped<a href="https://next-starters.catesworks.dev/blog/env-stack-migration#what-shipped" class="hash-link" aria-label="Direct link to What shipped" title="Direct link to What shipped" translate="no">​</a></h2>
<p>The pattern is now three composable skills — the app-side loader, the local
stack (containerized DB, ordered process orchestration, per-repo port blocks so
several stacks coexist), and the first-prod-deploy runbook — plus the two shared
packages. The first app migrated cleanly and verified end-to-end: local stack up,
seeded accounts logging in with correct roles, no captcha friction for tests. It
becomes the diff-template for the remaining eleven.</p>
<p>The meta-lesson: a shared <em>dependency</em> beats a shared <em>document</em>. Every one of
those forks started as "we all agree on the pattern." Agreement drifts; a
published package doesn't.</p>]]></content:encoded>
            <category>nextjs</category>
            <category>supabase</category>
            <category>env-config</category>
            <category>devops</category>
        </item>
    </channel>
</rss>