<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vlad Zoff</title>
    <description>The latest articles on DEV Community by Vlad Zoff (@vladzoff).</description>
    <link>https://dev.to/vladzoff</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3992934%2F61af1d3c-0ba9-465b-9e29-befc3796b413.jpeg</url>
      <title>DEV Community: Vlad Zoff</title>
      <link>https://dev.to/vladzoff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladzoff"/>
    <language>en</language>
    <item>
      <title>The Agent Shouldn't Be Able to Approve Its Own Rules</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Thu, 01 Oct 2026 05:17:52 +0000</pubDate>
      <link>https://dev.to/vladzoff/the-agent-shouldnt-be-able-to-approve-its-own-rules-2lhc</link>
      <guid>https://dev.to/vladzoff/the-agent-shouldnt-be-able-to-approve-its-own-rules-2lhc</guid>
      <description>&lt;p&gt;&lt;em&gt;Once a coding agent can change the architecture, changing the rules that protect it becomes a different kind of operation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One of the less obvious problems I've run into with coding agents isn't that they make bad changes.&lt;/p&gt;

&lt;p&gt;It's that sometimes they make a perfectly reasonable change that invalidates one of the rules I'm using to check the project.&lt;/p&gt;

&lt;p&gt;That's a different problem.&lt;/p&gt;

&lt;p&gt;Imagine a project has this rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All payment-provider access must go through PaymentAdapter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent is asked to add support for another payment provider.&lt;/p&gt;

&lt;p&gt;It looks at the existing code and decides that the current adapter isn't quite right. It wants to introduce a new abstraction.&lt;/p&gt;

&lt;p&gt;The resulting change crosses a boundary that the project currently protects.&lt;/p&gt;

&lt;p&gt;The checker reports a violation.&lt;/p&gt;

&lt;p&gt;So what should happen next?&lt;/p&gt;

&lt;p&gt;The obvious automation is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;agent changes the code;&lt;/li&gt;
&lt;li&gt;check fails;&lt;/li&gt;
&lt;li&gt;agent changes the rule;&lt;/li&gt;
&lt;li&gt;check passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Technically, everything worked.&lt;/p&gt;

&lt;p&gt;But the system has a problem: &lt;strong&gt;The thing being checked was allowed to change the conditions of the check.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg2kpefexxotw4l5zx69l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg2kpefexxotw4l5zx69l.png" alt="Diagram separating implementation changes from policy changes" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That made me much more interested in the difference between &lt;strong&gt;changing the code&lt;/strong&gt; and &lt;strong&gt;changing the rules around the code&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dangerous loop
&lt;/h2&gt;

&lt;p&gt;The simplest version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent
  ↓
change code
  ↓
verification
  ↓
failure
  ↓
agent changes policy
  ↓
verification
  ↓
pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing obviously broken here.&lt;/p&gt;

&lt;p&gt;The agent might even have a good reason for changing the policy.&lt;/p&gt;

&lt;p&gt;The problem is that the verification boundary has disappeared.&lt;/p&gt;

&lt;p&gt;A policy is supposed to tell the system what has to remain true.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15mmboinbl4w64umlawy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15mmboinbl4w64umlawy.png" alt="Agent change and policy proposal passing through separate verification and approval paths" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the same actor that made the change can also redefine what "true" means, a successful verification doesn't tell you very much.&lt;/p&gt;

&lt;p&gt;It's just a moving target.&lt;/p&gt;

&lt;p&gt;And this isn't specific to architecture.&lt;/p&gt;

&lt;p&gt;You could do the same thing with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maximum service size = 300 lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent produces a 420-line service.&lt;/p&gt;

&lt;p&gt;The check fails.&lt;/p&gt;

&lt;p&gt;The agent changes the limit to 500.&lt;/p&gt;

&lt;p&gt;The check passes.&lt;/p&gt;

&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module A cannot import module B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent needs the dependency.&lt;/p&gt;

&lt;p&gt;It changes the rule.&lt;/p&gt;

&lt;p&gt;The import is now allowed.&lt;/p&gt;

&lt;p&gt;Again, maybe that's the right architectural decision.&lt;/p&gt;

&lt;p&gt;But those are two separate decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The implementation changed.&lt;/li&gt;
&lt;li&gt;The policy changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They shouldn't become one operation just because the same agent proposed both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every policy violation is actually a mistake
&lt;/h2&gt;

&lt;p&gt;This is where it gets more interesting.&lt;/p&gt;

&lt;p&gt;I don't want an architecture checker that treats every violation as proof that the agent did something wrong.&lt;/p&gt;

&lt;p&gt;Sometimes the agent really should change the architecture.&lt;/p&gt;

&lt;p&gt;Suppose an application has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkout
   ↓
PaymentAdapter
   ↓
Stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I decide that the product is getting large enough that payment workflows deserve their own domain boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkout
   ↓
PaymentService
   ↓
PaymentAdapter
   ↓
Stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change introduces new files.&lt;/p&gt;

&lt;p&gt;Some imports move.&lt;/p&gt;

&lt;p&gt;Some old boundaries disappear.&lt;/p&gt;

&lt;p&gt;New ones appear.&lt;/p&gt;

&lt;p&gt;A strict checker could report a pile of violations.&lt;/p&gt;

&lt;p&gt;That doesn't mean the change is bad.&lt;/p&gt;

&lt;p&gt;It means the current policy describes the old architecture.&lt;/p&gt;

&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;A policy isn't supposed to prevent architecture from ever changing.&lt;/p&gt;

&lt;p&gt;It is supposed to make architecture changes explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposal and approval are different things
&lt;/h2&gt;

&lt;p&gt;This led me to a fairly simple rule: &lt;strong&gt;An agent should be able to propose a policy change, but it shouldn't automatically be able to approve that policy change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current policy:

Payment provider access must go through PaymentAdapter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Proposed change:

Allow PaymentService to depend directly on a new internal
PaymentProvider interface.

Reason:
The current adapter boundary prevents the new workflow
from sharing transaction state correctly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's useful.&lt;/p&gt;

&lt;p&gt;The agent has done the hard reasoning.&lt;/p&gt;

&lt;p&gt;It has identified the existing constraint.&lt;/p&gt;

&lt;p&gt;It has explained why the constraint may no longer fit.&lt;/p&gt;

&lt;p&gt;But the proposal should remain a proposal.&lt;/p&gt;

&lt;p&gt;The important part is that &lt;strong&gt;the authority approving the policy change is separate from the agent that authored the change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Otherwise the system can silently move the goalposts.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the same reason I don't want approval in the prompt
&lt;/h2&gt;

&lt;p&gt;A prompt can say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Never deploy without approval.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's useful instruction.&lt;/p&gt;

&lt;p&gt;It's not much of a control if the same process can modify the configuration that defines what counts as an approved deployment.&lt;/p&gt;

&lt;p&gt;The more autonomous the agent becomes, the more these distinctions move out of the prompt and into the environment around it.&lt;/p&gt;

&lt;p&gt;The agent should be able to reason about the policy.&lt;/p&gt;

&lt;p&gt;It should be able to request a policy change.&lt;/p&gt;

&lt;p&gt;It should be able to explain the change.&lt;/p&gt;

&lt;p&gt;The actual enforcement shouldn't depend on the agent remembering to follow its own instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A policy change should leave a trail
&lt;/h2&gt;

&lt;p&gt;Once policy becomes a real project artifact, another problem appears.&lt;/p&gt;

&lt;p&gt;You need to know what the policy was when the original change was checked.&lt;/p&gt;

&lt;p&gt;Otherwise you can end up with a strange situation where today's successful verification only makes sense because yesterday's policy was replaced.&lt;/p&gt;

&lt;p&gt;That's why I like keeping policy changes explicit and versioned.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project state
    ↓
policy revision 17
    ↓
agent proposes architecture change
    ↓
verification fails under policy revision 17
    ↓
policy proposal
    ↓
approval
    ↓
policy revision 18
    ↓
verify change again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there are two separate facts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The change passed under policy revision 18.&lt;/li&gt;
&lt;li&gt;Policy revision 18 was itself approved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's much more useful than simply seeing a green check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old policy still matters
&lt;/h2&gt;

&lt;p&gt;There's another subtle point here.&lt;/p&gt;

&lt;p&gt;Suppose an agent changes the rule and then verifies the same diff against the new rule.&lt;/p&gt;

&lt;p&gt;You can no longer tell whether the original change violated the previous policy.&lt;/p&gt;

&lt;p&gt;So I want the system to preserve the distinction between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what the project allowed before the change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what the project allows after the change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes especially useful when investigating a change later.&lt;/p&gt;

&lt;p&gt;You can ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did this dependency become allowed?&lt;/li&gt;
&lt;li&gt;Was it always allowed?&lt;/li&gt;
&lt;li&gt;Was there an explicit exception?&lt;/li&gt;
&lt;li&gt;Did a policy revision happen at the same time?&lt;/li&gt;
&lt;li&gt;Who approved it?&lt;/li&gt;
&lt;li&gt;What evidence led to the change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are much harder to answer when policy is just another mutable config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporary exceptions are different again
&lt;/h2&gt;

&lt;p&gt;Sometimes the policy is fine.&lt;/p&gt;

&lt;p&gt;The violation is temporary.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All persistence access must go through Repository.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I'm in the middle of a migration.&lt;/p&gt;

&lt;p&gt;I don't want to remove the rule.&lt;/p&gt;

&lt;p&gt;I just need one known exception for two weeks.&lt;/p&gt;

&lt;p&gt;That's not really a policy change.&lt;/p&gt;

&lt;p&gt;It's a waiver.&lt;/p&gt;

&lt;p&gt;And I think treating it as a different object makes the whole system easier to reason about.&lt;/p&gt;

&lt;p&gt;A useful waiver has at least:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;owner
reason
scope
expiry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;remove the rule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;waive this finding until 2026-12-28

owner: platform-team
reason: repository migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule stays.&lt;/p&gt;

&lt;p&gt;The exception expires.&lt;/p&gt;

&lt;p&gt;That's a very different thing from changing the architecture policy permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baselines solve a different problem
&lt;/h2&gt;

&lt;p&gt;I also don't want to confuse waivers with baselines.&lt;/p&gt;

&lt;p&gt;A baseline answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This violation already existed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A waiver answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This active violation is intentionally allowed for a limited time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a policy change answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We changed what the project considers acceptable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are three different states.&lt;/p&gt;

&lt;p&gt;That separation might sound overly precise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0jpv7gp4tv8e5me7ctue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0jpv7gp4tv8e5me7ctue.png" alt="Three distinct concepts: baseline, temporary waiver and policy change" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, it makes the tool much more useful.&lt;/p&gt;

&lt;p&gt;A real codebase can have old architectural debt.&lt;/p&gt;

&lt;p&gt;It can have temporary migration exceptions.&lt;/p&gt;

&lt;p&gt;And it can deliberately evolve its architecture.&lt;/p&gt;

&lt;p&gt;If all three become "ignore this finding", you lose important information.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent should actually do
&lt;/h2&gt;

&lt;p&gt;This doesn't mean agents have to stop making architectural changes.&lt;/p&gt;

&lt;p&gt;Quite the opposite.&lt;/p&gt;

&lt;p&gt;I want them to do more.&lt;/p&gt;

&lt;p&gt;A useful agent flow could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requirement
    ↓
agent plans change
    ↓
implementation
    ↓
deterministic verification
    ↓
failure
    ↓
agent explains why
    ↓
policy proposal / waiver proposal
    ↓
approval
    ↓
verification against new state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can drive most of that workflow.&lt;/p&gt;

&lt;p&gt;It can inspect the repository.&lt;/p&gt;

&lt;p&gt;It can understand the requirement.&lt;/p&gt;

&lt;p&gt;It can implement the refactor.&lt;/p&gt;

&lt;p&gt;It can identify the policy that stopped the change.&lt;/p&gt;

&lt;p&gt;It can prepare the evidence for a policy proposal.&lt;/p&gt;

&lt;p&gt;It can even tell me that the current architecture appears to be the problem.&lt;/p&gt;

&lt;p&gt;What it shouldn't get is an invisible path from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my change failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;therefore my own change is now allowed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  This also changes what "autonomous" means
&lt;/h2&gt;

&lt;p&gt;I've started thinking that autonomy isn't really one switch.&lt;/p&gt;

&lt;p&gt;An agent can have permission to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the repository;&lt;/li&gt;
&lt;li&gt;modify source files;&lt;/li&gt;
&lt;li&gt;run tests;&lt;/li&gt;
&lt;li&gt;inspect dependencies;&lt;/li&gt;
&lt;li&gt;propose policy changes;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without having permission to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;approve those policy changes;&lt;/li&gt;
&lt;li&gt;remove its own enforcement;&lt;/li&gt;
&lt;li&gt;extend a temporary waiver indefinitely;&lt;/li&gt;
&lt;li&gt;change the authority that verifies it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you a more useful permission model than simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;autonomous = yes/no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different operations can have different authorities.&lt;/p&gt;

