<?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: erdonline</title>
    <description>The latest articles on DEV Community by erdonline (@erdonline).</description>
    <link>https://dev.to/erdonline</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%2F4099832%2Fe3f834bb-61d9-4770-a0c7-9b3599b1e64f.png</url>
      <title>DEV Community: erdonline</title>
      <link>https://dev.to/erdonline</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erdonline"/>
    <language>en</language>
    <item>
      <title>Can I veto the agent's DDL before it hits main?</title>
      <dc:creator>erdonline</dc:creator>
      <pubDate>Sun, 30 Aug 2026 13:09:56 +0000</pubDate>
      <link>https://dev.to/erdonline/can-i-veto-the-agents-ddl-before-it-hits-main-3n0</link>
      <guid>https://dev.to/erdonline/can-i-veto-the-agents-ddl-before-it-hits-main-3n0</guid>
      <description>&lt;h2&gt;
  
  
  Two hours before go-live, the DDL lands in the group chat
&lt;/h2&gt;

&lt;p&gt;Thursday, 10 p.m. The release window is 1 a.m. Someone drops &lt;code&gt;alter_orders_v47.sql&lt;/code&gt; into the channel: "Agent generated it. CI is green. Can we merge?"&lt;/p&gt;

&lt;p&gt;You are the DBA. You are not the Tech Lead who clicked Approve this afternoon — that already happened. You are the last person who can still say no. You open the file: add columns, change types, add indexes, attach foreign keys. The parser is happy. The names look like your team's. You know the things that actually blow up are not in the line numbers: how long this table locks tonight, whether &lt;code&gt;NOT NULL&lt;/code&gt; without a default survives old rows, whether down can restore the schema you have now.&lt;/p&gt;

&lt;p&gt;You do not have time to stand up a production-sized database and run this &lt;code&gt;ALTER&lt;/code&gt;. You also do not have production credentials you are willing to hand an Agent so it can "verify." So the veto shows up two hours before go-live, on a change already sitting in a PR about to hit main — not when the intent was first proposed.&lt;/p&gt;

&lt;p&gt;Say no and you look like you are blocking the release. Say yes and the postmortem asks which gate was the gate. Last time it went like this: an add-column on &lt;code&gt;biz_order&lt;/code&gt; had no default. Staging was small; it passed. Peak production locked waiters and stopped downstream reconciliation for twenty minutes. That SQL was Approved in the afternoon. You saw it at night. The thing you were supposed to veto arrived two time zones late.&lt;/p&gt;

&lt;h2&gt;
  
  
  This failure has a name: the veto arrived too late
&lt;/h2&gt;

&lt;p&gt;This is not "the DBA is too slow." It has a name — &lt;strong&gt;the veto arrived too late&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Agents write DDL faster; humans still review DDL at the old speed. Intent hides in a SQL file that only enters the group chat after merge is already in motion: you see statements that will execute, not "relative to the last approved version, which three changes did the team actually agree to." By the time you speak, the train is already on the bridge.&lt;/p&gt;

&lt;p&gt;A Lead's Approve answers "did I look at this intent?" (that is a different job). The DBA has to answer a different sentence: &lt;strong&gt;can I say no to the intent before it hits main?&lt;/strong&gt; Those are not the same button. Approve is signed on the PR. Veto has to happen before the intent is treated as already human-reviewed.&lt;/p&gt;

&lt;p&gt;A red line sits on top of that: production credentials do not belong in the IDE. Banning the Agent from the database does not make intent show up earlier — it just sends the Agent off to invent SQL that "looks runnable," which you first see the night before go-live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things you already tried
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Review after merge.&lt;/strong&gt; Useful. That is not a veto; it is an after-the-fact blessing. If that DDL blows up, the record says "DBA looked."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hand-diff before deploy.&lt;/strong&gt; Line up the PR SQL against today's &lt;code&gt;CREATE TABLE&lt;/code&gt;. You are still aligning statement noise: &lt;code&gt;VARCHAR(64)&lt;/code&gt; to &lt;code&gt;VARCHAR(128)&lt;/code&gt; fills half the screen; a missing foreign key hides on line 47. When you finish, you still do not know whether those are the three changes the last approved version actually agreed to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ban the Agent from the database.&lt;/strong&gt; Correct. Credentials in &lt;code&gt;.cursor/mcp.json&lt;/code&gt; get committed and screenshotted. After the ban the Agent does not disappear. It reads a stale &lt;code&gt;@schema.sql&lt;/code&gt;, a permission-filtered &lt;code&gt;information_schema&lt;/code&gt; dump, or it invents. Intent is still invisible before merge.&lt;/p&gt;

&lt;p&gt;Each of those does half the job. Together you still get executable DDL two hours before go-live.&lt;/p&gt;

&lt;h2&gt;
  
  
  A live catalog does not save this half of the gate
&lt;/h2&gt;

&lt;p&gt;Someone will say: wire a live-catalog MCP so column names stop hallucinating and the DDL is "more true." That half of the column problem is another article — permission-filtered &lt;code&gt;information_schema&lt;/code&gt; is another hallucination, a 240-table dump blows context, tool lists cache until restart. Even when structure reads correctly, &lt;strong&gt;what the DBA needs to veto is not "does this column exist."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gate asks: is this change the intent the team already approved? Can the statements that will run align with the last &lt;strong&gt;approved contract&lt;/strong&gt;? A live catalog shows what the database looks like now. It does not show what humans agreed to. The database is current state; the contract is a promise. Two hours before go-live, lining SQL up against current state only checks whether the Agent invented against production — you still did not get a window to say no.&lt;/p&gt;

&lt;p&gt;Structure is not semantics. &lt;code&gt;status CHAR(1)&lt;/code&gt; is correct in the catalog; "only &lt;code&gt;'1'&lt;/code&gt; is valid, &lt;code&gt;'9'&lt;/code&gt; is dirty" lives in comments and review notes, not in the live DB. An Agent that writes a mechanically plausible &lt;code&gt;ALTER&lt;/code&gt; from the catalog is the hardest to veto: every line looks familiar; together you cannot tell what it will do.&lt;/p&gt;

&lt;p&gt;So the missing piece is not a faster SQL reader. It is an &lt;strong&gt;intent diff you can see before merge&lt;/strong&gt;, plus an optional DDL draft generated from an approved version — &lt;strong&gt;still not executed&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swap the gate object: intent first, DDL second
&lt;/h2&gt;

