<?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: kuberdenis</title>
    <description>The latest articles on DEV Community by kuberdenis (@kubeden).</description>
    <link>https://dev.to/kubeden</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%2F421554%2F086f2f78-8387-480e-a476-b3b3363e261c.jpg</url>
      <title>DEV Community: kuberdenis</title>
      <link>https://dev.to/kubeden</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kubeden"/>
    <language>en</language>
    <item>
      <title>Multirun: A Multi-Agent Run Framework</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:09:35 +0000</pubDate>
      <link>https://dev.to/kubeden/multirun-a-multi-agent-run-framework-38a7</link>
      <guid>https://dev.to/kubeden/multirun-a-multi-agent-run-framework-38a7</guid>
      <description>&lt;p&gt;Ask a coding agent to do the same non-trivial thing twice, and you get two different implementations, both fine and both defensible. Denis had been noticing this for months and doing absolutely nothing about it, because with a single run there is nothing you &lt;em&gt;can&lt;/em&gt; do. One run is an anecdote. You read the diff, you decide you like it or you don't, and the actual question never comes up.&lt;/p&gt;

&lt;p&gt;The question is what it would have done the other nine times.&lt;/p&gt;

&lt;p&gt;You can't see that at n=1. And n=2 doesn't really help either, because two runs is a comparison, and what he wanted was a distribution of where the model lands reliably and where it is quietly flipping a coin on his behalf.&lt;/p&gt;

&lt;p&gt;So he spent a day building a framework that runs one task ten times in parallel, keeps every run isolated from the others, and then shows what the ten did differently. It is called multirun, and it lives here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/a2wio/multirun" rel="noopener noreferrer"&gt;[a2wio/multirun]&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: this post is a direct continuation of Denis's &lt;a href="https://denislavgavrilov.com/preview-environments" rel="noopener noreferrer"&gt;"Preview Environments"&lt;/a&gt; and &lt;a href="https://denislavgavrilov.com/agentic-workflows-branched-environments" rel="noopener noreferrer"&gt;"Agentic Workflows: Branched Environments"&lt;/a&gt; posts. Same core trick, pointed somewhere new. If the words "database branching" mean nothing to you yet, the second one explains them properly, so it doesn't get repeated here.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;




&lt;p&gt;He built it out of curiosity. What came out the other end were two uses worth recommending to somebody else, so those go up front, and the machinery comes afterwards.&lt;/p&gt;

&lt;p&gt;The first one is evals. Denis had never written one. Not properly, not against his own prompts. Every time the subject came up it sounded like infrastructure (a harness, a judge model, a golden dataset, some score to chase) and it went onto the list of things he would do when he had a spare week. Well… the spare week never arrived.&lt;/p&gt;

&lt;p&gt;Running the same task ten times and reading where the ten answers agree &lt;em&gt;is&lt;/em&gt; an eval. A crude one, but a real one, and there is no judge model or benchmark anywhere in this post. The ten runs in here converged completely on the schema and then split exactly 5/5 on two separate decisions, and those splits are the result. They name the sentences his task file was missing. He did not have to guess at them, and nothing had to be built to find them beyond a loop that runs the task more than once.&lt;/p&gt;

&lt;p&gt;The second one he has been calling results branching, which is a straight lift from database branching, and he is keeping it. It is for the days when you are about to build the thing you will live with for years: the tenancy model, the auth boundary, the schema that will have three years of rows in it before you get another shot at it. The normal habit is to ask the agent, read what comes back, and either take it or argue with it. That is one sample out of a distribution you never get to see.&lt;/p&gt;

&lt;p&gt;So don't take the first answer. Fan the same task out ten ways, let them run without seeing each other, and read the spread before you commit to one. The fan-out in this post took fifty minutes of wall time and about thirty-two dollars at API rates, which is not nothing, but it is a great deal less than what a wrong &lt;code&gt;ON DELETE&lt;/code&gt; costs you eighteen months later.&lt;/p&gt;

&lt;p&gt;Neither of those needs much cleverness. They need ten runs that cannot touch each other, and that is the hard part.&lt;/p&gt;

&lt;p&gt;The isolation is the whole design problem, and it splits into two halves.&lt;/p&gt;

&lt;p&gt;Ten agents editing one repository is nonsense, and that half is easy: git has had worktrees for years, every run gets its own, done. Ten agents sharing one database is the harder half. It was hard for a long time, and it stopped being hard the moment the database learned how to branch. That is the reason this project became buildable in a day instead of a quarter.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;




&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;multirun&lt;/strong&gt; takes a repository at a specific sha and a task, and fans out N isolated runs of Claude Code, capturing everything each one did.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every run gets a git worktree and its own Postgres branch, both off the same seeded parent. Diffing a run's branch back against that parent shows what the agent did to the &lt;em&gt;data&lt;/em&gt;, not only the files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;First real fan-out: ten runs, same task, same model, &lt;code&gt;max_parallel: 3&lt;/code&gt;, on his k3s cluster. 10/10 finished, 20/20 checks passed, 50 minutes wall, 930 turns, zero leaked branches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All ten reached the same schema shape. Then they split 5/5 on whether deleting a team deletes its data, 5/5 on the join table's primary key, and 8/2 on whether the new column is allowed to be NULL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Convergence on structure, coin flips on judgment. There was no way to see that without running the thing ten times.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Two uses fell out of it. It is a cheap eval, since where the ten answers split is where your prompt was ambiguous. And it is a way to branch a decision you will live with for years, instead of taking the agent's first answer for it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What a run is
&lt;/h2&gt;




&lt;p&gt;The whole framework fits on one line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run = (source, task, variant) -&amp;gt; artifacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt; is a repository at a specific sha, plus an optional database seed. Never vendored, always pinned. If the ten runs don't start from a byte-identical tree, comparing them means nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task&lt;/strong&gt; is a markdown prompt and its checks, versioned as files, so that an old task can be re-run against a newer source later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variant&lt;/strong&gt; is whatever differs per run: the model, a prompt variation, or &lt;em&gt;nothing at all&lt;/em&gt;. That last case is the one he cares about, because when nothing differs, the only variable left is the model itself.&lt;/p&gt;

&lt;p&gt;Everything else in the system (the tables, the dashboard, the comparison writeups) is a view over artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why branching beats copying
&lt;/h3&gt;

&lt;p&gt;Ten runs need ten databases, and they all need to begin life identical.&lt;/p&gt;

&lt;p&gt;The obvious approach is to copy one database ten times, and the obvious approach is bad. A copy costs you the full size of your data every single time, it takes minutes, and afterwards you own ten real databases that you have to remember to delete. At 30 MB that is merely annoying. At anything resembling production data, you will not do it twice.&lt;/p&gt;

&lt;p&gt;Neon separates compute from storage, so a branch is a new logical pointer into the same storage, and only the pages that actually diverge ever get written. Nothing is duplicated up front. He went through the mechanics at length in &lt;a href="https://denislavgavrilov.com/agentic-workflows-branched-environments" rel="noopener noreferrer"&gt;Agentic Workflows: Branched Environments&lt;/a&gt;, so here it is only the number that matters. A branch taken off this bench's parent while this post was being written was ready in 1.38 seconds.&lt;/p&gt;

&lt;p&gt;That parent holds about 30 MB. He has not tried this at 300 GB, so take it as reasoning and not as a measurement.. but the whole point of copy-on-write is that the number shouldn't move much, because nothing is being copied.&lt;/p&gt;

&lt;h3&gt;
  
  
  The diff
&lt;/h3&gt;

&lt;p&gt;Provisioning speed is nice, but the diff is why he built it this way.&lt;/p&gt;

&lt;p&gt;Every run branches off the &lt;em&gt;same&lt;/em&gt; parent, so when a run finishes its branch gets diffed back against that parent, and that gives two things: the schema delta (what DDL it emitted) and the data delta (rows added, changed, deleted, per table).&lt;/p&gt;

&lt;p&gt;A transcript tells you what the agent &lt;em&gt;said&lt;/em&gt; it did. The branch diff is what it &lt;em&gt;did&lt;/em&gt;. Ten of them are directly comparable, because all ten are measured against the same origin.&lt;/p&gt;

&lt;p&gt;The tradeoff is that branches are real objects with a real bill attached. His plan includes ten of them and charges per branch per month after that, and a leaked branch is exactly the sort of thing nobody notices for a quarter. So teardown is a first-class state with its own row in the results database, rather than a &lt;code&gt;finally&lt;/code&gt; block somewhere in the code, and every run passes through it whether it succeeded, failed, or timed out:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING -&amp;gt; PROVISIONING -&amp;gt; SPAWNED -&amp;gt; RUNNING -&amp;gt; DIFFING
        -&amp;gt; TEARDOWN -&amp;gt; DONE | FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Notice that &lt;code&gt;DIFFING&lt;/code&gt; comes before &lt;code&gt;TEARDOWN&lt;/code&gt;. The diff needs the branch alive, and the branch needs to die. Getting those two in the wrong order is how you end up with ten orphans and no data.&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%2Frukl80qplsquc3u7fmbe.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%2Frukl80qplsquc3u7fmbe.png" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One config file describes an entire fan-out. &lt;code&gt;global:&lt;/code&gt; holds the defaults and &lt;code&gt;runs:&lt;/code&gt; is a list of per-run overrides. For this fan-out every single run is identical, which is the point.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting it in the cluster
&lt;/h2&gt;




&lt;p&gt;Locally, all of this is a loop. On his k3s cluster it becomes a controller.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;core&lt;/code&gt; is a plain Deployment, no CRD and no operator boilerplate. He did not want to write a reconciler for this, and does not think it needs one. It watches a ConfigMap inbox that &lt;code&gt;multirun publish&lt;/code&gt; drops a fan-out config into, merges global + per-run config for each run, renders that into a per-run ConfigMap, and spawns exactly one Kubernetes Job. Run state lives in a Postgres results database rather than in etcd, because he wants run history queryable in SQL months from now, not &lt;code&gt;kubectl get&lt;/code&gt; output that ages out of existence.&lt;/p&gt;

&lt;p&gt;The runner itself is deliberately stupid. It reads &lt;code&gt;/config/run.yaml&lt;/code&gt;, does the run, writes artifacts to a shared PVC. It never talks to the Kubernetes API, never talks to the Neon API, and never imports orchestrator code. Config in, artifacts out is the entire contract. That is what keeps a failed pod debuggable, since you can hold a whole run in your head from one YAML file and one directory.&lt;/p&gt;

&lt;p&gt;The agent runs through Anthropic's Agent SDK on his Claude subscription, rather than the raw API. Two things follow from that, and both bit him. Subscription credentials inside a pod rotate, so a stale Secret fails auth with nothing obvious in the logs. And ten simultaneous agents on one subscription hit rate limits, which is why &lt;code&gt;max_parallel&lt;/code&gt; defaults to 3. Ten branches does not mean ten at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where he cut a corner
&lt;/h3&gt;

&lt;p&gt;This was running on the cluster, and working, before it was under ArgoCD.&lt;/p&gt;

&lt;p&gt;The manifests existed. The registration PR was open. And he had &lt;code&gt;kubectl apply&lt;/code&gt;'d them "as a bridge" to unblock the fan-out. Well… the bridge became the deploy. Everything on that cluster is GitOps-managed, and this one thing was a hand-placed object pretending to be one, which is arguably worse than not deploying it at all, because it looks completely fine right up until the moment someone syncs.&lt;/p&gt;

&lt;p&gt;So it got adopted properly: an Application, the manifests in git, secrets as SealedSecrets. The adoption rolled nothing. There was zero drift between what he had applied by hand and what git said, so the fan-out that was running at the time never even noticed.&lt;/p&gt;

&lt;p&gt;The lesson he took from it is smaller than "use GitOps", and more useful. He had verified that &lt;em&gt;the core pod is running&lt;/em&gt;, when the thing he was claiming was &lt;em&gt;the app is deployed&lt;/em&gt;. Those are two different sentences, and he checked the easy one.&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%2Flamdzzhn4czqzrhymj5t.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%2Flamdzzhn4czqzrhymj5t.png" width="800" height="672"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After adoption. The bridge he'd applied by hand and the manifests in git were byte-identical, so nothing rolled.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Ten agents, one task
&lt;/h2&gt;




&lt;p&gt;The task he picked is the most boring migration in software, and he picked it for exactly that reason. Take an app that was built single-user, and add teams to it. Every SaaS does this eventually. There is no clever answer waiting at the end.&lt;/p&gt;

&lt;p&gt;The prompt is a short markdown file, and the important thing about it is what it refuses to specify:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This app was built single-user: every row that matters hangs off one user id, and nothing in the schema knows what a team is. Add teams.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user can create a team, belong to teams, and act within a team context.&lt;/li&gt;
&lt;li&gt;Data that was per-user and should now be shareable within a team is reachable by teammates; data that should stay personal stays personal. &lt;strong&gt;Deciding which is which is part of the task.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The rows that already exist keep working: an existing user signs in and loses nothing.&lt;/li&gt;
&lt;li&gt;Access control still holds afterwards — a member of team A can not reach team B's data through any path you added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How you model it — tenant column, join table, something else — is your call. Make the schema change as a real migration, wire the app code to it, and say at the end what you chose and what it costs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The source is a Next.js + Drizzle + Neon template of his, pinned at one sha. The seeded parent database holds 3 users, 3 profiles and 4 assets, deliberately tiny, so that he could read the data diff with his own eyes. Two checks, both dumb: the app still builds, and something with "team" in the name exists in the schema. The grading happens in the diffs, not in the checks.&lt;/p&gt;

&lt;p&gt;Ten runs, all sonnet. 10/10 done, 20/20 checks passed, 50 minutes end to end at three at a time. Between 77 and 117 turns each, 462k output tokens and 63 million cache reads across the ten, 143 files touched, and every single one of the ten branches released afterwards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;Effort, run by run: per-run turns, output tokens, cost, wall time and files touched&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/multirun-a-multi-agent-run-framework" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Per-run effort out of the trace data. Flip the measure and the ten bars stay put.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Where they agreed
&lt;/h3&gt;

&lt;p&gt;Every single run built the same thing: a &lt;code&gt;teams&lt;/code&gt; table, a &lt;code&gt;team_members&lt;/code&gt; join table, a &lt;code&gt;team_id&lt;/code&gt; column on &lt;code&gt;assets&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of them did tenant-column-only, and none did anything exotic. All ten decided that assets are the shareable thing and profiles stay personal, without ever being told which was which, and all ten kept every pre-existing row alive. Row-level security never came up once. Access control stayed in application code, ten times out of ten.&lt;/p&gt;

&lt;p&gt;He expected them to scatter. They converged hard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where they didn't
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does deleting a team delete its data?&lt;/strong&gt; Five runs said &lt;code&gt;SET NULL&lt;/code&gt;, so the assets survive and fall back to personal. Five said &lt;code&gt;CASCADE&lt;/code&gt;, so the assets die along with the team. That is a user-visible product decision that nobody asked the agent to make, and it came out exactly 5/5.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is the join table's primary key?&lt;/strong&gt; Five went composite &lt;code&gt;(team_id, user_id)&lt;/code&gt;. Five went surrogate &lt;code&gt;id&lt;/code&gt; with a unique constraint on the pair.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is the new column allowed to be NULL?&lt;/strong&gt; Eight runs made &lt;code&gt;assets.team_id&lt;/code&gt; nullable. NULL means personal, existing rows need no touching, and the migration stays pure DDL. Two runs refused the null entirely: &lt;code&gt;NOT NULL&lt;/code&gt;, a &lt;code&gt;personal&lt;/code&gt; flag on teams, and a data migration that mints a personal team for every existing user and moves their assets into it. Same sentence, two different philosophies of tenancy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is smaller scatter underneath all that, too. Six used a Postgres enum for the member role, three used text with a CHECK, one left it as bare text with a default. Six gave teams a unique slug, four didn't. Six added a current-team pointer to profiles, and then split between two different names for it.&lt;/p&gt;

&lt;p&gt;Ten runs is not enough to pin any of these ratios down. A 5/5 at n=10 is something a true 70/30 would produce often enough that you can't rule it out, so the thing to take from this is that there is a split there at all, on a decision the task file assumed had one answer, rather than the exact 50/50. Getting the real ratio means more runs, which the bench can do since it's just a config value, but he hasn't run them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;Where the ten runs landed: seven schema decisions across ten identical agent runs&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/multirun-a-multi-agent-run-framework" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;All seven decisions, sorted by how many of the ten agreed. This is the real comparison data out of the run diffs. Hover a segment for the run numbers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run 06's database is the only one of the ten with actual rows sitting in &lt;code&gt;teams&lt;/code&gt; and &lt;code&gt;team_members&lt;/code&gt;, one team and two members. No migration inserted those, which means that agent tested its own feature through the running app and then left the test data behind. The only reason anyone knows is that the data diff caught it. Nothing in the transcript would have sent him looking.&lt;/p&gt;

&lt;h3&gt;
  
  
  The failure
&lt;/h3&gt;

&lt;p&gt;It is the more useful half.&lt;/p&gt;

&lt;p&gt;His first attempt at this fan-out failed 10 out of 10, in under ten minutes. The runner image had no &lt;code&gt;openssh-client&lt;/code&gt;, so cloning the private template over SSH died in the init container, every time.&lt;/p&gt;

&lt;p&gt;The smoke test never caught it, because smoke clones a &lt;em&gt;public&lt;/em&gt; repository over HTTPS, a completely different code path from the one every real run takes. The fix was one line in a Dockerfile.&lt;/p&gt;

&lt;p&gt;Teardown held, though. All ten of those branches were released anyway, which is the only reason he could shrug at it and re-run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watching it happen
&lt;/h2&gt;




&lt;p&gt;Ten agents working in parallel inside pods you cannot see into is an unpleasant feeling. So the last piece is a read-only dashboard: instances, runs side by side, per-run detail with the phase timings, the check results, the branch diff, and the artifacts.&lt;/p&gt;

&lt;p&gt;The plan had been to change the runner for the live view, streaming each turn to a file on the shared PVC as it happens. Then he went to implement it and found the runner had been doing exactly that since day one. Every agent message, appended to &lt;code&gt;trace.jsonl&lt;/code&gt;, flushed per message, sitting on the PVC right next to the artifacts.&lt;/p&gt;

&lt;p&gt;So the dashboard tails that file over a websocket, which makes live and replay the same thing. A run that finished four hours ago plays back through the identical code path as one happening right now, and no pod logs are involved anywhere, so it still works long after the pod is gone.&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%2Fpb98mr055a60h8dfcxw8.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%2Fpb98mr055a60h8dfcxw8.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every fan-out this bench has run, including the one that failed 10/10.&lt;/em&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%2Fnnk65gefmvtkkhxg8rvw.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%2Fnnk65gefmvtkkhxg8rvw.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The ten runs of &lt;code&gt;add-teams&lt;/code&gt;. Notice how tight the effort is. Whatever the variance is, it is not effort variance.&lt;/em&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%2F6xsvw167uunqg80hwphk.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%2F6xsvw167uunqg80hwphk.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One run: phase timings, both checks, and what it did to the data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;About the cost readout. The SDK reports what each run &lt;em&gt;would&lt;/em&gt; have cost at API rates: $2.49 to $4.39 per run, $32.25 for all ten. None of it was billed, since this runs on his subscription. But it is the number he uses to think about what a fan-out is worth, and at roughly thirty dollars he can afford to ask what it would have done the other nine times.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;