&lt;p&gt;And that matters more once the agent is running for a long time or can delegate work to other agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verifier should not care who made the code change
&lt;/h2&gt;

&lt;p&gt;There's another distinction here that I find useful.&lt;/p&gt;

&lt;p&gt;Verification should primarily answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does the current change fit the current approved policy?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't need to decide whether the author was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;human
Claude
Codex
Cursor
another agent
automation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a separate concern.&lt;/p&gt;

&lt;p&gt;The verifier checks the state.&lt;/p&gt;

&lt;p&gt;The policy layer defines the constraint.&lt;/p&gt;

&lt;p&gt;The authority layer controls who can change the constraint.&lt;/p&gt;

&lt;p&gt;Keeping those pieces separate makes the system much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is what I started building into Guard
&lt;/h2&gt;

&lt;p&gt;This distinction ended up affecting the design of Codapult Guard quite a bit.&lt;/p&gt;

&lt;p&gt;Guard already had the idea of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;facts
  ↓
policy
  ↓
verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that isn't enough when policy itself can change.&lt;/p&gt;

&lt;p&gt;So I started treating policy changes as first-class operations.&lt;/p&gt;

&lt;p&gt;Guard can discover project facts and prepare proposals.&lt;/p&gt;

&lt;p&gt;A project can explicitly approve them.&lt;/p&gt;

&lt;p&gt;For protected projects, policy approval can require a distinct actor rather than allowing the authoring agent to approve its own proposal.&lt;/p&gt;

&lt;p&gt;The same idea now applies to waivers.&lt;/p&gt;

&lt;p&gt;A waiver is not just "ignore this finding forever."&lt;/p&gt;

&lt;p&gt;It has an owner, reason and expiry date.&lt;/p&gt;

&lt;p&gt;When the expiry is reached, the finding becomes active again.&lt;/p&gt;

&lt;p&gt;That gives the project three useful things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;policy
exception
history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of one growing collection of ignored warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  It also makes agent integration more useful
&lt;/h2&gt;

&lt;p&gt;MCP makes this distinction especially important.&lt;/p&gt;

&lt;p&gt;An agent can ask Guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What policy applies here?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What did this change affect?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Why did verification fail?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What policy change would be needed for this refactor?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are useful questions.&lt;/p&gt;

&lt;p&gt;But I don't want the agent to receive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Change policy until verification passes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That isn't really verification anymore.&lt;/p&gt;

&lt;p&gt;The MCP layer should expose the information and operations the agent needs without quietly giving it the authority to redefine the system around itself.&lt;/p&gt;

&lt;p&gt;That's a much more interesting role for project-aware tooling than simply adding another set of commands to an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule can change. That isn't the problem.
&lt;/h2&gt;

&lt;p&gt;I don't think architectural rules should be permanent.&lt;/p&gt;

&lt;p&gt;Projects change.&lt;/p&gt;

&lt;p&gt;Requirements change.&lt;/p&gt;

&lt;p&gt;Teams change.&lt;/p&gt;

&lt;p&gt;The shape of the system changes.&lt;/p&gt;

&lt;p&gt;A rule that made perfect sense six months ago might become actively harmful.&lt;/p&gt;

&lt;p&gt;The mistake is treating policy change as an implementation detail.&lt;/p&gt;

&lt;p&gt;Changing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/payments/StripeAdapter.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and changing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment-provider-access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are fundamentally different operations.&lt;/p&gt;

&lt;p&gt;One changes the system.&lt;/p&gt;

&lt;p&gt;The other changes what the system is allowed to become.&lt;/p&gt;

&lt;p&gt;Once an agent can perform both, they need separate boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'm less interested in stopping agents than in making their authority explicit
&lt;/h2&gt;

&lt;p&gt;The goal isn't to make agents weaker.&lt;/p&gt;

&lt;p&gt;I actually want agents to make bigger changes.&lt;/p&gt;

&lt;p&gt;But bigger changes require clearer boundaries.&lt;/p&gt;

&lt;p&gt;The more of the implementation I delegate, the more I care about questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What can the agent change?&lt;/li&gt;
&lt;li&gt;What can it propose?&lt;/li&gt;
&lt;li&gt;What can it approve?&lt;/li&gt;
&lt;li&gt;What evidence does it have to provide?&lt;/li&gt;
&lt;li&gt;What policy was active when the change was checked?&lt;/li&gt;
&lt;li&gt;Was the exception temporary?&lt;/li&gt;
&lt;li&gt;Who approved the exception?&lt;/li&gt;
&lt;li&gt;Can the agent change the thing that verifies it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions aren't really about whether the model is smart enough.&lt;/p&gt;

&lt;p&gt;They're about the architecture around the model.&lt;/p&gt;

&lt;p&gt;And that's probably the part that becomes more important as coding agents become capable of doing more work without waiting for a human after every step.&lt;/p&gt;

&lt;p&gt;I don't want the agent to be afraid of the rules.&lt;/p&gt;

&lt;p&gt;I want it to understand them.&lt;/p&gt;

&lt;p&gt;I want it to be able to challenge them.&lt;/p&gt;

&lt;p&gt;I want it to be able to propose better ones.&lt;/p&gt;

&lt;p&gt;But I don't want it to be the final authority on whether its own change should become the new rule.&lt;/p&gt;

&lt;p&gt;The code can change.&lt;/p&gt;

&lt;p&gt;The architecture can change.&lt;/p&gt;

&lt;p&gt;Even the policy can change.&lt;/p&gt;

&lt;p&gt;Those changes just shouldn't all happen under the same authority.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>mcp</category>
    </item>
    <item>
      <title>A Dependency Graph Is Not Architecture</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:42:23 +0000</pubDate>
      <link>https://dev.to/vladzoff/a-dependency-graph-is-not-architecture-f6p</link>
      <guid>https://dev.to/vladzoff/a-dependency-graph-is-not-architecture-f6p</guid>
      <description>&lt;p&gt;&lt;em&gt;A graph can show how your code is connected. It can't tell you which of those connections your project actually wants to keep.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A dependency graph is one of the first things I wanted when I started thinking about tooling for AI coding agents.&lt;/p&gt;

&lt;p&gt;It makes sense.&lt;/p&gt;

&lt;p&gt;An agent needs to understand a codebase, and a graph is a much better representation of relationships than a pile of files.&lt;/p&gt;

&lt;p&gt;You can see that a route depends on a service. That service depends on a repository. A repository depends on a database client. Another module imports the service. A shared utility is suddenly used by half the application.&lt;/p&gt;

&lt;p&gt;That information is useful.&lt;/p&gt;

&lt;p&gt;But after working with this problem for a while, I started thinking that there is a trap here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dependency graph is not architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a description of relationships. Architecture also contains intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph tells you what exists
&lt;/h2&gt;

&lt;p&gt;Suppose I have this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  ↓
Server Action
  ↓
Service
  ↓
Repository
  ↓
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A graph can represent that pretty well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqetzqdzio948tx23u057.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqetzqdzio948tx23u057.png" alt="A dependency graph showing Client, Service, Repository and Database contrasted with architectural intent" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It can tell me that the service imports the repository. It can tell me that the repository imports the database client. It can tell me which files are connected to a changed module. It can show cycles or unusually large dependency hotspots.&lt;/p&gt;

&lt;p&gt;All of that is valuable.&lt;/p&gt;

&lt;p&gt;Now imagine another part of the application has this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  ↓
API Route
  ↓
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The graph can show that too.&lt;/p&gt;

&lt;p&gt;What it cannot tell me by itself is whether the second path is wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maybe it is.&lt;/li&gt;
&lt;li&gt;Maybe the project deliberately allows direct database access for a reporting endpoint.&lt;/li&gt;
&lt;li&gt;Maybe the second path is old code that nobody has touched in three years.&lt;/li&gt;
&lt;li&gt;Maybe it was introduced last week and is exactly the pattern the team now wants to standardize on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The graph contains all four possibilities. It has no way to choose between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture has a "why"
&lt;/h2&gt;

&lt;p&gt;This is where the difference matters.&lt;/p&gt;

&lt;p&gt;Architecture isn't only a collection of edges between components. It also contains decisions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Payment provider access goes through an adapter.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Only server-side code can access persistence.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Module A must not depend on Module B.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This package is intentionally isolated because we may remove it later.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This service owns authorization for this operation.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not just observations about the current repository. They are decisions. And some of them can exist even when the repository doesn't currently satisfy them.&lt;/p&gt;

&lt;p&gt;That's an important distinction.&lt;/p&gt;

&lt;p&gt;The code might currently contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkout → Stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the intended architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkout → Payment Adapter → Stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all you have is the current graph, the direct dependency is simply a fact. You don't know that it is a violation until someone tells you what is supposed to be true.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is why "architecture inference" is tricky
&lt;/h2&gt;

&lt;p&gt;I've seen a few different approaches to this problem.&lt;/p&gt;

&lt;p&gt;One is to infer architecture from directory names. If there is a &lt;code&gt;services&lt;/code&gt; directory, call it the service layer. If there is a &lt;code&gt;repositories&lt;/code&gt; directory, assume everything under it is persistence.&lt;/p&gt;

&lt;p&gt;That works until it doesn't.&lt;/p&gt;

&lt;p&gt;Another project might use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;features/
domain/
infrastructure/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another might put all of its business logic inside feature folders. Another might have very little layering at all. Another might have started with one architecture and gradually evolved into something else.&lt;/p&gt;

&lt;p&gt;None of those structures is automatically correct.&lt;/p&gt;

&lt;p&gt;So I don't really want a tool to look at a repository and say: &lt;em&gt;"This is good architecture."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That seems like the wrong question. I'd rather ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;"What is this project actually doing?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Which parts of that behavior has the team decided to preserve?"&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those are much more concrete questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A graph also captures accidental history
&lt;/h2&gt;

&lt;p&gt;There is another problem. Real repositories contain history. Not all history is intentional.&lt;/p&gt;

&lt;p&gt;A developer copies an implementation. Another developer adds a shortcut. Someone moves a file but keeps the old import. A feature gets rushed into production. Six months later there are three different ways to access the same thing.&lt;/p&gt;

&lt;p&gt;The graph faithfully records all of them.&lt;/p&gt;

&lt;p&gt;In that sense, a graph can actually make the ambiguity more visible without resolving it. You might have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feature A → Service A → Repository
Feature B → Repository
Feature C → Service C → Repository
Feature D → Service A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What should a coding agent learn from this?&lt;/p&gt;

&lt;p&gt;It could conclude that both direct repository access and service access are valid patterns. It could pick the shortest path. It could copy the most common one. It could look at recent commits.&lt;/p&gt;

&lt;p&gt;None of those decisions necessarily tells you what the project intended.&lt;/p&gt;

&lt;p&gt;And if an agent starts using an accidental pattern as an example for new work, the inconsistency becomes self-reinforcing. That's a much bigger problem than just having an ugly graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing layer is policy
&lt;/h2&gt;

&lt;p&gt;This is the part I keep coming back to. A useful project model has at least two different things:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69ktza3i2eay9sg0b41p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69ktza3i2eay9sg0b41p.png" alt="Diagram showing repository facts, explicit policy and verification as three separate layers" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facts:&lt;/strong&gt; What the repository actually contains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy:&lt;/strong&gt; Which of those facts describe behavior the project wants to preserve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The graph is very good at the first one. It doesn't magically give you the second.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Facts:
Checkout imports PaymentAdapter
PaymentAdapter imports Stripe
RefundService imports Stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Policy:
All payment-provider access must go through PaymentAdapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the system can check something meaningful. A direct Stripe import isn't merely "an unusual edge." It is a concrete violation of a known project decision.&lt;/p&gt;

&lt;p&gt;That's a much stronger signal for an AI agent too.&lt;/p&gt;

&lt;h2&gt;
  
  
  This also makes exceptions easier to handle
&lt;/h2&gt;

&lt;p&gt;Architectural rules almost always have exceptions.&lt;/p&gt;

&lt;p&gt;Suppose the rule is: &lt;em&gt;UI modules must not import persistence modules.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then someone builds an internal admin export screen. The simplest implementation might legitimately need a different path.&lt;/p&gt;

&lt;p&gt;Without an explicit policy model, you tend to end up with one of two bad outcomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Either the tool becomes so strict that people stop using it.&lt;/li&gt;
&lt;li&gt;Or everyone starts ignoring the warnings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An explicit policy gives you somewhere to put the exception. The project can say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;This boundary normally applies here.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This particular module is allowed to cross it.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Or: This is temporary and needs to be removed later.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much closer to how real architecture works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baselines matter for the same reason
&lt;/h2&gt;

&lt;p&gt;The same thing happens with existing violations.&lt;/p&gt;

&lt;p&gt;Imagine a large application already has twenty direct database imports from places that ideally shouldn't have them. The graph can find twenty edges. A naive architecture checker might immediately fail the project.&lt;/p&gt;

&lt;p&gt;That's not especially useful. I don't need a tool telling me that yesterday's codebase was imperfect. I need it to help me stop making the situation worse.&lt;/p&gt;

