Help us shape the future of Go! Take the 2026 Go Developer Survey

godwit

module
v0.0.0-...-4aa7ecc Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: Apache-2.0

README

godwit

The bar-tailed godwit holds the world record for the longest non-stop migration: ~13,500 km without landing. Your database migrations should be as reliable.

godwit is a crash-safe PostgreSQL migration service for pipelines. Migrations are plain SQL files; they run under a statement-level journal in the target database, so a replica killed mid-run is taken over by another and resumed from the last committed statement. There is no dirty state and no repair. Apache 2, PostgreSQL only, one binary that is both the service and the CLI.

Flyway, Liquibase and Atlas moved undo, dry runs, lint and drift detection behind paid tiers, and none of them is a service: every pipeline rebuilds the same glue around a CLI. godwit is that glue done once. The honest side-by-side, including what godwit lacks, is in docs/start/comparison.md.

Three things that are actually different

The journal is in the target database, committed with the DDL. A tx statement and its done row commit together; a CREATE INDEX CONCURRENTLY gets a write-ahead intent and a verifier that inspects pg_index after a crash; a backfill commits its rows and its cursor in the same transaction. So kill -9 at any point leaves a state the next attempt can read and continue from, and nothing has to be repaired by hand. The point-by-point crash timeline is in the journal page.

A directive is a change godwit executes, not a recipe it prints. A -- godwit: comment line says what the migration wants; godwit renders the lock-safe statements against the target's own catalog at plan time — the real primary key, column type and nullability — and freezes them onto the plan, so the run applies what the pull request showed:

-- 20260901121000_customer_id_text.up.sql
-- godwit: change-type orders.customer_id text using='customer_id::text'
-- godwit: assert 'SELECT count(*) FROM orders WHERE customer_id IS NULL' = 0
$ godwit plan --target app --dir db/migrations --rollout expand-contract
1 migration will be applied to app. This runs in two halves. Now, godwit applies only what adds: new
columns, indexes and constraints, which the application it is already running does not have to know
about. A generated column change writes into a new column and holds the two in step with a trigger, so
writes never stop while it fills. Nothing is renamed and nothing is dropped yet. When you confirm,
godwit runs the rest: the renames, and the drops it held back. Until you confirm, the old columns are
still there holding the data you started with, and that is the way back. The run waits at
awaiting_contract; confirm it with godwit confirm on the pull request.

godwit will perform the following actions:

  # 20260901121000_customer_id_text  (written by a directive)
  -- godwit: change-type orders.customer_id text using='customer_id::text'
  -- godwit: assert 'SELECT count(*) FROM orders WHERE customer_id IS NULL' = 0
  ~ table "public"."orders" {
      ~ customer_id     = bigint -> text
      + customer_id_old = bigint NOT NULL
        # (3 unchanged attributes hidden)
    }
  note: leaves public.orders.customer_id_old for rollback; drop it with `-- godwit: drop-column public.orders.customer_id_old`

Plan: 0 to add, 1 to change, 0 to destroy.

That is fourteen statements. --plan-format statements prints them: the trigger keeps both columns in sync while the batches walk the table, the batches resume from their journalled cursor after a crash, one statement is godwit's own count of the rows the backfill has still to reach so a using= that never converges cannot become the irreversible swap, and the rename waits in awaiting_contract — where the count is asked again — until a human confirms it. Ten operations exist; everything godwit will not do safely is refused by name. directives.

The plan is a contract, and it applies before the merge. godwit plan --target --save stores the admitted plan with an observation of the live target; migrate binds to that plan and refuses with the exact diff when the target moved underneath. On a pull request the GitHub Action turns that into: lint and plan as a sticky comment, godwit apply bound to the reviewed plan, godwit confirm for the contract phase, and a godwit/applied commit status that stays pending until the whole migration is on the database. By the time the branch lands, main describes a schema the target already has. plans, the GitHub Action.

Quickstart

go install github.com/SamuelMolling/godwit/cmd/godwit@main   # needs gcc (libpg_query) and Go 1.26
docker pull ghcr.io/samuelmolling/godwit:main               # or the image: linux/amd64 + arm64, distroless

# the service's own store, and a database to migrate
psql -U postgres \
  -c "CREATE ROLE godwit LOGIN PASSWORD 'godwit' CREATEDB" \
  -c "CREATE DATABASE godwit_store OWNER godwit" \
  -c "CREATE ROLE app LOGIN PASSWORD 'app'" \
  -c "CREATE DATABASE app OWNER app"