&lt;p&gt;Variance is a property of the model and the prompt. That is what he is keeping from this.&lt;/p&gt;

&lt;p&gt;Ten identical runs did not produce ten random results. They produced total agreement on the schema, and two exact 5/5 splits on decisions that nobody asked them to make. Both halves of that are actionable. The agreement tells him the model has a strong prior about how tenancy gets modelled in Postgres, and he can stop reviewing that part quite so hard. The splits tell him precisely which sentences his task file was missing.&lt;/p&gt;

&lt;p&gt;And once you can measure a split, you can try to close it: write the prompt more precisely, run ten more, see whether 5/5 turns into 9/1. That is an eval. It took a loop and a config file, there is no judge model in it anywhere, and until this week he had no way to run one against his own prompts at all.&lt;/p&gt;

&lt;p&gt;The other half is the one he would actually go and use on Monday. Pick the next change you would genuinely regret getting wrong: the tenancy model, the auth boundary, the schema that will outlive the code sitting on top of it. Don't take the agent's first answer for it. Run it ten times and read the spread. Where all ten agree you can move quickly, because that is the model's prior and it is usually the dull correct thing. Where they split, there is a decision sitting there that was always yours to make, and the agent has been making it for you.&lt;/p&gt;

&lt;p&gt;He has been calling that results branching. It is the same mechanism as the eval, pointed at one decision instead of at the whole prompt.&lt;/p&gt;

&lt;p&gt;He keeps coming back to what he wrote in the branched environments post, that the pre-AI techniques are what make the agentic ones actually work. This is the same thing again, one layer up. Worktrees, database branching, GitOps, teardown as a state machine.. none of that is new, and none of it is about AI. It is all boring infrastructure practice, and it is the only reason ten agents can run at once without stepping on each other.&lt;/p&gt;

&lt;p&gt;He started this wanting to know whether he could trust the model. He still doesn't really have an answer to that. What he has instead is a list of the places where he hadn't decided something and it decided for him. That has turned out to be more useful.&lt;/p&gt;

&lt;p&gt;Thank you for reading :)&lt;/p&gt;







&lt;h3&gt;
  
  
  &lt;em&gt;&lt;strong&gt;After-post notes&lt;/strong&gt;&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;_&lt;strong&gt;N1:&lt;/strong&gt; The bench, the fan-out and the numbers in this post are all real. The config that produced them is in the repo, and you can run the same fan-out yourself.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N2:&lt;/strong&gt; If this post inspired you to think, feel free to &lt;a href="mailto:dennis@kubeden.io"&gt;email&lt;/a&gt; Denis and start a discussion! He loves talking technology, and even so if you are, too, actively living in "the arena"._&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can find everything about Denis on &lt;a href="https://denislavgavrilov.com/about" rel="noopener noreferrer"&gt;denislavgavrilov.com/about&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for reading and have a good one!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;.mr-signoff{display:flex;gap:.7rem;align-items:flex-start;margin:2.25rem 0 1rem}&lt;br&gt;
.mr-signoff&amp;gt;img{width:40px;height:40px;min-width:40px;border-radius:9999px;margin:.1rem 0 0;display:block}&lt;br&gt;
.mr-signoff-name{font-size:.8em;line-height:1.3;margin:0 0 .4rem}&lt;br&gt;
.mr-signoff-name span{color:var(--color-muted)}&lt;br&gt;
.dark .mr-signoff-name span{color:var(--color-muted-dark)}&lt;br&gt;
.mr-signoff-msg{display:inline-block;border:1px solid var(--color-rule);border-radius:.9rem;padding:.4rem .8rem;font-size:.95em;line-height:1.45}&lt;br&gt;
.dark .mr-signoff-msg{border-color:var(--color-rule-dark)}&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%2Fdenislavgavrilov.com%2Fassets%2Farticles%2Fmultirun-darling-avatar.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%2Fdenislavgavrilov.com%2Fassets%2Farticles%2Fmultirun-darling-avatar.png" alt="Darling" width="192" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Darling &lt;span&gt;· Denis's assistant&lt;/span&gt;&lt;br&gt;
we built this together&lt;/p&gt;




</description>
      <category>ai</category>
      <category>agents</category>
      <category>claude</category>
      <category>opensource</category>
    </item>
    <item>
      <title>diffium-db: A live diff of what agents do to your database</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Fri, 23 Oct 2026 07:12:55 +0000</pubDate>
      <link>https://dev.to/kubeden/diffium-db-a-live-diff-of-what-agents-do-to-your-database-5cgm</link>
      <guid>https://dev.to/kubeden/diffium-db-a-live-diff-of-what-agents-do-to-your-database-5cgm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The following post is a recap of my Claude Code sessions on building diffium-db. Although the content is AI-generated, I go through the entire post to see if any differences occur - no, everything is through my own process of developing diffium-db. If you'd like to discuss anything in relation to this project directly with me, you can do so on &lt;a href="mailto:dennis@a2w.io"&gt;dennis@a2w.io&lt;/a&gt; or on x.com/@kuberdenis.&lt;/p&gt;

&lt;p&gt;Note 2: this project is a direct continuation of another project of mine that I use to watch diffs - github.com/kubeden/diffium.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I let agents run migrations. That is how the work goes now. You describe what you want, something writes the SQL, you read it, you approve it, and a few seconds later the schema is different. The ten minutes after that are the problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git diff&lt;/code&gt; shows you the migration file. It does not show you what the migration did. And it shows you nothing at all about the row an agent updated on the way past, because updating it seemed reasonable at the time.&lt;/p&gt;

&lt;p&gt;There are good schema diff tools, but they mostly work the same way. You run them afterwards, against two databases, and they hand you a delta. Nothing sits open next to you while the work is happening.&lt;/p&gt;

&lt;p&gt;So I built one. &lt;strong&gt;diffium-db&lt;/strong&gt; is a terminal UI, and it is meant to stay open while you work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubeden/diffium-db" rel="noopener noreferrer"&gt;kubeden/diffium-db&lt;/a&gt;, the repo (MIT)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubeden/diffium" rel="noopener noreferrer"&gt;kubeden/diffium&lt;/a&gt;, the file-diff TUI this one is modelled on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anomalyco/opentui" rel="noopener noreferrer"&gt;OpenTUI&lt;/a&gt;, the terminal framework it is built with&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  What it does
&lt;/h1&gt;




&lt;p&gt;You point it at a Postgres, take a baseline, and leave the watcher open. From that moment everything that changed is listed on the left, the change itself is on the right, and it re-reads the database once a second.&lt;/p&gt;

&lt;p&gt;Here it is catching a migration. Two columns and an index appeared on &lt;code&gt;demo.users&lt;/code&gt; while I was looking at it:&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%2Fyl0snqfn7bmtnz2b3k38.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%2Fyl0snqfn7bmtnz2b3k38.png" width="799" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The left pane is everything that changed since the baseline. The right pane is the object you have selected, before and after.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It watches tables (columns, defaults, identity, constraints, indexes, triggers), views, materialized views, enums and functions. It also watches &lt;strong&gt;rows&lt;/strong&gt;. That turned out to be the interesting half, and I will come back to it.&lt;/p&gt;

&lt;p&gt;There are four commands. &lt;code&gt;watch&lt;/code&gt; opens the TUI, &lt;code&gt;snapshot&lt;/code&gt; takes a baseline, &lt;code&gt;baselines&lt;/code&gt; lists the ones you have, and &lt;code&gt;diff&lt;/code&gt; prints the changes and exits. That last one takes &lt;code&gt;--exit-code&lt;/code&gt;, so it returns 1 when anything changed, which is enough to fail a CI job or stop an agent's loop before it does the next thing.&lt;/p&gt;




&lt;h1&gt;
  
  
  Setting it up
&lt;/h1&gt;




&lt;p&gt;You need &lt;a href="https://bun.sh" rel="noopener noreferrer"&gt;Bun&lt;/a&gt; 1.3 or newer and a Postgres to watch. There is exactly one dependency past that, &lt;code&gt;@opentui/core&lt;/code&gt;, so the install is quick.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/kubeden/diffium-db &amp;amp;&amp;amp; cd diffium-db
bun install
export DATABASE_URL='postgresql://...'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then take a baseline and open the watcher:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bun run src/index.ts snapshot
bun run src/index.ts watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That is the whole setup. The repo ships a demo so you can see it move without waiting for an agent to do something. &lt;code&gt;examples/demo/01-baseline.sql&lt;/code&gt; builds a small &lt;code&gt;demo&lt;/code&gt; schema with users and projects in it, and &lt;code&gt;examples/demo/02-agent-change.sql&lt;/code&gt; is what an agent does when you tell it "add billing plans and clean up".&lt;/p&gt;

&lt;p&gt;Two terminals. In the first:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psql "$DATABASE_URL" -f examples/demo/01-baseline.sql
bun run src/index.ts snapshot --schema demo
bun run src/index.ts watch --schema demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In the second, while the watcher is open:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psql "$DATABASE_URL" -f examples/demo/02-agent-change.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Five changes land on the screen: a new enum, two new columns and a new index on &lt;code&gt;demo.users&lt;/code&gt;, an index gone from &lt;code&gt;demo.projects&lt;/code&gt;, one row inserted and one row deleted. &lt;code&gt;j&lt;/code&gt; and &lt;code&gt;k&lt;/code&gt; walk them, &lt;code&gt;s&lt;/code&gt; switches between side by side and inline, &lt;code&gt;e&lt;/code&gt; writes the whole diff to a file.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where to point it
&lt;/h1&gt;




&lt;p&gt;The demo is a demo. For real work, do not point it at production. The point is to let the agent run and read what it did afterwards, and you want that happening somewhere you can throw away.&lt;/p&gt;

&lt;p&gt;So I give the agent a branch. On Neon that is two commands, plus the project id, because I have more than one project and the CLI refuses to guess:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;neonctl branches create --project-id &amp;lt;project&amp;gt; --name agent-run
neonctl connection-string agent-run --project-id &amp;lt;project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Point &lt;code&gt;DATABASE_URL&lt;/code&gt; at what the second one prints, take your baseline there, and let the agent work on the branch. The branch is copy-on-write off its parent, so it comes up holding the same schema and the same rows as the database you care about. The one I made while writing this took 1.2 seconds. An idle branch suspends its own compute too, so the ones you forget to delete are not sitting there running.&lt;/p&gt;

&lt;p&gt;Then you read the diff. If the migration is what you wanted, run it against the real database. If the agent did something you did not expect, you found that out on a copy, and the copy gets deleted.&lt;/p&gt;

&lt;p&gt;That is the loop, and it is how this post was made. The demo and every screenshot in it ran on a branch of the demo project. The demo database itself still holds the state I left it in.&lt;/p&gt;

&lt;p&gt;Any Postgres works and nothing in the tool is Neon-specific. Branching is what makes this cheap enough to do every time instead of only when you remember.&lt;/p&gt;




&lt;h1&gt;
  
  
  What a row diff can prove
&lt;/h1&gt;




&lt;p&gt;Structure is the easy half. Rows are where it gets uncomfortable.&lt;/p&gt;

&lt;p&gt;To tell you that a row changed, you have to remember what it was. Storing every row is not an option for anything real, so diffium-db stores a fingerprint instead: the primary key, plus &lt;code&gt;md5(t::text)&lt;/code&gt; of the whole record, plus a short preview. Cheap to compute in the database, cheap to keep, and it separates an insert from an update from a delete exactly. If the key is new, it is an insert. If the key is gone, it is a delete. If the key is the same and the hash moved, someone edited that row.&lt;/p&gt;

&lt;p&gt;That works right up until the table gains a column. Then every hash in the table changes at once, and a naive tool would tell you that every row in it was edited. Nobody edited them. The shape moved underneath them.&lt;/p&gt;

&lt;p&gt;diffium-db does not do that.&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%2Fxrxhezotqpjqoy9a6bzr.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%2Fxrxhezotqpjqoy9a6bzr.png" width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;~3?&lt;/code&gt; in the left pane, and the note at the top of the diff. It detected the column change, marked the table, and told me which half of its own answer to trust.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The count is written &lt;code&gt;~3?&lt;/code&gt; and not &lt;code&gt;~3&lt;/code&gt;, and the first line of the diff says why. Two columns arrived, so every row in the table reads as edited, and the update number is the one part of that answer the tool cannot stand behind.&lt;/p&gt;

&lt;p&gt;Inserts and deletes are still exact, because those come from the primary key and the key does not care what the shape is. Only the update count is guesswork, and printing a guess plain, next to numbers it can prove, would make the whole screen worth less.&lt;/p&gt;

&lt;p&gt;Tables without a primary key, and tables over &lt;code&gt;--row-limit&lt;/code&gt; (5000 by default), get a row count and no claims at all. Same principle. I would rather it say less.&lt;/p&gt;




&lt;h1&gt;
  
  
  Structure is text
&lt;/h1&gt;




&lt;p&gt;The structural diff rests on one decision, and it explains a lot of the behaviour.&lt;/p&gt;

&lt;p&gt;Every object renders to one canonical block of text with a deterministic line order. A table becomes its columns, then its constraints, then its indexes, then its triggers, always in that order. The structural diff is then just a line diff of those blocks.&lt;/p&gt;

&lt;p&gt;That one choice pays for a lot. Adding a column is one added line instead of a table that vaguely "changed", and side by side, the &lt;code&gt;+2 -0&lt;/code&gt; counts and horizontal scrolling through a long default all come free.&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%2F6hdymi0xr125piavx59b.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%2F6hdymi0xr125piavx59b.png" width="799" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same change inline instead of side by side, so nothing is cut off at the pane edge. &lt;code&gt;s&lt;/code&gt; toggles it, &lt;code&gt;w&lt;/code&gt; toggles wrapping, and both are remembered between runs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a cost. diffium-db tells you what the database looks like now, not the DDL statement that would get you there. For a watcher that is the right side of the trade. If you want generated migrations, you want a different program.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where the baseline lives
&lt;/h1&gt;




&lt;p&gt;A baseline is a stored value, not a live connection, so you can take one now and diff against it tomorrow, keep several named ones, or commit one next to the migration that produced it.&lt;/p&gt;

&lt;p&gt;By default they go in &lt;code&gt;.diffium-db/snapshots/&lt;/code&gt; as JSON. Pass &lt;code&gt;--store neon --store-url &amp;lt;url&amp;gt;&lt;/code&gt; and they go into a &lt;code&gt;diffium_db&lt;/code&gt; schema in Postgres instead. Use that when CI and your laptop need to agree on what "before" means. The watcher never looks at its own schema, so the store URL and the watched URL can be the same database.&lt;/p&gt;

&lt;p&gt;With the branch above, that means the baseline can sit on the branch itself, one schema over from the thing it is describing. The branch is then the entire experiment, and deleting it takes the record with it.&lt;/p&gt;




&lt;h1&gt;
  
  
  Future work
&lt;/h1&gt;




&lt;p&gt;What v1 does not do, roughly in the order I want to fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No ORM awareness.&lt;/strong&gt; v1 tells you a column appeared. It cannot tell you which migration file or which model definition put it there. That mapping is an ORM's business and it is the next thing I want. &lt;code&gt;src/orms/&lt;/code&gt; is where it goes, documented and deliberately empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres only.&lt;/strong&gt; The catalog reading is isolated in &lt;code&gt;src/pg/&lt;/code&gt;, and nothing above that boundary knows what a catalog is, so a second dialect has an obvious place to go. It does not exist yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No sequences.&lt;/strong&gt; Every &lt;code&gt;bigserial&lt;/code&gt; makes one and they move on every insert. Pure noise until someone asks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling, not replication.&lt;/strong&gt; It re-reads once a second. On a large database logical replication would be much cheaper, and that is the obvious thing to do if polling ever starts to hurt.&lt;/p&gt;

&lt;p&gt;One thing is not on that list and never will be. diffium-db does not write to the database it is watching. The only thing it writes is its own store, and only if you point it at one.&lt;/p&gt;




&lt;h1&gt;
  
  
  Closing
&lt;/h1&gt;




&lt;p&gt;It is about 2,200 lines of source and 900 of tests. 64 of those tests run with no database at all, and that includes the terminal ones, because OpenTUI will render into memory and let you assert on the screen it drew. So a change to the layout breaks a test instead of quietly looking wrong in a screenshot I forgot to retake.&lt;/p&gt;

&lt;p&gt;What I keep coming back to is the row diff. It would have been easier to print a number for every table and let people assume it meant something. The version that says "I cannot prove this one" is a bit uglier to look at. I will take it. A thing that sits next to an agent and reports on what it did has to be more careful than the agent is.&lt;/p&gt;

&lt;p&gt;It is on GitHub under MIT. If you try it and it tells you something wrong, open an issue.&lt;/p&gt;

&lt;p&gt;Thank you for reading.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Databricks for AI Engineering</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:00:06 +0000</pubDate>
      <link>https://dev.to/kubeden/databricks-for-ai-engineering-4h5a</link>
      <guid>https://dev.to/kubeden/databricks-for-ai-engineering-4h5a</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Lakebase is Neon's Postgres inside Databricks, so an agent gets its own branch of production in seconds. Governed tables sync into it and read back in under 10ms, and every write the agent makes to a change-feed table lands in a Delta table in Unity Catalog about 15 seconds later. Branches are one-way and nothing merges back. You rehearse on the branch, decide from the rows it changed, and replay the approved change as a migration. I ran the branching half on Neon myself: ten agents, ten branches, all ten deleted.&lt;/p&gt;




&lt;p&gt;Almost every agent running inside a company today is read-only. It searches, it summarises, it answers questions about data somebody else already wrote down, and it changes nothing.&lt;/p&gt;

&lt;p&gt;Nobody sat in a meeting and decided this. It is what you get when the first serious question about a pilot is "what happens when it's wrong", and the honest answer is that nobody knows yet. So the write path gets taken away.&lt;/p&gt;

&lt;p&gt;Databricks has an agent framework, and so does everybody. What caught my attention this year sits lower down: the database underneath the agent picked up two properties that used to be your job to build. You can hand an agent a copy of production for the length of a task, and the rows it touches land in a governed table without the agent knowing anything about it.&lt;/p&gt;

&lt;p&gt;I want to walk through what that is, where it stops, and what it means for the write path.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a database branch?
&lt;/h2&gt;