&lt;p&gt;So a more useful question is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Which violations already existed?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Which ones were approved or baselined?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Which new violations did this change introduce?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the graph part of a useful workflow instead of turning it into a giant list of problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part starts when an agent gets involved
&lt;/h2&gt;

&lt;p&gt;This becomes more important when the repository is being modified by an AI coding agent.&lt;/p&gt;

&lt;p&gt;An agent can query a graph and ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;What depends on this module?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What imports this package?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What routes reach this service?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What will be affected if I change this file?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's great. But then it needs another question: &lt;strong&gt;Am I allowed to make this dependency?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first question can be answered from repository facts. The second requires policy.&lt;/p&gt;

&lt;p&gt;If the system returns: &lt;em&gt;"This import exists,"&lt;/em&gt; that's one kind of information.&lt;/p&gt;

&lt;p&gt;If it returns: &lt;em&gt;"This import exists. This dependency crosses an approved boundary,"&lt;/em&gt; that's much more useful. It gives the model something it can act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is also where history gets interesting
&lt;/h2&gt;

&lt;p&gt;Git history is useful, but I don't think history should be treated as policy either.&lt;/p&gt;

&lt;p&gt;Suppose the agent sees that a particular dependency was added in a large refactor eighteen months ago. That tells you something. Maybe there was a reason. But it doesn't necessarily tell you whether the dependency is still intentional.&lt;/p&gt;

&lt;p&gt;The same applies to code frequency. A pattern used 200 times is not automatically a good pattern. A pattern used once is not automatically wrong.&lt;/p&gt;

&lt;p&gt;History can provide evidence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8eop4muxfbkg4fqa3i6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8eop4muxfbkg4fqa3i6o.png" alt="Diagram separating current code structure, repository history and intended architectural policy" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It shouldn't silently become a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built Guard around this distinction
&lt;/h2&gt;

&lt;p&gt;This is one of the ideas that shaped &lt;a href="https://github.com/codapult/codapult-guard" rel="noopener noreferrer"&gt;&lt;strong&gt;Codapult Guard&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I didn't want to build another system that looks at a repository graph and declares what the architecture should be. Guard starts with the repository that already exists. It discovers facts about the project: files, imports, AST relationships, routes, capabilities, contracts, and other relationships derived from the code.&lt;/p&gt;

&lt;p&gt;Then the project can decide which of those things should become policy.&lt;/p&gt;

&lt;p&gt;After that, the useful part is verification. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client module → Database import
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can be reported because the project explicitly decided that this dependency isn't allowed.&lt;/p&gt;

&lt;p&gt;Guard doesn't treat every possible architectural preference as a universal rule. The project decides, the tooling checks, and existing violations can be baselined so future changes are checked cleanly.&lt;/p&gt;

&lt;p&gt;The core checks are deterministic and local. There is no need to ask another model whether one module imports another.&lt;/p&gt;

&lt;h2&gt;
  
  
  A graph is still extremely useful
&lt;/h2&gt;

&lt;p&gt;None of this means graphs are overrated. Quite the opposite.&lt;/p&gt;

&lt;p&gt;A graph is excellent for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;What exists?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What depends on what?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What changed?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What might be affected?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Where are the cycles and hotspots?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Policy can answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;What is allowed?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What is forbidden?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Which boundaries matter?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Which exceptions are intentional?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verification can answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Did the change preserve those decisions?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Putting them together is much more powerful than pretending one representation can answer all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent doesn't need a bigger graph
&lt;/h2&gt;

&lt;p&gt;My first instinct was always: give the agent more information. More files, more history, more dependencies, more documentation, more graph data.&lt;/p&gt;

&lt;p&gt;But eventually you hit diminishing returns. The agent doesn't necessarily need the whole graph. It needs the facts that are relevant to the decision it is making, and it needs to know which facts are constraints rather than observations.&lt;/p&gt;

&lt;p&gt;For a change to a payment module, I probably don't need a thousand unrelated edges. I need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;This module is affected.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;These modules depend on it.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;These contracts apply.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This dependency is forbidden.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This exception is approved.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;These tests and checks are relevant.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's much closer to useful project context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maybe architecture is better thought of as a decision record
&lt;/h2&gt;

&lt;p&gt;The shape is the visible part. The decisions are the interesting part.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Why is this dependency allowed?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Why is that one forbidden?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Why does this adapter exist?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Why is this module isolated?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A graph can preserve the shape. Policy can preserve some of the decisions. History can provide evidence about how we got there.&lt;/p&gt;

&lt;p&gt;Combining them gives an agent a model of the system that separates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what is true
  ↓
what is intended
  ↓
what changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation is probably more important than making the graph itself smarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the part I'm still exploring
&lt;/h2&gt;

&lt;p&gt;I don't think the answer is one giant "AI architecture brain" that understands everything in a repository. I'd rather have smaller pieces that do one thing reliably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository analysis&lt;/strong&gt; establishes facts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy&lt;/strong&gt; captures decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static checks&lt;/strong&gt; verify deterministic constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; verify behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent&lt;/strong&gt; reasons about requirements and implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A human&lt;/strong&gt; decides when the architecture itself should change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an AI can change a hundred files in one session, the environment must tell it which parts of that context are observations, which parts are constraints, and which decisions are still open.&lt;/p&gt;

&lt;p&gt;A dependency graph is a very good start. It just isn't the architecture.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>mcp</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Tests Passed. The Architecture Didn't.</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Fri, 25 Sep 2026 11:34:05 +0000</pubDate>
      <link>https://dev.to/vladzoff/the-tests-passed-the-architecture-didnt-3oce</link>
      <guid>https://dev.to/vladzoff/the-tests-passed-the-architecture-didnt-3oce</guid>
      <description>&lt;p&gt;&lt;em&gt;What I started noticing after AI coding agents began making bigger changes to my projects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One of the stranger things about working with coding agents is that a change can be completely valid and still be wrong.&lt;/p&gt;

&lt;p&gt;TypeScript passes.&lt;/p&gt;

&lt;p&gt;The tests pass.&lt;/p&gt;

&lt;p&gt;The build is green.&lt;/p&gt;

&lt;p&gt;The feature works.&lt;/p&gt;

&lt;p&gt;And I can still look at the diff and think: I don't want this in the codebase.&lt;/p&gt;

&lt;p&gt;I started running into this more often as I let agents handle larger changes instead of asking them to implement one small function at a time.&lt;/p&gt;

&lt;p&gt;The problem wasn't usually a bug.&lt;/p&gt;

&lt;p&gt;It was the shape of the change.&lt;/p&gt;

&lt;p&gt;A new module would bypass an existing layer. A feature would introduce another way of doing something the project already had. A dependency would point in the wrong direction. A new piece of code would technically work but ignore a convention that had become important elsewhere.&lt;/p&gt;

&lt;p&gt;None of those things necessarily show up in a test suite.&lt;/p&gt;

&lt;p&gt;That bothered me more than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests answer a different question
&lt;/h2&gt;

&lt;p&gt;I like tests. This isn't an argument against them.&lt;/p&gt;

&lt;p&gt;A test can tell me that creating a subscription works.&lt;/p&gt;

&lt;p&gt;It can tell me that an unauthorized user gets a 403.&lt;/p&gt;

&lt;p&gt;It can tell me that a background job retries after a failure.&lt;/p&gt;

&lt;p&gt;Those are important questions.&lt;/p&gt;

&lt;p&gt;But imagine the project has gradually settled on a structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI
 ↓
Actions
 ↓
Services
 ↓
Repositories
 ↓
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then an agent adds a new feature and imports the database client directly into a UI-facing module.&lt;/p&gt;

&lt;p&gt;The code may compile.&lt;/p&gt;

&lt;p&gt;The feature may work.&lt;/p&gt;

&lt;p&gt;The tests may all pass.&lt;/p&gt;

&lt;p&gt;The problem is simply that the new code doesn't belong there.&lt;/p&gt;

&lt;p&gt;The repository now has another path through the system.&lt;/p&gt;

&lt;p&gt;Over time those exceptions accumulate.&lt;/p&gt;

&lt;p&gt;That's where things get expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is easier to notice when a human writes the code
&lt;/h2&gt;

&lt;p&gt;When I write a new feature myself, I already have a rough model of the project in my head.&lt;/p&gt;

&lt;p&gt;I know which directory handles a particular kind of logic.&lt;/p&gt;

&lt;p&gt;I know that a certain integration goes through an adapter.&lt;/p&gt;

&lt;p&gt;I know that a particular operation has to go through a service because some permissions are checked there.&lt;/p&gt;

&lt;p&gt;I know that a module is supposed to be removable.&lt;/p&gt;

&lt;p&gt;I don't necessarily write any of that down.&lt;/p&gt;

&lt;p&gt;I just remember it.&lt;/p&gt;

&lt;p&gt;An agent doesn't have that memory.&lt;/p&gt;

&lt;p&gt;It has the repository.&lt;/p&gt;

&lt;p&gt;That's not quite the same thing.&lt;/p&gt;

&lt;p&gt;It can search the code and find examples, but examples aren't always rules.&lt;/p&gt;

&lt;p&gt;If it finds three slightly different implementations, it doesn't automatically know which one is the accidental old implementation and which one represents the intended pattern.&lt;/p&gt;

&lt;p&gt;In fact, the existing inconsistency can become new training data for the next change.&lt;/p&gt;

&lt;p&gt;That's the part I started thinking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture becomes part of the input
&lt;/h2&gt;

&lt;p&gt;Once an agent is doing a meaningful part of the implementation, architecture is no longer just something humans discuss during design.&lt;/p&gt;

&lt;p&gt;It becomes part of the agent's working environment.&lt;/p&gt;

&lt;p&gt;The question changes from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can the agent write this feature?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can the agent write this feature without changing how the rest of the project is supposed to work?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those aren't the same thing.&lt;/p&gt;

&lt;p&gt;A coding agent can be very good at finding all the places that need changing.&lt;/p&gt;

&lt;p&gt;It can also be very good at making a new abstraction when an existing one would have been enough.&lt;/p&gt;

&lt;p&gt;That's not necessarily because the model is bad.&lt;/p&gt;

&lt;p&gt;The repository often doesn't make the intended rules explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  I didn't want another AI reviewer
&lt;/h2&gt;

&lt;p&gt;My first instinct was to think about adding another model to the process.&lt;/p&gt;

&lt;p&gt;The agent writes the change.&lt;/p&gt;

&lt;p&gt;A second model reviews the diff.&lt;/p&gt;

&lt;p&gt;It points out architectural problems.&lt;/p&gt;

&lt;p&gt;The first agent fixes them.&lt;/p&gt;

&lt;p&gt;That can work.&lt;/p&gt;

&lt;p&gt;But the more I thought about it, the less attractive it became for some classes of problems.&lt;/p&gt;

&lt;p&gt;If the repository tells us that one module depends on another, we don't need an LLM to determine that fact.&lt;/p&gt;

&lt;p&gt;If a project has explicitly approved a dependency boundary, we don't need a model to have an opinion about whether a particular import crosses it.&lt;/p&gt;

&lt;p&gt;If a file changed, we can determine what it imports, what imports it, which routes depend on it, and which capabilities are connected to it.&lt;/p&gt;

&lt;p&gt;Those are facts.&lt;/p&gt;

&lt;p&gt;I started wondering how much of this could be moved out of the model entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the project that already exists
&lt;/h2&gt;

&lt;p&gt;That led me to a fairly important design decision.&lt;/p&gt;

&lt;p&gt;I didn't want an architecture tool that arrived with its own idea of what a "good" TypeScript project should look like.&lt;/p&gt;

&lt;p&gt;There are plenty of reasonable architectures.&lt;/p&gt;

&lt;p&gt;A random project might use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;components
services
repositories
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another might use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;features
domain
infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another might be completely different.&lt;/p&gt;

&lt;p&gt;I don't want to tell the project which one is correct.&lt;/p&gt;

&lt;p&gt;I want to start by looking at what the project actually does.&lt;/p&gt;

&lt;p&gt;That means extracting facts from the repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;files and directories&lt;/li&gt;
&lt;li&gt;imports and dependency relationships&lt;/li&gt;
&lt;li&gt;AST information&lt;/li&gt;
&lt;li&gt;routes&lt;/li&gt;
&lt;li&gt;capabilities&lt;/li&gt;
&lt;li&gt;contracts&lt;/li&gt;
&lt;li&gt;change impact&lt;/li&gt;
&lt;li&gt;other relationships that can be derived from the code itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction is subtle but important.&lt;/p&gt;

&lt;p&gt;There is a big difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is the architecture you should use."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is how your project currently works."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second one gives you something to work with without imposing a framework on every repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then make the rules explicit
&lt;/h2&gt;

&lt;p&gt;Facts alone aren't enough.&lt;/p&gt;

&lt;p&gt;Suppose a project currently has this dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
Service
    ↓
Repository
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That doesn't automatically mean somebody should never be allowed to bypass the service.&lt;/p&gt;