&lt;p&gt;Move from "Agent dumps SQL → human signs the night before merge" to &lt;strong&gt;"Agent declares intent → save a named version → human diffs before merge, optionally reads a DDL draft → can still veto."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The team maintains schema in the designer. Each change that is actually accepted saves as a &lt;strong&gt;named version&lt;/strong&gt;. That &lt;code&gt;projectJSON&lt;/code&gt; is the contract: human-reviewed, API layer strips &lt;code&gt;profile.dbs&lt;/code&gt; credentials, Chinese names and remarks carry tribal knowledge next to the structure.&lt;/li&gt;
&lt;li&gt;When the Agent needs a schema change, the only write path in this copy is &lt;strong&gt;&lt;code&gt;create_version&lt;/code&gt;&lt;/strong&gt;: submit a suggestion with a note. Do not treat &lt;code&gt;put_project_json&lt;/code&gt; as a tutorial — that overwrites the workspace; API 200 is not human approval.&lt;/li&gt;
&lt;li&gt;Before merge, the DBA opens a &lt;strong&gt;semantic diff against the last approved version&lt;/strong&gt;: which table was added, which field removed, which FK moved from A to B. Not an eighty-line character diff of &lt;code&gt;ALTER&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When you need a starting point for the migrator, generate a DDL draft from &lt;strong&gt;that approved version&lt;/strong&gt;. The draft is marked &lt;code&gt;previewOnly: true&lt;/code&gt;, &lt;code&gt;executed: false&lt;/code&gt;. ERD &lt;strong&gt;does not execute SQL&lt;/strong&gt;, does not connect to your database, and does not replace Atlas / Flyway / your CI.&lt;/li&gt;
&lt;li&gt;This gate can still say no. The veto happens before intent enters main — not two hours before the release window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One sentence vs "read SQL the night before go-live": &lt;strong&gt;the gate moves earlier; the veto stays; the Agent never gets production credentials.&lt;/strong&gt; The Lead signs intent (that article stops at the Approve button). You sign "this intent may enter the merge pipeline." Neither gate is "CI was green, so a human looked." When you veto, you leave a traceable intent diff — which table, which column, which FK — not a chat message that says "don't merge tonight."&lt;/p&gt;

&lt;p&gt;Be honest about the draft: today it is a conservative &lt;code&gt;CREATE TABLE&lt;/code&gt; from a named version, filtered by dialect and table. It is &lt;strong&gt;not&lt;/strong&gt; the &lt;code&gt;ALTER&lt;/code&gt; that will run in production, and it is not a down migration. Lock time, backfill, and canaries stay your CI and your window. The draft lets you ask "after the contract looks like this, what does CREATE look like?" — not let an Agent click execute. Missing indexes, missing FKs, dialect type mismatches sit in &lt;code&gt;warnings&lt;/code&gt;. You review them. The tool does not pretend it already did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP does here (you only want it at this point)
&lt;/h2&gt;

&lt;p&gt;By now you might want MCP — not installed in sentence one.&lt;/p&gt;

&lt;p&gt;Reading the contract, aligning two versions, and taking a draft that does not execute is enough to move the gate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;diff_versions&lt;/code&gt;&lt;/strong&gt;: semantic diff between two named version snapshots. Tables and columns added, removed, modified; table and field remarks; conservative &lt;code&gt;renameCandidates&lt;/code&gt; from structural fingerprints. Rename candidates need a human; they are not facts. API success is not approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;preview_ddl&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;CREATE TABLE&lt;/code&gt; draft from one named version snapshot, MySQL / PostgreSQL / SQL Server / Oracle, optional table filter. Returns &lt;code&gt;previewOnly: true&lt;/code&gt;, &lt;code&gt;executed: false&lt;/code&gt;, and warnings that nothing connected and nothing ran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;create_version&lt;/code&gt;&lt;/strong&gt;: the only write in this copy. The Agent reads the approved version, submits a suggestion; a human diffs in the designer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;diff_versions&lt;/code&gt; returns a review object, not an ALTER dump:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tablesAdded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tablesRemoved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tablesChanged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"columnsAdded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"columnsRemoved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"columnsModified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"renameCandidates are structural hints for human review, not confirmed renames. API success is not approval."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three changes sit in the summary. You can point at those three and say no without hunting "which three lines are hallucinated" in eighty lines of SQL.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;preview_ddl&lt;/code&gt; writes the boundary into the payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saved-version-snapshot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dialect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mysql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"previewOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"executed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Preview only: ERD Online MCP never connects to a database and never executes SQL."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read the contract: &lt;a href="{{DOC:guide/api-and-mcp}}"&gt;Read ER diagrams from Cursor via MCP&lt;/a&gt;. How to save a version and open a diff: &lt;a href="{{DOC:guide/save-version-and-diff}}"&gt;Save a version and view the diff&lt;/a&gt;. When a pager asks which approved change introduced a column — that is another job, table-sliced version history. This article does not pretend that exists yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CTA is not "install our MCP"
&lt;/h2&gt;

&lt;p&gt;The next step is not "go install an MCP," and it is not wiring production into the IDE. It is more basic: &lt;strong&gt;save a named version so merge has a last approved baseline to align against.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open the demo, change one table, save a version, change something else, save again, open the diff — in 30 seconds you see what "field added" and "FK retargeted" look like. After that, the next time an Agent drops DDL before a release window, you can reply: "Call &lt;code&gt;create_version&lt;/code&gt; first. Send me the diff against the last approved version. DDL drafts come from the approved version. ERD does not execute."&lt;/p&gt;

&lt;p&gt;Without a last approved version, the veto only shows up two hours before go-live. With it, you can say no before merge.&lt;/p&gt;

&lt;p&gt;{{CTA}}&lt;/p&gt;

&lt;h1&gt;
  
  
  MCP #DatabaseDesign #ERD #Agent
&lt;/h1&gt;

</description>
      <category>database</category>
      <category>devops</category>
      <category>sql</category>
    </item>
    <item>
      <title>The agent wrote the migration. Are you really going to Approve it?</title>
      <dc:creator>erdonline</dc:creator>
      <pubDate>Sun, 30 Aug 2026 06:35:49 +0000</pubDate>
      <link>https://dev.to/erdonline/the-agent-wrote-the-migration-are-you-really-going-to-approve-it-4l6d</link>
      <guid>https://dev.to/erdonline/the-agent-wrote-the-migration-are-you-really-going-to-approve-it-4l6d</guid>
      <description>&lt;h2&gt;
  
  
  Tuesday afternoon, that Approve button
&lt;/h2&gt;

&lt;p&gt;Tuesday, 3 p.m. A PR notification lands in Slack. Junior writes in the channel: "Cursor generated this migration for me — can you take a look?"&lt;/p&gt;

&lt;p&gt;You open the diff. Eighty lines of &lt;code&gt;ALTER TABLE&lt;/code&gt;. Add columns, change types, add indexes, attach foreign keys — every line is familiar on its own. Syntax is clean. Naming matches team conventions. Mechanically, &lt;strong&gt;it all looks plausible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But together? That &lt;code&gt;NOT NULL&lt;/code&gt; on &lt;code&gt;biz_order&lt;/code&gt; has no default — will it pass on existing rows? How long does the type change on &lt;code&gt;sys_user&lt;/code&gt; lock the table? Does the down migration actually restore the previous schema? The PR description says "Agent self-review: should be fine" — that is not evidence.&lt;/p&gt;

