Engineering

Why your CMS schema should live in code, not a GUI

Most headless CMSes let you define your content structure by clicking buttons in a dashboard. That feels fast — until you need to review a change, roll it back, or onboard a new engineer.

PM

Priya M.

Engineering

May 8, 20266 min read
Why your CMS schema should live in code, not a GUI

Most headless CMSes let you define your content structure by clicking buttons in a dashboard. That feels fast — until you need to review a change, roll it back, or onboard a new engineer who has no idea why a field exists.

The GUI schema problem

When your schema lives in a database, it is invisible to your version control. You can't see the diff when someone renames a field. You can't review it in a pull request. You can't roll it back without writing a migration by hand. And when something breaks in production, you're squinting at a timestamp in the CMS audit log trying to figure out what changed.

This is the standard experience with most popular headless CMSes — Contentful, Sanity, even Strapi's older GUI mode. The schema is stored as data, not code.

Code as the source of truth

When the schema is a TypeScript config file that lives in your repository, all of that changes.

Every field definition, validation rule, and access control policy is a line of code. You commit it. You review it. You diff it. If you rename a field from `body` to `content`, the change appears in your pull request like any other change. A reviewer can catch it before it ships.

defineCollection({
  slug: 'posts',
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'content', type: 'richText' },  // renamed from 'body'
    { name: 'status', type: 'select', options: ['draft', 'published'] },
  ],
})

Type safety as a side effect

When the schema is code, generating types from it is trivial. Dyrected generates TypeScript types from your collection definitions — which means your editor knows what fields exist, what types they are, and when you're querying something that doesn't exist.

No more `doc.body as string`. No more runtime surprises when a field was renamed six months ago and nobody updated the frontend.

What this looks like in practice

The workflow looks like this: you open your config file, add a field, commit the change, open a pull request. Your teammate reviews it. It merges. Dyrected picks up the schema change and updates the admin UI and the database automatically.

No separate migration step. No clicking through a dashboard. No schema state that exists somewhere outside your repo.

The tradeoff

The obvious tradeoff is that non-technical teammates can't change the schema themselves. A content editor can't add a new field by clicking a button.

But that's the point. Schema changes are engineering decisions. They affect the database, the API, the frontend, and the types. They deserve a code review. Content editors should own the *content inside the structure* — not the structure itself.

Dyrected draws that line explicitly: engineering defines the schema in code, marketing and content teams edit what's inside it. Clear separation. No surprises.