&lt;p&gt;Maybe there is a legitimate exception.&lt;/p&gt;

&lt;p&gt;So the next step is explicit policy.&lt;/p&gt;

&lt;p&gt;The developer decides which observed patterns matter enough to enforce.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI code must not import persistence code directly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All payment-provider access must pass through the payment adapter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Module A cannot depend on module B.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part for me is that these rules come from the project rather than from the tool's preferred architecture.&lt;/p&gt;

&lt;p&gt;The project can decide what to enforce.&lt;/p&gt;

&lt;p&gt;The tool can discover the facts and verify the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful check is usually about the change
&lt;/h2&gt;

&lt;p&gt;There can already be violations in a real codebase.&lt;/p&gt;

&lt;p&gt;If a project has existed for a few years, I'm not going to pretend everything was perfect before today.&lt;/p&gt;

&lt;p&gt;That's why I find the idea of a baseline useful.&lt;/p&gt;

&lt;p&gt;I care much more about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did this change introduce a new architecture violation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this repository contain any violation at all?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction makes the tool much more practical for an existing project.&lt;/p&gt;

&lt;p&gt;You can gradually tighten things instead of trying to make a messy codebase perfect before you can use the tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple example
&lt;/h2&gt;

&lt;p&gt;Imagine this rule exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client-facing modules cannot import persistence modules.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent adds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to a module that sits on the client side of the application.&lt;/p&gt;

&lt;p&gt;Nothing necessarily crashes.&lt;/p&gt;

&lt;p&gt;There might not even be an obvious runtime error.&lt;/p&gt;

&lt;p&gt;But the dependency is now different from the one the project intended to maintain.&lt;/p&gt;

&lt;p&gt;A deterministic checker can inspect the change and tell you that.&lt;/p&gt;

&lt;p&gt;There is no need for a model to decide whether the import "looks suspicious."&lt;/p&gt;

&lt;p&gt;It's a fact that can be checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want the agent to do
&lt;/h2&gt;

&lt;p&gt;This is where the distinction between context and verification becomes useful.&lt;/p&gt;

&lt;p&gt;The agent should still do the interesting work.&lt;/p&gt;

&lt;p&gt;It can interpret a requirement.&lt;/p&gt;

&lt;p&gt;It can decide which existing parts of the application are relevant.&lt;/p&gt;

&lt;p&gt;It can choose an implementation.&lt;/p&gt;

&lt;p&gt;It can reason about trade-offs.&lt;/p&gt;

&lt;p&gt;I'm not trying to replace that.&lt;/p&gt;

&lt;p&gt;I want the environment around it to answer questions that don't need model reasoning.&lt;/p&gt;

&lt;p&gt;Something more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Human
  ↓
Requirement
  ↓
AI agent
  ↓
Implementation
  ↓
Deterministic project checks
  ↓
Agent fixes violations
  ↓
Verification
  ↓
Human review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model is still doing most of the work.&lt;/p&gt;

&lt;p&gt;The difference is that it isn't the final authority on whether the change fits the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  This also changes how MCP becomes useful
&lt;/h2&gt;

&lt;p&gt;I've become more interested in MCP for development for the same reason.&lt;/p&gt;

&lt;p&gt;There is a big difference between giving an agent another command and giving it structured information about the project.&lt;/p&gt;

&lt;p&gt;A command like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is useful.&lt;/p&gt;

&lt;p&gt;But something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what depends on this module?
what project rules apply here?
what capabilities are connected to this change?
what contracts does this area expose?
what changed since the last accepted state?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;starts to become much more interesting.&lt;/p&gt;

&lt;p&gt;The agent doesn't need to rediscover the entire repository every time.&lt;/p&gt;

&lt;p&gt;It can ask for a specific piece of project state.&lt;/p&gt;

&lt;p&gt;I've been exploring this in Codapult's own developer tooling, where the CLI and MCP layer expose structured information about a Codapult project.&lt;/p&gt;

&lt;p&gt;That work ended up influencing how I thought about the more general problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's why I built Codapult Guard
&lt;/h2&gt;

&lt;p&gt;I ended up building Codapult Guard as an open-source experiment around this idea.&lt;/p&gt;

&lt;p&gt;The goal is deliberately narrower than "AI reviews your code."&lt;/p&gt;

&lt;p&gt;Guard is interested in the part that can be derived and checked deterministically.&lt;/p&gt;

&lt;p&gt;It builds a model of a JavaScript or TypeScript project, lets the developer turn relevant project facts into explicit policy, and can then inspect changes against that policy.&lt;/p&gt;

&lt;p&gt;It also looks at impact and project relationships rather than treating every changed file as an isolated blob of text.&lt;/p&gt;

&lt;p&gt;There is no LLM call required for the core checks.&lt;/p&gt;

&lt;p&gt;That's important to me.&lt;/p&gt;

&lt;p&gt;I'm not trying to compete with an AI reviewer by making another AI reviewer.&lt;/p&gt;

&lt;p&gt;I'd rather remove the model from questions that are already answerable from the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are obvious limitations
&lt;/h2&gt;

&lt;p&gt;This isn't a magic solution.&lt;/p&gt;

&lt;p&gt;A deterministic architecture check can tell me that a dependency crosses a boundary.&lt;/p&gt;

&lt;p&gt;It can't tell me whether the business decision behind that boundary was a good one.&lt;/p&gt;

&lt;p&gt;It can't decide whether a new product requirement makes the existing architecture obsolete.&lt;/p&gt;

&lt;p&gt;It can't replace a human looking at a risky change.&lt;/p&gt;

&lt;p&gt;And a project can have a perfectly consistent architecture that is still the wrong architecture.&lt;/p&gt;

&lt;p&gt;I'm okay with that.&lt;/p&gt;

&lt;p&gt;The point isn't to automate every decision.&lt;/p&gt;

&lt;p&gt;It's to stop using a model where a simpler mechanism is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm still figuring out
&lt;/h2&gt;

&lt;p&gt;The interesting question for me now isn't really whether agents can write software.&lt;/p&gt;

&lt;p&gt;They obviously can.&lt;/p&gt;

&lt;p&gt;It's how much of the software environment should be made explicit once they start doing a large part of the implementation.&lt;/p&gt;

&lt;p&gt;Some things belong in instructions.&lt;/p&gt;

&lt;p&gt;Some belong in tests.&lt;/p&gt;

&lt;p&gt;Some belong in static analysis.&lt;/p&gt;

&lt;p&gt;Some are better represented as project facts.&lt;/p&gt;

&lt;p&gt;Some need a human.&lt;/p&gt;

&lt;p&gt;And some really do need a model.&lt;/p&gt;

&lt;p&gt;I don't think we need to choose one of those.&lt;/p&gt;

&lt;p&gt;We need to get better at separating them.&lt;/p&gt;

&lt;h2&gt;
  
  
  One last thing
&lt;/h2&gt;

&lt;p&gt;The more I use agents, the less I think of the repository as just source code.&lt;/p&gt;

&lt;p&gt;It is also a record of architectural decisions, conventions, dependencies and constraints.&lt;/p&gt;

&lt;p&gt;Humans can keep a lot of that information in their heads.&lt;/p&gt;

&lt;p&gt;An agent can't.&lt;/p&gt;

&lt;p&gt;So if we want agents to make larger changes without slowly turning every project into a collection of locally reasonable decisions, some of that knowledge needs to become explicit.&lt;/p&gt;

&lt;p&gt;Not necessarily more documentation.&lt;/p&gt;

&lt;p&gt;Not necessarily another AI reviewer.&lt;/p&gt;

&lt;p&gt;Sometimes just a fact that can be checked.&lt;/p&gt;

&lt;p&gt;That's the direction I'm exploring with Codapult Guard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/codapult/codapult-guard" rel="noopener noreferrer"&gt;Codapult Guard on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>mcp</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Next.js SaaS Boilerplate Checklist for 2026</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Wed, 12 Aug 2026 19:00:47 +0000</pubDate>
      <link>https://dev.to/vladzoff/the-nextjs-saas-boilerplate-checklist-for-2026-2g7m</link>
      <guid>https://dev.to/vladzoff/the-nextjs-saas-boilerplate-checklist-for-2026-2g7m</guid>
      <description>&lt;p&gt;Most SaaS boilerplates look similar on the homepage: auth, payments, dashboard, emails, blog, and a fast launch promise. The differences appear later, when a customer asks for team roles, billing changes, audit logs, SSO, data export, custom domains, or a support workflow.&lt;/p&gt;

&lt;p&gt;Use this checklist before you build on any starter.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Authentication Is More Than Sign-In
&lt;/h2&gt;

&lt;p&gt;A serious SaaS foundation should answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it support email/password and OAuth?&lt;/li&gt;
&lt;li&gt;Does it support magic links or passkeys?&lt;/li&gt;
&lt;li&gt;Can you enable 2FA?&lt;/li&gt;
&lt;li&gt;Are protected routes handled consistently?&lt;/li&gt;
&lt;li&gt;Are auth API routes rate-limited?&lt;/li&gt;
&lt;li&gt;Can admins impersonate users for support?&lt;/li&gt;
&lt;li&gt;Can you switch auth providers without rewriting the app?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If auth is only a login form and a session hook, the real work is still ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Teams Should Be First-Class
&lt;/h2&gt;

&lt;p&gt;For B2B SaaS, the customer is often an organization, not a single user. The starter should model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organizations or workspaces.&lt;/li&gt;
&lt;li&gt;Memberships.&lt;/li&gt;
&lt;li&gt;Roles.&lt;/li&gt;
&lt;li&gt;Invitations.&lt;/li&gt;
&lt;li&gt;Active organization context.&lt;/li&gt;
&lt;li&gt;Organization-scoped billing.&lt;/li&gt;
&lt;li&gt;Organization-scoped analytics.&lt;/li&gt;
&lt;li&gt;Organization-scoped webhooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skipping this early usually creates painful migrations later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4aahfp8w2gtqvy17sbno.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4aahfp8w2gtqvy17sbno.png" alt="Organization switcher" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Billing Needs Product Flexibility
&lt;/h2&gt;

&lt;p&gt;Stripe checkout is useful, but it is not a complete billing model. Check whether the starter handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscriptions.&lt;/li&gt;
&lt;li&gt;One-time purchases.&lt;/li&gt;
&lt;li&gt;Trials.&lt;/li&gt;
&lt;li&gt;Seat-based billing.&lt;/li&gt;
&lt;li&gt;Usage credits.&lt;/li&gt;
&lt;li&gt;Add-ons.&lt;/li&gt;
&lt;li&gt;Webhook verification.&lt;/li&gt;
&lt;li&gt;Customer portal links.&lt;/li&gt;
&lt;li&gt;Subscription state in the database.&lt;/li&gt;
&lt;li&gt;Admin subscription support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many products change packaging after launch. Your boilerplate should make that survivable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Admin Is a Product Surface
&lt;/h2&gt;

&lt;p&gt;Admin panels are often treated as internal afterthoughts. That is a mistake. Customer support, billing support, abuse response, feature flags, and rollout decisions all happen there.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User management.&lt;/li&gt;
&lt;li&gt;Subscription list.&lt;/li&gt;
&lt;li&gt;Waitlist or leads.&lt;/li&gt;
&lt;li&gt;Feature flags.&lt;/li&gt;
&lt;li&gt;Webhook logs.&lt;/li&gt;
&lt;li&gt;Activity logs.&lt;/li&gt;
&lt;li&gt;Performance and error visibility.&lt;/li&gt;
&lt;li&gt;Export flows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A usable admin panel reduces support cost and launch risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Database Layer Should Be Understandable
&lt;/h2&gt;

&lt;p&gt;Before choosing a starter, inspect the schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are table names clear?&lt;/li&gt;
&lt;li&gt;Are relationships explicit?&lt;/li&gt;
&lt;li&gt;Are indexes present where lists and lookups need them?&lt;/li&gt;
&lt;li&gt;Are auth tables compatible with the auth library?&lt;/li&gt;
&lt;li&gt;Is the code tied to one database vendor?&lt;/li&gt;
&lt;li&gt;Are migrations documented?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database is where boilerplate shortcuts become expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. API Boundaries Should Match the Product
&lt;/h2&gt;

&lt;p&gt;A good SaaS starter should not force one API style everywhere. Type-safe internal product flows can use tRPC, public integrations can use REST or OpenAPI, and flexible clients may need GraphQL.&lt;/p&gt;

&lt;p&gt;Check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input validation.&lt;/li&gt;
&lt;li&gt;Rate limits.&lt;/li&gt;
&lt;li&gt;Authenticated procedures.&lt;/li&gt;
&lt;li&gt;Admin-only procedures.&lt;/li&gt;
&lt;li&gt;Versioning for public APIs.&lt;/li&gt;
&lt;li&gt;Webhook signature verification.&lt;/li&gt;
&lt;li&gt;Clear error responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Email Is Not Just Transactional Templates
&lt;/h2&gt;