export GODWIT_MASTER_KEY=$(openssl rand -hex 32) GODWIT_TOKENS='admin:admin:s3cret'
godwit serve --store-dsn postgres://godwit:godwit@localhost/godwit_store &

export GODWIT_SERVER=http://localhost:8474 GODWIT_TOKEN=s3cret
godwit target add app --provider static --dsn postgres://app:app@localhost/app
godwit lint --dir db/migrations                     # hazards, parse errors, malformed directives
godwit plan --target app --dir db/migrations        # what would run, against the live target (--save stores it)
godwit migrate --target app --dir db/migrations     # streams the run; exit 0 when applied
godwit target status app --dir db/migrations

That single-server form executes submitted SQL on the store server as the store role, which is why it needs CREATEDB and why serve warns about it on every start. Anywhere a token is shared, add --scratch-dsn pointing at a PostgreSQL that holds nothing (security). The full walkthrough — the local up/status/down loop with no service at all, writing a migration from an ORM schema, and the first CI step — is docs/start/getting-started.md.

What's inside

Crash-safe engine statement-level journal, write-ahead intents and verifiers for non-transactional statements, batched backfills resumed from their cursor — the journal protocol
Leased service any replica claims a run, a lost lease is taken over, transient failures retry with backoff, a re-run of the same pipeline job re-attaches instead of queueing a second — leases
Hazard gate H001–H010 from a real PostgreSQL parser, each carrying the safe form as ready-to-copy SQL; refused unless acknowledged in the run — hazards
Directives ten -- godwit: operations godwit expands against the target's catalog and freezes onto the plan; backfill holds a sync trigger for the length of its batches — directives
Assertions -- godwit: assert '<select>' = 0 makes a condition about the data a statement of the plan, journalled and re-checked at confirm time — assertions
Admission the hazard gate, an out-of-order guard, and a replay of the target's recorded history plus the new files on a throwaway database, before anything is queued — admission
Plan as contract the admitted plan is stored with an observation of the target; migrate binds to it, re-plans what other runs explain, refuses the rest, and records a migration already applied by hand instead of executing it — plans
Apply before merge composite GitHub Action: lint and plan on the pull request, godwit apply, godwit confirm, godwit revert, verify on the merge commit; ArgoCD hooks and a Helm chart — the GitHub Action
Merge gate the apply sets the godwit/applied commit status; make it a required check and the pull request cannot merge until the migration is on the database — the merge signal
Expand → contract the rollout is split by statement: the run parks in awaiting_contract and ConfirmRollout resumes the same run where it stopped — rollout policies
Revert scoped to what the run actually applied, never to the directory it submitted; a plan that would destroy rows is refused, not warned about — revert
Version targets --to <version> stops a run short; the migrations above it stay on the plan marked withheld, so the report cannot be read as the whole set — version targets
Repeatables and checkpoints R__ files re-applied whenever their content changes; godwit checkpoint collapses old history into one file the replay runs instead — repeatable migrations, checkpoints
Fleet view godwit migrations: which target has which migration, keyed by version and checksum, so the same version meaning two things in staging and production is loud — the fleet view
Drift and baseline a schema fingerprint after every successful run, a periodic monitor, events and accept — drift
Adopting an existing database target adopt --version for a schema godwit never journalled, target adopt --from-journal for one whose journal it did not write — deployment
Migrations from a schema godwit diff writes the next up/down pair from a DDL file or from Prisma, GORM, Django, Alembic, Rails, Drizzle or any command, all rendered client-side — generating migrations from a schema
ORM drift gate godwit lint --server <url> --target <t> fails (E005) when the committed SQL no longer expresses the ORM schema — keeping the generated sql and the orm schema together
API, CLI and UI connect (gRPC + JSON) with scoped bearer tokens (read, pipeline, operator, admin); the same binary is the CLI; serve --ui adds an operator UI at /ui — API, configuration, deployment
Credentials static (AES-256-GCM in the store), kubernetes (mounted secret), vault (KV or dynamic) — security
Operations per-target timeouts and search_path, admission limits, Prometheus /metrics, audit on every mutation, webhook and Slack notifications — deployment, configuration

Documentation

The manual is docs/, grouped by what you are doing.

Start evaluating: a first run, the approaches and what each costs, how godwit compares
Use opening pull requests: the App, the Action, neither, Backstage, and every command
Run operating the service: deployment, configuration, security, runbook
Internals the journal protocol, run states and leases, admission, drift, and the API
Decisions why godwit is shaped this way, and what was refused

