Atlas CE Drop-In Replacement: What ptah-compat Found in wpmgr
We ran ptah-compat, an Atlas CE drop-in replacement, on wpmgr. It wrote an RLS policy migration Atlas CE skipped, and found schema.sql out of date.
Ptah helps you plan, review, and apply database migrations. Try in your browser
ptah-compat is the part of Ptah that stands in for the Atlas CLI. You point
it at an existing Atlas project and run the same commands. It uses the
project’s atlas.hcl, migration directory and atlas.sum without changes.
We came across wpmgr by chance and tried it there. wpmgr is an open-source, self-hosted WordPress fleet manager. It uses Atlas Community Edition, and it relies heavily on PostgreSQL row-level security: its migrations create 285 policies. We found a few problems that had nothing to do with Ptah and sent fixes, which wpmgr merged. It also decided to stay on Atlas CE for now.
The most useful result came last, so here it is first. wpmgr still owes a
RESTRICTIVE RLS policy on one of its tables. We declared it in the project’s
schema file and asked both tools for the next migration. Atlas CE reported the
directory in sync and wrote nothing. ptah-compat wrote the policy migration.
Afterwards atlas migrate validate still accepted the directory, so both tools
can keep working on the same files. The rest of the post shows how we got
there, including the schema drift we found on the way.
We used Atlas CE v1.3.0, ptah-compat 0.10.0 and PostgreSQL 16.15, on two wpmgr
commits:
2c471d2
from before the fixes and
8b81b3e
from after them. Every command and its output is in the
supporting files.
How wpmgr uses Atlas
Section titled “How wpmgr uses Atlas”ADR-002 in wpmgr’s
DECISIONS.md
explains the choice. With Atlas CE, one file, db/schema.sql, drives two
tools. sqlc reads it to generate typed Go queries, and atlas migrate diff
compares it with the migrations to write the next one. The same ADR says the
project won’t use Atlas features that need the Pro license or EULA. It calls
Atlas’s open-core model a risk and keeps goose as the fallback.
Using ptah-compat as an Atlas CE drop-in replacement
Section titled “Using ptah-compat as an Atlas CE drop-in replacement”For wpmgr, “drop-in” has a concrete meaning. ptah-compat reads the
env "local" block that atlas.hcl already has, uses the same
ATLAS_DEV_URL, and writes migrations and atlas.sum in Atlas’s format.
Nothing else changes. The server still applies its embedded migrations at
startup, self-hosters never run either tool, and the atlas binary still
works on the same directory. Ptah is MIT-licensed, which matters to a project
that ruled out Atlas’s commercial features on license grounds.
The documented command no longer worked
Section titled “The documented command no longer worked”The README tells contributors to create a migration with
atlas migrate diff <name> --env local. At 2c471d2 the command failed before
it compared anything:
You have a checksum error in your migration directory.
L111: 20260803000000_m101_vuln_severity_unknown_feed_alternation.sql was addedatlas.sum hadn’t been updated since m100. We re-hashed the directory and ran
the command again. This time Atlas couldn’t load schema.sql, because a policy
on sites refers to site_shares, and the file creates site_shares further
down.
The bigger problem was what the file said. We applied all 145 migrations to an
empty database and got 120 tables and 285 policies in public. schema.sql
declared 97 tables and 231 policies. sqlc generates its code from schema.sql,
so some generated queries still used columns that
m50 had dropped from
backup_schedules. One of them fails on a migrated database:
ERROR: column "notify_on_completion" does not existptah-compat doesn’t read sqlc queries. What it compares is schema.sql with
the migrations: migrate diff applies every migration to a throwaway database,
loads schema.sql, and writes any difference out as a migration. That
difference is what left sqlc generating queries for dropped columns. We found
the failing query separately, by preparing the generated SQL on a migrated
database. We described what we found in wpmgr#759 and sent a
fix in wpmgr#760.
The fix wpmgr merged
Section titled “The fix wpmgr merged”wpmgr took the database changes in
wpmgr#762: the corrected
schema.sql, the regenerated sqlc code and a re-hashed atlas.sum. The
maintainer also moved four statements so the file loads into an empty database
in one pass. On the resulting commit, 8b81b3e, we ran both tools:
atlas migrate diff probe --env localptah-compat migrate diff probe --env localBoth gave the same answer:
The migration directory is synced with the desired state, no changes to be madeatlas migrate validate --env local passed as well.
Adding a missing RLS policy
Section titled “Adding a missing RLS policy”wpmgr’s README names the main limitation: Atlas CE “cannot diff RLS policies
without login.” In practice Atlas generates the table and index changes, and a
developer adds the RLS statements to the migration by hand. No tool checks
those statements against schema.sql.
wpmgr already has a case like this waiting. Migration
m132 lists seven tables
that still need a RESTRICTIVE site_scope policy. We added the first of them,
for site_media_settings, to schema.sql, with the same predicate m132 uses
for the other tables. Then we asked each tool for a new migration.
Atlas CE printed the same “synced” message and didn’t create a file, even
though the policy existed only in schema.sql.
ptah-compat created this migration and updated atlas.sum:
DROP POLICY IF EXISTS "site_media_settings_site_scope" ON "site_media_settings"; CREATE POLICY "site_media_settings_site_scope" ON "site_media_settings" AS RESTRICTIVE FOR ALL USING (coalesce(current_setting('app.site_scope', true), '') <> 'on' OR site_id = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] )) WITH CHECK (coalesce(current_setting('app.site_scope', true), '') <> 'on' OR site_id = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ));A second run of ptah-compat reported the directory as synced. After that,
atlas migrate validate accepted the directory with the new file in it. A team
can let ptah-compat write the policy migrations and keep using atlas for
everything else.
Checking for drift in CI
Section titled “Checking for drift in CI”The new header of schema.sql from
#762 says that nothing checks
the file automatically yet. A migration that doesn’t update schema.sql still
builds and passes CI.
Our #760 proposal included a CI
job for this. It applies the migrations with ptah-compat, compares the result
with schema.sql, and fails if they differ.
If it can’t run the comparison at all, it exits with code 2, so a broken setup
never looks like a pass. On the proposal branch the job passed. We then added a
migration without touching schema.sql, and the job failed with this output:
DRIFT: db/schema.sql and apps/api/migrations/ describe different schemas.The statements below turn the migrations' schema into schema.sql's.Bring schema.sql up to the migrations, or add the migration schema.sql expects:
--- 20260928093327_schema_drift.sql -- ALTER statements: -- ALTER TABLE "drift_probe" DROP CONSTRAINT IF EXISTS "drift_probe_pkey"; -- WARNING: This will delete all data! DROP TABLE IF EXISTS "drift_probe" CASCADE;The SQL shows what schema.sql implies, which is that there is no
drift_probe table. The fix is to add the table to schema.sql. Nobody should
run this SQL.
Where things stand
Section titled “Where things stand”For now, wpmgr isn’t adopting ptah-compat, either as a required CI step or
as the documented way to write migrations. The maintainer didn’t want to add a
new required tool before launch. That’s a fair decision, and it’s theirs to
make. Keeping schema.sql in sync automatically is still open in
#759.
Here is what we measured. With Atlas CE, the next site_scope policy has to be
written by hand, and if someone forgets it, migrate diff still reports that
everything is in sync. With ptah-compat, the same command in the same
directory writes the policy migration. Trying it means installing one binary.
Going back is just as easy, because atlas still validates the directory after
ptah-compat has written to it.
These results come from two wpmgr commits, one added policy and PostgreSQL 16. We didn’t run any paid Atlas edition, so we make no claims about it.
Try it on your own project
Section titled “Try it on your own project”You can check whether this matters for your project without switching
anything. Work in a separate clone or git worktree, and point ATLAS_DEV_URL
at an empty PostgreSQL server with the same major version as production. If
your migrations create roles, as wpmgr’s do, also set
PTAH_DEV_SERVER_DISPOSABLE=1, and only for a server you can throw away.
Declare one change in your schema file that Atlas CE may skip, such as an RLS
policy. Then run, with the env name your atlas.hcl uses:
go install ptah.run/cmd/ptah-compat@v0.10.0atlas migrate diff trial --env localptah-compat migrate diff trial --env localatlas migrate validate --env localCompare what each diff writes. If ptah-compat writes a migration before you
change anything, your schema file and your migrations already disagree, which
is what happened on wpmgr. Delete the copy when you’re done.
For moving a whole project, the
adoption guide goes step by
step, and the
Atlas compatibility overview
lists what ptah-compat supports.
Put Ptah to work
Example files
- drift-probe.sql
- expected/atlas-checksum.txt
- expected/guard-drift.txt
- expected/ptah-policy-migration.sql
- expected/sqlc-query.txt
- expected/synced.txt
- measured/01-versions-atlas.txt
- measured/02-versions-ptah-compat.txt
- measured/03-main-atlas-diff.txt
- measured/04-main-atlas-hash.txt
- measured/05-main-atlas-diff-after-hash.txt
- measured/06-replay-apply.txt
- measured/07-replay-counts.txt
- measured/08-main-schema-tables.txt
- measured/09-main-schema-policies.txt
- measured/10-adopted-schema-tables.txt
- measured/11-adopted-schema-policies.txt
- measured/12-main-query.txt
- measured/13-adopted-query.txt
- measured/14-adopted-atlas-validate.txt
- measured/15-adopted-atlas-diff.txt
- measured/16-adopted-ptah-diff.txt
- measured/17-policy-atlas-diff.txt
- measured/18-policy-atlas-status.txt
- measured/19-policy-ptah-diff.txt
- measured/20-policy-ptah-status.txt
- measured/21-policy-migration.sql
- measured/22-policy-ptah-diff-again.txt
- measured/23-policy-atlas-validate.txt
- measured/24-guard-in-sync.txt
- measured/25-guard-hash.txt
- measured/26-guard-drift.txt
- measured/27-go-install.txt
- measured/commands.json
- README.md
- site-scope-policy.sql
- verified.json