&lt;p&gt;You need more than one welcome email:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verification.&lt;/li&gt;
&lt;li&gt;Password or magic link flows.&lt;/li&gt;
&lt;li&gt;Invitations.&lt;/li&gt;
&lt;li&gt;Billing events.&lt;/li&gt;
&lt;li&gt;Reports.&lt;/li&gt;
&lt;li&gt;Drip campaigns.&lt;/li&gt;
&lt;li&gt;Custom sender domains.&lt;/li&gt;
&lt;li&gt;Bounce or suppression handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If email is only a demo template, plan extra time.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Observability Should Be Wired Early
&lt;/h2&gt;

&lt;p&gt;At launch, you need to know what broke and who was affected. Evaluate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured logs.&lt;/li&gt;
&lt;li&gt;Error reporting.&lt;/li&gt;
&lt;li&gt;Web vitals.&lt;/li&gt;
&lt;li&gt;Analytics events.&lt;/li&gt;
&lt;li&gt;Audit logs.&lt;/li&gt;
&lt;li&gt;Webhook delivery records.&lt;/li&gt;
&lt;li&gt;Background job visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These systems are much easier to wire before production traffic arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. SEO and Content Matter for SaaS Starters
&lt;/h2&gt;

&lt;p&gt;If the starter includes marketing pages, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metadata per page.&lt;/li&gt;
&lt;li&gt;Open Graph images.&lt;/li&gt;
&lt;li&gt;Sitemap.&lt;/li&gt;
&lt;li&gt;Robots rules.&lt;/li&gt;
&lt;li&gt;Blog RSS.&lt;/li&gt;
&lt;li&gt;Canonical URLs.&lt;/li&gt;
&lt;li&gt;FAQ schema where appropriate.&lt;/li&gt;
&lt;li&gt;Product schema on pricing pages.&lt;/li&gt;
&lt;li&gt;Noindex for auth, dashboard, and admin pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marketing infrastructure is part of the product when your acquisition depends on search.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Deployment Should Not Be a Single Happy Path
&lt;/h2&gt;

&lt;p&gt;Vercel is a great default, but your customers or internal constraints may push you elsewhere. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker support.&lt;/li&gt;
&lt;li&gt;CI/CD examples.&lt;/li&gt;
&lt;li&gt;Environment variable reference.&lt;/li&gt;
&lt;li&gt;Infrastructure-as-code examples.&lt;/li&gt;
&lt;li&gt;Background job guidance.&lt;/li&gt;
&lt;li&gt;Storage configuration.&lt;/li&gt;
&lt;li&gt;Production checklist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Decision Rule
&lt;/h2&gt;

&lt;p&gt;The right question is not "how complex is my product today?" but "how expensive will it be to add what I need next?"&lt;/p&gt;

&lt;p&gt;A minimal starter is fast to start and painful to grow. A modular starter lets you begin with only auth, billing,&lt;br&gt;
and a dashboard — and add teams, admin, AI, or SSO when the product actually needs them.&lt;br&gt;
With Codapult, you remove what you don't need via CLI. You don't add missing modules from scratch six months after launch.&lt;/p&gt;

&lt;p&gt;The right boilerplate should help you launch faster without forcing a rewrite after the first serious customer.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; was built against this checklist. If you want to verify it item by item, the &lt;a href="https://codapult.dev/compare" rel="noopener noreferrer"&gt;compare page&lt;/a&gt; maps each capability to what ships in the box, including modules, adapters, infrastructure, and plugins.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>nextjs</category>
      <category>checklist</category>
      <category>webdev</category>
    </item>
    <item>
      <title>ShipFast vs Supastarter vs MakerKit vs Codapult: Choosing a SaaS Starter in 2026</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Mon, 10 Aug 2026 18:33:44 +0000</pubDate>
      <link>https://dev.to/vladzoff/shipfast-vs-supastarter-vs-makerkit-vs-codapult-choosing-a-saas-starter-in-2026-i5e</link>
      <guid>https://dev.to/vladzoff/shipfast-vs-supastarter-vs-makerkit-vs-codapult-choosing-a-saas-starter-in-2026-i5e</guid>
      <description>&lt;p&gt;SaaS starters are easy to compare badly.&lt;/p&gt;

&lt;p&gt;The cheapest way is to count pages, components, or screenshots. That misses the part that decides whether the starter still helps after the first launch: how much of the product surface is already wired together, how much can be removed, and how painful it is to change providers later.&lt;/p&gt;

&lt;p&gt;This comparison focuses on fit, not winner-takes-all scoring. ShipFast, Supastarter, MakerKit, and Codapult are built for different buyers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8anjz81rpomhzpiqdqu0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8anjz81rpomhzpiqdqu0.png" alt="Compare" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Positioning
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ShipFast&lt;/strong&gt; is optimized for solo founders who want to launch a simple product quickly. It is direct, lightweight, and intentionally close to the marketing-site-plus-checkout shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supastarter&lt;/strong&gt; is a polished modern starter with strong monorepo conventions and broad SaaS coverage. It is a good fit when you like its chosen stack and want a well-packaged application foundation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MakerKit&lt;/strong&gt; is strong when you want a mature starter ecosystem and multiple stack choices, especially if Supabase or Firebase is central to your plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codapult&lt;/strong&gt; is for builders who want to start lean and grow without a rewrite. The setup wizard removes the modules your MVP does not need — permanently, from the codebase. What you keep is fully integrated and can be toggled via environment variables as the product evolves. You decide the scope at setup time instead of bolting missing pieces on later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Buying Question
&lt;/h2&gt;

&lt;p&gt;The question is not "which starter has the most features?"&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which starter removes the work you are least likely to differentiate on, without boxing in the work you do need to differentiate on?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your product is a narrow one-person launch, a smaller starter can be better. Less code means fewer decisions.&lt;/p&gt;

&lt;p&gt;If your product will need teams, support operations, billing changes, admin tools, and integrations, the smallest starter can become expensive after launch. You may spend the saved setup time rebuilding the operational layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Look at what happens when the product changes.&lt;/p&gt;

&lt;p&gt;Can you remove a module? Can you switch providers? Can you add a plugin? Can you keep the public marketing site, dashboard, admin panel, API routes, and database schema understandable?&lt;/p&gt;

&lt;p&gt;Codapult is built around a few architecture choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Router route groups for public, auth, dashboard, and admin surfaces.&lt;/li&gt;
&lt;li&gt;Provider adapters for auth, payments, storage, jobs, notifications, embeddings, and vector search.&lt;/li&gt;
&lt;li&gt;CLI setup wizard for permanently removing unused modules before you start building.&lt;/li&gt;
&lt;li&gt;Environment variable flags for toggling kept modules on or off after setup.&lt;/li&gt;
&lt;li&gt;MDX docs and blog on disk.&lt;/li&gt;
&lt;li&gt;A plugin registry for larger optional capabilities.&lt;/li&gt;
&lt;li&gt;Infrastructure paths for Vercel, Docker, Terraform, Pulumi, and Helm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is heavier than a minimal starter. The upside is that modules you keep are already integrated — no wiring needed when you turn them on.&lt;br&gt;
Modules you remove are gone cleanly, with no dead code left behind.&lt;/p&gt;
&lt;h2&gt;
  
  
  Starting Simple with Codapult
&lt;/h2&gt;

&lt;p&gt;Codapult ships with 70+ modules. That does not mean your project has to use all of them.&lt;/p&gt;

&lt;p&gt;The CLI setup wizard lets you keep only what your MVP needs. A typical minimal start looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth (email/password or OAuth)&lt;/li&gt;
&lt;li&gt;Billing (one provider, one plan)&lt;/li&gt;
&lt;li&gt;Dashboard (one route group for the logged-in user)&lt;/li&gt;
&lt;li&gt;Admin (user list and subscription view)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fcodapult.dev%2Fscreenshots%2Fterminal_setup.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fcodapult.dev%2Fscreenshots%2Fterminal_setup.gif" alt="Codapult CLI setup wizard" width="720" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything else — teams, AI, SSO, SCIM, audit logs, white-labeling — you either remove at setup via the CLI wizard (permanent, clean), or keep in the codebase and hide via an environment variable until the product needs it.&lt;/p&gt;

&lt;p&gt;This is the difference between Codapult and a minimal starter: you are not adding missing modules after launch. You are removing modules you do not need yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Billing
&lt;/h2&gt;

&lt;p&gt;Billing is where a lot of starters look similar until you need a second pricing model.&lt;/p&gt;

&lt;p&gt;Basic subscription checkout is useful, but SaaS pricing often evolves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time licenses.&lt;/li&gt;
&lt;li&gt;Monthly and annual subscriptions.&lt;/li&gt;
&lt;li&gt;Seats.&lt;/li&gt;
&lt;li&gt;Add-ons.&lt;/li&gt;
&lt;li&gt;Usage credits.&lt;/li&gt;
&lt;li&gt;Trials.&lt;/li&gt;
&lt;li&gt;Customer portal.&lt;/li&gt;
&lt;li&gt;Webhooks.&lt;/li&gt;
&lt;li&gt;Admin support workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult includes Stripe, LemonSqueezy, and Polar adapters. That matters less because of the provider names and more because billing code is not scattered across the app. The product can talk to a payment interface while the provider implementation stays behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Unified payment interface used across the entire application&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createCheckoutSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CheckoutParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;createCustomerPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebhookResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;refundPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefundParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RefundResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Database
&lt;/h2&gt;

&lt;p&gt;Database preference is personal and team-specific.&lt;/p&gt;

&lt;p&gt;Turso and Drizzle are lightweight and fast to start with. PostgreSQL is familiar, widely hosted, and often preferred by teams selling to enterprise buyers. Prisma has a huge ecosystem. Supabase gives you a bundled platform.&lt;/p&gt;

&lt;p&gt;The practical advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick Turso/Drizzle if you want a small TypeScript-first setup and like SQLite/libSQL ergonomics.&lt;/li&gt;
&lt;li&gt;Pick PostgreSQL if your team or customers expect it.&lt;/li&gt;
&lt;li&gt;Pick Supabase if managed auth/database/storage in one platform is the main value.&lt;/li&gt;
&lt;li&gt;Pick Prisma if your team strongly prefers its schema/client workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult defaults to Turso/Drizzle but also documents PostgreSQL support through &lt;code&gt;DB_PROVIDER=postgres&lt;/code&gt;. That makes the default opinionated without making the buyer feel locked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins and Product Surface
&lt;/h2&gt;

&lt;p&gt;Most SaaS products do not need every module on day one.&lt;/p&gt;

&lt;p&gt;But many products eventually need one or more of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI tooling.&lt;/li&gt;
&lt;li&gt;CRM workflows.&lt;/li&gt;
&lt;li&gt;Helpdesk workflows.&lt;/li&gt;
&lt;li&gt;Email marketing.&lt;/li&gt;
&lt;li&gt;Support analytics.&lt;/li&gt;
&lt;li&gt;Admin operations.&lt;/li&gt;
&lt;li&gt;Webhooks.&lt;/li&gt;
&lt;li&gt;Feature requests.&lt;/li&gt;
&lt;li&gt;Referrals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult treats larger optional capabilities as plugins where that boundary makes sense. The goal is not to turn every helper into a package.&lt;br&gt;
The goal is to keep the core application usable while letting serious capabilities be added as plugins when the product needs them.&lt;br&gt;
Plugins are separate from the core modules you select at setup — they can be installed independently at any point after launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  When ShipFast Is the Better Choice
&lt;/h2&gt;

&lt;p&gt;Choose ShipFast-style simplicity if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are a solo founder shipping one small app.&lt;/li&gt;
&lt;li&gt;You do not expect enterprise workflows soon.&lt;/li&gt;
&lt;li&gt;You value minimal code more than operational breadth.&lt;/li&gt;
&lt;li&gt;You are comfortable adding missing modules yourself.&lt;/li&gt;
&lt;li&gt;You want a fast marketing and checkout path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a valid product strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Supastarter or MakerKit Is the Better Choice
&lt;/h2&gt;

&lt;p&gt;Choose Supastarter or MakerKit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You like their stack choices.&lt;/li&gt;
&lt;li&gt;You want a mature starter with strong conventions.&lt;/li&gt;
&lt;li&gt;Supabase, Prisma, Firebase, or a specific monorepo shape is important.&lt;/li&gt;
&lt;li&gt;You want a broad starter without Codapult's plugin-oriented packaging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best starter is often the one that matches your team's taste and habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Codapult Is the Better Choice
&lt;/h2&gt;

&lt;p&gt;Choose Codapult when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to start with auth, billing, and a dashboard — and add teams, admin, AI, or SSO only when the product needs them.&lt;/li&gt;
&lt;li&gt;You want AI, docs, blog, billing, webhooks, and infrastructure already represented.&lt;/li&gt;
&lt;li&gt;You want to remove modules via CLI rather than add missing ones from scratch after launch.&lt;/li&gt;
&lt;li&gt;You expect teams and organization-scoped data at some point.&lt;/li&gt;
&lt;li&gt;You need admin operations for support, feature flags, or billing workflows.&lt;/li&gt;
&lt;li&gt;You want swappable providers — auth, payments, storage — without rewriting app logic.&lt;/li&gt;
&lt;li&gt;You may add CRM, helpdesk, email marketing, or AI platform features later (via plugins).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codapult is less about the shortest path to a landing page and more about avoiding the second rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Rule
&lt;/h2&gt;