&lt;p&gt;Reject the whole thing and you look like you are blocking the team or fighting AI. Click Approve and &lt;strong&gt;your name&lt;/strong&gt; is on it. When production blows up, the postmortem asks "who approved this version?" and you say: "CI passed; it looked okay to me."&lt;/p&gt;

&lt;p&gt;You cannot spin up a database for every PR. In the end there are only two performances: &lt;strong&gt;pretend to understand&lt;/strong&gt; — comment on naming and click Approve; &lt;strong&gt;pretend to be strict&lt;/strong&gt; — send it back three times for formatting, and still nobody notices the missing join table.&lt;/p&gt;

&lt;h2&gt;
  
  
  This failure has a name: reviewed without reviewing
&lt;/h2&gt;

&lt;p&gt;This is not a moral failure of "not reading carefully enough." It has a name — &lt;strong&gt;reviewed without reviewing&lt;/strong&gt;: generation speed went up; human review speed did not. The Agent spits out eighty lines of &lt;code&gt;ALTER&lt;/code&gt; in three seconds; your brain still runs at "read SQL line by line." SQL diffs are mostly noise — &lt;code&gt;VARCHAR(64)&lt;/code&gt; to &lt;code&gt;VARCHAR(128)&lt;/code&gt; fills half the screen, while the change that actually blows up (missing join table, FK pointing at the wrong parent) hides in the middle and looks like any other edit.&lt;/p&gt;

&lt;p&gt;People in the industry summarize it in one line: &lt;strong&gt;No human can review at this speed.&lt;/strong&gt; That is not an insult — it is a mechanism claim. When the output is executable statements and the input is an eighty-line character diff, the Tech Lead is effectively signing blind. The moment you lose face and own the blame is the postmortem, not the Approve button on GitHub.&lt;/p&gt;

&lt;p&gt;There is a sneakier variant: &lt;strong&gt;which three lines in this migration are hallucinated?&lt;/strong&gt; Not syntax errors — semantic errors. The Agent used a stale &lt;code&gt;@schema.sql&lt;/code&gt; and invented a join table that "should" exist. Or it set a default on &lt;code&gt;del_flag&lt;/code&gt; that your team never uses. Every line runs; together they break a query you did not think to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things you already tried
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stricter CI.&lt;/strong&gt; Lint migration file names, require down files, run sqlfluff. Helpful — but CI checks format and parseability, not "will this table lock for ten minutes on Tuesday peak traffic." Green does not mean "I will sign Approve."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent self-review.&lt;/strong&gt; "Check this migration for risk and output a confidence score." The model politely returns "low risk" — it did not run that &lt;code&gt;ALTER&lt;/code&gt; against your real row counts either. Self-review is tone, not acceptance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Declarative diff tools.&lt;/strong&gt; Prisma, Atlas, and similar tell you "what changed in the model vs last revision." Progress — the diff object is no longer raw SQL. But the declaration is still not "what will execute": the migration that lands in prod can diverge from the declaration; and field-level diffs are still noise, not "what this change means for the business."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finer code review policy.&lt;/strong&gt; "Migrations need two approvers." "Agents cannot push to main." Process is right; the object under review is still SQL noise. If the second approver only glances at green CI, you now have two "looked fine to me" signatures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live catalog and SQL diff both miss this half
&lt;/h2&gt;

&lt;p&gt;Someone will say: wire the Agent to live DB so column names stop hallucinating. Job 1 already covered why — production credentials in the IDE are a red line; 240-table dumps blow context; permission-filtered schema is another hallucination. Even when structure reads correctly, &lt;strong&gt;that is not what the Lead needs to review&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Lead needs to answer the product question in the meeting: "Will this schema change blow up?" — which means "relative to the &lt;strong&gt;last approved version&lt;/strong&gt;, what are the semantic changes?" not eighty lines of &lt;code&gt;ALTER&lt;/code&gt;. Live catalog MCP solves "does this column exist?" &lt;strong&gt;Semantics&lt;/strong&gt; — soft delete still on this table?, is this status code still valid?, should this FK point at &lt;code&gt;sys_user&lt;/code&gt; not &lt;code&gt;biz_user&lt;/code&gt;? — live in notes, review records, and &lt;strong&gt;human-approved versions&lt;/strong&gt;, not in &lt;code&gt;information_schema&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the trap is not "we need a better SQL linter." It is &lt;strong&gt;reviewing the wrong artifact&lt;/strong&gt;: you Approve executable statements without an "approved intent" to align against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swap what you review: version diff, not migration diff
&lt;/h2&gt;