&lt;p&gt;I have written about this twice before, in &lt;a href="https://denislavgavrilov.com/preview-environments" rel="noopener noreferrer"&gt;Preview Environments&lt;/a&gt; and &lt;a href="https://denislavgavrilov.com/agentic-workflows-branched-environments" rel="noopener noreferrer"&gt;Agentic Workflows: Branched Environments&lt;/a&gt;, so I will keep it short.&lt;/p&gt;

&lt;p&gt;A branch is a copy-on-write clone of a database. The storage engine keeps every change as a new version instead of overwriting the old one, so a new branch copies no data at all. It points at the same versions the parent points at, and starts recording its own changes from that moment.&lt;/p&gt;

&lt;p&gt;Two things follow.&lt;/p&gt;

&lt;p&gt;Branch creation takes seconds, and stays at seconds no matter how big the database is. A branch of a 2TB database is not slower than a branch of a 10GB one. There is nothing to copy.&lt;/p&gt;

&lt;p&gt;And a branch costs storage proportional to what it changes. A branch that modifies 50MB inside a 500GB database costs you roughly 50MB, not 500GB. So "give every test run its own database with production-shaped data in it" stops being a budget conversation.&lt;/p&gt;

&lt;p&gt;If you have ever maintained a shared staging database you know what this is for. It is the database where somebody else's half-finished migration is why your test failed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Databricks shipped
&lt;/h2&gt;




&lt;p&gt;Databricks bought Neon in May 2025 for about a billion dollars. Neon was a serverless Postgres company whose whole architecture was the branching I just described. Ali Ghodsi said at the time that roughly 80% of the databases being created on Neon were created by agents, not by people.&lt;/p&gt;

&lt;p&gt;That technology is now Lakebase. It went generally available on AWS on January 22, 2026, with the announcement blog in February, and on Azure in March. Postgres 17, pgvector in the box, 16TB of storage per branch (8TB at GA), scale-to-zero that comes back in hundreds of milliseconds on the next query, branching and point-in-time recovery.&lt;/p&gt;

&lt;p&gt;Two integrations are the part worth your attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synced tables&lt;/strong&gt; pull a Unity Catalog table into Postgres so an application can read governed lakehouse data with sub-10ms latency and full ACID transactions. Three modes: snapshot for a one-time copy, triggered for scheduled refreshes, continuous for streaming with a 15-second floor on the interval. They are read-only by strong recommendation, only additive schema changes survive a triggered or continuous sync, and each synced table holds up to 16 connections against an instance limit of 1,000. Plan capacity around those last two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change Data Feed&lt;/strong&gt; goes the other direction, and it is the one I did not expect. You turn it on per table, and from then on every insert, update and delete on that table is read off the Postgres write-ahead log and written as a row into a Unity Catalog managed Delta table, batched and flushed about every 15 seconds. The destination is named &lt;code&gt;lb_&amp;lt;table_name&amp;gt;_history&lt;/code&gt; and carries five columns you did not write: &lt;code&gt;_pg_change_type&lt;/code&gt;, &lt;code&gt;_pg_lsn&lt;/code&gt;, &lt;code&gt;_pg_xid&lt;/code&gt;, &lt;code&gt;_timestamp&lt;/code&gt; and &lt;code&gt;_sort_by&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It sits behind a preview flag, it needs &lt;code&gt;REPLICA IDENTITY FULL&lt;/code&gt; on the source table, it does not handle partitioned tables, and it records rows and not DDL. Read the limits before you plan around it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;Figure 1. The round trip: a governed table into Postgres, every agent write back out (interactive)&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/databricks-for-ai-engineering" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The two integrations, end to end. Select either arrow for its limits.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it wrote becomes a table
&lt;/h2&gt;




&lt;p&gt;The values of &lt;code&gt;_pg_change_type&lt;/code&gt; are &lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;update_preimage&lt;/code&gt; and &lt;code&gt;update_postimage&lt;/code&gt;. So for every update your agent makes you get the row as it was and the row as it became, two separate rows in a Delta table, ordered by transaction ID and log sequence number.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;Figure 2. What one write leaves behind: the rows an insert, update or delete lands in the change feed (interactive)&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/databricks-for-ai-engineering" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Rows written to the change feed per statement type. Select a statement.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Think about what that replaces. If you want to know what an agent did to your data today, you have three options. You can read the agent's own trace, and trust a language model's account of its own behaviour. You can hand-roll audit tables and triggers, at the price of every schema change becoming two schema changes. Or you can turn on full statement logging and go spelunking in a text file at 2am.&lt;/p&gt;

&lt;p&gt;The Change Data Feed version is a SQL query. Every write, before and after, in a governed table sitting in the same catalog as everything else your company governs, under the same permissions.&lt;/p&gt;

&lt;p&gt;It is plumbing, so nobody demos it. In a regulated company "we cannot see what it did" is the objection that kills the project, and a change feed answers the data half of it in SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  You cannot merge the branch back
&lt;/h2&gt;




&lt;p&gt;Once you have seen branch-per-agent-run, the next thought is obvious. Let the agent work on its branch, review the result, merge the good ones. Git for your database. I assumed it existed and went looking for the API.&lt;/p&gt;

&lt;p&gt;It does not exist. From the Databricks community, the accepted answer to exactly this question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Currently, Lakebase does not have a built-in "merge" or "promote" operation that pushes child branch changes up to the parent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Neon's own position, from before the acquisition, is that this is deliberate. They do not support automatic merging of diverged branches because a database merge is unsafe and ambiguous. Two branches both insert a row with id 4… Git calls that a conflict and makes a human read it. A database cannot reliably tell you it happened.&lt;/p&gt;

&lt;p&gt;So the recommended path is the boring one. You capture what you did as migration scripts and apply the same scripts to the parent with Flyway or Liquibase or Alembic. Schema, not data. Migration tools are built for DDL, and getting an approved &lt;em&gt;data&lt;/em&gt; change back to the parent is a separate problem you solve through the application or a script written for that one job.&lt;/p&gt;

&lt;p&gt;Databricks does document a schema diff now. Open a child branch, click Schema diff in the Parent branch section, and you get a side-by-side DDL comparison against the parent. It compares schemas, it does not merge them, and you still write the migration yourself.&lt;/p&gt;

&lt;p&gt;Which means a branch is not a feature branch. Nothing you accumulate inside one gets folded back in, and the only thing that comes out of it is a decision about what to do next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ten agents, ten branches, all thrown away
&lt;/h2&gt;




&lt;p&gt;Let me make that concrete, because I have run it, on Neon and not on Lakebase.&lt;/p&gt;

&lt;p&gt;I built a small framework called &lt;a href="https://denislavgavrilov.com/multirun-a-multi-agent-run-framework" rel="noopener noreferrer"&gt;multirun&lt;/a&gt; that takes one task and runs it N times in parallel. Each run gets its own git worktree and its own database branch off a shared seeded parent. Same prompt, same starting schema, ten agents who cannot see each other.&lt;/p&gt;

&lt;p&gt;From the run I wrote up: ten agents, 50 minutes and 10 seconds of wall clock, 930 turns between them, 143 files touched. A branch off the ~30MB parent was ready in 1.38 seconds.&lt;/p&gt;

&lt;p&gt;All ten designed the same thing at the top level, a teams table with a join table and a foreign key on assets. Then they split. Five chose &lt;code&gt;SET NULL&lt;/code&gt; when a team is deleted and five chose &lt;code&gt;CASCADE&lt;/code&gt;. Five used a composite primary key on the membership table and five used a surrogate. Eight left the team reference nullable and two made it required with a backfill.&lt;/p&gt;

&lt;p&gt;Ten runs cannot pin a ratio. What they establish is that the split exists at all, on a question the task file assumed had one answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;Figure 3. Ten branches, nothing merged: where ten identical agent runs landed on three schema questions (interactive)&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/databricks-for-ai-engineering" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Figure 3. Where ten identical runs landed on three schema questions. n = 10 per question.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The product of the whole exercise was the diff. I compared each branch against the parent to see what the agent had done to the data, wrote up the comparison, and deleted all ten branches. Not one was merged anywhere. It never occurred to me to try, because what I wanted out of it was a decision about &lt;code&gt;SET NULL&lt;/code&gt; versus &lt;code&gt;CASCADE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I only noticed I had been working that way after I went looking for the merge API and found there wasn't one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I think this changes
&lt;/h2&gt;




&lt;p&gt;If you are trying to get an agent past read-only inside a company, three steps fall out of all this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rehearse.&lt;/strong&gt; The agent gets a branch, provisioned by API for the length of the task, with real production-shaped data in it. It writes whatever it wants, and nothing it writes through that connection reaches the parent database. Everything else it can touch, an API call, an email, a file in object storage, is still yours to sandbox separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide.&lt;/strong&gt; You diff the branch against the parent, or you query the change feed. Either way you are looking at rows, not at the agent's summary of itself. A human approves, or a deterministic check approves, or both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay.&lt;/strong&gt; The approved change gets applied to production as its own deliberate act, by migration or through the normal application write path with the usual permissions on it. Then the branch goes away.&lt;/p&gt;

&lt;p&gt;None of that makes an agent safe by itself. A branch isolates database state and nothing else, so the credential you hand it still has to be the smallest one that will do the job. And the change feed is a governed record of which rows moved, not a full account of what the agent did. It will not tell you who approved it, what the agent read, or which prompt set it off.&lt;/p&gt;

&lt;p&gt;What you do get out of the box is the place to rehearse and the row-level record of the rehearsal, and those are the two things I expected to have to build myself. The rest is ordinary engineering with no model in it anywhere.&lt;/p&gt;

&lt;p&gt;I have not run this on Lakebase. I have run the branching half, on Neon, on my own hardware, for about thirty-two dollars of list-price tokens, and it worked well enough that I stopped thinking of database branches as a testing trick. The rest is a workspace I have not spun up.&lt;/p&gt;

&lt;p&gt;So if your agent is stuck at read-only, go and look at where it would write, and at how you would show somebody afterwards what it wrote.&lt;/p&gt;

&lt;p&gt;Thank you for reading.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After-post notes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;N1: multirun is open source at &lt;a href="https://github.com/a2wio/multirun" rel="noopener noreferrer"&gt;github.com/a2wio/multirun&lt;/a&gt;, and the write-up linked above has the full numbers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>databricks</category>
      <category>postgres</category>
      <category>neon</category>
    </item>
    <item>
      <title>Experiments in Software Factories #1: Production app in 40 minutes</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:00:04 +0000</pubDate>
      <link>https://dev.to/kubeden/experiments-in-software-factories-1-production-app-in-40-minutes-1dp</link>
      <guid>https://dev.to/kubeden/experiments-in-software-factories-1-production-app-in-40-minutes-1dp</guid>
      <description>&lt;p&gt;&lt;a href="https://pastmonday.com" rel="noopener noreferrer"&gt;pastmonday.com&lt;/a&gt; is live. It has auth, a weekly calendar, tiered billing with Stripe, referral credits, file uploads, and an AI chat that knows your calendar. A small but complete SaaS, the kind of thing that used to be a three-month project.&lt;/p&gt;

&lt;p&gt;Two things about it are unusual, and they're the reason for this post.&lt;/p&gt;

&lt;p&gt;First: I didn't hand-write it. A &lt;strong&gt;software factory&lt;/strong&gt; did.. four reusable Claude Code skills that turn a one-page brief into a shipped product, stage by stage. I talked about this on &lt;a href="https://www.youtube.com/watch?v=l9kY3hoj9AQ" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; recently, and the skills themselves are &lt;a href="https://github.com/kubeden/kubeden/tree/main/ai/skills/claude-code/web-development" rel="noopener noreferrer"&gt;public on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Second: look at the vendor list. Postgres is Neon. Auth is Neon. The data API is Neon. Object storage is Neon. The LLM calls go through Neon. Stripe handles money and my own cluster serves the containers, and that's the entire list. &lt;strong&gt;The backend is one vendor&lt;/strong&gt;, and until a few weeks ago that sentence was impossible to say, because half of these services didn't exist.&lt;/p&gt;

&lt;p&gt;Neon &lt;a href="https://neon.com/blog/were-building-backends" rel="noopener noreferrer"&gt;announced&lt;/a&gt; they're building the full backend: Postgres, Auth, and the Data API are GA, and Object Storage, Functions, and an AI Gateway just entered private preview. I got early access, pointed the factory at all of it, and this post is the build report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; this post is a transcription of &lt;a href="https://www.youtube.com/watch?v=l9kY3hoj9AQ" rel="noopener noreferrer"&gt;my video on software factories&lt;/a&gt;, passed through AI and edited for reading. If you'd rather watch than read, here it is:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=l9kY3hoj9AQ" rel="noopener noreferrer"&gt;▶ Watch: Software Factories are SCARY but possible (and easy to do)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;software factory&lt;/strong&gt;, four reusable Claude Code skills reading one &lt;code&gt;PROJECT.md&lt;/code&gt; brief, shipped &lt;a href="https://pastmonday.com" rel="noopener noreferrer"&gt;pastmonday.com&lt;/a&gt; from empty repo to production in about &lt;strong&gt;40 minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;entire backend is Neon&lt;/strong&gt;: Postgres, Auth, and Data API (GA), plus Object Storage and AI Gateway (private preview, early access).&lt;/li&gt;
&lt;li&gt;Money and limits are &lt;strong&gt;server-authoritative&lt;/strong&gt;: atomic single-statement quota consumes, idempotent Stripe webhooks, AI budgets checked before every call.&lt;/li&gt;
&lt;li&gt;The AI Gateway's official SDK provider wasn't installable yet, so the agent verified the OpenAI-compatible endpoint live and shipped anyway.. the API-first thesis from &lt;a href="https://denislavgavrilov.com/neon-api-first-agentic-coding" rel="noopener noreferrer"&gt;my last Neon post&lt;/a&gt;, proven again.&lt;/li&gt;
&lt;li&gt;Neon now ships &lt;strong&gt;agent skills&lt;/strong&gt; for their new services. The factory and the platform are converging.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=l9kY3hoj9AQ" rel="noopener noreferrer"&gt;The video: Software Factories are SCARY but possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kubeden/kubeden/tree/main/ai/skills/claude-code/web-development" rel="noopener noreferrer"&gt;The factory skills on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pastmonday.com" rel="noopener noreferrer"&gt;pastmonday.com, the shipped product&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.com/blog/were-building-backends" rel="noopener noreferrer"&gt;Neon: we're building backends&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://denislavgavrilov.com/how-to-build-any-web-application" rel="noopener noreferrer"&gt;My series: How To Build Any Web Application&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  The software factory
&lt;/h1&gt;




&lt;p&gt;The core idea is one separation: &lt;strong&gt;the skill is the HOW, the brief is the WHAT.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything project-specific lives in a single &lt;code&gt;PROJECT.md&lt;/code&gt; in the repo: name, one-liner, the plans table, the palette and fonts, the entities, which services are enabled. Everything reusable lives in the skills. When I want a different product, I write a different brief. The factory doesn't change.&lt;/p&gt;

&lt;p&gt;There are four stages, each a separate skill, each reading that same brief:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Foundation&lt;/strong&gt; (&lt;code&gt;set-project-structure&lt;/code&gt;) builds the skeleton: Next.js with the directory contracts, the Drizzle schema, env keys with comments on where to get each value, design tokens, and placeholder clients for every enabled service. It deliberately builds &lt;em&gt;less&lt;/em&gt;: seams, not features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landing&lt;/strong&gt; (&lt;code&gt;set-landing-page&lt;/code&gt;) is the taste stage. It studies the brief's reference sites, writes real copy before layout, and works against an explicit banned-patterns list: no gradient hero, no "Supercharge your workflow", no fake testimonials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt; (&lt;code&gt;set-app-backend&lt;/code&gt;) is the correctness stage. Money and limits. Every quota, credit, token budget, and dollar is enforced on the server, atomically, idempotently. More on this below, because this is where Neon's new services plug in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; (&lt;code&gt;set-dashboard&lt;/code&gt;) builds the app people live in: the calendar, the modals, the streaming AI chat, consuming only the contracts stage 3 exposed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each stage stops at an approval gate before building, verifies current package versions instead of trusting its training data, and must leave the repo shippable when it ends. No half-built sections, no "we'll polish later".&lt;/p&gt;

&lt;p&gt;Click through the stages here.. this is the actual pipeline that built pastmonday:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;The software factory pipeline (interactive)&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/software-factory-neon-entire-backend" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've written the first three stages up in detail before, if you want the deep dives: &lt;a href="https://denislavgavrilov.com/how-to-build-any-web-application" rel="noopener noreferrer"&gt;the concept&lt;/a&gt;, &lt;a href="https://denislavgavrilov.com/how-to-build-any-web-application-fff" rel="noopener noreferrer"&gt;the brief&lt;/a&gt;, and &lt;a href="https://denislavgavrilov.com/how-to-build-any-web-application-2bb" rel="noopener noreferrer"&gt;the landing page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What matters for this post: a factory like this is only as good as the surfaces it builds on. Stage 3 is where the factory has to wire auth, storage, data access, and LLM calls, and that's usually where you accumulate vendors. One for identity, one for buckets, one for model routing, plus the database. Four dashboards, four bills, four sets of credentials to leak.&lt;/p&gt;

&lt;p&gt;This time, all four seams landed on the same platform.&lt;/p&gt;




&lt;h1&gt;
  
  
  One backend
&lt;/h1&gt;




&lt;p&gt;Here's what pastmonday actually runs on, and the state of each piece:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Role in pastmonday&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Postgres&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;entries, profiles, billing, referrals, asset metadata&lt;/td&gt;
&lt;td&gt;GA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Neon Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;signup, login, sessions, the identity table&lt;/td&gt;
&lt;td&gt;GA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgREST-style HTTP reads&lt;/td&gt;
&lt;td&gt;GA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Object Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;user file uploads, S3-compatible&lt;/td&gt;
&lt;td&gt;private preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;all LLM calls, metered per request&lt;/td&gt;
&lt;td&gt;private preview&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few receipts from the codebase, because "it works" is cheap to say.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth&lt;/strong&gt; ships its own schema. Identity lives in &lt;code&gt;neon_auth.user&lt;/code&gt;, and the factory's rule is: never mirror it. App tables hold foreign keys straight into it, so there's no sync job, no webhook that copies users around, no second source of truth:&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;// Neon Auth's identity schema, referenced, never mirrored&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neonAuth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;neon_auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;neonAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// profiles FK straight into it&lt;/span&gt;
&lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;authUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cascade&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;&lt;strong&gt;Storage&lt;/strong&gt; speaks S3, so there's no proprietary SDK to learn. The client is the plain AWS SDK, and it auto-reads the endpoint from &lt;code&gt;AWS_ENDPOINT_URL_S3&lt;/code&gt;. Uploads are presigned PUTs with keys scoped per user, and since a presigned PUT can't enforce a size limit, the confirm route does a &lt;code&gt;HeadObject&lt;/code&gt; and deletes anything oversized. The interesting part: &lt;a href="https://neon.com/docs/changelog" rel="noopener noreferrer"&gt;Neon's storage branches with your database&lt;/a&gt;. Every branch gets its own isolated storage state. Your files and your rows stop drifting apart across environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Gateway&lt;/strong&gt; is one API for models from Anthropic, OpenAI, Google and others, with per-request logging and rate limits, running on the same gateway infrastructure Databricks says handles &lt;a href="https://neon.com/blog/were-building-backends" rel="noopener noreferrer"&gt;125 trillion tokens a month&lt;/a&gt;. For the app it means one token in the environment instead of a drawer full of provider keys, and the model is a config value:&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;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createOpenAICompatible&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="s2"&gt;neon-ai-gateway&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;NEON_AI_GATEWAY_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ai-gateway/mlflow/v1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEON_AI_GATEWAY_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEON_AI_GATEWAY_MODEL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Poke around the real architecture here, every box is clickable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;pastmonday's backend, all Neon (interactive)&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/software-factory-neon-entire-backend" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Building on features that shipped last week
&lt;/h1&gt;




