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, drift by hand
Reverse
none; the gate is read-only

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.

When the log is newer than the local database, 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.

34 sec space to pause f for fullscreen
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.

  1. 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.

  2. `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.

  3. 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.

36 sec space to pause f for fullscreen
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.

  1. 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.

  2. 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.

  3. `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.

46 sec space to pause f for fullscreen
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.

  1. A column and an index added straight to the local database. The log has not moved.

  2. 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.

  3. `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.

2 min 15 sec space to pause f for fullscreen
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.

Retrofit has not shipped yet. No list, no noise, one message when it does.