Also: examples (copy-ready pipelines), examples/deploy (the service on ingress-nginx, ECS, Docker Compose and a plain VM), deploy/helm/godwit, deploy/argocd, the two-replica crash demo, and AGENTS.md for contributors.

Design principles

  1. The journal is the truth. Progress lives in the target database, committed with the DDL.
  2. Plain SQL. Any stack can produce it; godwit runs it.
  3. Roll forward. Down migrations are required, but the production path is expand → contract, and godwit schedules the contract.
  4. Safety is not a paid tier.

Status

v1 in progress: PostgreSQL only, API-first. Version stays 0.0.1 until v1 has run in production — nothing here has.

make all (lint, proto lint, the suites at 100% statement coverage, build) is the gate on every commit. Outside it, make e2e drives the built binary against PostgreSQL in Docker while SIGKILLing replicas mid-statement, make load measures a ten-million-row backfill and a thousand-migration target, and make chaos kills godwit in the gaps the crash rig cannot reach. Their numbers and knobs are in docs/internals/testing.md.

Directories

Path Synopsis
Package assets holds the godwit logo files embedded for the UI.
Package assets holds the godwit logo files embedded for the UI.
cmd
godwit command
Command godwit is the entry point for the godwit CLI and service.
Command godwit is the entry point for the godwit CLI and service.
gen
internal
admission
Package admission decides what a submitted migration set becomes on a target and whether it may run.
Package admission decides what a submitted migration set becomes on a target and whether it may run.
authz
Package authz decides who may command godwit, over the API and through the forge, and serves no transport.
Package authz decides who may command godwit, over the API and through the forge, and serves no transport.
cli
Package cli wires the godwit command tree.
Package cli wires the godwit command tree.
comment
Package comment parses a pull request comment into the godwit command it names.
Package comment parses a pull request comment into the godwit command it names.
config
Package config loads the godwit.yaml project file with env overrides.
Package config loads the godwit.yaml project file with env overrides.
controlplane
Package controlplane holds the state store, the lease-based scheduler and drift monitoring.
Package controlplane holds the state store, the lease-based scheduler and drift monitoring.
creds
Package creds resolves target-database credentials through pluggable providers.
Package creds resolves target-database credentials through pluggable providers.
creds/credstest
Package credstest holds the conformance suite every credential provider must pass.
Package credstest holds the conformance suite every credential provider must pass.
limits
Package limits holds the bounds, decided by configuration, that a request is admitted within.
Package limits holds the bounds, decided by configuration, that a request is admitted within.
link
Package link composes the UI addresses that leave the process; it sits outside ui and imports nothing of godwit's, because ui already reaches notify and could not be called back from it.
Package link composes the UI addresses that leave the process; it sits outside ui and imports nothing of godwit's, because ui already reaches notify and could not be called back from it.
lint
Package lint checks a migration directory the way a pull request gate does: no database involved.
Package lint checks a migration directory the way a pull request gate does: no database involved.
metrics
Package metrics exposes the service's Prometheus metrics.
Package metrics exposes the service's Prometheus metrics.
notify
Package notify delivers godwit events to the outside world.
Package notify delivers godwit events to the outside world.
redact
Package redact decides which part of an error godwit publishes and which part stays in the server log.
Package redact decides which part of an error godwit publishes and which part stays in the server log.
report
Package report renders what a plan or a run does to a database, in the words the terminal and the pull request both read.
Package report renders what a plan or a run does to a database, in the words the terminal and the pull request both read.
schemasource
Package schemasource turns a description of the desired database into PostgreSQL DDL without a database.
Package schemasource turns a description of the desired database into PostgreSQL DDL without a database.
server
Package server assembles and runs the godwit service.
Package server assembles and runs the godwit service.
surface/api
Package api serves the godwit control-plane API over connect (gRPC + JSON).
Package api serves the godwit control-plane API over connect (gRPC + JSON).
surface/githubapp
Package githubapp receives GitHub App deliveries and turns a verified one into an authorised command.
Package githubapp receives GitHub App deliveries and turns a verified one into an authorised command.
surface/ui
Package ui serves the operator web UI on top of the service's own API implementation.
Package ui serves the operator web UI on top of the service's own API implementation.
version
Package version exposes build-time version metadata.
Package version exposes build-time version metadata.
test
e2e
Package e2e drives the built godwit binary against a real PostgreSQL; run it with `make e2e`.
Package e2e drives the built godwit binary against a real PostgreSQL; run it with `make e2e`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL