Your ER Diagram Should Be the Source of Truth, Not a Stale Picture

You know the diagram I mean.

It's pinned in the team wiki. It was beautiful in month one. Someone exported it to a PNG, dropped it into Confluence, and felt good about life. Then the real work started: a migration here, a users.phone column there, a junction table nobody bothered to draw. Eighteen months later the diagram shows seven tables and the database has twenty-three. New hires open it, trust it, and ship a bug.

The diagram didn't lie. It just stopped being true the moment it became a picture.

I built diagram2code because I got tired of this exact failure mode. The thesis is simple and a little opinionated: your ER diagram should be the source of truth, and your SQL should be generated from it โ€” not the other way around.

A picture is a snapshot. Source of truth is a contract.

The reason diagrams drift is that we treat them as documentation, and documentation is the first thing to rot. It lives in a different tool, in a different repo, on a different update cadence than the code. There is no compiler that fails when the picture and the schema disagree, so they always, eventually, disagree.

The fix isn't "remember to update the diagram." We've been telling each other that for twenty years and it has never once worked. The fix is to make the diagram load-bearing. If the SQL is generated from the diagram, then the diagram can't drift, because the diagram is the thing you change to change the database.

And the nice part is the input is just text you already know how to write:

erDiagram
    USER ||--o{ ORDER : places
    USER {
        int id PK
        string email
        string name
    }
    ORDER {
        int id PK
        int user_id FK
        decimal total
        datetime created_at
    }

That's standard Mermaid erDiagram syntax (PlantUML entity diagrams work too). It lives in your repo, in a .mmd file, next to the code it describes. It renders in GitHub, in your editor, on the web converter. It diffs in a pull request like any other file. And it generates real DDL:

CREATE TABLE "user" (
    "id" SERIAL PRIMARY KEY,
    "email" VARCHAR(255),
    "name" VARCHAR(255)
);

CREATE TABLE "order" (
    "id" SERIAL PRIMARY KEY,
    "user_id" INTEGER,
    "total" DECIMAL,
    "created_at" TIMESTAMP
);

ALTER TABLE "order"
    ADD CONSTRAINT "fk_order_user_id"
    FOREIGN KEY ("user_id") REFERENCES "user" ("id");

Notice that the ||--o{ relationship in the diagram became a real foreign key โ€” order.user_id references user(id) โ€” without you writing the constraint by hand. Same diagram, four SQL dialects (PostgreSQL, MySQL, SQLite, Oracle), and ORM models for eight frameworks โ€” GORM, SQLAlchemy, TypeORM, Prisma, JPA, Django, Sequelize, EF Core โ€” if your stack wants those instead of raw DDL. One canonical text file, many generated outputs.

"But diagrams can't express everything"

True. A plain ER diagram can't say "this column is NOT NULL" or "price must be positive." That's usually the excuse for keeping the real truth in hand-written SQL and letting the diagram stay decorative.

So the diagram carries that metadata too, as directives in comments:

PRODUCT {
    int id PK
    %% ::CHECK(price > 0), ::NN
    decimal price
    %% ::DEFAULT(0)
    int stock
}

::NN for NOT NULL, ::CHECK(...) for constraints, ::DEFAULT(...), ::ENUM(...), ::INDEX(...), ::UNIQUE(...), relationship-level onDelete: CASCADE, and more. They're plain comments, so the diagram still renders cleanly anywhere โ€” but now it carries enough to be the real definition, not a cartoon of it. When the diagram can express the constraint, the constraint has no reason to live anywhere else.

The payoff: make CI care about your schema

Here's where treating the diagram as canonical stops being a philosophy and starts saving you on a Tuesday afternoon.

Once the diagram is the source of truth, your CI can review schema changes the same way it reviews code. diagram2code ships a GitHub Action that does exactly this. Drop it into a workflow step:

- uses: diagram2code/diagram2code@v1
  with:
    database: postgres
    fail-on-destructive: true

Now, on any pull request that touches a .mmd or .puml diagram, the Action calls the hosted API and posts a sticky comment with two things: the generated SQL for the new state of the schema, and a human-readable diff of what actually changed between the old diagram and the new one โ€” tables added, columns altered, constraints created.

Reviewers stop squinting at diagram syntax in a diff and start reading "added orders.discount_code, changed users.phone from required to optional." That's the review you actually wanted.

And fail-on-destructive: true is the part I'm most attached to. The diff engine knows which changes are dangerous โ€” dropping a column, dropping a table, narrowing a column's type โ€” and the Action will fail the build when it sees one. Not block forever; just make someone stop and say it out loud. A dropped column should never sneak through code review as a one-line edit to a picture nobody fully read. Make it loud. Make it a red check.

Comparing two versions, for free

You don't need a pipeline to feel this. There's a free visual diff endpoint for comparing any two diagram versions:

curl -X POST https://diagram2code.com/api/v1/diff \
  -H "Content-Type: application/json" \
  -d '{"oldDiagramType":"mermaid","oldDiagram":"<old diagram>","newDiagramType":"mermaid","newDiagram":"<new diagram>"}'

It returns a structured plus human-readable diff with a summary โ€” additions, deletions, modifications, and a destructiveCount. It's the same engine the Action uses, and it's free, no account needed. Paste two versions of a schema and immediately see whether you're about to do something you'll regret.

That destructiveCount is, honestly, the whole argument in one integer. If a schema change reports destructive operations, that's not a detail to bury at the bottom of a 400-line PR. It's a conversation.

What this actually changes

I'm not claiming a diagram replaces migrations or your DBA's judgment. The database is still the database. What changes is where the truth lives.

When the diagram is canonical:

  • New engineers read the diagram and it's correct, because incorrect would have failed CI.
  • Schema changes get reviewed as schema changes, in plain English, not reverse-engineered from a migration file.
  • "We dropped that column on purpose" becomes an explicit, acknowledged decision instead of an incident three weeks later.

The diagram stops being the thing you update last and forget. It becomes the thing you change first, because changing it is how you change the database.

If you want to try it, the repo is here โ€” and the GitHub Action is a few-line drop-in you'll find in the README. Point it at one .mmd file in one repo and watch the first PR comment show up. Worst case, you learn something uncomfortable about how far your real schema has drifted from the picture on the wiki.

Better to learn it from a bot than from production.