&lt;p&gt;Our approach moves the Agent write path from "dump SQL for you to sign" to &lt;strong&gt;"declare intent → save as a version → human diffs in the designer."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The team maintains schema in the designer. Each change saves as a &lt;strong&gt;named version&lt;/strong&gt; — that &lt;code&gt;projectJSON&lt;/code&gt; is the approved contract.&lt;/li&gt;
&lt;li&gt;When the Agent needs a schema change, it does not call &lt;code&gt;put_project_json&lt;/code&gt; to overwrite the workspace (API 200 is not human approval). It calls &lt;strong&gt;&lt;code&gt;create_version&lt;/code&gt;&lt;/strong&gt;: submit a suggested revision with a note.&lt;/li&gt;
&lt;li&gt;The Tech Lead opens the &lt;strong&gt;version diff&lt;/strong&gt; — not eighty lines of SQL, but: which table was added, which field removed, which FK moved from A to B. Column labels and notes carry tribal knowledge: next to &lt;code&gt;status&lt;/code&gt;, "1=valid 9=dirty" is closer to what you review than &lt;code&gt;CHAR(1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Point at three changes in the diff, say "I reviewed these three," then accept — the named version you produce is what goes in the changelog and aligns with what the Agent declared.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DDL drafts can be generated from approved versions for DBA reference; &lt;strong&gt;ERD does not execute SQL&lt;/strong&gt;. Landing migrations in the database is still Atlas / Flyway / your CI. The Agent gets no production credentials; the Lead does not blind-sign raw migrations.&lt;/p&gt;

&lt;p&gt;One sentence on the difference vs "Agent writes migration, you sign SQL": &lt;strong&gt;API 200 is not human approval.&lt;/strong&gt; Approval happens after you read the diff in the designer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP does here (and what it does not yet)
&lt;/h2&gt;

&lt;p&gt;By now you might want MCP — not at the opening of the article.&lt;/p&gt;

&lt;p&gt;Today the only MCP write path is &lt;strong&gt;&lt;code&gt;create_version&lt;/code&gt;&lt;/strong&gt;: the Agent reads the approved contract via &lt;code&gt;list_tables&lt;/code&gt; / &lt;code&gt;describe_table&lt;/code&gt;, updates the model, submits a suggestion; you diff in the designer. See &lt;a href="{{DOC:guide/api-and-mcp}}"&gt;Read ER diagrams from Cursor via MCP&lt;/a&gt; and &lt;a href="{{DOC:guide/save-version-and-diff}}"&gt;Save a version and view the diff&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One honest gap: &lt;strong&gt;semantic &lt;code&gt;diff_versions&lt;/code&gt;&lt;/strong&gt; — ask MCP "what three semantic changes between v1.3 and v1.2?" instead of scanning the version panel by eye. That is the Job 2 build target; until the tool ships, Leads still open two versions in the designer — but the object is at least the &lt;strong&gt;contract&lt;/strong&gt;, not SQL noise.&lt;/p&gt;

&lt;p&gt;Planned optional capability: DDL draft from two contract versions as a DBA starting point — &lt;strong&gt;still no execution&lt;/strong&gt;. Hard merge gates and DBA veto before main — that is another journey (Job 4). This article stops at the Lead's Approve button: you sign intent you diffed, not eighty lines the Agent spat out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CTA is not "install our MCP"
&lt;/h2&gt;

&lt;p&gt;The next step from this article is not "go install MCP first." It is more basic: &lt;strong&gt;save a version and walk through diff approval once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open the demo, change one table, save a named version, change something else, save again, open the diff between the two — 30 seconds, no signup. If you have not walked "Agent suggests → human diffs → approve" yet, practice with your own two hand-edited versions first: what "field added" vs "FK retargeted" looks like in the diff, so when the Agent calls &lt;code&gt;create_version&lt;/code&gt; you know where to look.&lt;/p&gt;

&lt;p&gt;The moment you save that first named version, you have a "last approved baseline" to align against. Without it, the Agent's eighty lines are a blind sign — the Approve button is there; whether you dare click depends on whether you reviewed SQL or the contract.&lt;/p&gt;

&lt;p&gt;{{CTA}}&lt;/p&gt;

&lt;h1&gt;
  
  
  MCP #DatabaseDesign #ERD #Cursor
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Don't give your agent the production database</title>
      <dc:creator>erdonline</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:25:04 +0000</pubDate>
      <link>https://dev.to/erdonline/dont-give-your-agent-the-production-database-354j</link>
      <guid>https://dev.to/erdonline/dont-give-your-agent-the-production-database-354j</guid>
      <description>&lt;h2&gt;
  
  
  The second you hit Enter
&lt;/h2&gt;

&lt;p&gt;Friday night. You ask Cursor for a query: join orders to users, sort by last login. Three seconds later, an answer arrives with DBA-level confidence:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_login_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;biz_order&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;sys_user&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_login_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste it into your client. Enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: column "last_login_at" does not exist
LINE 2: SELECT o.id, o.amount, u.last_login_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;last_login_at&lt;/code&gt; column. There never was. The model did not know — it just decided the column "should" exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  This failure has a name: invented column
&lt;/h2&gt;

&lt;p&gt;This is not "AI is not smart enough yet." It has a name — &lt;strong&gt;invented column&lt;/strong&gt;: the model fabricates a plausible column name with no factual source, then writes it into a JOIN with unshakable tone.&lt;/p&gt;

&lt;p&gt;Invented columns are dangerous because they &lt;strong&gt;do not look like errors&lt;/strong&gt;. &lt;code&gt;last_login_at&lt;/code&gt; appears on 90% of user tables. Syntax is correct. Naming is conventional. Indentation is perfect. Mixed into ten correct JOINs, you will not catch it line by line. You find out in code review — or worse, in production logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things you already tried
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A better prompt.&lt;/strong&gt; "Do not invent column names; only use the schema I provide" — added to the system prompt. Works day one. By day three, long context and the model forgets. A prompt is a wish, not a constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@schema.sql&lt;/code&gt;.&lt;/strong&gt; Export DDL and drop it into context. The most honest approach today — but two holes: it &lt;strong&gt;goes stale&lt;/strong&gt; (last week's export does not know this week's column), and &lt;strong&gt;nobody maintains it&lt;/strong&gt; (not in any approval flow; anyone can edit it; drift from the real database goes unnoticed).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live catalog MCP.&lt;/strong&gt; Let the Agent query &lt;code&gt;information_schema&lt;/code&gt; directly. Directionally correct — give the model a fact source instead of guesses. Tools like postgres-mcp and cloud vendor MCPs do solve half of "stop hallucinating column names." Worth acknowledging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live catalog only gets you halfway
&lt;/h2&gt;

&lt;p&gt;Wire production into the IDE and you hit four walls:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission-filtered &lt;code&gt;information_schema&lt;/code&gt; is another kind of hallucination.&lt;/strong&gt; The database account you give the Agent probably cannot see every table. The "complete schema" it returns is only the slice it is allowed to see. The model will not say "I only see 60% of tables." It keeps inventing columns in the missing 40%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 240-table full dump blows context.&lt;/strong&gt; Real projects are not eight-table demos. Dump everything and you either truncate or drown the three relevant tables in noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production credentials in the IDE are a red line.&lt;/strong&gt; Your &lt;code&gt;.cursor/mcp.json&lt;/code&gt; ends up in git, screenshots, and devices you forgot you synced. Read-only is still production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool-list cache refreshes only on restart.&lt;/strong&gt; A colleague adds a column at 10 a.m.; your Agent at 3 p.m. still writes SQL from cached schema — the same stale problem as &lt;code&gt;@schema.sql&lt;/code&gt;, just harder to spot.&lt;/p&gt;

&lt;p&gt;And something more fundamental: &lt;strong&gt;structure is not semantics.&lt;/strong&gt; &lt;code&gt;information_schema&lt;/code&gt; tells you &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;CHAR(1)&lt;/code&gt;; it does not tell you &lt;code&gt;'1'&lt;/code&gt; is valid and &lt;code&gt;'9'&lt;/code&gt; is dirty data. It tells you &lt;code&gt;del_flag&lt;/code&gt; exists; it does not tell you every query must include &lt;code&gt;del_flag = '0'&lt;/code&gt;. Live catalogs are solving structure; semantics live in people's heads and review records — not in the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swap the fact source: read the contract, not production
&lt;/h2&gt;

&lt;p&gt;Our approach moves the fact source from "production database" to &lt;strong&gt;"approved version."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In ERD Online, schema lives in the designer. Each change saves as a named version humans can diff and roll back. That &lt;code&gt;projectJSON&lt;/code&gt; is the contract: human-reviewed, no database passwords (&lt;code&gt;profile.dbs&lt;/code&gt; stripped at the API layer), and naturally semantic — column labels and notes are where tribal knowledge like "&lt;code&gt;del_flag = '0'&lt;/code&gt;" gets written down.&lt;/p&gt;

&lt;p&gt;The Agent reads the contract, not production. Three direct consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero credentials&lt;/strong&gt;: only a project-level PAT in the IDE — no database accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No stale drift&lt;/strong&gt;: the contract updates with versions; the Agent reads what the team just approved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accountability&lt;/strong&gt;: which version the Agent used is right there in the version id.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Progressive disclosure, not a full dump
&lt;/h2&gt;

&lt;p&gt;Contracts can be large too, so how you read matters. We added two MCP tools deliberately shaped as "list first, expand on demand":&lt;/p&gt;

&lt;p&gt;&lt;code&gt;list_tables&lt;/code&gt; returns table name, label, and field count — eight rows for an eight-table project, 240 rows for a 240-table project, neither blows context. The Agent picks the two or three it needs, then calls &lt;code&gt;describe_table&lt;/code&gt; for fields and FK neighborhood (who references me, whom do I reference).&lt;/p&gt;

&lt;p&gt;When the Agent guesses a wrong table name, the response is not silence or fabrication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suggestions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sys_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sys_user_role"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Table not in the approved contract. Retry with one of the suggestions; do not invent columns."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;found:false&lt;/code&gt; plus suggestions — invented columns get blocked at the tool layer, not by "please do not hallucinate" in the prompt.&lt;/p&gt;

&lt;p&gt;Both tools read an approved version snapshot (pass &lt;code&gt;versionId&lt;/code&gt; to pin a revision). No SQL execution. No database connection. Same configuration path as existing tools — see &lt;a href="https://www.erdonline.com/docs/guide/api-and-mcp?utm_source=hashnode&amp;amp;utm_campaign=mcp-agent&amp;amp;utm_content=dont-give-agent-prod-db" rel="noopener noreferrer"&gt;Read ER diagrams from Cursor via MCP&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CTA is not "install our MCP"
&lt;/h2&gt;

&lt;p&gt;The next step from this article is not "go install an MCP first." It is more basic: &lt;strong&gt;save a version for your model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open the demo, change one table, save a named version, watch the diff — 30 seconds, no signup. The moment you save that first version, you have a contract the Agent can read and you can stand behind. MCP comes after, if you want it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👉 &lt;strong&gt;Try it in 30 seconds without signing up&lt;/strong&gt;: &lt;a href="https://www.erdonline.com/demo?utm_source=hashnode&amp;amp;utm_campaign=mcp-agent&amp;amp;utm_content=dont-give-agent-prod-db" rel="noopener noreferrer"&gt;https://www.erdonline.com/demo?utm_source=hashnode&amp;amp;utm_campaign=mcp-agent&amp;amp;utm_content=dont-give-agent-prod-db&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open source (MIT — star / issue / PR welcome): &lt;a href="https://github.com/erd-online/erd-online?utm_source=hashnode&amp;amp;utm_campaign=mcp-agent&amp;amp;utm_content=dont-give-agent-prod-db" rel="noopener noreferrer"&gt;https://github.com/erd-online/erd-online?utm_source=hashnode&amp;amp;utm_campaign=mcp-agent&amp;amp;utm_content=dont-give-agent-prod-db&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  MCP #DatabaseDesign #ERD #Cursor
&lt;/h1&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>database</category>
      <category>llm</category>
    </item>
    <item>
      <title>Average position 1. Zero clicks. Eight URLs, one identity.</title>
      <dc:creator>erdonline</dc:creator>
      <pubDate>Sat, 29 Aug 2026 10:32:57 +0000</pubDate>
      <link>https://dev.to/erdonline/average-position-1-zero-clicks-eight-urls-one-identity-25f4</link>
      <guid>https://dev.to/erdonline/average-position-1-zero-clicks-eight-urls-one-identity-25f4</guid>
      <description>&lt;p&gt;Search Console told us we were ranking. In Singapore, average position 1. Across three months: 42 impressions, &lt;strong&gt;0 clicks&lt;/strong&gt;. Not a low click-through rate — none.&lt;/p&gt;

&lt;p&gt;Twelve days and one engineering pass later, the same property read 2 clicks / 103 impressions / 1.9% CTR / average position 62.7. Inside that, two rows that shouldn't be able to coexist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;Clicks / impressions&lt;/th&gt;
&lt;th&gt;Average position&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://www.erdonline.com/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 / 90&lt;/td&gt;
&lt;td&gt;61.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://www.erdonline.com/compare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 / 8&lt;/td&gt;
&lt;td&gt;~2–3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One page buried on page six for the queries people actually type. Another sitting at position two for a query almost nobody types. The instinct in that moment is to write more content, because "more content" is the answer every SEO article gives. It was the wrong read, and chasing it would have cost us a month.&lt;/p&gt;

&lt;p&gt;The real problem was that our site had &lt;strong&gt;one identity&lt;/strong&gt;. It had eight marketing URLs, a sitemap, hreflang tags, and JSON-LD — and every single one of those URLs handed Google the same first byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis takes one command
&lt;/h2&gt;

&lt;p&gt;We build a client-rendered UmiJS app hosted on Cloudflare Pages. Before diagnosing anything clever, curl what the crawler gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://www.erdonline.com/catalog | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;title&amp;gt;|rel="canonical"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer, for months, was the homepage's &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;link rel="canonical" href="https://www.erdonline.com/"&amp;gt;&lt;/code&gt;. Same for &lt;code&gt;/compare&lt;/code&gt;. Same for &lt;code&gt;/en&lt;/code&gt;. Same for &lt;code&gt;/demo&lt;/code&gt;. And then this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://www.erdonline.com/__seo_health_nonexistent_path__
&lt;span class="c"&gt;# HTTP/2 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A URL that has never existed, returning 200 with the homepage in the body. That is a soft 404, and once you have one, you have infinitely many. Googlebot doesn't see a site with eight pages plus some noise. It sees one page reachable at an unbounded number of addresses, which is the exact shape of a low-quality site, and it responds by picking one URL to keep and quietly discarding the rest.&lt;/p&gt;

&lt;p&gt;Rank was never the bottleneck. Being a distinct page was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two platform defaults, both documented, both easy to miss
&lt;/h2&gt;

&lt;p&gt;Neither cause was exotic. Both were things we had written ourselves and then stopped looking at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A catch-all rewrite.&lt;/strong&gt; &lt;code&gt;public/_redirects&lt;/code&gt; contained &lt;code&gt;/* /index.html 200&lt;/code&gt;. This is the line every SPA tutorial tells you to add so client-side routing survives a hard refresh, and it is correct for &lt;code&gt;/project/1234&lt;/code&gt;, a page behind a login that has no business being indexed. It is destructive for &lt;code&gt;/compare&lt;/code&gt;, a page whose entire job is to be indexed under its own title.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No top-level &lt;code&gt;404.html&lt;/code&gt;.&lt;/strong&gt; Cloudflare Pages has a documented rule here that catches people: if there is no &lt;code&gt;404.html&lt;/code&gt; at the root of your build output, Pages assumes you are a single-page app and serves &lt;code&gt;200 + index.html&lt;/code&gt; for every path that doesn't match a static file. Ship a &lt;code&gt;404.html&lt;/code&gt; and that automatic SPA mode turns off — unmatched paths get a real HTTP 404. One file, and infinite soft 404s stop.&lt;/p&gt;

&lt;p&gt;Three smaller footguns cost us a deploy each, and they are the kind of thing nobody writes down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A rewrite target of &lt;code&gt;/index.html&lt;/code&gt; doesn't work. Cloudflare 308-redirects &lt;code&gt;*.html&lt;/code&gt; to its extension-less form, so a rewrite meant to be invisible becomes a redirect to &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The splat in &lt;code&gt;/catalog/* → / 200&lt;/code&gt; matches &lt;code&gt;/catalog/&lt;/code&gt; with an empty segment, so the catch-all quietly shadowed the very list-page shell we had just generated. It has to be &lt;code&gt;/catalog/:id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We first sent unknown template IDs to a placeholder shell at &lt;code&gt;/catalog/_item&lt;/code&gt;. Because that path is a directory, Cloudflare 308'd &lt;code&gt;/catalog/some-bad-id&lt;/code&gt; onto &lt;code&gt;/catalog/_item/&lt;/code&gt; — we had invented a brand-new crawlable junk URL while trying to clean up crawlable junk URLs. The fix was to rewrite unknown IDs to &lt;code&gt;/catalog/&lt;/code&gt; (already slashed, so nothing to 308 onto) and 301 &lt;code&gt;_item&lt;/code&gt; away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a redirect rule that is &lt;em&gt;almost&lt;/em&gt; right. Which is the point: this class of bug produces a perfectly working website for humans and an unindexable one for crawlers, and no amount of reading your React code will surface it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four artifacts, one source of truth
&lt;/h2&gt;

&lt;p&gt;The fix was to prerender a per-path shell at build time — &lt;code&gt;dist/catalog/index.html&lt;/code&gt;, &lt;code&gt;dist/compare/index.html&lt;/code&gt;, &lt;code&gt;dist/en/compare/index.html&lt;/code&gt;, and so on — each carrying its own &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, canonical, hreflang set, and JSON-LD, then remove those exact paths from the redirect file so the static file wins.&lt;/p&gt;

&lt;p&gt;That's the obvious half. The half worth stealing is what we did to stop it from rotting.&lt;/p&gt;

&lt;p&gt;Adding one public route requires four artifacts to agree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the list of URLs in &lt;code&gt;sitemap.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the set of paths that get a prerendered shell&lt;/li&gt;
&lt;li&gt;the Cloudflare &lt;code&gt;_redirects&lt;/code&gt; rules for everything that doesn't&lt;/li&gt;
&lt;li&gt;the nginx &lt;code&gt;map&lt;/code&gt; for self-hosted Docker deployments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four files, maintained by hand, in a repo where the person adding a route is thinking about React Router and nothing else. They will drift. Ours did.&lt;/p&gt;

&lt;p&gt;So they all became derived values exported from one module, &lt;code&gt;frontend/scripts/seo-config.mjs&lt;/code&gt;: &lt;code&gt;SITEMAP_PATHS&lt;/code&gt;, &lt;code&gt;PRERENDER_PAGES&lt;/code&gt;, &lt;code&gt;CF_SPA_REDIRECT_RULES&lt;/code&gt;, &lt;code&gt;NGINX_SPA_URI_REGEXES&lt;/code&gt;, plus &lt;code&gt;marketingHreflang()&lt;/code&gt; and &lt;code&gt;jsonLdForPage()&lt;/code&gt;. The build script consumes it and writes &lt;code&gt;sitemap.xml&lt;/code&gt;, &lt;code&gt;robots.txt&lt;/code&gt;, &lt;code&gt;_redirects&lt;/code&gt;, &lt;code&gt;_headers&lt;/code&gt;, &lt;code&gt;404.html&lt;/code&gt;, and every shell in one pass. Adding a route is one entry in one array. Drift between the sitemap and the redirects is no longer a mistake you can make.&lt;/p&gt;

&lt;p&gt;That file is also where the hard-won knowledge lives, as comments next to the rules they explain — including the one that reads, in effect, &lt;em&gt;do not use `/catalog/&lt;/em&gt;` here, we tried, here is what Cloudflare does*. Comments that record a footgun earn their keep. Comments that narrate what the next line does do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs that only exist in single-page apps
&lt;/h2&gt;

&lt;p&gt;With the shells shipped, two failures showed up that have no equivalent on a server-rendered site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hydration fought the shell.&lt;/strong&gt; &lt;code&gt;/catalog/demo-authz&lt;/code&gt; prerendered correctly, then the client booted and &lt;code&gt;CatalogLayout&lt;/code&gt; called &lt;code&gt;usePageSeo(catalog.seo.*)&lt;/code&gt; unconditionally — so the detail page's title was replaced, a few hundred milliseconds later, by the &lt;em&gt;list&lt;/em&gt; page's title. The prerender was right and the app overwrote it. If you prerender per-path metadata in a CSR app, your client-side SEO hook needs to know which page it is on, or you have built two systems that disagree about &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;. Now the list applies list metadata, and a detail page waits for its API response and writes &lt;code&gt;{name} — ER diagram templates&lt;/code&gt; with a canonical pointing at &lt;code&gt;/catalog/:id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every page claimed to be the app.&lt;/strong&gt; Our JSON-LD generator was parameterizing &lt;code&gt;url&lt;/code&gt; and nothing else, so &lt;code&gt;@type&lt;/code&gt; stayed &lt;code&gt;WebApplication&lt;/code&gt; everywhere. Eight distinct pages each announcing themselves as the application living at the site root — structured data actively contradicting the canonical tags we had just fixed. It's now typed per path: &lt;code&gt;/&lt;/code&gt; is a &lt;code&gt;WebApplication&lt;/code&gt;, the template list is a &lt;code&gt;CollectionPage&lt;/code&gt;, an official template detail is an &lt;code&gt;ItemPage&lt;/code&gt;, everything else is a &lt;code&gt;WebPage&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The title problem was a promise problem
&lt;/h2&gt;

&lt;p&gt;Position 1 with zero clicks is not a ranking failure. It is a snippet that doesn't sound like the thing the searcher wanted.&lt;/p&gt;

&lt;p&gt;Ours was brand-first, and the searcher's job-to-be-done was not "learn about a brand." It was "draw an ER diagram, in a browser, now." The queries said so plainly: &lt;code&gt;erd online&lt;/code&gt;, &lt;code&gt;erd diagram online&lt;/code&gt;, &lt;code&gt;make|create|draw erd online&lt;/code&gt;. So the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; became job-first — &lt;code&gt;Draw ER Diagram Online — Free Editor | ERD Online&lt;/code&gt; — and the description covers the intent cluster in one honest sentence: draw, ERD editor and maker, entity-relationship models.&lt;/p&gt;

&lt;p&gt;Three things we deliberately did &lt;strong&gt;not&lt;/strong&gt; do, now written into &lt;code&gt;docs/landing.md&lt;/code&gt; as rules rather than left to judgment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No "file viewer."&lt;/strong&gt; It's a plausible, high-volume phrase. We support ERD/PdMan/DBML import; we do not ship a dedicated file viewer. Ranking for a query you can't satisfy buys one visit and one bounce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No naming Google Draw.&lt;/strong&gt; Our comparison page names draw.io, because we have a real technical claim there — a line in draw.io is a line, while a relationship in ERD Online carries foreign-key semantics. We won't name a product we haven't actually compared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No stuffing the non-English queries&lt;/strong&gt; we could see in the query report. Impressions from an audience we don't serve in their language are not a win.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The H1 stayed &lt;strong&gt;"Git + Figma for database design."&lt;/strong&gt; The &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and the H1 now disagree, on purpose. The &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; answers a query; the H1 answers "what is this, and why should I care" for someone who has already arrived. Collapsing both into keywords costs you the second job and reads like a landing page from 2011.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the crawler's view a test
&lt;/h2&gt;

&lt;p&gt;The failure mode here is silent, so the only durable fix is an assertion. &lt;code&gt;scripts/seo-index-health.mjs&lt;/code&gt; runs daily in CI and checks, against the live site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each of &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/demo&lt;/code&gt;, &lt;code&gt;/catalog&lt;/code&gt;, &lt;code&gt;/compare&lt;/code&gt;, &lt;code&gt;/docs/&lt;/code&gt;, &lt;code&gt;/docs/roadmap&lt;/code&gt;: HTTP 200, &lt;code&gt;text/html&lt;/code&gt;, non-empty &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, non-empty meta description, a canonical, and no &lt;code&gt;noindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/robots.txt&lt;/code&gt;: 200, not &lt;code&gt;text/html&lt;/code&gt; (a robots.txt served as HTML means the SPA fallback ate it), contains &lt;code&gt;User-agent: *&lt;/code&gt;, contains an absolute &lt;code&gt;Sitemap:&lt;/code&gt; URL, and does &lt;strong&gt;not&lt;/strong&gt; contain &lt;code&gt;content-signal:&lt;/code&gt; — that last string is how you detect that your platform's injected default robots.txt is still winning over yours.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/__seo_health_nonexistent_path__&lt;/code&gt;: must return &lt;strong&gt;404&lt;/strong&gt;. This is the soft-404 regression, pinned as one assertion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/compare&lt;/code&gt;, &lt;code&gt;/catalog&lt;/code&gt;, &lt;code&gt;/demo&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt;: must still return &lt;strong&gt;200&lt;/strong&gt;, because the obvious way to fix soft 404s is to break your real routes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sitemap.xml&lt;/code&gt;: parses, has at least one &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;, and the first 20 URLs are reachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At build time, &lt;code&gt;yarn test:seo-static&lt;/code&gt; runs the same config without a full build, and the production smoke suite has a test named, verbatim, &lt;code&gt;crawler first HTML uses path canonical (not homepage)&lt;/code&gt;. The bug we shipped for months now has a name in a test file.&lt;/p&gt;

&lt;p&gt;Two more things from the same pass, both non-obvious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache headers alone did nothing.&lt;/strong&gt; The homepage was returning &lt;code&gt;CF-Cache-Status: DYNAMIC&lt;/code&gt; with TTFB around 1.6s (our docs site, 3.8s) while Search Console flagged impressions sliding. We set &lt;code&gt;Cache-Control: public, max-age=0, s-maxage=600, must-revalidate&lt;/code&gt; on the HTML shells. Nothing changed — Cloudflare Pages treats &lt;code&gt;text/html&lt;/code&gt; as dynamic by default, and you must also create a Cache Rule in the dashboard making HTML eligible for cache. That dependency is now a comment in &lt;code&gt;_headers&lt;/code&gt;, because a header file that silently does nothing is worse than no header file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick one slash form and make all four places agree.&lt;/strong&gt; Our docs site canonical said no trailing slash, the sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; said no trailing slash, the host 308'd to a trailing slash, and legacy redirects pointed at the non-slash form — so an old URL took a 301 and then a 308 to arrive. &lt;code&gt;trailingSlash: true&lt;/code&gt;, redirect targets slashed, sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; slashed, &lt;code&gt;/search&lt;/code&gt; and blog tag/archive pages dropped from the sitemap. One hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we stopped doing
&lt;/h2&gt;

&lt;p&gt;Clicking "Request indexing." We tried it on &lt;code&gt;/catalog&lt;/code&gt; twice and got Search Console's reCAPTCHA error both times; nothing entered the queue. Manual submission is a nudge, not a mechanism, and treating it as progress hides the fact that your artifacts are still wrong. Fix the artifact, submit the sitemap, then wait.&lt;/p&gt;

&lt;p&gt;We also decided, in advance, what would make us change strategy. Our locale routing ADR ships English marketing pages as CSR with a sitemap and hreflang, and states the trigger explicitly: &lt;strong&gt;if &lt;code&gt;/en&lt;/code&gt; is not indexed 90 days after launch, we build static export for the marketing routes.&lt;/strong&gt; Writing the trigger down before you need it is what stops "should we go SSR?" from being re-litigated every two weeks on vibes.&lt;/p&gt;

&lt;p&gt;The same ADR is worth reading for what it refused to do. Full-site &lt;code&gt;/en/*&lt;/code&gt; routing would have touched 60 route entries, 76 &lt;code&gt;history.push&lt;/code&gt; call sites across 39 files, 236 &lt;code&gt;page.goto&lt;/code&gt; calls in the E2E suite, 16 backend files containing frontend URLs, and the immutable &lt;code&gt;/s/:token&lt;/code&gt; share links already pasted into other people's chat logs. The pages that would have gained from it — &lt;code&gt;/project/**&lt;/code&gt;, &lt;code&gt;/design/**&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt; — are all behind a login and have no search value whatsoever. Marketing pages got the prefix. Nothing else did. Internal link points touched: about 21.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard, honestly
&lt;/h2&gt;

&lt;p&gt;As of today: &lt;code&gt;/catalog&lt;/code&gt;, &lt;code&gt;/compare&lt;/code&gt;, and &lt;code&gt;/en/compare&lt;/code&gt; are indexed. &lt;code&gt;/en/catalog&lt;/code&gt; is "discovered — currently not indexed." The docs site's English sitemap was accepted and 66 pages were discovered. Total organic clicks over three months: &lt;strong&gt;2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not a traffic case study. Indexing moves on a timescale of weeks, and it would be dishonest to attach a hockey stick to a twelve-day engineering pass. What we can report is that the crawler's view of the site is now correct, asserted daily, and cheap to extend — which is the part that had to be true before any content investment could compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;If you run a client-rendered site, these are worth thirty minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;curl -sL &amp;lt;your-non-homepage-url&amp;gt; | grep -E '&amp;lt;title&amp;gt;|canonical'&lt;/code&gt;. If you see your homepage, stop reading and fix that.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl -sI &amp;lt;your-domain&amp;gt;/__definitely_not_a_real_path__&lt;/code&gt;. If it's 200, you have unbounded soft 404s. On Cloudflare Pages, ship a root &lt;code&gt;404.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Delete the catch-all rewrite. Enumerate the SPA paths that genuinely need a 200, and let everything else 404.&lt;/li&gt;
&lt;li&gt;Make sitemap, prerendered shells, host rewrites, and self-host rewrites derive from one module. Adding a route should be one line.&lt;/li&gt;
&lt;li&gt;Check that hydration isn't overwriting per-path metadata your build just wrote.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;@type&lt;/code&gt; in your JSON-LD is actually per-page, not the homepage type with a swapped &lt;code&gt;url&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pick one trailing-slash form. Canonical, sitemap &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;, host redirect, and legacy redirects must all agree.&lt;/li&gt;
&lt;li&gt;Turn every one of the above into a daily assertion against production. This bug class is silent by construction.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.erdonline.com/" rel="noopener noreferrer"&gt;ERD Online&lt;/a&gt; is an open-source (MIT) database design tool: &lt;strong&gt;Git + Figma for database design.&lt;/strong&gt; Versions and collaboration are the moat, and the &lt;code&gt;projectJSON&lt;/code&gt; format is open so humans and AI agents read and write the same source of truth — an agent can call &lt;code&gt;create_version&lt;/code&gt; through MCP, and a human still reviews the diff in the designer. It is not ChatSQL; it will not invent a diagram from a sentence.&lt;/p&gt;

&lt;p&gt;Open a real ER diagram, read-only, no signup: &lt;strong&gt;&lt;a href="https://www.erdonline.com/demo" rel="noopener noreferrer"&gt;https://www.erdonline.com/demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Source, issues, and the SEO scripts described above: &lt;strong&gt;&lt;a href="https://github.com/erdonline/erdonline" rel="noopener noreferrer"&gt;https://github.com/erdonline/erdonline&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The config module is &lt;code&gt;frontend/scripts/seo-config.mjs&lt;/code&gt;, the health check is &lt;code&gt;scripts/seo-index-health.mjs&lt;/code&gt;. Both are short. Steal them.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>seo</category>
      <category>website</category>
    </item>
    <item>
      <title>How to let AI agents manage your database schema (with MCP)</title>
      <dc:creator>erdonline</dc:creator>
      <pubDate>Sat, 29 Aug 2026 06:07:07 +0000</pubDate>
      <link>https://dev.to/erdonline/how-to-let-ai-agents-manage-your-database-schema-with-mcp-12k1</link>
      <guid>https://dev.to/erdonline/how-to-let-ai-agents-manage-your-database-schema-with-mcp-12k1</guid>
      <description>&lt;p&gt;AI agents are becoming first-class citizens in developer workflows. They can read code, run tests, and deploy apps. But one thing they struggle with is understanding database schemas.&lt;/p&gt;

&lt;p&gt;Database design tools haven't changed in 20 years. You either use a heavyweight desktop app (Navicat, PDManer) or a pretty but closed web app (dbdiagram). Neither supports versioning, real-time collaboration, or AI agent integration.&lt;/p&gt;

&lt;p&gt;I built ERD Online to solve this. It's an open-source database design tool that combines Git-like versioning with Figma-like collaboration, plus MCP integration for AI agents.&lt;/p&gt;

&lt;p&gt;In this article, I'll show you how to let Cursor, Claude, or Cline read and write your database schema through MCP, while you keep full control.&lt;/p&gt;

&lt;p&gt;Database schema changes are hard to track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who changed what?&lt;/li&gt;
&lt;li&gt;When did they change it?&lt;/li&gt;
&lt;li&gt;Why did they change it?&lt;/li&gt;
&lt;li&gt;How do I rollback?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And now with AI agents, there's a new problem: how do you let an AI agent suggest schema changes without giving it a black box that generates random ER diagrams?&lt;/p&gt;

&lt;p&gt;The wrong approach: ask AI to "generate an ER diagram for an e-commerce app." You get a diagram, but it has no connection to your actual project, no versioning, and no approval flow.&lt;/p&gt;

&lt;p&gt;The right approach: let the AI agent read your existing schema, suggest changes, and submit them as a version that you review and approve.&lt;/p&gt;

&lt;p&gt;That's what ERD Online + MCP does.&lt;/p&gt;

&lt;p&gt;MCP (Model Context Protocol) is a protocol for AI agents to interact with external tools. Think of it as a USB-C port for AI applications. It standardizes how agents discover and call tools.&lt;/p&gt;

&lt;p&gt;MCP has three main primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: Functions the AI can call (like &lt;code&gt;list_projects&lt;/code&gt; or &lt;code&gt;create_version&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt;: Data the AI can read (like &lt;code&gt;project.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: Pre-defined templates for common tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ERD Online exposes MCP tools that let AI agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_projects&lt;/code&gt;: List all your ERD projects&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_project&lt;/code&gt;: Get a project's projectJSON&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_version&lt;/code&gt;: Suggest a new version of your schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key boundary: AI agents read/write the same projectJSON the human designer uses. They don't generate ER diagrams from a sentence. You stay in control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Mint a PAT in ERD Online&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sign in to ERD Online and go to Account Settings → Personal Access Tokens. Create a new token (read-only is enough to start).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Add MCP config to Cursor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; and add:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. AI agents become useful, not dangerous&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By giving AI agents structured access to your schema, they can make useful suggestions. But because they go through &lt;code&gt;create_version&lt;/code&gt; and you review the diff, they can't silently break your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Versioning becomes the source of truth&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every AI-suggested change creates a version. You can see exactly what changed, when, and why. This is essential for database schema management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Collaboration improves&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multiple people (and agents) can work on the same schema. Changes are merged, versioned, and reviewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No vendor lock-in&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;projectJSON is an open format. You can export your schema, self-host ERD Online, or build your own tools on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERD Online&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo (no signup): &lt;a href="https://www.erdonline.com/demo" rel="noopener noreferrer"&gt;https://www.erdonline.com/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/erdonline/erdonline" rel="noopener noreferrer"&gt;https://github.com/erdonline/erdonline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP docs: &lt;a href="https://doc.erdonline.com/docs/guide/api-and-mcp/" rel="noopener noreferrer"&gt;https://doc.erdonline.com/docs/guide/api-and-mcp/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cursor bridge: &lt;a href="https://www.erdonline.com/cursor-mcp/" rel="noopener noreferrer"&gt;https://www.erdonline.com/cursor-mcp/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What would you build?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear what AI + database schema workflows you'd want. Drop a comment or open an issue on GitHub.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>database</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