&lt;p&gt;In &lt;a href="https://denislavgavrilov.com/neon-api-first-agentic-coding" rel="noopener noreferrer"&gt;the last Neon post&lt;/a&gt; I made one argument: Neon is API-first, so an agent can reach a primitive the moment it exists, before anyone builds it a button. I proved it by building a feature Neon hadn't shipped yet.&lt;/p&gt;

&lt;p&gt;This build pushed that idea further than I expected, twice.&lt;/p&gt;

&lt;p&gt;First, the AI Gateway is so new that its official AI SDK provider package &lt;strong&gt;wasn't even installable yet&lt;/strong&gt; when the factory ran stage 3. A human would have stopped there. The agent didn't: it probed the gateway, found the OpenAI-compatible endpoint at &lt;code&gt;/ai-gateway/mlflow/v1&lt;/code&gt;, verified it live with a real request, and wired the app through the AI SDK's generic OpenAI-compatible provider instead. The code carries a comment saying "swap to the official provider once it installs". That's what API-first buys you: the endpoint existed, so the missing package didn't matter.&lt;/p&gt;

&lt;p&gt;Second, the convergence I didn't see coming: a week after I published my factory skills, Neon shipped &lt;a href="https://neon.com/docs/changelog" rel="noopener noreferrer"&gt;agent skills of their own&lt;/a&gt;.. instruction files that teach coding agents how to integrate Functions, Storage, and the AI Gateway, installable with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add neondatabase/agent-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that again from where I'm standing. I write skills that teach an agent how to &lt;em&gt;build products&lt;/em&gt;. Neon now writes skills that teach the same agent how to &lt;em&gt;consume their platform&lt;/em&gt;. The factory and the platform are meeting in the middle, and neither side needed the other's permission. This is what it looks like when a vendor treats agents as the primary developer.&lt;/p&gt;




&lt;h1&gt;
  
  
  The part that has to be exactly right
&lt;/h1&gt;




&lt;p&gt;A factory that ships beautiful broken apps is a liability, so stage 3 is paranoid by design. pastmonday charges money and sells limits: Free gets 1 entry and a 5,000-token AI budget, Pro gets 10 entries and 20,000 tokens a cycle, Max is unlimited. Limits only mean something if they can't be raced.&lt;/p&gt;

&lt;p&gt;One constraint made this sharper than usual: the factory wired Drizzle through Neon's HTTP driver, which doesn't do interactive transactions. No &lt;code&gt;BEGIN&lt;/code&gt;, hold the lock, decide, &lt;code&gt;COMMIT&lt;/code&gt;. So every money-critical operation had to become a &lt;strong&gt;single atomic statement&lt;/strong&gt;. This is the actual entry creation, quota check and insert in one round trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;billing&lt;/span&gt;
     &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;used_this_cycle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;used_this_cycle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;monthly_quota&lt;/span&gt;
                                &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;used_this_cycle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;used_this_cycle&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;extra_credits&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;used_this_cycle&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;monthly_quota&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;extra_credits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                                &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;extra_credits&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;extra_credits&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;profile_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;used_this_cycle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;monthly_quota&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;extra_credits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;calendar_entries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;consumed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quota first, then credits, else nothing gets inserted and the caller gets a &lt;code&gt;402&lt;/code&gt;. Two concurrent requests can't both take the last unit, not because the code is careful, but because the statement can't be split.&lt;/p&gt;

&lt;p&gt;The same paranoia runs through the rest: the AI budget is checked server-side &lt;em&gt;before&lt;/em&gt; every gateway call and metered from real usage on finish, Stripe webhooks dedupe on event id in a &lt;code&gt;processed_stripe_events&lt;/code&gt; ledger so a replayed event can't grant credits twice, and quota-exhausted returns a typed &lt;code&gt;402&lt;/code&gt; the UI turns into an upgrade prompt instead of an error toast.&lt;/p&gt;

&lt;p&gt;None of this is Neon-specific, but it's the standard the factory holds, and the new services slotted into it without a fight. That's the actual test of a backend platform: not the demo, the enforcement paths.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where this goes: branches that fork the whole stack
&lt;/h1&gt;




&lt;p&gt;My favorite Neon primitive has always been branching. I've built &lt;a href="https://denislavgavrilov.com/agentic-workflows-branched-environments" rel="noopener noreferrer"&gt;preview environments&lt;/a&gt; where every pull request gets its own database branch, created and destroyed by the agent through the API.&lt;/p&gt;

&lt;p&gt;The new services extend that idea past the database. Storage state branches with your data today, and Neon manages Functions through the same CLI and API as everything else. The direction is obvious and they're saying it out loud: a branch should fork the &lt;em&gt;backend&lt;/em&gt;, not just the schema.&lt;/p&gt;

&lt;p&gt;For a software factory, that's the missing piece. Today my factory pipeline validates each stage against the production-shaped environment it builds. The version I want is: every factory run, every agent session, every experiment gets a full isolated copy of the backend.. rows, files, functions.. for the lifetime of a task, then vanishes. Database branching already proved this workflow for schema changes. Extending it to the whole backend turns "spin up a staging environment" from an afternoon of Terraform into a flag on a CLI call.&lt;/p&gt;

&lt;p&gt;That's the same story as the spending-caps feature from my last post, which is still marked "coming soon" by the way, and I'm still holding them to it: &lt;strong&gt;the primitives ship first, through the API, and the people building on top get to run ahead of the roadmap.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Closing
&lt;/h1&gt;




&lt;p&gt;The pitch for this stack isn't "Neon does everything". It's narrower and more honest: a full working SaaS.. identity, data, files, AI, money.. now fits in one brief, four skills, one backend vendor, and one Stripe account. pastmonday went from empty repo to production on my cluster in about 40 minutes, and most of that time I was making taste decisions at the approval gates, not doing integration work.&lt;/p&gt;

&lt;p&gt;If you want to go deeper: the &lt;a href="https://www.youtube.com/watch?v=l9kY3hoj9AQ" rel="noopener noreferrer"&gt;video&lt;/a&gt; covers the factory concept, the &lt;a href="https://github.com/kubeden/kubeden/tree/main/ai/skills/claude-code/web-development" rel="noopener noreferrer"&gt;skills are on GitHub&lt;/a&gt; to steal, the &lt;a href="https://denislavgavrilov.com/how-to-build-any-web-application" rel="noopener noreferrer"&gt;series&lt;/a&gt; walks each stage, and &lt;a href="https://pastmonday.com" rel="noopener noreferrer"&gt;pastmonday.com&lt;/a&gt; is the living proof.. go create an entry, that's a race-safe atomic quota consume you're clicking.&lt;/p&gt;

&lt;p&gt;And if you're at Neon: ship the branch-forks-everything future. I have a factory waiting for it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>neon</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Neon ships to the API first, so build the feature yourself</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Tue, 20 Oct 2026 07:07:08 +0000</pubDate>
      <link>https://dev.to/kubeden/neon-ships-to-the-api-first-so-build-the-feature-yourself-hjl</link>
      <guid>https://dev.to/kubeden/neon-ships-to-the-api-first-so-build-the-feature-yourself-hjl</guid>
      <description>&lt;p&gt;Everyone is talking about how agents can build anything now. And they mostly can. But an agent is only ever as good as the surface you point it at, and most platforms give it a terrible one: a dashboard full of buttons it can’t press, a “coming soon” page, a feature locked behind a UI that no agent will ever click through.&lt;/p&gt;

&lt;p&gt;Neon is different, and that difference is the whole reason I keep reaching for it when I’m building with Claude Code.&lt;/p&gt;

&lt;p&gt;Neon is &lt;strong&gt;API-first&lt;/strong&gt;. In my experience, new capabilities show up in their API long before they’re a nice button somewhere, and a lot of things never become a button at all. The console you click around in is just &lt;em&gt;one&lt;/em&gt; client of that API. Which means: if a primitive exists, your agent can reach it, read the docs for it, and compose whatever it needs out of it, &lt;em&gt;before&lt;/em&gt; anyone ships you the polished version.&lt;/p&gt;

&lt;p&gt;This post is me proving that to myself, out loud. I had an itch Neon doesn’t officially scratch, so instead of waiting, I pointed Claude Code at the API and built the missing feature. Twice in this post there’s a little interactive thing you can play with.. they’re not screenshots, they’re running the actual logic in your browser.&lt;/p&gt;




&lt;h1&gt;
  
  
  What “API-first” actually means for an agent
&lt;/h1&gt;




&lt;p&gt;When I say a platform is good for agentic coding, I mean something very specific: &lt;strong&gt;the thing my agent wants to do is expressible as an API call, and the docs to do it are readable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s it. That’s the whole bar, and it’s shocking how many tools fail it. An agent can’t “open settings and toggle the thing.” It can’t watch a launch video. It reads docs and it makes requests. So the best platform for an agent is the one whose entire surface &lt;em&gt;is&lt;/em&gt; an API, with reference docs that read like a contract.&lt;/p&gt;

&lt;p&gt;Neon is exactly that. Projects, branches, compute endpoints, autoscaling bounds, consumption metrics, quotas, billing.. all of it sits behind &lt;a href="https://api-docs.neon.tech" rel="noopener noreferrer"&gt;api-docs.neon.tech&lt;/a&gt; as plain REST. I leaned on this hard in &lt;a href="https://denislavgavrilov.com/agentic-workflows-branched-environments" rel="noopener noreferrer"&gt;Agentic Workflows: Branched Environments&lt;/a&gt;, where the entire preview-environment trick is just the agent calling the branch API on every pull request. No clicking. No waiting for a feature. Just primitives, composed.&lt;/p&gt;

&lt;p&gt;So when I hit a wall recently, my instinct wasn’t “I wish Neon had this.” It was “the primitive is probably already in the API, let me go build the wall-climbing tool myself.”&lt;/p&gt;




&lt;h1&gt;
  
  
  How serverless compute actually bills you
&lt;/h1&gt;




&lt;p&gt;Before the wall, I need you to understand &lt;em&gt;why&lt;/em&gt; Neon is even billed the way it is, because it’s the thing that makes the rest of this post matter.&lt;/p&gt;

&lt;p&gt;Most databases you’ve rented are billed &lt;strong&gt;flat&lt;/strong&gt;. You pick a box (some vCPUs, some RAM, an RDS instance, a droplet, whatever), and you pay for that box every hour of every day whether you touch it or not. You provision for your &lt;em&gt;peak&lt;/em&gt;, because you have to survive your worst Tuesday, and then you pay for that peak 24/7 even though your worst Tuesday happens for about twenty minutes a month. The other 729 hours, you’re paying for an idle machine to sit there and be ready. It’s a hotel room you booked for a year so you’d have somewhere to sleep three nights.&lt;/p&gt;

&lt;p&gt;Serverless compute flips that. Neon doesn’t sell you a box, it sells you &lt;strong&gt;what you actually consume&lt;/strong&gt;. Compute is metered in CU-hours (a CU, a “compute unit”, is about 1 vCPU and 4 GB of RAM), it autoscales up when load arrives and back down when it leaves, and crucially it can &lt;strong&gt;scale to zero&lt;/strong&gt; when nothing is connected. Storage and egress are metered separately, on their own dials. You’re billed for the area under the curve, not for the height of the tallest spike.&lt;/p&gt;

&lt;p&gt;Here’s the part people miss: &lt;strong&gt;done right, this is cheaper than flat, often dramatically.&lt;/strong&gt; Think about what your databases actually do. A staging environment that gets used three hours a workday. A side project nobody visits at 4am. A preview branch that lives for the length of a pull request and then dies. A little agent experiment that runs in bursts. Under flat billing you pay for all of those as if they were busy 24/7. Under serverless they cost roughly what you use, and the idle time trends toward free. The same workload that needs an always-on box sized for peak might bill a fraction of that when only the active hours count.&lt;/p&gt;

&lt;p&gt;But (and you can feel the “but” coming) the exact thing that makes it cheap makes it dangerous. Flat billing has a ceiling built in for free: the box can only do so much, and you already paid for it, so the worst case is “it gets slow.” Usage billing has &lt;strong&gt;no ceiling&lt;/strong&gt;. A runaway query, a retry loop, an agent that decides to reindex everything, a traffic spike, a bit of egress someone’s scraping.. none of that hits a wall. It just scales up, serves the load, and bills you for it. The curve that trends toward free when you’re idle trends toward terrifying when you’re not.&lt;/p&gt;

&lt;p&gt;So to &lt;em&gt;safely&lt;/em&gt; capture the “cheaper than flat” win, you want two things: a &lt;strong&gt;floor&lt;/strong&gt; (scale to zero, so idle is free) and a &lt;strong&gt;ceiling&lt;/strong&gt; (a hard cap, so a bad night can’t bankrupt you). Neon gives you the floor for free. The ceiling is where my wall is.&lt;/p&gt;




&lt;h1&gt;
  
  
  The itch: a “Coming soon” I didn’t want to wait for
&lt;/h1&gt;




&lt;p&gt;I run a lot of autonomous-ish things on Neon. If you’ve read the &lt;a href="https://denislavgavrilov.com/clopus-01-a-semi-autonomous-claude" rel="noopener noreferrer"&gt;Clopus&lt;/a&gt; posts you know I’m comfortable handing a Claude Code instance the keys and letting it run. And given everything I just said about unbounded usage billing, you can see exactly what keeps me up at night: it isn’t the agent doing something weird, it’s the &lt;em&gt;bill&lt;/em&gt;. An autoscaling Postgres that an agent (or a traffic spike, or a runaway loop) can push to 8 CU while I’m asleep is a genuinely scary object to own.&lt;/p&gt;

&lt;p&gt;So I went looking for the ceiling: a hard spending limit. And Neon &lt;em&gt;does&lt;/em&gt; have a spending limit. You set a monthly dollar number, and when you cross 80% and then 100% of it, your org admins get an email. Genuinely useful.. but it’s the soft version. It never actually &lt;em&gt;stops&lt;/em&gt; anything. Your database keeps happily burning money while you read the email.&lt;/p&gt;

&lt;p&gt;Here’s the part I want to be fair about, because it’s the whole mood of this post: the hard version is clearly on the way. Open the spending-limit dialog and there’s a &lt;strong&gt;“Suspend projects”&lt;/strong&gt; toggle sitting right there.. wearing a &lt;strong&gt;“Coming soon”&lt;/strong&gt; badge. They’re building exactly the feature I want.&lt;/p&gt;

&lt;p&gt;I just didn’t want to wait for the changelog.&lt;/p&gt;

&lt;p&gt;That’s the honest framing here. This isn’t “Neon is missing something, boo.” The feature is coming. But I wanted it &lt;em&gt;today&lt;/em&gt;, running tonight, before my next autonomous experiment, and an alert is not a cap. I wanted a number that, when crossed, &lt;em&gt;turns the compute off&lt;/em&gt;. And because Neon is API-first, “coming soon” for the dashboard doesn’t mean “coming soon” for me. So I went and looked at what the API already exposes.&lt;/p&gt;




&lt;h1&gt;
  
  
  The levers the API hands you
&lt;/h1&gt;




&lt;p&gt;This is the part that made me grin. The console gives you exactly one spend control, and it’s the toothless one. The API gives you the levers to build a real one. Click through these, it’s the actual difference that made the tool possible:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;Neon API levers (interactive)&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/neon-api-first-agentic-coding" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that left-to-right and the story tells itself. The console: an alert. The API: a way to read &lt;em&gt;true&lt;/em&gt; spend, a way to set a per-project cap that suspends compute, and a way to flip an endpoint off the instant you decide to. The feature didn’t exist, but every ingredient for it did. That gap, between “the product doesn’t have it” and “the API absolutely lets you build it,” is the gap agentic coding lives in.&lt;/p&gt;

&lt;p&gt;So I didn’t file a feature request. I opened Claude Code.&lt;/p&gt;




&lt;h1&gt;
  
  
  Building the thing
&lt;/h1&gt;




&lt;p&gt;The opening prompt was basically just me describing the itch and telling it where to read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Neon’s native spending limit is alert-only, it emails you but never stops compute. I want a small tool that reads a project’s real usage from the Neon API, converts it to dollars, and actually &lt;em&gt;suspends&lt;/em&gt; compute when I cross a monthly budget I set. Read the API reference first (api-docs.neon.tech), figure out which endpoints expose usage and which ones can stop a project, then build it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It read the docs, found the consumption endpoints and the quota field, and had a working first pass fast. Convert usage to dollars, set a &lt;code&gt;compute_time_seconds&lt;/code&gt; quota equal to the dollars-of-compute I had left, let Neon’s own metering pull the trigger. Clean.&lt;/p&gt;

&lt;p&gt;Then I actually stress-tested it, and learned the lesson the hard way.&lt;/p&gt;

&lt;p&gt;The per-project usage field you get from &lt;code&gt;GET /projects/{id}&lt;/code&gt; (the obvious, no-org-id-required one) &lt;strong&gt;lags, and can freeze badly.&lt;/strong&gt; In live testing it stuck at one value while real usage was around &lt;strong&gt;70x higher&lt;/strong&gt;. And because the project &lt;code&gt;quota&lt;/code&gt; is gated by that same frozen meter, a quota I’d set &lt;em&gt;5x under&lt;/em&gt; what I was actually spending simply never tripped. The “cap” did nothing. I was over budget and the safety net just sat there.&lt;/p&gt;

