Hasura – Migrations, Metadata and CI/CD

July 30, 20263 min readUpdated 8/21/2026

Everything so far assumed a console and a running engine. This lesson is about how a schema change gets from your laptop to production without anybody clicking anything — and about the awkward question of who owns the schema when your application already runs its own migrations.

Three directories

The Hasura CLI keeps a project as files:

hasura init stayhub-hasura --endpoint http://localhost:8081

migrations/     # SQL: the database schema itself
metadata/       # YAML: tracked tables, relationships, permissions, actions
seeds/          # SQL: reference data

The distinction matters. Migrations change the database. Metadata changes the API over it. They are separate directories because they are separate concerns — and they are deployed together because a permission referring to a column that does not exist yet is a broken deploy.

hasura migrate create add_cancellation_reason --from-server
hasura metadata export
hasura migrate apply --database-name default
hasura metadata apply
hasura metadata reload

Order is not negotiable

Migrations first, then metadata. Metadata that references a new column fails to apply if the column is not there yet, and the failure leaves you with a half-configured API.

Going the other way — dropping a column — the order reverses: remove it from metadata first, then drop it. Otherwise the engine boots with inconsistent metadata and quietly drops the broken object, which is lesson 3’s failure mode arriving in production.

Who owns the schema?

Here is the real question, and most tutorials skip it.

The demo app runs Alembic. Its models are SQLAlchemy, its migrations are Python, and its schema is owned by the backend. Hasura reads that database but did not create it.

Running hasura migrate as well would mean two tools writing DDL to the same database with two separate version tables, and no coordination between them. That is a genuinely bad time.

So pick one owner:

If Hasura is the only writer of DDL — a Hasura-first project with no significant backend — use hasura migrate for everything. It is coherent and the tooling is designed for it.

If an application owns the schema — which is the common case, and the demo app’s case — let it keep ownership. Use Alembic, Flyway or Prisma for DDL, and use Hasura only for metadata:

alembic upgrade head          # the schema
hasura metadata apply         # the API over it
hasura metadata reload        # re-read the new columns

Do not half-do this. A migrations/ directory that is sometimes authoritative is worse than not having one.

A pipeline

name: deploy-hasura
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install the Hasura CLI
        run: curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash

      - name: Apply the schema
        run: alembic upgrade head
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

      - name: Apply metadata
        run: |
          hasura metadata apply \
            --endpoint "$HASURA_ENDPOINT" --admin-secret "$HASURA_ADMIN_SECRET"
          hasura metadata reload \
            --endpoint "$HASURA_ENDPOINT" --admin-secret "$HASURA_ADMIN_SECRET"
        env:
          HASURA_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }}
          HASURA_ADMIN_SECRET: ${{ secrets.HASURA_ADMIN_SECRET }}

Two things to add before you trust it. Run hasura metadata diff on pull requests so a reviewer sees the API change, not just the YAML. And check get_inconsistent_metadata after applying — the apply can succeed while leaving objects inconsistent, and you want the pipeline to fail rather than the API to quietly shrink.

CI/CD in v3 (DDN)

On the v3 sections. Everything marked v3 (DDN) is taken from the official Hasura DDN documentation as read on 2026-08-21 and was not run locally — it shows configuration, never claimed output. The v2 material was executed against a running engine.

v3 replaces apply-in-place with build-and-promote:

ddn supergraph build create

Each build is immutable and gets its own unique GraphQL endpoint. You can point a test suite at a build before anything else sees it, then promote it. The entire category of “somebody applied metadata to production” disappears, because applying is no longer the operation.

And migrations are no longer Hasura’s job

hasura migrate has no v3 successor. Schema migration belongs to your own tool, and you tell Hasura about the result by re-introspecting:

alembic upgrade head
ddn connector introspect my_connector
ddn model add my_connector new_table
ddn supergraph build create

For teams who use the v2 CLI to version schema and metadata together, this is a real workflow change. For teams already in the second camp above — application owns DDL, Hasura owns metadata — v3 simply makes official the split you already had.

Next

Lesson 14 covers making it fast.