All demos/ Pull and sync
DEMO-03-SYNCSEP 2026
Retrofit stops the app before it runs against the wrong schema.
Before the app runs, one Makefile check compares the operation log in git with the local database’s schema. When the log is newer, make db-sync brings the database forward. When the database is newer, make db-capture appends the change to the log.
- Stack
- make + git + PostgreSQL
- Change
- AddColumn by pull,
driftby hand - Reverse
- none; the gate is read-only
Chapters
make run checks the database against the log before the app starts. Today it passes.
02
The log is newer than the database36 secA column arrives by git pull. The gate reports the version, and make db-sync applies it.
03
The database is newer than the log46 secA column and an index are added with psql. No version matches, and make db-capture appends what the log is missing.
04
Run it end to end2 min 15 secThe project, the Makefile, a git pull with make db-sync, a hand change with make db-capture, and the log from the seal down.Summary
You pull teammates’ code and schema changes every day, and the local database is the one thing the pull does not update.
The database is the one part of a checkout that git pull cannot update, and
the one part of your work that git push cannot send. So every laptop holds
two copies of the schema: the operation log, a text file beside the code that
git moves, and the local database, which only its owner moves. They disagree
in exactly two ways. The log moved and the database did not, after a pull. Or
the database moved and the log did not, after a change made by hand.
The check is a schema-only pg_dump and one Retrofit command, as a
prerequisite of the team’s run target. identify --expect last reports the cut
the dump is at and exits non-zero when that is not the head of the log. A
database that is behind gets expect-mismatch and the command that brings it
forward. A database changed by hand gets from-dump-no-match and the list of
what the log is missing. The other two targets are the two fixes, one per
direction.
make run exits 2 before
the app starts, reports the cut it is at and the head of the log, and make db-sync migrates it there from its own dump. When the database is newer
than the log, the same check exits 2 listing what the log is missing, and
make db-capture appends it to the log as operations the next pull delivers.Retrofit never connects to a database. The dump is the only input, psql
applies what migrate emits, make is the gate, and git carries the log. The
team is already on Retrofit when the demo opens. The log is sealed at v1,
the version the team declared stable, and the demo does not seal it again.
This is the team’s Makefile in full, the file the recordings run. The
gate is the three lines under check-db; the two targets after it are the
two fixes.
The team's Makefile
# The team's Makefile. Three targets matter here.
#
# make run check the local database against the operation log,
# then start the app
# make db-sync bring the local database forward to the head of the log
# make db-capture append what the local database has and the log lacks
#
# check-db is the whole idea. It is an ordinary prerequisite of run, so the
# check happens on every run without anyone remembering to do it. It costs
# one schema-only dump and one Retrofit command, and it stops for one of two
# reasons:
#
# expect-mismatch the log moved and the database did not. A pull
# brought new operations. Fix: make db-sync
# from-dump-no-match the database moved and the log did not. A change
# was made by hand. Fix: make db-capture
#
# Retrofit reads the dump. pg_dump and psql are what touch the database.
# -q drops Retrofit's note tier; warnings and errors still print, and
# nothing here filters or rewords them.
DB ?= postgres://postgres@localhost:55437/shop
SCHEMA ?= shop
.PHONY: run check-db db-sync db-capture
run: check-db
./bin/app "$(DB)"
check-db:
pg_dump --schema-only --schema=$(SCHEMA) "$(DB)" > .db.dump.sql
retrofit identify .db.dump.sql --expect last -q
db-sync:
pg_dump --schema-only --schema=$(SCHEMA) "$(DB)" > .db.dump.sql
retrofit migrate --from-dump .db.dump.sql -q | psql "$(DB)" -v ON_ERROR_STOP=1 -q
db-capture:
pg_dump --schema-only --schema=$(SCHEMA) "$(DB)" > .db.dump.sql
retrofit drift .db.dump.sql -q
Chapter 01The gate
How do we find out the local database and the log disagree?
The project at the start: the operation log beside the code, sealed at v1, the version the team declared stable, and a local database built from it. The team’s Makefile read on screen: check-db is a prerequisite of run, one schema-only dump and one identify --expect last, with db-sync and db-capture as the two fixes. At this point the two copies agree, so make run reports the seal and starts the app.
Read transcript
pull-and-sync-demo: the gate
Generated from the demo script. Every command below is run verbatim by the recording.
1/3 the project
an orders service. its schema is an operation log in git, next to the code, sealed at v1, the version the team declared stable:
ls
retrofit op show --include-sealed --tail 1 -k reason -q
our laptop has a database built from that log, with rows in it:
psql "$DB" -tAc 'SELECT count(*) FROM shop.orders' | sed 's/^/ local orders: /'
so there are two copies of the schema here: the log, which git moves, and the database, which only we move.
2/3 the three lines in the Makefile
the team’s run command checks one copy against the other before it starts the app:
sed -n '/^run:/,/identify/p' Makefile
check-db is a prerequisite of run, so it happens on every run. it takes a schema-only dump and asks identify one question: is this database at the head of the log? identify reports the cut the dump is at, and exits non-zero when that is not the cut it was told to expect. the other two targets are the two fixes. we will use each one:
sed -n '/^db-sync:/,$p' Makefile
3/3 in sync
right now the two copies agree, so the app starts:
make run
They stop agreeing in two ways. The log moves and the database does not: that is a pull. The database moves and the log does not: that is a change made by hand. The next two chapters are those two. The same check catches both, with a different code and a different fix for each.
The log beside the code, sealed at v1, the version the team declared stable, and a local database built from it with rows in it. Two copies of the schema: git moves one, only we move the other.
`check-db` is an ordinary prerequisite of `run`, so it happens on every run. It takes a schema-only dump and asks `identify --expect last` whether the database is at the head of the log. `db-sync` and `db-capture` are the two fixes.
The two copies agree. `identify` reports the seal the database is at, exits 0, and the app starts. The next two chapters are the two ways they stop agreeing.
Chapter 02The log is newer than the database
A schema change arrived by git pull. How does the database catch up?
git pull brings one unsealed AddColumn in the operation log, in the same commit as the code that reads it. make run exits 2 with expect-mismatch and reports the seal the local database is at, the operation the log expects, and the command that applies the difference. migrate --from-dump previews the one statement, make db-sync applies it, and the app reads its new column.
Read transcript
pull-and-sync-demo: pull and sync
Generated from the demo script. Every command below is run verbatim by the recording.
1/3 pull
a teammate added a column to orders, changed the app to read it, and pushed. we pull:
git pull -q && git log --oneline --no-decorate -1
one commit carries the code change and the schema change together:
git show --stat --oneline HEAD | tail -3
the schema change is one unsealed operation. nothing has deployed it, so nobody sealed it:
retrofit op show -q | cut -c1-72
2/3 the gate reports the cut
the log moved. our database did not. the app now reads a column our database does not have, and make run checks before the app can fail:
make run
exit status 2
exit 2, before the app started. identify reported the cut our database is at, the v1 seal, and the cut the log expects, the teammate’s operation. the hint is the command that applies the difference between the two.
3/3 sync and run
db-sync runs that command from the same dump. first, the difference:
retrofit migrate --from-dump .db.dump.sql -q
one statement. apply it:
make db-sync
the same check, unchanged, now passes, and the app reads its new column:
make run
The log was newer than our database. The gate reported the cut, and db-sync applied the difference to the head, computed from its own dump.
One unsealed `AddColumn` arrives in the operation log, in the same commit as the code that reads it. It is not sealed, because nothing has deployed it.
The log moved and the database did not. `check-db` exits 2 before the app starts, with `expect-mismatch`: the seal the database is at, the operation the log expects, and the `migrate` command that applies the difference.
`migrate --from-dump` previews the one `ALTER`, computed from the dump rather than a history table. `make db-sync` applies it. `make run` passes, and the three orders are still there.
Chapter 03The database is newer than the log
We changed the database by hand. How does that become the team's change?
A column and an index are added straight to the local database. Nothing about the app changed, so it would start; make run exits 2 anyway, with a different code, from-dump-no-match, lists both additions, and names the drift command. drift --dry-run previews two operations, make db-capture appends them to the log unsealed, the gate passes, a second dry run finds nothing, and the commit pushes the two lines for the next pull.
Read transcript
pull-and-sync-demo: capture a hand change
Generated from the demo script. Every command below is run verbatim by the recording.
1/3 change the database by hand
not every change starts as an operation. we add a column and an index straight to our database, with psql:
psql "$DB" -qc 'ALTER TABLE shop.orders ADD COLUMN gift_message text'
psql "$DB" -qc 'CREATE INDEX orders_placed_at_idx ON shop.orders (placed_at)'
2/3 the database matches no cut
now the database moved and the log did not. nothing about the app changed, so it would start. the gate stops anyway, with a different code:
make run
exit status 2
nothing on the log matches our database. the nearest cut is the head, and the two lines under it are what our database has beyond it. left like this, the change stays on this laptop: nobody who pulls gets it.
3/3 capture and push
the hint names drift. it reads the same dump from the log’s side and reports the operations the log is missing:
retrofit drift .db.dump.sql --dry-run -q
db-capture appends them to the log, unsealed:
make db-capture
the check passes again, and a second dry run finds nothing:
make run
retrofit drift .db.dump.sql --dry-run -q | tail -1
the change goes out in a commit, two lines in the log. the next pull delivers it:
git add -A && git commit -qm 'orders: gift_message and an index on placed_at' && git push -q
git show --stat --oneline HEAD | tail -1
Our database was newer than the log. The gate reported that no cut matched, and db-capture appended what was missing. One seal, three unsealed operations, and the demo sealed nothing more.
A column and an index added straight to the local database. The log has not moved.
The database moved and the log did not. A different code, `from-dump-no-match`, with both additions listed and the `drift` command that previews them. The app would have started; the gate stops because the change would stay on this laptop.
`drift --dry-run` previews the two operations. `make db-capture` appends them to the log, unsealed. The gate passes, a second dry run finds nothing, and the commit carries two lines in the log.
Chapter 04Run it end to end
Start with a team already on Retrofit, read the Makefile, watch make run pass, pull a teammate’s AddColumn, watch make run exit 2 and make db-sync fix it, add a column and an index with psql, watch make run exit 2 the other way and make db-capture fix that, push, and read the log: one seal, three unsealed operations.
Read transcript
pull-and-sync-demo: complete walkthrough
Generated from the demo script. Every command below is run verbatim by the recording.
a team already on Retrofit
an orders service. its schema is an operation log in git, next to the code, sealed at v1, the version the team declared stable:
ls
retrofit op show --include-sealed --tail 1 -k reason -q
our laptop has a database built from that log, with rows in it:
psql "$DB" -tAc 'SELECT count(*) FROM shop.orders' | sed 's/^/ local orders: /'
so there are two copies of the schema here: the log, which git moves, and the database, which only we move.
the three lines in the Makefile
the team’s run command checks one copy against the other before it starts the app:
sed -n '/^run:/,/identify/p' Makefile
check-db is a prerequisite of run, so it happens on every run. it takes a schema-only dump and asks identify one question: is this database at the head of the log? identify reports the cut the dump is at, and exits non-zero when that is not the cut it was told to expect. the other two targets are the two fixes. we will use each one:
sed -n '/^db-sync:/,$p' Makefile
right now the two copies agree, so the app starts:
make run
they stop agreeing in two ways. the log moves and the database does not: that is a pull. the database moves and the log does not: that is a change made by hand. the same check catches both, with a different code and a different fix for each.
we pull from the team
meanwhile, a teammate adds a column to orders, changes the app to read it, and pushes. no seal: nothing has deployed it. we pull:
git pull -q && git log --oneline --no-decorate -1
one commit carries the code change and the schema change together:
git show --stat --oneline HEAD | tail -3
the schema change is one unsealed operation in the log:
retrofit op show -q | cut -c1-72
the gate reports the cut
the log moved. our database did not. the app now reads a column our database does not have, and make run checks before the app can fail:
make run
exit status 2
exit 2, before the app started. identify reported the cut our database is at, the v1 seal, and the cut the log expects, the teammate’s operation. the hint is the command that applies the difference between the two. no history table was consulted: pg_dump read the database, identify the dump.
sync and run
db-sync runs that command from the same dump. first, the difference:
retrofit migrate --from-dump .db.dump.sql -q
one statement. apply it:
make db-sync
the same check, unchanged, now passes, and the app reads its new column:
make run
we change the database by hand
not every change starts as an operation. we add a column and an index straight to our database, with psql:
psql "$DB" -qc 'ALTER TABLE shop.orders ADD COLUMN gift_message text'
psql "$DB" -qc 'CREATE INDEX orders_placed_at_idx ON shop.orders (placed_at)'
the database matches no cut
now the database moved and the log did not. nothing about the app changed, so it would start. the gate stops anyway, with a different code:
make run
exit status 2
nothing on the log matches our database. the nearest cut is the head, and the two lines under it are what our database has beyond it. left like this, the change stays on this laptop: nobody who pulls gets it.
capture and push
the hint names drift. it reads the same dump from the log’s side and reports the operations the log is missing:
retrofit drift .db.dump.sql --dry-run -q
db-capture appends them to the log, unsealed:
make db-capture
the check passes again, and a second dry run finds nothing:
make run
retrofit drift .db.dump.sql --dry-run -q | tail -1
the change goes out in a commit, two lines in the log. the next pull delivers it:
git add -A && git commit -qm 'orders: gift_message and an index on placed_at' && git push -q
git show --stat --oneline HEAD | tail -1
what the log says now
retrofit op show --include-sealed -q | tail -4 | awk -F'[(,]' '{print " " $1 " " $2}'
one seal and three unsealed operations. the seal marks v1, the version the team declared stable; everything after it is what the team is working on. a teammate’s column arrived by pull and ours left by push, and each time the two copies disagreed, the same check said which way and what to run. the next seal comes when the team declares the next version stable.