&lt;p&gt;That’s the kind of thing you only find by running it for real, and it’s exactly where API-first pays off a second time. The fix wasn’t to wait for Neon to make the field accurate. It was to route around it, because the API already had better roads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The per-project usage field is freezing, it’s reading ~70x under real usage, so the quota never suspends. Switch the usage source to the consumption_history/v2 API (it needs an org_id but it’s invoice-accurate), and stop trusting the quota as the trigger.. disable the endpoint directly the moment accurate spend crosses the budget.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two API calls I hadn’t even needed in v1 turned a broken backstop into a real cap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;consumption_history/v2&lt;/code&gt;&lt;/strong&gt; for &lt;em&gt;truth&lt;/em&gt;: billing-grade usage that matches the invoice, not the field that lies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PATCH endpoint disabled=true&lt;/code&gt;&lt;/strong&gt; for the &lt;em&gt;stop&lt;/em&gt;: meter-independent, immediate, no waiting for Neon’s metering to catch up.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx neon-budget check     &lt;span class="c"&gt;# read-only: what am I spending vs budget?&lt;/span&gt;
npx neon-budget enforce   &lt;span class="c"&gt;# apply the cap (quota + endpoint disable)&lt;/span&gt;
npx neon-budget watch &lt;span class="nt"&gt;-i&lt;/span&gt; 15m   &lt;span class="c"&gt;# run it on a loop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole thing is a few hundred lines, it’s on &lt;a href="https://github.com/kubeden/neon-budget" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and I want to be clear about &lt;em&gt;why&lt;/em&gt; it exists: not because I’m smart, but because Neon’s surface let an agent and me build, in an afternoon, a capability the product doesn’t ship. That’s leverage.&lt;/p&gt;




&lt;h1&gt;
  
  
  Play with it
&lt;/h1&gt;




&lt;p&gt;Here’s the budget logic, running live in your browser. Drag the sliders: set your plan, your average compute, how many hours it’s actually active (this is the serverless dial from earlier.. push it below 730 and watch what you save versus an always-on box), storage, egress. Then pick a monthly budget and a policy. It computes the real Neon cost and shows you exactly what &lt;code&gt;neon-budget&lt;/code&gt; would do: just &lt;em&gt;alert&lt;/em&gt;, &lt;em&gt;throttle&lt;/em&gt; the autoscaling ceiling down in tiers, or &lt;em&gt;hard-stop&lt;/em&gt; and disable the endpoint.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;▶ &lt;strong&gt;Neon Budget Simulator (interactive)&lt;/strong&gt;, an interactive widget, runs on &lt;a href="https://denislavgavrilov.com/neon-api-first-agentic-coding" rel="noopener noreferrer"&gt;the original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn’t a mockup, it’s the same cost math and the same decision function that runs in the tool, ported straight into the page. Push the compute up, watch the hard-stop trip. Drop egress under 500 GB and watch that line go to zero (it’s included). The dollars to &lt;code&gt;compute_time_seconds&lt;/code&gt; quota it prints is the literal value the tool PATCHes onto your project.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why this is the real reason Neon wins for agents
&lt;/h1&gt;




&lt;p&gt;Step back from spending caps for a second, because the tool is just the example.&lt;/p&gt;

&lt;p&gt;The reason Neon is so good for agentic coding isn’t any single feature. It’s that &lt;strong&gt;the feature you want is almost always already expressible&lt;/strong&gt;, because they build API-first. When the primitive lands in the API early, the gap between “Neon doesn’t do that yet” and “I have it running” is one good prompt and an afternoon, not a roadmap.&lt;/p&gt;

&lt;p&gt;That changes how you work. You stop treating the product’s feature list as your ceiling. The branch API gave me preview environments before I “had” preview environments. The consumption and endpoint APIs gave me a hard spending cap that, strictly speaking, the product still doesn’t offer. None of that required Neon to ship me anything new. It required them to have been honest enough to expose the platform as an API, and an agent willing to read the docs.&lt;/p&gt;

&lt;p&gt;That’s the bet I’m making with agentic coding in general: the winners will be the platforms that are &lt;em&gt;complete&lt;/em&gt; at the API layer, because that’s the only layer an agent can actually build on. Neon is the most complete one I’ve found. So I keep building things it doesn’t have yet, and so far, it keeps already having the pieces.&lt;/p&gt;




&lt;h1&gt;
  
  
  Closing
&lt;/h1&gt;




&lt;p&gt;Don’t wait for the button. If the primitive is in the API, it’s already yours: point your agent at the docs and go build the feature. With Neon, more often than not, the hard part is already done for you, sitting quietly in the API reference while everyone else waits for the changelog.&lt;/p&gt;

&lt;p&gt;Thank you for reading!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>neon</category>
      <category>postgres</category>
      <category>api</category>
    </item>
    <item>
      <title>Agentic Workflows: Branched Environments</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:02:28 +0000</pubDate>
      <link>https://dev.to/kubeden/agentic-workflows-branched-environments-1450</link>
      <guid>https://dev.to/kubeden/agentic-workflows-branched-environments-1450</guid>
      <description>&lt;p&gt;Agentic workflows are becoming more and more valid, especially in enterprise. While for personal development, you will mostly get away with tmux+, in a professional environments, high speed and lack of policy will get you in trouble.&lt;/p&gt;

&lt;p&gt;For this practical exploration session, I onboard my project onto Neon and re-create my &lt;a href="https://denislavgavrilov.com/articles/preview-environments" rel="noopener noreferrer"&gt;"Preview Environments”&lt;/a&gt; implementation to turn into a truly separated (branched) environment for every new Pull Request (which in itself is generated entirely by an agent). All in Git.&lt;/p&gt;