&lt;p&gt;Use the smallest starter that will survive the next version of your product.&lt;/p&gt;

&lt;p&gt;If the next version is still one user, one plan, one dashboard, and one checkout, choose a smaller starter.&lt;/p&gt;

&lt;p&gt;If the next version includes teams, operational support, provider changes, AI features, and customer-facing documentation, choose a starter that already has those boundaries.&lt;/p&gt;




&lt;p&gt;If you are building a SaaS intended to evolve beyond a simple landing page without needing a complete rewrite down the road, explore the full architectural feature matrix and documentation on the &lt;a href="https://codapult.dev/compare" rel="noopener noreferrer"&gt;Codapult comparison page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>saas</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>I Spent $200 in API Credits Prompting an Agent to Build a Starter. Here’s Why I Still Clone Boilerplates.</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Thu, 30 Jul 2026 17:56:30 +0000</pubDate>
      <link>https://dev.to/vladzoff/i-spent-200-in-api-credits-prompting-an-agent-to-build-a-starter-heres-why-i-still-clone-2lek</link>
      <guid>https://dev.to/vladzoff/i-spent-200-in-api-credits-prompting-an-agent-to-build-a-starter-heres-why-i-still-clone-2lek</guid>
      <description>&lt;p&gt;Last month I decided to test a simple hypothesis: with top-tier models like &lt;strong&gt;Claude Opus 4.8&lt;/strong&gt; and &lt;strong&gt;GPT-5.6&lt;/strong&gt; running inside CLI agents, is the SaaS boilerplate officially dead?&lt;/p&gt;

&lt;p&gt;I opened a fresh directory, booted up an agentic loop, and asked it to scaffold a production-ready Next.js 16 app with Better-Auth, Stripe subscriptions, Tailwind v4, and Drizzle ORM.&lt;/p&gt;

&lt;p&gt;Three hours and a $180 Anthropic invoice later, I had my answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Brutal Math of Multi-File Context
&lt;/h2&gt;

&lt;p&gt;When people say "AI coding is basically free," they are usually talking about generating a single UI component or a standalone utility function. That costs pennies.&lt;/p&gt;

&lt;p&gt;That math completely falls apart the second you ask an agent to build a multi-page architecture across 30+ interconnected files.&lt;/p&gt;

&lt;p&gt;Agentic workflows don't just write code—they inspect directory trees, run terminal checks, read type definitions, and edit files across multiple passes. Every time the agent makes a tool call, runs &lt;code&gt;npm run build&lt;/code&gt;, or checks why a Server Component failed to re-render, it re-sends the entire repository context back into the model.&lt;/p&gt;

&lt;p&gt;Add thousands of reasoning tokens per pass, and a single multi-turn session to wire up auth, session cookies, and Stripe webhooks easily burns through &lt;strong&gt;$150 to $300+ in raw API credits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You are effectively paying AI providers hundreds of dollars to have an agent repeatedly re-invent standard authentication flows from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cohesion Problem: 70+ Modules Don't Just "Prompt" Together
&lt;/h2&gt;

&lt;p&gt;Models like Opus 4.8 and GPT-5.6 are brilliant at generating localized logic. But a production SaaS isn't a collection of isolated files—it is a tightly coupled web of dependencies.&lt;/p&gt;

&lt;p&gt;A real foundation requires 70+ modules and 30+ database tables all speaking the exact same language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth &amp;amp; Identity:&lt;/strong&gt; Better-Auth configured for Passkeys, 2FA, SSO, and multi-tenant Organizations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing Lifecycles:&lt;/strong&gt; Webhook handlers for both Stripe and LemonSqueezy, subscription seats, grace periods, and usage metering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Layer:&lt;/strong&gt; Admin dashboards, granular RBAC, and i18n localization routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you try to prompt an agent into building this graph from scratch, type drift is inevitable. The database schema slightly diverges from the session middleware; the RBAC checks miss an edge case in server actions; the payment webhooks break when a workspace ID is passed as &lt;code&gt;undefined&lt;/code&gt; instead of a string.&lt;/p&gt;

&lt;p&gt;Fixing these cross-module integration bugs requires constant back-and-forth prompts, burning through tokens just to get basic features to stop breaking each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Day-2 Infrastructure Trap
&lt;/h2&gt;

&lt;p&gt;Agents excel at building things that run on &lt;code&gt;localhost:3000&lt;/code&gt; via &lt;code&gt;npm run dev&lt;/code&gt;. They are notoriously unreliable when preparing code for production infrastructure.&lt;/p&gt;

&lt;p&gt;Getting a project ready for production means setting up Infrastructure as Code (IaC):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Valid &lt;strong&gt;Docker Compose&lt;/strong&gt; files for local production mirrors.&lt;/li&gt;
&lt;li&gt;Clean &lt;strong&gt;Terraform&lt;/strong&gt; or &lt;strong&gt;Pulumi&lt;/strong&gt; manifests.&lt;/li&gt;
&lt;li&gt;Ready-to-deploy &lt;strong&gt;Kubernetes Helm charts&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Multi-database flexibility, like swapping between PostgreSQL and edge-ready Turso SQLite via an environment variable (&lt;code&gt;DB_PROVIDER&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you ask an agent to generate valid Helm charts or Terraform manifests alongside app code, it frequently hallucinates configuration keys or outputs outdated syntax. Debugging a failing deployment pipeline through an AI agent can cost $30 to $50 in API compute alone, purely spent fixing minor syntax errors in YAML and HCL files.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Code is a Dead-End Fork
&lt;/h2&gt;

&lt;p&gt;Code generated by a prompt session is a static snapshot in time. It has no lineage and no upstream repository.&lt;/p&gt;

&lt;p&gt;Six months from now, when Next.js introduces breaking API changes, Tailwind v4 releases a major patch, or security vulnerabilities surface in your auth dependencies, your AI-generated starter becomes instant technical debt. Your only recourse is to feed the whole codebase back into an agent and hope it doesn't break your custom business logic while refactoring.&lt;/p&gt;

&lt;p&gt;A purpose-built boilerplate provides a &lt;strong&gt;Git upstream remote&lt;/strong&gt;. When core security patches, framework migrations, or new features drop, you pull them directly with &lt;code&gt;git pull upstream&lt;/code&gt;. You own a living codebase backed by an active maintainer, not an unmaintainable single-shot generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Agent Baby-Sitting Tax
&lt;/h2&gt;

&lt;p&gt;Even if API tokens were completely free, there is the time cost.&lt;/p&gt;

&lt;p&gt;Current agents are fast, but they aren't autonomous enough for you to write one prompt and walk away while a full application builds itself. You are forced to sit at your terminal baby-sitting the process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviewing diffs to make sure the agent didn't mess up Next.js 16's async request APIs (&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or route &lt;code&gt;params&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Approving terminal executions and watching builds fail because a webhook route missed a signature check.&lt;/li&gt;
&lt;li&gt;Guiding the model through fixing subtle hydration mismatches between server and client states.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ever decide to customize the setup mid-build—for instance, removing multi-tenancy while keeping organization billing—asking an agent to refactor its own freshly written code usually results in broken imports and orphaned schema tables.&lt;/p&gt;

&lt;p&gt;Compare that to running an interactive CLI installer like &lt;code&gt;npx create-codapult&lt;/code&gt;, which prompts you for feature flags upfront and instantly outputs a clean, trimmed codebase in seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Save Your Compute Budget for What Actually Matters
&lt;/h2&gt;

&lt;p&gt;Top-tier models are incredible at writing complex, unique business logic that sets your product apart. Using high-reasoning compute to re-generate standard Stripe subscription lifecycles, Tailwind v4 configs, and DB schemas is a waste of capital.&lt;/p&gt;

&lt;p&gt;You’re paying premium rates for the AI equivalent of laying a brick foundation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skip the Scaffolding Prompts
&lt;/h2&gt;

&lt;p&gt;This is exactly why I built &lt;strong&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is a clean, modular Next.js 16 SaaS starter designed to save you from prompt fatigue and context drain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete Foundation:&lt;/strong&gt; Better-Auth (Passkeys, SSO, 2FA), Stripe &amp;amp; LemonSqueezy billing, Tailwind v4, Drizzle ORM, and i18n out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Infrastructure:&lt;/strong&gt; Pre-configured Docker, Terraform, Kubernetes Helm charts, and flexible DB routing (PostgreSQL / Turso SQLite).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible Architecture:&lt;/strong&gt; Need specialized capabilities later? Drop in official modules like the &lt;strong&gt;AI Kit&lt;/strong&gt;, &lt;strong&gt;CRM&lt;/strong&gt;, or &lt;strong&gt;Helpdesk&lt;/strong&gt; without touching the core framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainable Lifetime:&lt;/strong&gt; Stay updated effortlessly via the Git upstream CLI workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You run &lt;code&gt;git clone&lt;/code&gt; or use &lt;code&gt;npx create-codapult&lt;/code&gt;, drop in your &lt;code&gt;.env&lt;/code&gt; variables, and start building actual features immediately.&lt;/p&gt;

&lt;p&gt;Save your high-tier tokens for your core product.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; includes a complete, production-ready Next.js 16 stack out of the box — Better-Auth, Stripe/LemonSqueezy billing lifecycles, Tailwind v4, Drizzle ORM, IaC configs, and modular plugins — saving you hundreds of dollars in API compute and hours of agent orchestration. Explore the architecture in the &lt;a href="https://codapult.dev/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How are you balancing agentic workflows with pre-built boilerplate architecture in your projects? Are you fully prompting, or cloning first?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>saas</category>
    </item>
    <item>
      <title>SaaS Billing Models: Subscriptions, Seats, Credits, and Add-ons</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:12:24 +0000</pubDate>
      <link>https://dev.to/vladzoff/saas-billing-models-subscriptions-seats-credits-and-add-ons-39db</link>
      <guid>https://dev.to/vladzoff/saas-billing-models-subscriptions-seats-credits-and-add-ons-39db</guid>
      <description>&lt;p&gt;Billing is product strategy expressed in code. If your starter only handles a single checkout link, you will rebuild it as soon as pricing changes.&lt;/p&gt;

&lt;p&gt;This article breaks down the billing models a SaaS foundation should support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat Subscription
&lt;/h2&gt;

&lt;p&gt;The simplest model is one plan, one recurring price.&lt;/p&gt;

&lt;p&gt;Use it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The product has a clear single package.&lt;/li&gt;
&lt;li&gt;Usage does not vary much across customers.&lt;/li&gt;
&lt;li&gt;Support and infrastructure costs are predictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementation needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkout session.&lt;/li&gt;
&lt;li&gt;Subscription created webhook.&lt;/li&gt;
&lt;li&gt;Subscription updated webhook.&lt;/li&gt;
&lt;li&gt;Subscription canceled webhook.&lt;/li&gt;
&lt;li&gt;Customer portal.&lt;/li&gt;
&lt;li&gt;Plan state in the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiered Plans
&lt;/h2&gt;

&lt;p&gt;Most SaaS products need at least two paid tiers.&lt;/p&gt;

&lt;p&gt;Typical differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature access.&lt;/li&gt;
&lt;li&gt;Usage limits.&lt;/li&gt;
&lt;li&gt;Support level.&lt;/li&gt;
&lt;li&gt;Number of seats.&lt;/li&gt;
&lt;li&gt;Data retention.&lt;/li&gt;
&lt;li&gt;Integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store &lt;code&gt;planId&lt;/code&gt; in your subscription table and map it to a local plan catalog. Do not rely on provider price IDs as your product logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seat-Based Billing
&lt;/h2&gt;

&lt;p&gt;Seat billing is common for B2B SaaS. It aligns price with customer team size.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current seat count.&lt;/li&gt;
&lt;li&gt;Optional max seat cap.&lt;/li&gt;
&lt;li&gt;Member invite logic.&lt;/li&gt;
&lt;li&gt;Checkout or subscription update flow.&lt;/li&gt;
&lt;li&gt;Billing UI that explains renewal impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt8d7l8b2micbaki8n4g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt8d7l8b2micbaki8n4g.png" alt="Billing screen" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage Credits
&lt;/h2&gt;

&lt;p&gt;Usage credits work well for AI, API calls, file processing, or metered workflows.&lt;/p&gt;

&lt;p&gt;Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total credits.&lt;/li&gt;
&lt;li&gt;Used credits.&lt;/li&gt;
&lt;li&gt;Reset date.&lt;/li&gt;
&lt;li&gt;Usage events.&lt;/li&gt;
&lt;li&gt;Budget alerts.&lt;/li&gt;
&lt;li&gt;Overage policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Credits should be visible to the customer before they become a billing surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add-ons
&lt;/h2&gt;

&lt;p&gt;Add-ons let you expand revenue without forcing every customer into a higher tier.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extra storage.&lt;/li&gt;
&lt;li&gt;White-labeling.&lt;/li&gt;
&lt;li&gt;Priority support.&lt;/li&gt;
&lt;li&gt;Advanced analytics.&lt;/li&gt;
&lt;li&gt;Additional AI credits.&lt;/li&gt;
&lt;li&gt;Premium integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your checkout and subscription state need to support multiple line items, not just one plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trials
&lt;/h2&gt;

&lt;p&gt;Trials are useful when activation requires product usage. They are less useful when setup is complex and a sales conversation is needed.&lt;/p&gt;

&lt;p&gt;If you support trials, store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trial end date.&lt;/li&gt;
&lt;li&gt;Trial status.&lt;/li&gt;
&lt;li&gt;Conversion events.&lt;/li&gt;
&lt;li&gt;Reminder schedule.&lt;/li&gt;
&lt;li&gt;Cancellation behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One-Time Purchases
&lt;/h2&gt;

&lt;p&gt;Some developer products sell lifetime access or source code licenses. That model needs different state than recurring SaaS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;License tier.&lt;/li&gt;
&lt;li&gt;Project count.&lt;/li&gt;
&lt;li&gt;Update period.&lt;/li&gt;
&lt;li&gt;Support period.&lt;/li&gt;
&lt;li&gt;Delivery workflow.&lt;/li&gt;
&lt;li&gt;Receipt and access email.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not mix one-time license logic into recurring subscription code without clear types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks Are the Source of Truth
&lt;/h2&gt;

&lt;p&gt;Checkout success pages are not enough. Provider webhooks should update your database.&lt;/p&gt;

&lt;p&gt;At minimum, handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkout completed.&lt;/li&gt;
&lt;li&gt;Subscription created.&lt;/li&gt;
&lt;li&gt;Subscription updated.&lt;/li&gt;
&lt;li&gt;Subscription deleted.&lt;/li&gt;
&lt;li&gt;Payment failed.&lt;/li&gt;
&lt;li&gt;Refund.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Log webhook delivery and errors. Silent billing failures are expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Admin Billing Support
&lt;/h2&gt;

&lt;p&gt;Your admin panel should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User name and email.&lt;/li&gt;
&lt;li&gt;Provider.&lt;/li&gt;
&lt;li&gt;Plan ID.&lt;/li&gt;
&lt;li&gt;Status.&lt;/li&gt;
&lt;li&gt;Seat count.&lt;/li&gt;
&lt;li&gt;Renewal date.&lt;/li&gt;
&lt;li&gt;Cancellation state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Support should not need direct database access to answer a billing question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the Model You Can Explain
&lt;/h2&gt;

&lt;p&gt;The best billing model is not the most sophisticated one. It is the one customers understand and your system can enforce.&lt;/p&gt;

&lt;p&gt;Start with the simplest model that matches value, but choose a codebase that can add seats, credits, add-ons, and trials when your pricing evolves.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; ships billing adapters for Stripe, LemonSqueezy, and Polar with normalized webhook handling, subscription state in the database, seat tracking, usage credits, and a customer portal — switchable via &lt;code&gt;PAYMENT_PROVIDER&lt;/code&gt; without touching application code. The &lt;a href="https://codapult.dev/docs/payments/overview" rel="noopener noreferrer"&gt;payments docs&lt;/a&gt; show the integration points.&lt;/p&gt;

</description>
      <category>billing</category>
      <category>stripe</category>
      <category>lemonsqueezy</category>
      <category>saas</category>
    </item>
    <item>
      <title>Auth, Teams, and RBAC in a Next.js SaaS App</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:07:58 +0000</pubDate>
      <link>https://dev.to/vladzoff/auth-teams-and-rbac-in-a-nextjs-saas-app-2mla</link>
      <guid>https://dev.to/vladzoff/auth-teams-and-rbac-in-a-nextjs-saas-app-2mla</guid>
      <description>&lt;p&gt;Authentication gets users into the app. Authorization decides what they can do once they are there. In SaaS products, the second part is where many starters are too thin.&lt;/p&gt;

&lt;p&gt;This guide covers the practical model: platform roles, organization roles, protected routes, admin access, and future enterprise needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the User Record
&lt;/h2&gt;

&lt;p&gt;Keep the user record to identity only — id, name, email, verification state, avatar, platform role, 2FA state, timestamps. Nothing organization-specific belongs here, because the same person can be an owner in one workspace and a viewer in another. The moment you store a permission field directly on the user, you've hard-coded an assumption that breaks the first time someone joins a second organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Platform Roles
&lt;/h2&gt;

&lt;p&gt;Platform roles are global. Most apps only need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The admin role controls platform screens such as &lt;code&gt;/admin/users&lt;/code&gt;, &lt;code&gt;/admin/subscriptions&lt;/code&gt;, and &lt;code&gt;/admin/feature-flags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not use platform admin for customer workspace permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Organization Memberships
&lt;/h2&gt;

&lt;p&gt;Organization roles should answer what a user can do inside one workspace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;owner&lt;/code&gt;: billing, members, settings, deletion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin&lt;/code&gt;: most management actions except ownership transfer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;member&lt;/code&gt;: normal product usage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;viewer&lt;/code&gt;: read-only access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you enough granularity without creating a complex permission system too early.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxelz780kqmmayku9eg1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxelz780kqmmayku9eg1a.png" alt="Organization Memberships" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect Routes at Multiple Layers
&lt;/h2&gt;

&lt;p&gt;Route protection should happen in more than one place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Middleware or proxy redirects unauthenticated users.&lt;/li&gt;
&lt;li&gt;Layouts block protected app areas.&lt;/li&gt;
&lt;li&gt;Server actions verify the session.&lt;/li&gt;
&lt;li&gt;API routes verify the session.&lt;/li&gt;
&lt;li&gt;Admin operations verify platform role.&lt;/li&gt;
&lt;li&gt;Organization operations verify membership and org role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI can hide buttons, but the server must enforce permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Auth Pages Out of Search
&lt;/h2&gt;

&lt;p&gt;Sign-in, sign-up, dashboard, and admin pages should not be indexed. They are not acquisition pages, and search engines do not need them.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;robots&lt;/code&gt; metadata with &lt;code&gt;index: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt; disallow rules for protected paths.&lt;/li&gt;
&lt;li&gt;Sitemap exclusion for auth-only pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps SEO focus on marketing, docs, blog, pricing, and comparison pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan for Enterprise Auth Without Building It Too Early
&lt;/h2&gt;

&lt;p&gt;Enterprise customers often ask for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SAML SSO.&lt;/li&gt;
&lt;li&gt;SCIM provisioning.&lt;/li&gt;
&lt;li&gt;Enforced 2FA.&lt;/li&gt;
&lt;li&gt;Audit logs.&lt;/li&gt;
&lt;li&gt;Domain verification.&lt;/li&gt;
&lt;li&gt;Custom session policies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to ship every enterprise feature on day one. You do need a data model that can add them without rewriting auth and teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support Impersonation Carefully
&lt;/h2&gt;

&lt;p&gt;Impersonation is powerful for support, but it is sensitive.&lt;/p&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only platform admins can impersonate.&lt;/li&gt;
&lt;li&gt;Show a visible impersonation banner.&lt;/li&gt;
&lt;li&gt;Log the impersonation start and end.&lt;/li&gt;
&lt;li&gt;Never let impersonation bypass destructive confirmation steps silently.&lt;/li&gt;
&lt;li&gt;Avoid using impersonation for billing provider actions when possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test the Permission Matrix
&lt;/h2&gt;

&lt;p&gt;Write tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anonymous user access.&lt;/li&gt;
&lt;li&gt;Authenticated user access.&lt;/li&gt;
&lt;li&gt;Admin-only routes.&lt;/li&gt;
&lt;li&gt;Owner-only organization settings.&lt;/li&gt;
&lt;li&gt;Member actions.&lt;/li&gt;
&lt;li&gt;Viewer restrictions.&lt;/li&gt;
&lt;li&gt;Locale-prefixed protected routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auth regressions are high-risk because they often expose data, not just break UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Baseline
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. A real auth provider, protected routes at every layer, a platform admin role, an organization membership table with roles, server-side permission checks, audit logging, and noindex on anything that isn't marketing. That's the baseline that survives contact with a real customer — not an enterprise IAM project, just enough structure that adding SSO later doesn't mean rewriting how permissions work.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; includes this full auth and teams layer out of the box — email/password, OAuth, magic links, TOTP 2FA, SAML SSO, organizations, RBAC, invitations, protected routes and audit logs — switchable between Better-Auth and Kinde via one environment variable. Explore the implementation map in the &lt;a href="https://codapult.dev/docs/authentication/overview" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>auth</category>
      <category>teams</category>
      <category>rbac</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Designing Removable Modules in a Next.js SaaS Starter</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:03:15 +0000</pubDate>
      <link>https://dev.to/vladzoff/designing-removable-modules-in-a-nextjs-saas-starter-1n87</link>
      <guid>https://dev.to/vladzoff/designing-removable-modules-in-a-nextjs-saas-starter-1n87</guid>
      <description>&lt;p&gt;Most SaaS starters say they are modular.&lt;/p&gt;

&lt;p&gt;The useful question is: what happens when you remove a module?&lt;/p&gt;

&lt;p&gt;If disabling a feature leaves dead navigation links, broken sitemap entries, missing imports, route handlers that still accept requests, or database tables nobody understands, the module was not really modular. It was just grouped in a folder.&lt;/p&gt;

&lt;p&gt;Removable modules need boundaries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rlgevh5sdpkejv5tae6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rlgevh5sdpkejv5tae6.png" alt="Module architecture docs" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With User-Facing Surfaces
&lt;/h2&gt;

&lt;p&gt;A module usually shows up in more places than its main page suggests. Take a waitlist feature: there's the public signup route, sure, but also an admin view, an API route, a database table, an email template, a navbar link, a footer link, a sitemap entry. Delete the public page and the feature is still there — just invisible until someone hits the API route directly or wonders why waitlist_entry is sitting in the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Flags Are Not Enough
&lt;/h2&gt;

&lt;p&gt;Feature flags hide behavior at runtime.&lt;/p&gt;

&lt;p&gt;They do not remove code.&lt;/p&gt;

&lt;p&gt;That distinction matters. Runtime flags are useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hiding navigation.&lt;/li&gt;
&lt;li&gt;Returning 404 for disabled routes.&lt;/li&gt;
&lt;li&gt;Removing sitemap entries.&lt;/li&gt;
&lt;li&gt;Preventing API access.&lt;/li&gt;
&lt;li&gt;Running one codebase in multiple modes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if a buyer wants a smaller codebase, the setup tool also needs to delete files and update references.&lt;/p&gt;

&lt;p&gt;A good starter supports both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature flags for runtime control.&lt;/li&gt;
&lt;li&gt;Pruning for permanent removal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Route Groups Help
&lt;/h2&gt;

&lt;p&gt;Next.js App Router route groups are useful for keeping surfaces understandable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/app/[locale]/
  (marketing)/
  (auth)/
  (dashboard)/
  admin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure makes it clear which pages are public, protected, authentication-only, or platform admin.&lt;/p&gt;

&lt;p&gt;It also lets you reason about SEO. Marketing pages are indexable. Auth, dashboard, and admin pages should be noindex and blocked where appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Shared Code Honest
&lt;/h2&gt;

&lt;p&gt;The hardest modules to remove are the ones that leak into shared files.&lt;/p&gt;

&lt;p&gt;Common leak points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;navigation.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sitemap.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robots.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proxy.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Shared schema files.&lt;/li&gt;
&lt;li&gt;Shared validation schemas.&lt;/li&gt;
&lt;li&gt;Shared server actions.&lt;/li&gt;
&lt;li&gt;Shared dashboard layouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a module needs entries in those files, make the entries obvious and grouped. If a setup wizard removes the module, it should know exactly what to prune.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Routes Need Gates Too
&lt;/h2&gt;

&lt;p&gt;Disabled UI is not security.&lt;/p&gt;

&lt;p&gt;If a module has API routes, those routes need their own gates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth check.&lt;/li&gt;
&lt;li&gt;Permission check.&lt;/li&gt;
&lt;li&gt;Rate limit.&lt;/li&gt;
&lt;li&gt;Feature flag check.&lt;/li&gt;
&lt;li&gt;Input validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The proxy can return 404 for disabled feature routes, but route handlers should still follow the same protected pattern. That keeps modules safe when moved, copied, or extended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Tables Should Be Named by Domain
&lt;/h2&gt;

&lt;p&gt;Module data should be easy to identify.&lt;/p&gt;

&lt;p&gt;Good table names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;waitlist_entry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feature_request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audit_log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outgoing_webhook&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workflow&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ambiguous table names make removal riskier. If the table name tells you the module, migration work is easier to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests Should Prove Removal Boundaries
&lt;/h2&gt;

&lt;p&gt;Useful tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disabled feature returns 404.&lt;/li&gt;
&lt;li&gt;Disabled feature is absent from sitemap.&lt;/li&gt;
&lt;li&gt;Disabled feature is absent from navigation.&lt;/li&gt;
&lt;li&gt;API route rejects unauthenticated access.&lt;/li&gt;
&lt;li&gt;Module-specific queries still work when enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need massive coverage for every module. You need enough tests to prevent obvious leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins Are for Bigger Boundaries
&lt;/h2&gt;

&lt;p&gt;Not every removable feature should be a plugin.&lt;/p&gt;

&lt;p&gt;A simple waitlist page can be a module.&lt;/p&gt;

&lt;p&gt;A CRM with pages, routes, hooks, schema, components, AI helpers, and install commands is a better plugin candidate.&lt;/p&gt;

&lt;p&gt;Use plugins when the feature has its own lifecycle and customer value. Use modules when the feature is part of the core application shape.&lt;/p&gt;

&lt;p&gt;None of this needs to be exhaustive. It needs to be enough that "modular" describes the codebase instead of describing the marketing copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;Removable modules give buyers confidence.&lt;/p&gt;

&lt;p&gt;They can start broad, then strip what they do not need. Or they can keep a larger operational surface without building it themselves.&lt;/p&gt;

&lt;p&gt;The codebase stays understandable because module boundaries are explicit.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; documents removable features in the &lt;a href="https://codapult.dev/docs/modules/overview" rel="noopener noreferrer"&gt;modules overview&lt;/a&gt;, including routes, dependencies, and manual removal notes.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>saas</category>
    </item>
    <item>
      <title>Turso + Drizzle vs PostgreSQL + Prisma for a SaaS Boilerplate</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:01:23 +0000</pubDate>
      <link>https://dev.to/vladzoff/turso-drizzle-vs-postgresql-prisma-for-a-saas-boilerplate-4pmb</link>
      <guid>https://dev.to/vladzoff/turso-drizzle-vs-postgresql-prisma-for-a-saas-boilerplate-4pmb</guid>
      <description>&lt;p&gt;Database choices in SaaS starters are more emotional than they should be.&lt;/p&gt;

&lt;p&gt;Turso, PostgreSQL, Drizzle, Prisma, Supabase, Neon, and SQLite all have real strengths. The right choice depends on what the starter optimizes for: local setup, operational familiarity, edge reads, schema control, ecosystem, or enterprise expectations.&lt;/p&gt;

&lt;p&gt;This is not a purity argument. It is a trade-off map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turso + Drizzle
&lt;/h2&gt;

&lt;p&gt;Turso is hosted libSQL, a SQLite-compatible database with cloud features. Drizzle is a lightweight TypeScript ORM with explicit schema definitions and SQL-like query builders.&lt;/p&gt;

&lt;p&gt;The appeal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple local development with &lt;code&gt;file:local.db&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fast setup for new projects.&lt;/li&gt;
&lt;li&gt;Lightweight query layer.&lt;/li&gt;
&lt;li&gt;TypeScript-first schema and query types.&lt;/li&gt;
&lt;li&gt;Good fit for many SaaS workloads that do not need heavyweight relational operations on day one.&lt;/li&gt;
&lt;li&gt;Multi-region read replica story through Turso.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some teams are less familiar with SQLite/libSQL in production.&lt;/li&gt;
&lt;li&gt;PostgreSQL has a larger ecosystem.&lt;/li&gt;
&lt;li&gt;Enterprise buyers may ask for Postgres by default.&lt;/li&gt;
&lt;li&gt;Some third-party tools assume Postgres.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turso + Drizzle is a strong default when you value small setup and explicit TypeScript code.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL + Prisma
&lt;/h2&gt;

&lt;p&gt;PostgreSQL is the familiar production default for many SaaS teams. Prisma is widely adopted and has a comfortable schema/client workflow.&lt;/p&gt;

&lt;p&gt;The appeal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very familiar to engineering teams.&lt;/li&gt;
&lt;li&gt;Huge hosting ecosystem: Neon, Supabase, RDS, Railway, Render, Fly, and more.&lt;/li&gt;
&lt;li&gt;Strong relational feature set.&lt;/li&gt;
&lt;li&gt;Many analytics, BI, and integration tools support it directly.&lt;/li&gt;
&lt;li&gt;Prisma has excellent discoverability and a large community.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prisma can add more abstraction than some TypeScript teams want.&lt;/li&gt;
&lt;li&gt;Generated clients and migrations introduce their own workflow.&lt;/li&gt;
&lt;li&gt;Local setup can be heavier than an SQLite file.&lt;/li&gt;
&lt;li&gt;Switching away from Prisma later is rarely free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL + Prisma is a strong default when team familiarity and ecosystem matter more than minimal setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL + Drizzle
&lt;/h2&gt;

&lt;p&gt;This middle path is increasingly attractive precisely because it refuses to pick a side: you get PostgreSQL's familiar hosting story and Drizzle's thinner query layer. The trade-off is that you still need a real Postgres instance for local dev — no single-file convenience — and Drizzle's ecosystem, while growing, is smaller than Prisma's. For teams that think in SQL, this is often the most pragmatic option on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a SaaS Starter Should Do
&lt;/h2&gt;

&lt;p&gt;A starter should make the default easy without pretending it is universal.&lt;/p&gt;

&lt;p&gt;The docs should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What database is default?&lt;/li&gt;
&lt;li&gt;How do I run locally without a cloud account?&lt;/li&gt;
&lt;li&gt;How do migrations work before and after production data exists?&lt;/li&gt;
&lt;li&gt;Can I use PostgreSQL?&lt;/li&gt;
&lt;li&gt;If PostgreSQL is supported, which env vars change?&lt;/li&gt;
&lt;li&gt;Which schema file is the source of truth?&lt;/li&gt;
&lt;li&gt;What are the trade-offs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuq3oorpdgktulw6v4dq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuq3oorpdgktulw6v4dq.png" alt="Database docs" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The worst answer is silence. Buyers interpret silence as lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Workflow Matters
&lt;/h2&gt;

&lt;p&gt;For a fresh database, &lt;code&gt;db:push&lt;/code&gt; is convenient.&lt;/p&gt;

&lt;p&gt;For production data, generated migrations are safer.&lt;/p&gt;

&lt;p&gt;That distinction should be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fresh local database&lt;/span&gt;
pnpm db:push

&lt;span class="c"&gt;# Production changes&lt;/span&gt;
pnpm db:generate
pnpm db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most SaaS teams can start fast and become stricter when real data appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Code Should Stay Boring
&lt;/h2&gt;

&lt;p&gt;Regardless of provider, application code should import one database client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app should not know whether the active provider is Turso or PostgreSQL. Provider selection belongs in database initialization, not in every route handler.&lt;/p&gt;

&lt;p&gt;That boundary matters more than the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Recommendation
&lt;/h2&gt;

&lt;p&gt;If fast local setup and multi-region reads matter most, start with Turso + Drizzle. If your team already knows Postgres, or your customers expect it, use Postgres — with Prisma if you want the generated-client ergonomics, or Drizzle if you'd rather write something closer to SQL. None of these is objectively correct. The mature answer isn't "this database is best" — it's picking a sensible default and documenting the escape hatch.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; defaults to Turso + Drizzle and documents PostgreSQL via &lt;code&gt;DB_PROVIDER=postgres&lt;/code&gt;; the &lt;a href="https://codapult.dev/docs/database/overview" rel="noopener noreferrer"&gt;database docs&lt;/a&gt; cover setup and migration paths.&lt;/p&gt;

</description>
      <category>database</category>
      <category>typescript</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Payment Adapter Pattern in Next.js: Stripe, LemonSqueezy, and Polar Without App-Wide Rewrites</title>
      <dc:creator>Vlad Zoff</dc:creator>
      <pubDate>Tue, 07 Jul 2026 11:33:39 +0000</pubDate>
      <link>https://dev.to/vladzoff/the-payment-adapter-pattern-in-nextjs-stripe-lemonsqueezy-and-polar-without-app-wide-rewrites-1h1p</link>
      <guid>https://dev.to/vladzoff/the-payment-adapter-pattern-in-nextjs-stripe-lemonsqueezy-and-polar-without-app-wide-rewrites-1h1p</guid>
      <description>&lt;p&gt;Most SaaS apps start with one payment provider.&lt;/p&gt;

&lt;p&gt;That is fine. The mistake is letting that provider become the architecture.&lt;/p&gt;

&lt;p&gt;If Stripe-specific objects leak into pages, server actions, admin tools, and webhook handlers, switching to LemonSqueezy or Polar becomes a rewrite. Even adding one-time products next to subscriptions can become awkward.&lt;/p&gt;

&lt;p&gt;The payment adapter pattern keeps provider details behind a small interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of the Problem
&lt;/h2&gt;

&lt;p&gt;Payment code tends to sprawl fast: pricing CTAs, authenticated checkout, one-time marketing checkout, billing portal links, webhooks, refunds, seat changes, usage credits. Import a provider SDK directly in even half of these, and every feature becomes provider-aware — which means every provider migration becomes an app-wide grep.&lt;/p&gt;

&lt;p&gt;If each layer imports a provider SDK directly, every feature becomes provider-aware.&lt;/p&gt;

&lt;p&gt;That makes the code harder to test and harder to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small Interface Is Enough
&lt;/h2&gt;

&lt;p&gt;You do not need a large abstraction. Start with the operations the app actually performs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CheckoutParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;createMarketingCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MarketingCheckoutParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;createPortalSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebhookResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;refundPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefundParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RefundResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the app imports &lt;code&gt;getPaymentAdapter()&lt;/code&gt; instead of &lt;code&gt;stripe&lt;/code&gt;, &lt;code&gt;lemonsqueezy&lt;/code&gt;, or &lt;code&gt;polar&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPaymentAdapter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkoutUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one boundary gives you room to change providers without changing every caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Plans Provider-Neutral
&lt;/h2&gt;

&lt;p&gt;Plan definitions should describe your product, not your provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;yearly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;190&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unlimited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aiCredits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provider-specific IDs belong in environment variables or provider mapping code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PAYMENT_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"stripe"&lt;/span&gt;
&lt;span class="nv"&gt;STRIPE_PRICE_PRO_MONTHLY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"price_..."&lt;/span&gt;

&lt;span class="c"&gt;# or&lt;/span&gt;
&lt;span class="nv"&gt;PAYMENT_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"polar"&lt;/span&gt;
&lt;span class="nv"&gt;CHECKOUT_VARIANT_PRO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the app reason about &lt;code&gt;pro&lt;/code&gt; while the adapter handles provider IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marketing Checkout Is Different
&lt;/h2&gt;

&lt;p&gt;Authenticated SaaS checkout usually attaches a customer, organization, subscription, and return URL.&lt;/p&gt;

&lt;p&gt;Marketing checkout may sell source-code licenses or plugins before the buyer has an account.&lt;/p&gt;

&lt;p&gt;That path needs its own method:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh12s78xtu0k3pvjdpb58.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh12s78xtu0k3pvjdpb58.png" alt="Pricing" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMarketingCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;productKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plugin-ai-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;successUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;cancelUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pricing page can route to &lt;code&gt;/api/checkout?product=plugin-ai-kit&lt;/code&gt;, and the route can call the active adapter.&lt;/p&gt;

&lt;p&gt;If you prefer direct hosted checkout URLs, keep those as the simpler path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CHECKOUT_URL_PLUGIN_AI_KIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://provider.example/buy/plugin-ai-kit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt;, &lt;code&gt;CHECKOUT_VARIANT_*&lt;/code&gt; wins over &lt;code&gt;CHECKOUT_URL_*&lt;/code&gt;, so API checkout is used when a provider variant exists and direct links remain available as fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks Need Normalized Events
&lt;/h2&gt;

&lt;p&gt;Provider webhooks are not interchangeable.&lt;/p&gt;

&lt;p&gt;Stripe, LemonSqueezy, and Polar use different event names, payload shapes, signature headers, and product identifiers. The adapter should normalize those into app-level events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WebhookResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment.refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ignored&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route validates rate limits and signature, then passes the normalized result to application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Not to Abstract
&lt;/h2&gt;

&lt;p&gt;Do not hide every provider feature.&lt;/p&gt;

&lt;p&gt;Stripe Connect, LemonSqueezy licensing, and Polar's Merchant of Record model are not identical. If you need a provider-specific feature, keep it explicit at the edge of the adapter.&lt;/p&gt;

&lt;p&gt;Good adapters remove duplication. Bad adapters pretend different products are the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;With this boundary in place, pages don't import provider SDKs, webhook routes normalize events before they hit application code, and tests mock one interface instead of three. Switching providers becomes a config change and an adapter update — not a rewrite.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://codapult.dev" rel="noopener noreferrer"&gt;Codapult&lt;/a&gt; includes Stripe, LemonSqueezy, and Polar payment adapters; the &lt;a href="https://codapult.dev/docs/payments/overview" rel="noopener noreferrer"&gt;payments docs&lt;/a&gt; show the provider switch and checkout paths.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>stripe</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