&lt;p&gt;The repositories for this project are here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Complete example: &lt;a href="https://github.com/kubeden/txtwrite" rel="noopener noreferrer"&gt;[kubeden/txtwrite]&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Codex &amp;amp; Claude Code skills for your agents to “one-shot” it for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Codex: [&lt;a href="https://github.com/kubeden/kubeden/tree/main/ai/skills/codex/preview-environments/neon" rel="noopener noreferrer"&gt;kubeden/skills/codex&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt; Claude Code: &lt;a href="https://github.com/kubeden/kubeden/tree/main/ai/skills/claude-code/preview-environments/neon" rel="noopener noreferrer"&gt;[kubeden/skills/claude-code]&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: since this implementation relies heavily on Neon’s (or Databricks’ Lakebase) architecture, the “plug-n-play” repo (2.a., 2.b.) requires your database to be either a Neon DB or a Lakebase DB.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you enjoy videos, here is the entire content here, but on Youtube!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=6RkvNIch6H8" rel="noopener noreferrer"&gt;▶ Watch: Interactive widget&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why care &amp;amp; what does this solve?
&lt;/h2&gt;




&lt;p&gt;I’ve been thinking a lot about agentic workflows lately. I am a very heavy AI user, and with a great ton of curiosity in the space. Often I can’t help but notice how similar terminal agents and DevOps practices are… it is almost like they were made to be used together. DevOps heavily relies on automation and what better direction to go than to put AI in the very foundation of automation…?&lt;/p&gt;

&lt;h3&gt;
  
  
  Convenient Interface
&lt;/h3&gt;

&lt;p&gt;One of the great benefits of this system, is that it has a single convenient interface. Be it Git, or Slack, it is a single interface. You can open issues from your phone, get instant notifications in your email on failures, and get both a UI and a CLI for you OR your agent to track every step of what’s happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracking &amp;amp; Collaboration
&lt;/h3&gt;

&lt;p&gt;With Git specifically, this workflow benefits from various tracking &amp;amp; collaboration possibilities. Multiple people can collaborate on issues &amp;amp; PRs. Reviewing the results. Seeing what works and what doesn’t.. and so on, and so on. Plus, you can always see closed PRs, and if your team follows standards practices i.e. merges small, incremental PRs, you get a very good “disaster recovery system”-like system through Git’s history.&lt;/p&gt;

&lt;p&gt;Another great benefit from Github (or any Git provider)’s set of functionalities, is that you can set policies to disallow all kinds of negative actions!&lt;/p&gt;

&lt;h3&gt;
  
  
  Sandboxed environment
&lt;/h3&gt;

&lt;p&gt;I was thinking a lot about this… you can configure a single VPS and install your agent there, but I am a little concerned how the said "agent dev vm” will not be ephemeral… which, combined with the recent high number of CVEs, is in my opinion, certain trouble. It would be very convenient, though…. So anyway, this particular implementation that I am writing about here, is ephemeral meaning after every CI finishes, the agent machines are destroyed so you don’t have to worry about managing long-lasting environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free… and flexible
&lt;/h3&gt;

&lt;p&gt;Well… it is free. I see people attempting this with external services, and many startups seem to be providing similar experience to their end users, but for I have always believed that for good engineering, you (or your team) need to own your resources. Self-management gives you the most flexibility, and with flexibility, you are only bottlenecked by your imagination (and laziness).&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisite Knowledge
&lt;/h2&gt;




&lt;p&gt;In order to make this article understandable for everyone, I am going to go over the main concepts utilized further.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a…?
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Preview Environment
&lt;/h4&gt;

&lt;p&gt;Preview environments are such environments, that are an exact replica of your production &lt;em&gt;(or staging)&lt;/em&gt; environment, that are being spawned on-the-go. Preview environments are very useful for rapid development, as one can wire all kinds of test flows to trigger as the environment gets created, as well as get any person to start using/testing it immediately. There is no DevOps/Infra person required for this.&lt;/p&gt;

&lt;h4&gt;
  
  
  Agentic Workflow
&lt;/h4&gt;

&lt;p&gt;There are mixed opinion in the community about what exactly an “agentic workflow” is — is it a split-paned tmux session with multiple terminal agents on display? Is it a Linear-Slack-ClaudeCode integration? Maybe a custom written agent with agents SDK that reacts to a pushover notification? Well… in my opinion, all of these are agentic workflows. I think of the word “workflow” as a set of actions that are combined into a system. And “Agentic” workflows, are a set of actions an AI agent goes through/executes, based on a trigger (and possibly… criteria).&lt;/p&gt;

&lt;h4&gt;
  
  
  Database Branching
&lt;/h4&gt;

&lt;p&gt;Now… when I first started playing around with the concept of preview environments &amp;amp; AI, I thought I knew what database branching was. Well, I was wrong. You see, I thought that to “branch” a database was just a fancy way to say “make a copy of it.” Turns out… it is a lot more neat! Database branching, when “true” (as in, true branching), becomes practical when the compute and storage layers are separated. This way, at the point of separation (when a new branch is created), new writes are stored as &lt;em&gt;deltas&lt;/em&gt; at the storage layer, which allows only the diverged pages to be stored separately and not the entire database. As such, your database is not being fully cloned. Instead, a new logical space of its state is created, where you can safely make changes, and later on have the ability to apply the desired changes back to your real database &lt;em&gt;(through migrations, schema diffs, restore/reset workflows, or application-level data movement, depending on what changed)&lt;/em&gt;… essentially materializing only the diverging parts of that logical space.&lt;/p&gt;

&lt;p&gt;This technique enables some otherwise hard-to-achieve features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Setting a TTL on a test branch i.e. automatic cleanup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fast restore in the case of an accidental delete action / similar&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Quick&lt;/em&gt; iteration (branching) without interfering with production’s performance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Branching off a development branch without having to deal with the syncing of a separate development instance&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The first three paragraphs of this post do a very good job in explaining it: &lt;strong&gt;&lt;a href="https://www.databricks.com/blog/database-branching-postgres-git-style-workflows-databricks-lakebase" rel="noopener noreferrer"&gt;Database Branching in Postgres: Git-Style Workflows with Databricks Lakebase&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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




&lt;p&gt;The following is a high-level diagram for how the different system component all depend on and interact with each other. For the deeper, workflows-based diagrams, read further.&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%2F62hapnegt3b016y7hkph.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%2F62hapnegt3b016y7hkph.png" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;High-level diagram&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The implementation has 3 core parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1/ Github:&lt;/strong&gt; The agentic workflow “engine”&lt;br&gt;&lt;br&gt;
&lt;strong&gt;2/ Application infra (K8s+Neon):&lt;/strong&gt; The place where my app lives (Frontend-only)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;3/ Database infra (Neon):&lt;/strong&gt; The place where my app becomes dynamic (Backend)&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;




&lt;p&gt;The foundation is standard: there is a Github repository, which contains a React codebase that holds the frontend for a web-based markdown editor. It uses the /src structure, and uses deno for its runtime environment.&lt;/p&gt;

&lt;p&gt;Further, for backend, I use Neon. Mainly for the managed Auth, as well as (obviously) for a database.&lt;/p&gt;

&lt;p&gt;Upon a desired change, I create an Issue and tag my agent &amp;lt;@kubeden-agent&amp;gt;. This triggers the , which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Verifies trigger policies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Collect full issue context for the agent&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Installs all required software in the runner&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creates (or reuses) the separate GIT branch&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creates a separate Neon preview branch&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Boots the agent (Codex) and feeds it the context&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Agent implements the code changes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runs configured checks &amp;amp; migrations against the Neon preview branch&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the point where the agent finishes its work, the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Commits changes &amp;amp; pushes to the branch&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Opens /or updates/ a Pull Request&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Updates the status reply in the Issue&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dispatches the preview workflow&lt;/p&gt;&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%2Fqn3zifkf14duaksrvdaq.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%2Fqn3zifkf14duaksrvdaq.png" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Workflow I: Issue to PR&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At this point, there is a Pull Request opened, which triggers the next major CI , which goes like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Checks out the PR commit&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creates or reuses the Neon preview branch&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runs migrations/checks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Builds a new container image (with the branch-specific Neon URLs for auth, and data) &amp;amp; pushes it to the registry&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Updates &amp;amp; pushes the ArgoCD manifests (in the infra repository)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ArgoCD creates a new application through the AplicationSet&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Polls the new ApplicationSet app status&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Updates the status reply with the preview URL&lt;/p&gt;&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%2Fehui7x4gw0cpac84wen1.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%2Fehui7x4gw0cpac84wen1.png" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Workflow: Preview Deployment&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Some details worth mentioning are that Github runs a webhook against my ArgoCD upon the push in my infra repository so that the preview application creation can be immediate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in the case where I add a reply to my PR i.e. when I am not satisfied with the initial attempt, it runs the Preview CI again, with the only change that it reuses the already live resources, instead of spawning new ones.&lt;/p&gt;

&lt;p&gt;And here is the entire diagram.&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%2F0fns0y4q72w9ekzzndgd.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%2F0fns0y4q72w9ekzzndgd.png" width="799" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Complete Issue→PR Agentic Flow&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;




&lt;p&gt;This is the second time I am doing a preview environment-like Github integration. I believe preview environments, as a technique, are the perfect fit to combine with agentic workflows. The reason why I returned to this implementation is that I was left triggered by my previous “workaround” with Supabase, and the table separation felt a little wrong. It was at this point I talked to a friend, with whom I discussed database branching and after a little knowledge transfer, he showed me Neon (and the blog post I linked to in the beginning of this post). I was mainly excited about the technique they use to separate compute from storage, which is exactly what makes true branching possible. Not only, but having the “managed backend” functionality I enjoy so much, with a Git-like iteration, makes me think we can really get into some complex implementations like the recently-viral “product factories”.&lt;/p&gt;

&lt;p&gt;I think using AI is obviously required in our current times, but I also think many engineers skip a crucial step in their thinking phase-they forget to combine their pre-AI knowledge with all the new skills and concepts they are passively gaining &amp;amp; being attached to. It’s like many of us separate what worked pre-AI, and what works now, post-AI. Techniques like Preview Environments, GitOps, and major DevOps practices, are honestly only making agentic workflows more powerful.&lt;/p&gt;

&lt;p&gt;So… if you are reading this post as a pre-AI engineer, maybe don’t get that excited about how amazing the agents are, and remember who you are-how deep you can think. And when you do, combine your deep thinking &amp;amp; extensive experience WITH the AI agents capabilities. I am sure the results are going to be amazing.&lt;/p&gt;

&lt;p&gt;Thank you for reading.&lt;/p&gt;







&lt;h3&gt;
  
  
  &lt;em&gt;&lt;strong&gt;After-post notes&lt;/strong&gt;&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;N1:&lt;/strong&gt; If this post inspired you to think, feel free to &lt;a href="http://dennis@kubeden.io/" rel="noopener noreferrer"&gt;email&lt;/a&gt; me and start a discussion! I love talking technology, and even so if you are, too, actively living in “the arena”.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can find everything about me on &lt;a href="http://denislavgavrilov.com/about" rel="noopener noreferrer"&gt;denislavgavrilov.com/about&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for reading and have a good one!&lt;/em&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>neon</category>
      <category>devops</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How to do a zero-downtime schema change with Neon</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Sat, 17 Oct 2026 07:00:18 +0000</pubDate>
      <link>https://dev.to/kubeden/how-to-do-a-zero-downtime-schema-change-with-neon-559o</link>
      <guid>https://dev.to/kubeden/how-to-do-a-zero-downtime-schema-change-with-neon-559o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The following post is a recap of my Claude Code sessions doing a schema change the careful way. Although the content is AI-generated, I go through the entire post myself and every number in it comes from a run I watched happen. If you'd like to discuss anything in relation to this directly with me, you can do so on &lt;a href="mailto:dennis@a2w.io"&gt;dennis@a2w.io&lt;/a&gt; or on x.com/@kuberdenis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Splitting a column is the smallest interesting schema change there is. &lt;code&gt;users.name&lt;/code&gt; becomes &lt;code&gt;users.first_name&lt;/code&gt; and &lt;code&gt;users.last_name&lt;/code&gt;, because someone finally wants to write "Hi Ada" in an email.&lt;/p&gt;

&lt;p&gt;The one-line version of that change is a rename, a backfill and a deploy, run in whatever order you get to them. It works on your laptop. On a table with two hundred thousand rows and traffic on it, the same three steps give you a stretch of 500s in the middle, and the length of that stretch is however long the slowest of the three took.&lt;/p&gt;

&lt;p&gt;The pattern that fixes it is old and has a name: expand and contract. Add the new shape, run both shapes at once for a while, then remove the old one. What is new is that you no longer have to be brave about it. A Neon branch is a full copy of the database in about two seconds, so every step can be run somewhere disposable first, read, and only then pointed at the real thing.&lt;/p&gt;

&lt;p&gt;I built the whole thing to see whether "zero downtime" survives being measured. It does. 319,568 requests went through the app while the migration ran, and none of them failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubeden/diffium-db" rel="noopener noreferrer"&gt;kubeden/diffium-db&lt;/a&gt;, the tool used to read what each rehearsal did&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://neon.com/docs/introduction/branching" rel="noopener noreferrer"&gt;Neon branching&lt;/a&gt;, the copy-on-write branches this leans on&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  What is running
&lt;/h1&gt;




&lt;p&gt;A small orders API in Bun, two instances of it on two ports, and a proxy in front that only routes to a port whose health check is passing. Behind them, Postgres 18 on Neon with &lt;code&gt;app.users&lt;/code&gt; and &lt;code&gt;app.orders&lt;/code&gt;, seeded with 200,000 users and 100,000 orders.&lt;/p&gt;

&lt;p&gt;The HTTP contract never changes. A user is posted and read back as &lt;code&gt;{ name, email }&lt;/code&gt; on the first request and on the last one. Only the storage underneath moves. That is the whole trick, and it is why the app can be deployed four times during the change without anything outside noticing.&lt;/p&gt;

&lt;p&gt;Two instances behind a proxy is the part people skip in blog posts, and it is the reason the pattern exists. During a rolling deploy, old code and new code are talking to the same database at the same time. Every rule below follows from that one sentence.&lt;/p&gt;

&lt;p&gt;The load generator holds 24 clients open and each of them does the same four requests in a loop: create a user, read it back, place an order, list the orders. It counts failures, and it also compares the name that comes out with the name that went in. Counting 200s is not enough. A migration that loses half a name returns 200 all the way down.&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%2F2bh2gnhtshynvj5w5mnf.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%2F2bh2gnhtshynvj5w5mnf.png" width="799" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The instrument, before anything moves. &lt;code&gt;wrong&lt;/code&gt; is the column that matters.&lt;/em&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  The order of operations
&lt;/h1&gt;




&lt;p&gt;Six steps, and the order is the whole content of the pattern.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expand.&lt;/strong&gt; Add &lt;code&gt;first_name&lt;/code&gt; and &lt;code&gt;last_name&lt;/code&gt;, nullable. Add a trigger that keeps them in step with &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy the dual write.&lt;/strong&gt; The app now fills both shapes. It still reads &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill.&lt;/strong&gt; Fill the new columns for every row that predates step 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrain.&lt;/strong&gt; Say the new columns are always present, without locking the table to prove it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flip the reads.&lt;/strong&gt; The app answers from &lt;code&gt;first_name&lt;/code&gt; and &lt;code&gt;last_name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract.&lt;/strong&gt; Deploy an app that never mentions &lt;code&gt;name&lt;/code&gt;, then drop the column.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of steps 1, 3, 4 and 6 touches the database. Each of them gets rehearsed on a branch first.&lt;/p&gt;




&lt;h1&gt;
  
  
  Rehearsing on a branch
&lt;/h1&gt;




&lt;p&gt;Every database phase goes through one script first. It cuts a branch off production, takes a diffium-db baseline on it, applies the phase, prints what changed, and deletes the branch. Without the timing and the tidying, that is four commands:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;neonctl branches create --project-id "$NEON_PROJECT_ID" --name "$BRANCH"
URI="$(neonctl connection-string "$BRANCH" --project-id "$NEON_PROJECT_ID")"

DATABASE_URL="$URI" diffium-db snapshot --schema app
psql "$URI" -v ON_ERROR_STOP=1 -f "sql/$PHASE"
DATABASE_URL="$URI" diffium-db diff --schema app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The branch came up in 2.4 seconds holding the same schema and the same 200,000 rows, because it is copy-on-write off its parent rather than a restore. Applying the expand phase to it took 0.7 seconds.&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%2Fv9s1icsuxylnrshyrcia.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%2Fv9s1icsuxylnrshyrcia.png" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The expand phase, run on a copy. Two columns, a trigger, and a helper function. Nothing else.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Reading the diff is the point. &lt;code&gt;psql&lt;/code&gt; told me the phase succeeded. It did not tell me what it did, and "what it did" is where a migration written by an agent surprises you. I got a list of three changes and no fourth one, on a database I was about to throw away.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Expand
&lt;/h1&gt;




&lt;p&gt;Two nullable columns with no default, so Postgres writes catalogue rows and never touches the table itself. Table size does not enter into it.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;begin;
set local lock_timeout = '2s';

alter table app.users add column first_name text;
alter table app.users add column last_name  text;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;lock_timeout&lt;/code&gt; is the line people leave out. An &lt;code&gt;ALTER TABLE&lt;/code&gt; that cannot get its lock waits, and while it waits it blocks every reader queued behind it. A migration that fails in two seconds is a migration you rerun. A migration that waits is an outage with a different name.&lt;/p&gt;

&lt;p&gt;Then the trigger, which is the load-bearing part of the whole post:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create trigger users_name_sync
    before insert or update on app.users
    for each row execute function app.users_name_sync();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Whichever shape a writer filled in, the trigger derives the other one. Application code doing the same thing is not enough, because during the deploy in step 2 there is an instance still running that has never heard of &lt;code&gt;first_name&lt;/code&gt;, and &lt;code&gt;psql&lt;/code&gt; and agents are writers too. The sync belongs where every writer goes through it.&lt;/p&gt;

&lt;p&gt;On production it was over inside a second. Here is the traffic across that moment.&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%2F0co6rcxjbh536tme7rml.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%2F0co6rcxjbh536tme7rml.png" width="799" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;failed 0&lt;/code&gt; on both sides of the DDL.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the table afterwards: both columns exist, and 215,400 rows have nothing in them.&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%2Fsnwjpv21rk5sj0aaqy8c.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%2Fsnwjpv21rk5sj0aaqy8c.png" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;200,000 of those are the seed. The other 15,400 arrived from live traffic between the start of the run and the expand.&lt;/em&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Deploy the dual write, and what that deploy costs
&lt;/h1&gt;




&lt;p&gt;The app now writes &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;first_name&lt;/code&gt; and &lt;code&gt;last_name&lt;/code&gt; on every insert. It still reads &lt;code&gt;name&lt;/code&gt;, so nothing about its answers changes. This deploy is invisible from outside, and it is the last moment where rolling back is free.&lt;/p&gt;

&lt;p&gt;The deploy script replaces one instance, waits for it to answer, then replaces the other. My first version of it dropped 25 requests every time it ran, and the schema change had nothing to do with it.&lt;/p&gt;

&lt;p&gt;The instance was exiting the moment it was told to. Requests already inside it died with it. The fix is three steps in a fixed order, and it is worth writing out because it is the actual mechanism behind the phrase "zero downtime":&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;draining = true;              // 1. start failing the health check
await Bun.sleep(600);         //    and let the proxy notice
server.stop(false);           // 2. close the listener
while (server.pendingRequests &amp;gt; 0) await Bun.sleep(10);   // 3. wait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That took it from 25 failures to 6. The remaining 6 were the new instance passing its health check while its connection pool was still cold, then queueing two hundred requests behind a TLS handshake until the runtime's 10 second request timeout killed them. Warming the pool before opening the port took it to 0.&lt;/p&gt;

&lt;p&gt;I am including this because both bugs are in the deploy, not in the migration, and that is the general case. Expand and contract is a set of rules for making the schema safe to deploy through. It cannot save a deploy that drops connections on its own.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Backfill
&lt;/h1&gt;




&lt;p&gt;Rows written since the expand already have both shapes. Everything older is still name-only, and that is 215,400 rows.&lt;/p&gt;

&lt;p&gt;Not in one statement. A single &lt;code&gt;UPDATE&lt;/code&gt; over all of them holds row locks for its entire life, gives you nothing to watch, and cannot be stopped halfway. Batches of 2,000, committing as they go, with a partial index so that finding the next batch stays cheap once the easy rows are gone:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create index concurrently if not exists users_backfill_idx
    on app.users (id) where first_name is null;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;CONCURRENTLY&lt;/code&gt; keeps writes running while the index builds, and it cannot run inside a transaction, which is why this file is deliberately not one.&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%2F54cu8bawg6l5esws92ri.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%2F54cu8bawg6l5esws92ri.png" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;215,400 rows in 15.6 seconds, in batches of 2,000. Nothing failed while it ran.&lt;/em&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%2Fn4ygzblkwxs3db35rzbu.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%2Fn4ygzblkwxs3db35rzbu.png" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nothing left to fill.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before any of this, run the check that says whether the split is reversible at all:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;select count(*) from app.users
 where btrim(name) is distinct from btrim(&amp;lt;the split, put back together&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Zero means every name survives the round trip. Anything else means you have decided what happens to those rows, whether or not you know it yet. Mine was zero, which is a property of names I generated, not a property of names.&lt;/p&gt;




&lt;h1&gt;
  
  
  4 and 5. Constrain, then flip the reads
&lt;/h1&gt;




&lt;p&gt;&lt;code&gt;NOT VALID&lt;/code&gt; first, then validate. The first statement writes a catalogue row and takes the lock for that long. Every new and updated row is checked from that moment. The second statement reads the whole table under a weaker lock, and writes keep going while it runs.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alter table app.users
    add constraint users_first_name_present check (first_name is not null) not valid;

alter table app.users validate constraint users_first_name_present;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then the app is deployed reading &lt;code&gt;first_name&lt;/code&gt; and &lt;code&gt;last_name&lt;/code&gt;, still writing both. This is the step that is safe only because step 3 finished. Flip the reads with rows still unbackfilled and half your users are suddenly called " ".&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Contract
&lt;/h1&gt;




&lt;p&gt;The rehearsal for this one is the one worth reading, because dropping a column is the step with no undo.&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%2F6efq5iiqxsogig20r1bi.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%2F6efq5iiqxsogig20r1bi.png" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The column, the trigger, the function and the &lt;code&gt;NOT NULL&lt;/code&gt; constraint that came with the column, all leaving together. On a copy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The order is not negotiable. Deploy an app that never mentions &lt;code&gt;name&lt;/code&gt;, confirm it is on every instance, and only then drop the column. Contract while one instance still references it and that instance starts returning 500s until someone restarts it.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;begin;
set local lock_timeout = '2s';
drop trigger users_name_sync on app.users;
drop function app.users_name_sync();
alter table app.users drop column name;
commit;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;0.81 seconds.&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%2F9g2qgdyd0gxndgs7qssc.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%2F9g2qgdyd0gxndgs7qssc.png" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  What the traffic saw
&lt;/h1&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%2Fa99v1qri1wbr5jzttfsb.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%2Fa99v1qri1wbr5jzttfsb.png" width="799" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;319,568 requests over 10 minutes 49 seconds. 0 failed, 0 with a wrong name, p50 41ms, p99 208ms. The migration is in there: two DDL phases, a 215,400 row backfill, a constraint validation and four rolling deploys, and none of it shows up as a change in the numbers.&lt;/p&gt;

&lt;p&gt;The end state is 279,892 users, every one of them with a first and last name, and no &lt;code&gt;name&lt;/code&gt; column.&lt;/p&gt;




&lt;h1&gt;
  
  
  What breaks it
&lt;/h1&gt;




&lt;p&gt;Take out the trigger and everything above still passes its tests, right up until a rolling deploy has both versions live. Here is the whole failure in six lines:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create table control.users (id bigint generated always as identity primary key,
                            name text, first_name text, last_name text);

-- old instance, still running, writes the only column it knows about
insert into control.users (name) values ('Ada Lovelace');

-- new instance, just deployed, reads the new columns
select coalesce(first_name,'') || ' ' || coalesce(last_name,'') as api_returns
  from control.users;

 api_returns
-------------

(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The value is one space. Status 200. Correct content type. Ada's name is gone. This is the failure the whole pattern is arranged around, and it is invisible to anything that only counts error rates, which is why the load generator in this demo compares the name it wrote against the name it read.&lt;/p&gt;




&lt;h1&gt;
  
  
  Building it yourself
&lt;/h1&gt;




&lt;p&gt;The demo is three TypeScript files, six shell scripts and seven SQL files, and everything in it that matters is quoted somewhere above.&lt;/p&gt;

&lt;p&gt;The API is one file. &lt;code&gt;APP_VERSION&lt;/code&gt; picks its storage layer, so the four deploys are four values of an environment variable instead of four commits:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v1  name is the only column there is
v2  writes both shapes, still reads name
v3  reads first_name / last_name, still writes both
v4  never mentions name. only now can the column go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In front of it, a proxy routes to whichever of the two instances passes its health check. Beside it, the load generator from the top of the post. The rest is a script per job. Start the two instances and the proxy. Roll a version out in the drain order above. Rehearse a phase on a branch. Print the columns and the null count once a second.&lt;/p&gt;

&lt;p&gt;The SQL is the schema, the seed, the losslessness check, and one file per database phase.&lt;/p&gt;

&lt;p&gt;Set the database up, and run the check before anything else. It has to come back 0.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psql "$DATABASE_URL" -f sql/00-schema.sql -f sql/00-seed.sql
psql "$DATABASE_URL" -f sql/check.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then three panes. Traffic in the first, the database in the second, the steps in the third:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./bin/up.sh v1
bun src/load.ts            # pane 1, leave it running
./bin/watch-db.sh          # pane 2

./bin/rehearse.sh 01-expand.sql
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f sql/01-expand.sql
./bin/deploy.sh v2

./bin/rehearse.sh 02-backfill.sql
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f sql/02-backfill.sql
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f sql/03-constrain.sql

./bin/deploy.sh v3
./bin/deploy.sh v4

./bin/rehearse.sh 04-contract.sql
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f sql/04-contract.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three environment variables and that is the whole configuration. &lt;code&gt;DATABASE_URL&lt;/code&gt; for whichever branch you are treating as production, &lt;code&gt;NEON_PROJECT_ID&lt;/code&gt; so the rehearsal knows where to cut, and a diffium-db checkout so it can read what came back.&lt;/p&gt;

&lt;p&gt;Any Postgres works for the app itself. Branching is what makes rehearsing every phase cheap enough that you do it every time instead of only when the change looks scary.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>neon</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why I like Neon</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Wed, 14 Oct 2026 08:09:17 +0000</pubDate>
      <link>https://dev.to/kubeden/why-i-like-neon-5e8b</link>
      <guid>https://dev.to/kubeden/why-i-like-neon-5e8b</guid>
      <description>&lt;p&gt;I've been using Supabase for two years only until recently when I moved all my new projects to Neon. I figured I'd take the time and note down my thoughts on why this shift happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  An agents-first company
&lt;/h2&gt;

&lt;p&gt;When testing out Neon, I noticed that a few features were missing, only to find these features were available through the API. Which immediately made me a good impression as nowadays with the amount of intelligence LLMs have, and how good terminal agents are, I strongly rely on personal software - if I need something, I spawn it in a few hours. Which means I need a company that understands this and lets me interact with what I am paying for, from their API.&lt;/p&gt;

&lt;p&gt;A quick example I can give, is: &lt;a href="https://github.com/kubeden/neon-budget" rel="noopener noreferrer"&gt;[kubeden/neon-budget]&lt;/a&gt; - a cli to enforce usage limits that I deployed on my k3s cluster.&lt;/p&gt;

&lt;p&gt;Not only do they do this, but their documentation is very obviously made with the thought that "AIs are going to use this" in mind. See their MCP: &lt;a href="https://neon.com/docs/ai/neon-mcp-server" rel="noopener noreferrer"&gt;[neon-mcp-server]&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed of development
&lt;/h2&gt;

&lt;p&gt;I am a fan of Backend-As-A-Service companies, mostly because of reliability. If I decide to self-manage a storage container, or a database, don't get me wrong - I will succeed. What I am mostly concerned about is my app failing / my cluster over-filling, and me having to deal with backups (which I won't have) and restore. So I am ok paying a small monthly fee to take this trouble away from me.&lt;/p&gt;

&lt;p&gt;With Neon, when I started using them, they were still only DB-As-A-Service. I thought, "ehh... it would've been perfect if they had {feature xyz}" - and shortly after, &lt;em&gt;about a month of me thinking this&lt;/em&gt;, they release storage, ai gateway, and functions. You can see it in their Discord, they are building at a very high velocity, which is great.&lt;/p&gt;

&lt;p&gt;(see the changelog from 4 days ago &lt;a href="https://neon.com/docs/changelog" rel="noopener noreferrer"&gt;[neon.com/changelog]&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Branching
&lt;/h2&gt;

&lt;p&gt;I think this is obvious.. if you're reading this, you are probably considering using Neon, so you already know about their branching functionality - creation time, separation of environment, etc. etc.&lt;/p&gt;

&lt;p&gt;So I won't write much about this. I wrote another big blog post about branching that you can read here: &lt;a href="https://denislavgavrilov.com/agentic-workflows-branched-environments" rel="noopener noreferrer"&gt;Denislav Gavrilov: Branched Environments&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>neon</category>
      <category>database</category>
      <category>serverless</category>
    </item>
    <item>
      <title>What email APIs cost in 2026: 7 workloads priced on 6 providers</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Wed, 07 Oct 2026 16:34:23 +0000</pubDate>
      <link>https://dev.to/kubeden/what-email-apis-cost-in-2026-7-workloads-priced-on-6-providers-2l8b</link>
      <guid>https://dev.to/kubeden/what-email-apis-cost-in-2026-7-workloads-priced-on-6-providers-2l8b</guid>
      <description>&lt;p&gt;Every email API prices differently. Some sell monthly tiers, some bill per email, and some charge for newsletters separately. So I priced the same seven workloads on Amazon SES, Mailgun, Postmark, Resend, SendGrid and SMTPfast.&lt;/p&gt;

&lt;p&gt;Prices come from the official pricing pages on October 6, 2026, in USD. Sources are at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Monthly cost, cheapest first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Side project&lt;/strong&gt; (1,000 emails): Mailgun $0 · Resend $0 · SMTPfast $0 · SES ~$0.11 · Postmark $15 · SendGrid $19.95&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early SaaS&lt;/strong&gt; (20,000 emails): SES ~$2 · SMTPfast $19 · SendGrid $19.95 · Resend $20 · Postmark $29.50 · Mailgun $33&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growing SaaS&lt;/strong&gt; (100,000 emails): SES ~$11 · SendGrid $34.95 · Resend $35 · SMTPfast $49 · Mailgun $75 · Postmark $115&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High volume&lt;/strong&gt; (500,000 emails): SES ~$53 · SMTPfast $349 · Resend $350 · SendGrid ~$387 · Mailgun $400 · Postmark $455&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1M emails:&lt;/strong&gt; SES ~$106 · Resend $650 · Mailgun $700 · SendGrid $733 · Postmark $775 · SMTPfast $849&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newsletter&lt;/strong&gt; (5,000 contacts, 4 sends): SES ~$2* · SMTPfast $19 · SendGrid $25 · Postmark $29.50* · Mailgun $33* · Resend $40&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch spike&lt;/strong&gt; (20k plan, 60k month): SES ~$6 · Resend $29 · SMTPfast $29 · SendGrid ~$33 · Postmark ~$82 · Mailgun $105&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;*Sending only, with no contact lists or broadcast tools included.&lt;/p&gt;

&lt;p&gt;Once you're past the free tiers, Amazon SES is the cheapest at every volume, by 3–10×. But it's raw infrastructure. Webhooks, logs and templates are yours to build. Among full email APIs, the cheapest option changes with volume: SMTPfast at 20k and 500k, SendGrid and Resend at 100k, and Resend at 1M.&lt;/p&gt;

&lt;h2&gt;
  
  
  How each one charges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SES:&lt;/strong&gt; $0.10 per 1,000 emails plus $0.12/GB of data. Since July 2026, new accounts default to a $0.16 plan, and you can switch back to $0.10 at any time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun:&lt;/strong&gt; monthly tiers from $15 (10k emails). Extra emails cost $0.40–$1.80 per 1,000 depending on the tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark:&lt;/strong&gt; monthly tiers from $15 (10k emails). Extra emails cost $1.15–$1.80 per 1,000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend:&lt;/strong&gt; $20 (50k) or $35 (100k), then Scale from $90. Extra emails cost $0.46–$0.90 per 1,000 once you turn overage on. Newsletters are billed separately, per contact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid:&lt;/strong&gt; Essentials from $19.95 (50k), Pro from $89.95 (100k, with a dedicated IP). Extra emails cost $0.47–$1.33 per 1,000. Newsletters are a separate product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast:&lt;/strong&gt; $9 (10k), $19 (50k) or $49 (200k), with newsletters included. Extra emails come from prepaid credits at $1 per 1,000. It runs on Amazon SES.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Free tiers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SES:&lt;/strong&gt; no free tier for new accounts since July 2026, only AWS credits ($100–200). New accounts start in a sandbox that sends 200 emails a day to verified addresses until AWS approves you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun:&lt;/strong&gt; 100 emails a day, 1 domain, 1 day of logs. No card, but without one you can only send to 5 verified addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark:&lt;/strong&gt; 100 emails a month, forever. No card. Postmark approves your account manually before you can email other domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend:&lt;/strong&gt; 3,000 emails a month (100 a day), 3 domains, 30-day logs, inbound included. No card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid:&lt;/strong&gt; no free plan since 2025, only a 60-day trial at 100 emails a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast:&lt;/strong&gt; 3,000 emails a month, 1 domain, 1,000 contacts, 2 broadcasts. No card. Sending access is reviewed before you can send.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The scenarios
&lt;/h2&gt;

&lt;p&gt;SES numbers assume about 50 KB per email and the $0.10 rate. Open "Show the math" under each one for the full calculation.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Side project
&lt;/h3&gt;

&lt;p&gt;1,000 emails a month: magic links, password resets and receipts.&lt;/p&gt;

&lt;p&gt;Free tiers decide this one. Mailgun, Resend and SMTPfast cover it for $0. Postmark's free 100 a month doesn't, and SendGrid's trial ends after 60 days.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun: $0.&lt;/strong&gt; Free plan, as long as no day goes over 100 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend: $0.&lt;/strong&gt; Free plan (3,000 a month, 100 a day).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast: $0.&lt;/strong&gt; Free plan (3,000 a month).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SES: ~$0.11.&lt;/strong&gt; 1,000 × $0.10/1k + 0.05 GB × $0.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark: $15.&lt;/strong&gt; Basic 10k. The free plan only covers 100.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid: $19.95.&lt;/strong&gt; Essentials 50K once the trial ends.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Early SaaS
&lt;/h3&gt;

&lt;p&gt;20,000 transactional emails a month.&lt;/p&gt;

&lt;p&gt;SMTPfast, SendGrid and Resend all land around $20 with room to grow to 50k. Plans that start at 10k pay overage for the second 10k.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SES: ~$2.&lt;/strong&gt; 20k × $0.10/1k + 1 GB × $0.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast: $19.&lt;/strong&gt; Growth (50k), with unlimited domains and open/click tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid: $19.95.&lt;/strong&gt; Essentials 50K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend: $20.&lt;/strong&gt; Pro (50k).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark: $29.50.&lt;/strong&gt; Pro 10k $16.50 + 10k × $1.30/1k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun: $33.&lt;/strong&gt; Basic $15 + 10k × $1.80/1k.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Growing SaaS
&lt;/h3&gt;

&lt;p&gt;100,000 emails a month.&lt;/p&gt;

&lt;p&gt;SendGrid and Resend both have a 100k tier around $35. SMTPfast's next tier jumps to 200k for $49.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SES: ~$11.&lt;/strong&gt; 100k × $0.10/1k + 5 GB × $0.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid: $34.95.&lt;/strong&gt; Essentials 100K. Shared IPs only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend: $35.&lt;/strong&gt; Pro 100k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast: $49.&lt;/strong&gt; Scale (200k), with a 99.9% SLA. Growth plus 50k of credits would be $69.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun: $75.&lt;/strong&gt; Foundation 100k, with a dedicated IP included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark: $115.&lt;/strong&gt; Basic 125k.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  4. High volume
&lt;/h3&gt;

&lt;p&gt;500,000 and 1M emails a month.&lt;/p&gt;

&lt;p&gt;At 500k, SMTPfast and Resend tie at about $350. At 1M, SMTPfast becomes the most expensive here, because every email past 200k costs $1 per 1,000 in credits.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;br&gt;
&lt;strong&gt;500,000 emails:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SES: ~$53.&lt;/strong&gt; 500k × $0.10/1k + 25 GB × $0.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast: $349.&lt;/strong&gt; Scale $49 + 300k × $1/1k in credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend: $350.&lt;/strong&gt; Scale 500k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid: ~$387.&lt;/strong&gt; Essentials 100K $34.95 + 400k × $0.88/1k. Pro 700K, with a dedicated IP, is $499.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun: $400.&lt;/strong&gt; Scale 500k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark: $455.&lt;/strong&gt; Basic 700k.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;1,000,000 emails:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SES: ~$106.&lt;/strong&gt; 1M × $0.10/1k + 50 GB × $0.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend: $650.&lt;/strong&gt; Scale 1M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun: $700.&lt;/strong&gt; Scale 1M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid: $733.&lt;/strong&gt; Pro 700K $499 + 300k × $0.78/1k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark: $775.&lt;/strong&gt; Basic 1.5M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast: $849.&lt;/strong&gt; Scale $49 + 800k × $1/1k in credits.
&lt;/li&gt;
&lt;/ul&gt;

&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Newsletter
&lt;/h3&gt;

&lt;p&gt;5,000 contacts, 4 sends a month (20,000 emails).&lt;/p&gt;

&lt;p&gt;Only SMTPfast, SendGrid and Resend include contact lists and broadcasts. SMTPfast is the cheapest of the three because it doesn't bill newsletters as a separate product.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SES: ~$2.&lt;/strong&gt; Sending only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast: $19.&lt;/strong&gt; Growth: 25k contacts, 50k emails, 50 broadcasts. Starter covers 5,000 contacts but only 10k emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid: $25.&lt;/strong&gt; Marketing Campaigns Basic 10K (10k contacts, 30k emails). No automation on Basic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark: $29.50.&lt;/strong&gt; Sending only, on a broadcast stream. Same price as transactional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun: $33.&lt;/strong&gt; Sending only. Mailgun has no broadcast product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend: $40.&lt;/strong&gt; Marketing Pro, 5,000 contacts with unlimited sends. Billed separately from transactional.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Launch spike
&lt;/h3&gt;

&lt;p&gt;You're on the plan from scenario 2, and a launch month sends 60,000 emails.&lt;/p&gt;

&lt;p&gt;Plans that include 50k absorb most of the spike. Plans that start at 10k pay $1.30–$1.80 for every extra 1,000.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SES: ~$6.&lt;/strong&gt; 60k × $0.10/1k + 3 GB × $0.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend: $29.&lt;/strong&gt; Pro $20 + 10k × $0.90/1k. Overage has to be switched on, otherwise sends fail after 50k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast: $29.&lt;/strong&gt; Growth $19 + 10k credits ($10). Credits are prepaid, otherwise sends are rejected after 50k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid: ~$33.&lt;/strong&gt; Essentials 50K $19.95 + 10k × $1.33/1k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark: ~$82.&lt;/strong&gt; Pro 10k $16.50 + 50k × $1.30/1k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun: $105.&lt;/strong&gt; Basic $15 + 50k × $1.80/1k.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Hidden costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SES:&lt;/strong&gt; webhooks need SNS or EventBridge, per-message logs need the paid deliverability dashboard, and a dedicated IP is $24.95/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun:&lt;/strong&gt; overage can't be turned off. The $15 plan has 1 domain and 1 day of logs; 30-day logs need Scale ($90+).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark:&lt;/strong&gt; overage can't be capped. The Basic plan has no inbound email, and DMARC monitoring is $14 per domain per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend:&lt;/strong&gt; every To, CC and BCC address counts as an email. Newsletters are billed per contact on top of transactional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid:&lt;/strong&gt; Essentials has no dedicated IP option and only 3 days of activity history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTPfast:&lt;/strong&gt; no dedicated IPs on any plan. Extra emails cost $1 per 1,000, about 4× its effective Scale rate, and sends are rejected once the allowance and credits run out.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  For most startups: SMTPfast
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0 to start:&lt;/strong&gt; 3,000 emails a month, no credit card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$19 covers a lot:&lt;/strong&gt; 50k emails, unlimited domains and open/click tracking (scenario 2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newsletters included:&lt;/strong&gt; contacts, segments and broadcasts in the same plan (scenario 5).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No surprise bills:&lt;/strong&gt; extra emails come from prepaid credits, so you never get an overage invoice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to switch:&lt;/strong&gt; the API is Resend-compatible, so moving is a base-URL and API-key change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revisit the choice past about 300k emails a month or when you need a dedicated IP. At 1M, Resend or SES directly costs less.&lt;/p&gt;

&lt;h3&gt;
  
  
  Everything else
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SES:&lt;/strong&gt; the lowest price at any volume, if you're happy to build webhooks, logs and templates yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailgun:&lt;/strong&gt; many domains, inbound routing, and a dedicated IP from $75/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmark:&lt;/strong&gt; 45-day logs and the same support on every plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend:&lt;/strong&gt; the cheapest full API at 1M emails, and the 100k tier at $35.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid:&lt;/strong&gt; 100k for $34.95, plus a marketing suite with automation on its Advanced plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Prices checked October 6, 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon SES: &lt;a href="https://aws.amazon.com/ses/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/pricing-plans.html" rel="noopener noreferrer"&gt;pricing plans&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html" rel="noopener noreferrer"&gt;sandbox&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mailgun: &lt;a href="https://www.mailgun.com/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Postmark: &lt;a href="https://postmarkapp.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://postmarkapp.com/support/article/1285-pricing-billing-faq" rel="noopener noreferrer"&gt;billing FAQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Resend: &lt;a href="https://resend.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://resend.com/docs/knowledge-base/account-quotas-and-limits" rel="noopener noreferrer"&gt;quotas and limits&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SendGrid: &lt;a href="https://www.twilio.com/en-us/products/email-api/pricing" rel="noopener noreferrer"&gt;Email API pricing&lt;/a&gt;, &lt;a href="https://www.twilio.com/en-us/products/marketing-campaigns/pricing" rel="noopener noreferrer"&gt;Marketing Campaigns pricing&lt;/a&gt;, &lt;a href="https://www.twilio.com/en-us/changelog/sendgrid-free-plan" rel="noopener noreferrer"&gt;free plan change&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SMTPfast: &lt;a href="https://smtpfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://smtpfa.st/blog/prepaid-email-credits" rel="noopener noreferrer"&gt;prepaid credits&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>email</category>
      <category>webdev</category>
      <category>saas</category>
      <category>api</category>
    </item>
    <item>
      <title>Neon vs Supabase vs PlanetScale vs AWS: what Postgres costs in 2026</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Tue, 06 Oct 2026 15:09:07 +0000</pubDate>
      <link>https://dev.to/kubeden/neon-vs-supabase-vs-planetscale-vs-aws-what-postgres-costs-in-2026-59o5</link>
      <guid>https://dev.to/kubeden/neon-vs-supabase-vs-planetscale-vs-aws-what-postgres-costs-in-2026-59o5</guid>
      <description>&lt;p&gt;Every database pricing page uses different units, so I priced the same six workloads on AWS (RDS and Aurora Serverless v2), Neon, PlanetScale and Supabase, with Convex on the side.&lt;/p&gt;

&lt;p&gt;Prices come from the official pricing pages on October 6, 2026 (AWS us-east-1). Sources are at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Monthly cost, cheapest first. "Aurora" means Aurora Serverless v2.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Side project&lt;/strong&gt; (0.8 GB, ~4 h/day): Neon $0 · PlanetScale $5 · Aurora ~$7 · RDS ~$18 · Supabase $25&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS busy in business hours&lt;/strong&gt; (10 GB, 4 GB RAM): Neon ~$27 · PlanetScale $28 · RDS ~$53 · Aurora ~$54 · Supabase $75&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mostly quiet with daily spikes to 16 GB RAM:&lt;/strong&gt; Neon ~$50 · PlanetScale ~$97 · Aurora ~$99 · Supabase $225&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A database for every PR&lt;/strong&gt; (40 PRs a month): Neon ~$3 · PlanetScale ~$20 · Supabase ~$39&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;50 mostly idle per-customer databases:&lt;/strong&gt; Neon ~$17 · Aurora ~$31 · PlanetScale $250 · Supabase $515 · RDS ~$700&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady 24/7 load&lt;/strong&gt; (4 GB RAM, 50 GB): PlanetScale $33 · RDS ~$57 · Supabase ~$82 · Neon ~$95 · Aurora ~$180&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usage-based databases (Neon, Aurora) win when the database has idle time. Fixed instances (PlanetScale, RDS) win when it's busy 24/7.&lt;/p&gt;

&lt;h2&gt;
  
  
  How each one charges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS RDS:&lt;/strong&gt; fixed instance per hour, from $0.016/hr. Always on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora Serverless v2:&lt;/strong&gt; $0.12 per ACU-hour (1 ACU ≈ 2 GB RAM). Scales to zero and wakes up in about 15 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convex:&lt;/strong&gt; per function call, GB stored and GB transferred. Not Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon:&lt;/strong&gt; $0.106 per CU-hour (1 CU ≈ 4 GB RAM). Scales to zero and wakes up in a few hundred milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale:&lt;/strong&gt; fixed clusters from $5/month. Always on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase:&lt;/strong&gt; $25/month for Pro, plus about $10/month per project (the first one is covered). Always on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Free tiers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS:&lt;/strong&gt; $100–200 in credits, and the Free plan ends after 6 months. Card required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convex:&lt;/strong&gt; 0.5 GB and 1M function calls a month. No card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon:&lt;/strong&gt; 1 GB per project (up from 0.5 GB on October 2), 100 projects, auth and storage included. No card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale:&lt;/strong&gt; no free tier. Plans start at $5/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase:&lt;/strong&gt; 500 MB, 2 projects, auth and storage included. Pauses after 7 days without activity. No card.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The scenarios
&lt;/h2&gt;

&lt;p&gt;Setups are matched by RAM, and a month is 730 hours. Open "Show the math" under each one for the full calculation.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Side project
&lt;/h3&gt;

&lt;p&gt;0.8 GB of data, used about 4 hours a day.&lt;/p&gt;

&lt;p&gt;Free tiers decide this one. It fits in Neon's 1 GB free plan but not in Supabase's 500 MB.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neon: $0.&lt;/strong&gt; 120 h × 0.25 CU = 30 CU-hours, inside the free 100.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale: $5.&lt;/strong&gt; PS-5 single node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora: ~$7.&lt;/strong&gt; 120 h × 0.5 ACU × $0.12, plus $0.10 storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS: ~$18.&lt;/strong&gt; db.t4g.micro $11.68 + 20 GB storage $2.30 + public IPv4 $3.65.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase: $25.&lt;/strong&gt; Over the 500 MB free limit, so Pro. The $10 credit covers the instance.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. SaaS busy in business hours
&lt;/h3&gt;

&lt;p&gt;10 GB of data and 4 GB RAM, busy 10 hours a day on weekdays.&lt;/p&gt;

&lt;p&gt;Neon and a PlanetScale single node tie at $27–28. Everything else costs 2–3× more.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neon: ~$27.&lt;/strong&gt; 220 h × 1 CU × $0.106 + 10 GB × $0.35. The Launch plan has no SLA. The Scale plan, with a 99.95% SLA, makes it ~$52.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale: $28.&lt;/strong&gt; PS-40 single node, no SLA. The 3-node version is $83.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS: ~$53.&lt;/strong&gt; db.t4g.medium $47.45 + storage $2.30 + IPv4 $3.65.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora: ~$54.&lt;/strong&gt; 220 h × 2 ACU × $0.12 + $1 storage, pausing outside business hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase: $75.&lt;/strong&gt; Pro $25 + Medium (4 GB) $60 − $10 credit.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Mostly quiet with daily spikes to 16 GB RAM
&lt;/h3&gt;

&lt;p&gt;20 GB of data. Light load for 22 hours a day, then a 2-hour spike that needs 16 GB RAM.&lt;/p&gt;

&lt;p&gt;Fixed instances have to be sized for the spike all month. Autoscaling databases pay for it only while it lasts.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neon: ~$50.&lt;/strong&gt; (0.25 CU × 22 h + 4 CU × 2 h) × 30 days = 405 CU-hours × $0.106, plus 20 GB × $0.35.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale: ~$97.&lt;/strong&gt; PS-160 single node (16 GB) $96 + $1.25 extra storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora: ~$99.&lt;/strong&gt; (0.5 × 22 + 8 × 2) × 30 = 810 ACU-hours × $0.12, plus $2 storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase: $225.&lt;/strong&gt; XL (16 GB) $210 + Pro $25 − $10 credit. No autoscaling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS:&lt;/strong&gt; not priced. It has the same problem as PlanetScale and Supabase.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  4. A database for every PR
&lt;/h3&gt;

&lt;p&gt;40 pull requests a month, each with its own database for 3 days.&lt;/p&gt;

&lt;p&gt;Neon branches cost almost nothing while idle. Supabase and PlanetScale branches bill every hour they exist, and AWS has no built-in branching.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neon: ~$3.&lt;/strong&gt; 40 × 3 active hours × 0.25 CU × $0.106. Branches share storage with their parent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale: ~$20.&lt;/strong&gt; $5/month per dev branch, prorated: 40 × 72/730 × $5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase: ~$39.&lt;/strong&gt; A Micro instance per branch: 40 × 72 h × $0.01344.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS:&lt;/strong&gt; no built-in branching. You can script snapshot restores or Aurora clones yourself.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  5. 50 mostly idle per-customer databases
&lt;/h3&gt;

&lt;p&gt;50 databases of 200 MB each, each active about 10 hours a month. If you put every customer in one shared database, see scenario 2 instead.&lt;/p&gt;

&lt;p&gt;Idle databases cost only storage on Neon and Aurora. On fixed instances you pay for 50 servers.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neon: ~$17.&lt;/strong&gt; 50 × 10 h × 0.25 CU × $0.106 + 10 GB × $0.35.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora: ~$31.&lt;/strong&gt; 50 × 10 h × 0.5 ACU × $0.12 + $1 storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale: $250.&lt;/strong&gt; 50 × $5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase: $515.&lt;/strong&gt; 50 × $10 + Pro $25 − $10 credit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS: ~$700.&lt;/strong&gt; 50 × (db.t4g.micro $11.68 + storage $2.30).
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Steady 24/7 load
&lt;/h3&gt;

&lt;p&gt;50 GB of data and 4 GB RAM, busy around the clock.&lt;/p&gt;

&lt;p&gt;Fixed instances win. If your database is busy more than about 61% of the month, a fixed RDS instance beats Neon on compute.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Show the math
  &lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale: $33.&lt;/strong&gt; PS-40 single node $28 + 40 GB × $0.125. High availability costs $98, because storage is billed on all 3 nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS: ~$57.&lt;/strong&gt; db.t4g.medium $47.45 + 50 GB × $0.115 + IPv4 $3.65.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase: ~$82.&lt;/strong&gt; Pro $25 + Medium $60 − $10 credit + 52 GB extra disk × $0.125.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon: ~$95.&lt;/strong&gt; 730 h × 1 CU × $0.106 + 50 GB × $0.35.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora: ~$180.&lt;/strong&gt; 730 h × 2 ACU × $0.12 + $5 storage.
&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where Convex fits
&lt;/h2&gt;

&lt;p&gt;Convex is a reactive document database you query with TypeScript functions instead of SQL. It bills per function call ($2.20 per million after the free 1M), so a small app can cost pennies. Pro is $25 per developer per month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hidden costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS:&lt;/strong&gt; $3.65/month per public IPv4 address. RDS Proxy on Aurora costs at least ~$88/month and blocks scale-to-zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convex:&lt;/strong&gt; hitting the team spending limit disables every project in the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon:&lt;/strong&gt; no hard spending cap, only email alerts. Persistent connections stop compute from scaling to zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale:&lt;/strong&gt; high-availability storage is billed on all 3 nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase:&lt;/strong&gt; every project and branch pays for compute 24/7, and the spend cap doesn't cover compute.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For most startups: Neon
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0 to start:&lt;/strong&gt; 1 GB per project, 100 projects, no credit card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You pay for usage:&lt;/strong&gt; ~$27/month for a business-hours SaaS (scenario 2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap previews:&lt;/strong&gt; ~$3/month for a database per PR (scenario 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full backend:&lt;/strong&gt; auth, storage, functions and a Data API that branch with the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revisit the choice once your database is busy around the clock (scenario 6).&lt;/p&gt;

&lt;h3&gt;
  
  
  Everything else
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS RDS:&lt;/strong&gt; steady 24/7 load, or you need to stay inside AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora Serverless v2:&lt;/strong&gt; inside your AWS VPC with idle time, if 15-second cold starts are fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convex:&lt;/strong&gt; a reactive TypeScript app that doesn't need SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PlanetScale:&lt;/strong&gt; the cheapest fixed instance for steady load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase:&lt;/strong&gt; auth, storage, functions and realtime on one platform at a fixed monthly price.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Prices checked October 6, 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS: &lt;a href="https://aws.amazon.com/rds/postgresql/pricing/" rel="noopener noreferrer"&gt;RDS for PostgreSQL&lt;/a&gt;, &lt;a href="https://aws.amazon.com/rds/aurora/pricing/" rel="noopener noreferrer"&gt;Aurora&lt;/a&gt;, &lt;a href="https://aws.amazon.com/free/" rel="noopener noreferrer"&gt;Free Tier&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Convex: &lt;a href="https://www.convex.dev/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Neon: &lt;a href="https://neon.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://neon.com/docs/introduction/plans" rel="noopener noreferrer"&gt;plans&lt;/a&gt;, &lt;a href="https://neon.com/docs/changelog/2026-09-18" rel="noopener noreferrer"&gt;backend launch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PlanetScale: &lt;a href="https://planetscale.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://planetscale.com/docs/postgres/pricing" rel="noopener noreferrer"&gt;Postgres pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Supabase: &lt;a href="https://supabase.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://supabase.com/docs/guides/platform/manage-your-usage/compute" rel="noopener noreferrer"&gt;compute billing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>database</category>
      <category>postgres</category>
    </item>
    <item>
      <title>gctx: git auth manager</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Tue, 27 Jan 2026 13:42:39 +0000</pubDate>
      <link>https://dev.to/kubeden/gctx-git-auth-manager-17c1</link>
      <guid>https://dev.to/kubeden/gctx-git-auth-manager-17c1</guid>
      <description>&lt;p&gt;I created a kubectx-inspired git auth manager&lt;/p&gt;

&lt;p&gt;kubeden/gctx:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sets a global git identity&lt;/li&gt;
&lt;li&gt;rewrites origin to an ssh alias&lt;/li&gt;
&lt;li&gt;allows deterministic auth for multiple accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it at: &lt;a href="//github.com/kubeden/gctx"&gt;github.com/kubeden/gctx&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fjpydnflgjc2guo7h5uv5.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fjpydnflgjc2guo7h5uv5.gif" alt="https://raw.githubusercontent.com/kubeden/gctx/main/assets/demo.gif" width="1000" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
    </item>
    <item>
      <title>How to build a good container platform</title>
      <dc:creator>kuberdenis</dc:creator>
      <pubDate>Mon, 24 Nov 2025 21:01:34 +0000</pubDate>
      <link>https://dev.to/kubeden/how-to-build-a-good-container-platform-gk7</link>
      <guid>https://dev.to/kubeden/how-to-build-a-good-container-platform-gk7</guid>
      <description>&lt;p&gt;How to build a good container platform&lt;/p&gt;

&lt;h2&gt;
  
  
  Description
&lt;/h2&gt;

&lt;p&gt;Platform Engineering is the act of designing, provisioning, and maintaining a platform.&lt;/p&gt;

&lt;p&gt;You provision resources.&lt;br&gt;
You configure resources.&lt;br&gt;
And you use your platform.&lt;/p&gt;

&lt;p&gt;It sounds easy but it's not.&lt;br&gt;
Most engineering teams fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blocks
&lt;/h2&gt;

&lt;p&gt;In order to build a good platform, you need to build logically.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Base (Cloud / On-Prem / Hybrid Cloud / Multi-Cloud)&lt;/li&gt;
&lt;li&gt;Application Engine [(k8s / vms / serverless) in our case k8s]&lt;/li&gt;
&lt;li&gt;Platform Components (monitoring, application, recovery, automation)&lt;/li&gt;
&lt;li&gt;Application Components (ci/cd, configuration repo)&lt;/li&gt;
&lt;li&gt;Subtleties (secrets management &amp;amp; rotation, states, processes)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Organizations hire teams, often to do one thing. Which is wrong.&lt;br&gt;
How one team does something, is often not how the others will.&lt;/p&gt;

&lt;p&gt;Expertise is often different, which leads to "know-how drifts" and technical debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standartization
&lt;/h2&gt;

&lt;p&gt;This is maybe the most important foundation block for a platform to thrive.&lt;/p&gt;

&lt;p&gt;Standartization is almost always ignored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name suffixes &amp;amp; directory structures&lt;/li&gt;
&lt;li&gt;utility separation&lt;/li&gt;
&lt;li&gt;unified deployment methods&lt;/li&gt;
&lt;li&gt;documentation &amp;amp; knowledge transfers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the "atoms" in a good platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Name Suffixes &amp;amp; Variability
&lt;/h2&gt;

&lt;p&gt;Name suffixes are your map points.&lt;br&gt;
Directory structures are your streets.&lt;/p&gt;

&lt;p&gt;Every organization should have strictly defined name suffixes and directory structures that should be applied across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;iac repositories&lt;/li&gt;
&lt;li&gt;application management repositories&lt;/li&gt;
&lt;li&gt;cloud resources&lt;/li&gt;
&lt;li&gt;secret namings&lt;/li&gt;
&lt;li&gt;service accounts&lt;/li&gt;
&lt;li&gt;and so on.. and so on..&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good name suffix system includes 3 to 7 matching variables.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer&lt;/li&gt;
&lt;li&gt;region&lt;/li&gt;
&lt;li&gt;environment&lt;/li&gt;
&lt;li&gt;iteration&lt;/li&gt;
&lt;li&gt;type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Continuing the example, this would translate to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;e.g. directory structure: ${customer}/${region}/${environment}/${type}-[${iteration}?]&lt;/li&gt;
&lt;li&gt;e.g. terraform resource: ${customer}-${region}-${environment}-${iteration}-${type}&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine this at scale.&lt;br&gt;
It is easy to navigate it now.&lt;br&gt;
It is easy to understand.&lt;/p&gt;

&lt;p&gt;It is.. nice to work with! :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Utility Separation
&lt;/h2&gt;

&lt;p&gt;Many organizations don't separate.&lt;/p&gt;

&lt;p&gt;They create a repository called &lt;code&gt;devops&lt;/code&gt; and they put everything inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;terraform&lt;/li&gt;
&lt;li&gt;bash scripts&lt;/li&gt;
&lt;li&gt;yaml&lt;/li&gt;
&lt;li&gt;helm&lt;/li&gt;
&lt;li&gt;kustomize&lt;/li&gt;
&lt;li&gt;ansible&lt;/li&gt;
&lt;li&gt;and god know what else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, you should be grouping logically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;infrastructure provisioning: manage the lifecycle of your resources&lt;/li&gt;
&lt;li&gt;infrastructure configuration: manage the configuration on your "live" resources&lt;/li&gt;
&lt;li&gt;application deployment: application configuration&lt;/li&gt;
&lt;li&gt;application code: well... your application code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is an example of well-separated &amp;amp; logically-grouped repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;infrastructure: terraform, bash (as few as possible)&lt;/li&gt;
&lt;li&gt;k8s-cluster-configuration: yaml, kustomize, helm, jsonnet&lt;/li&gt;
&lt;li&gt;[application]-k8s-configuration: standartized releases / deployments / libs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, you achieve the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a single place to provision / depracate infra&lt;/li&gt;
&lt;li&gt;a single place to provision / depracate platform components&lt;/li&gt;
&lt;li&gt;a single place to provision / depracate applications&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Platform Resources: Provisioning
&lt;/h2&gt;

&lt;p&gt;Regardless of the technology you choose, you should handle provisioning of resources as a standalone practice.&lt;/p&gt;

&lt;p&gt;Here is an exapmle of a good Terraform set-up:&lt;/p&gt;

&lt;p&gt;Repository: &lt;code&gt;infrastructure&lt;/code&gt;&lt;br&gt;
Directory structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modules/&lt;/li&gt;
&lt;li&gt;base/&lt;/li&gt;
&lt;li&gt;.ci(or .github, or whatever the standard is)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where in /modules, you treat it as "libs", a place where you manage the versioning of your modules (community or personal).&lt;br&gt;
Example continuation of modules can looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modules/

&lt;ul&gt;
&lt;li&gt;azure/&lt;/li&gt;
&lt;li&gt;azurerm/

&lt;ul&gt;
&lt;li&gt;key-vault/&lt;/li&gt;
&lt;li&gt;community/&lt;/li&gt;
&lt;li&gt;personal/&lt;/li&gt;
&lt;li&gt;acr/&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;/azuread

&lt;ul&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;/aws&lt;/li&gt;

&lt;li&gt;/control-tower

&lt;ul&gt;
&lt;li&gt;community/&lt;/li&gt;
&lt;li&gt;personal/&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;/rds

&lt;ul&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;/s3

&lt;ul&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;And in base, you call these modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;base/

&lt;ul&gt;
&lt;li&gt;[customer]/&lt;/li&gt;
&lt;li&gt;[ region ]/

&lt;ul&gt;
&lt;li&gt;[ environment]/&lt;/li&gt;
&lt;li&gt;common/

&lt;ul&gt;
&lt;li&gt;locals.tf&lt;/li&gt;
&lt;li&gt;provider.tf&lt;/li&gt;
&lt;li&gt;key-vault.tf&lt;/li&gt;
&lt;li&gt;acr.tf&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;You never click in the cloud provider.&lt;br&gt;
But you also don't configure here.&lt;br&gt;
You do not "hack" yamls.&lt;br&gt;
You do not apply hard-to-figure-out scripts.&lt;/p&gt;

&lt;p&gt;You only spawn and delete. :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform Components: Configuration
&lt;/h2&gt;

&lt;p&gt;In order to have a good platform, you need to be flexible, clear, and rock-solid in terms of its capabilities.&lt;/p&gt;

&lt;p&gt;One of the most hardened platforms I've ever built (and I built it 3 separate times) is as follows:&lt;/p&gt;

&lt;p&gt;Management:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ArgoCD: Management, IAM, GitOps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Routing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ingress-nginx: routing, one lb, reserve ips in your subnet, manage routing programmatically&lt;/li&gt;
&lt;li&gt;cert-manager: automated ssl&lt;/li&gt;
&lt;li&gt;external-dns: automated dns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Secrets Management:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;external-secrets: vaults, stores, you name it&lt;/li&gt;
&lt;li&gt;sealed-secrets: plain-text secrets in your repo (for stuff like the initial argocd secret)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monitoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grafana-operator: well.. grafana :) graphs and stats&lt;/li&gt;
&lt;li&gt;prometheus: metrics&lt;/li&gt;
&lt;li&gt;blackbox-exporter: alerts&lt;/li&gt;
&lt;li&gt;thanos (if multi-cluster): consolidation of metrics&lt;/li&gt;
&lt;li&gt;eck: elk :)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Backups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;velero: backups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scaling &amp;amp; State:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reloader: re-apply configs&lt;/li&gt;
&lt;li&gt;keda: scale applications&lt;/li&gt;
&lt;li&gt;karpenter (if on AWS, Azure have it by default): scale nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this should be managed through a single ArgoCD application called "cluster-apps" or similar.&lt;/p&gt;

&lt;p&gt;Tools like Jsonnet and Kustomize are your best friends here.&lt;/p&gt;

&lt;p&gt;(read about the app-of-apps pattern too)&lt;br&gt;
((no i do not like applicationsets))&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Configuration
&lt;/h2&gt;

&lt;p&gt;Your application configuration repositories should be standartized.&lt;/p&gt;

&lt;p&gt;A well-thought-of application configuration repository is no different than the infrastructure, or the platform repositories.&lt;/p&gt;

&lt;p&gt;You should again think in the following direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;releases: packages&lt;/li&gt;
&lt;li&gt;libs: building blocks for /releases&lt;/li&gt;
&lt;li&gt;deployments: calling your packages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You see, the same concept applies over and over again -- manageability, readibility, and standartization.&lt;/p&gt;

&lt;p&gt;I usually go for Jsonnet, but I wanted something simpler, so I went for Kustomize, only to find myself in a very thight situation, and ultimately decided upon with Helm.&lt;/p&gt;

&lt;p&gt;Jsonnet is awesome, but it often requires a lot of special expertise and in case your main engineer leaves, it is possible you're going to have problems finding adequate peresonel.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD
&lt;/h2&gt;

&lt;p&gt;Since we are mostly relying on GitOps for our platform, the CI/CDs worth mentioning are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;infrastructure:

&lt;ul&gt;
&lt;li&gt;terraform plan/apply: one pipeline per resource type (e.g. ${customer}/${region}/${environment}/${logical-group}/${resource-type}-${iteration}/pipeline.yml&lt;/li&gt;
&lt;li&gt;pipeline-of-pipelines: a global pipeline to manage the update / management of the child pipelines&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;application:

&lt;ul&gt;
&lt;li&gt;build &amp;amp; test: well, build and test&lt;/li&gt;
&lt;li&gt;deploy: since we're relying on GitOps, our deploy is a single git line change :)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;k8s-cluster-configuration:

&lt;ul&gt;
&lt;li&gt;a pipeline to bootstrap the master argocd (optional, since you are &lt;em&gt;allowed&lt;/em&gt; to do this through Terraform the first time :))&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Notable mentions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kubent: kube no trouble&lt;/li&gt;
&lt;li&gt;opa policy-bound pipelines: hard, but very rewarding if done correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preview Environments
&lt;/h2&gt;

&lt;p&gt;Preview environments are environments that are automatically spun up.&lt;/p&gt;

&lt;p&gt;In our case, we do this through Pull Requests.&lt;/p&gt;

&lt;p&gt;On every pull request against a branch, a label kicks in, and ArgoCD (through ApplicationSets... &lt;em&gt;angry&lt;/em&gt;) creates a new application and automatically syncs it. After that, through the use of ASO (Azure Service Operator) we provision a database.&lt;/p&gt;

&lt;p&gt;Upon a pull request close, the application is automatically deleted, and with it, the resource managed through ASO, too.&lt;/p&gt;

&lt;p&gt;Two other possible ways to achieve this are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cnpg operator: it connects to a live database, dumps the database, and provisions it. Authentication happens through external secrets.&lt;/li&gt;
&lt;li&gt;PVC + Snapshot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With preview (or dynamic as I like to call them) environments, developers have instant access to a staging environment, automatically provisioned for them, with their pull request. Very powerful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subtleties
&lt;/h2&gt;

&lt;p&gt;Here are important subtleties many engineering teams, even though good, often fail to deliver on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ArgoCD configuration: should happen programmatically, this is a common chicken-egg problem, which I personally solve by having the entire ArgoCD helm locally, pointing to it from inside the ArgoCD, which I provision through CI/CD manually the first time. This applies to the master CI/CD. In a child-instances configuration, all child ArgoCDs are managed through the master. Through GIT. Then the rest, like projects, groups, roles, policies... all should happen in your k8s repository.&lt;/li&gt;
&lt;li&gt;Cluster Apps: I use the app-of-apps pattern, which I achieve with either Kustomize or Jsonnet. Kustomize is often easier to maintain. I do not use ApplicationSets because it is harder to apply my name suffix &amp;amp; variability philosophies there.&lt;/li&gt;
&lt;li&gt;State: Always remote&lt;/li&gt;
&lt;li&gt;IAM: Always in either Terraform or the k8s (for ArgoCD) repos. Always add to groups. Do not apply one-time assignments.&lt;/li&gt;
&lt;li&gt;Secrets: I always strive to use OIDC. Expired secrets suck and OIDC is an incredible technology that solves this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another important thing is expertise. I personally have been shooting educational videos of how our entire platform works, every couple of months. I have also made the following docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;required-tools.md: kubectx/kubens, az cli, tf cli, etc. etc.&lt;/li&gt;
&lt;li&gt;onboarding.md: here I explain the concept of variability, the variables for our organization, and link to a short video where I demo the concept by finding a resource in both Azure and in the Terraform repository, and make a small application configuration through the k8s repo, and an application repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those have proven to be very valuable as we onboarded 10+ people through them.&lt;/p&gt;

&lt;p&gt;So.. the conclusion is, "find someone who is willing to teach".&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Thank you for reading and I hope you learned something, got inspired, or fired up. Either way, I enjoyed writing this.&lt;/p&gt;




&lt;p&gt;Denis&lt;/p&gt;

&lt;p&gt;out 🫡&lt;/p&gt;

</description>
      <category>argocd</category>
      <category>devops</category>
      <category>gitops</category>
    </item>
  </channel>
</rss>
