Atlas CE Doesn't Manage Postgres RLS. Ptah Does.
Atlas Community leaves PostgreSQL row-level security outside schema management. We tested Ptah Compat to manage tables, RLS policies, and grants together.
Ptah helps you plan, review, and apply database migrations. Try in your browser
dmikalova/tasks uses PostgreSQL row-level
security to keep users’ tasks separate. Its schema workflow had a split:
Atlas applied the tables from HCL, then a TypeScript wrapper ran db/rls.sql
through psql. A comment in the wrapper explained the reason: “Atlas community
edition does not manage RLS.”
We replaced that sequence with Ptah Compat in
our fork.
The existing policies and grants now sit beside the tables in db/schema.hcl.
A schema preview includes policy changes, and the ordinary apply restores RLS
drift without a separate SQL step. No login or subscription is required for
these objects in Ptah.
The examples below use Ptah Compat 0.11.4 and PostgreSQL 16.15. The application PR and its shared deployment workflow dependency are open proposals. The application PR remains a draft until that dependency is available; this is not an announcement of upstream adoption. Full inputs and recorded results are in the supporting files.
Bring security into the schema plan
Section titled “Bring security into the schema plan”The original RLS file enabled and forced row-level security on eight tables. It declared 32 policies and granted application roles access to the schema and tables. Reapplying the file dropped and recreated the policies even when their definitions had not changed.
That file made the database work, but the Atlas schema preview did not cover its security declarations. A table plan could be empty while a policy had drifted. Moving those declarations into the desired schema makes the same comparison responsible for both.
The fork keeps the existing table columns, indexes, and constraints. It adds
row_security blocks with enabled = true and enforced = true to the same
eight tables. The policy names, commands, and expressions stay the same.
For example, the existing SELECT policy on projects becomes this HCL block:
policy "projects_user_select" { on = table.projects for = SELECT using = "user_id = current_setting('app.user_id')::uuid"}The application still sets app.user_id in each transaction. Ptah manages
the database declaration; the application’s authentication and transaction
context remain responsible for supplying the user identity.
HCL permission blocks declare schema USAGE and the existing table privileges.
The app_role variable selects tasks_app locally and tasks-role in
production. The existing bootstrap continues to create roles and credentials.
The original SQL also granted access to all existing sequences, but this
schema has no sequences, so there were no sequence grants to transfer.
Apply the complete desired state
Section titled “Apply the complete desired state”With the local application role already created by the project’s bootstrap,
we applied the fork’s schema to a disposable database. From the fork root,
DATABASE_URL names the migration connection:
ptah-compat schema apply \ --url "$DATABASE_URL" \ --to file://db/schema.hcl \ --schema tasks \ --var app_role=tasks_app \ --auto-approveThe result includes all 11 tables, all 32 policies, and ENABLE/FORCE RLS on the eight intended tables. The local application role has the declared schema and table permissions. Ptah reads this HCL directly, so these commands do not need a separate dev database.
The preview uses the same command with --dry-run:
ptah-compat schema apply \ --url "$DATABASE_URL" \ --to file://db/schema.hcl \ --schema tasks \ --var app_role=tasks_app \ --dry-runAfter apply, it reports:
Schema is synced, no changes to be madeThe project’s deno task db:apply and deno task db:diff commands remain the
entry points for developers. The wrapper now invokes ptah-compat; diff uses
schema apply --dry-run so its inputs and planning path match apply. We
removed the standalone rls subcommand because schema apply now owns those
objects. The existing seed and truncate commands still use psql.
Make policy drift visible
Section titled “Make policy drift visible”A successful first apply does not show whether the tool will notice a changed
policy. On the disposable database, we changed the projects SELECT predicate
to false and disabled FORCE RLS:
ALTER POLICY projects_user_select ON tasks.projects USING (false);ALTER TABLE tasks.projects NO FORCE ROW LEVEL SECURITY;The same preview command now includes both repairs:
Planned schema changes:-- Apply the policies of tasks.projects to its ownerALTER TABLE "tasks"."projects" FORCE ROW LEVEL SECURITY;-- Modify RLS policy projects_user_select on table tasks.projects: using_expressionDROP POLICY IF EXISTS "projects_user_select" ON "tasks"."projects";CREATE POLICY "projects_user_select" ON "tasks"."projects" FOR SELECT USING (user_id = current_setting('app.user_id')::uuid);The plan explains the effect of FORCE: it subjects the table owner to the
policies too. For the changed predicate, Ptah emits a drop and recreate of that
policy. It does not repeat the old SQL file’s drop and recreate for every
unchanged policy.
After preview, a catalog query still reported qual = false and
forced = false. Preview had left the database untouched. We reran the apply
command, then confirmed that the original user predicate and FORCE setting
were restored.
For the final check, we requested the plan as JSON:
ptah-compat schema apply \ --url "$DATABASE_URL" \ --to file://db/schema.hcl \ --schema tasks \ --var app_role=tasks_app \ --dry-run --format '{{ json . }}'{"Changes":{}}The empty Changes object means there is no remaining schema change to
apply. A regression in the fork performs the same drift, preview, repair, and
empty-plan checks through the project’s wrapper.
Check the handover against the old workflow
Section titled “Check the handover against the old workflow”We also created a separate database using Atlas Community 1.3.1 with the original HCL, then applied the original RLS SQL. Ptah’s plan against that database was empty: the new declarations described the state the previous workflow had already created.
PostgreSQL schema-only dumps matched between that database and a fresh Ptah apply, including policies, grants, indexes, and constraints. The comparison excluded only pg_dump’s random restriction tokens. We also checked a database with grants to both application roles; selecting one role did not revoke the other role’s existing grants.
The project’s full test suite passed: 327 tests, 274 steps, no failures.
That includes the existing RLS and ownership tests and the new schema-drift
regression. Type checks and lint passed too. A separate production-role check
used tasks-role without relying on the local bootstrap’s default grants.
Follow the production deployment path too
Section titled “Follow the production deployment path too”The local wrapper was only part of the integration. Production calls a
reusable workflow from project-standards, and that workflow installs and
runs the database tool directly. Replacing the local executable would leave
that deployment path using Atlas.
The companion PR adds an optional database configuration. The tasks config selects Ptah and the production role:
{ "database": { "tool": "ptah", "vars": { "app_role": "tasks-role" } }}The shared workflow installs the pinned Ptah Compat release under the atlas
command name. It forwards the same role variable to preview and apply and
retains its existing guard against destructive changes. Projects that do not
select Ptah keep their current Atlas path. This belongs in the shared
workflow because the project’s caller workflow is generated and maintained
by conformance automation.
We ran the actual deployment shell against a disposable PostgreSQL database. It created the schema with the production role’s grants, then produced an empty plan on repetition. When we added an extra table containing a row, the existing guard refused the proposed drop and left the row in place. These checks cover the deployment commands; the proposed integration has not been deployed to the project’s live Cloud Run environment.
To try the same workflow, see the Ptah documentation or open the browser playground without installing a binary.
Put Ptah to work
Example files
- commands/apply.txt
- commands/preview.txt
- commands/settled.txt
- db/schema.hcl
- init-db.sql
- measured/after-preview.txt
- measured/after-repair.txt
- measured/application-tests.txt
- measured/atlas-schema.sql
- measured/bootstrap.txt
- measured/both-roles-plan.json
- measured/catalog-comparison.json
- measured/catalog.txt
- measured/commands.json
- measured/drift-preview.txt
- measured/drift.txt
- measured/fresh-apply.txt
- measured/handover-plan.txt
- measured/postgres-version.txt
- measured/ptah-schema.sql
- measured/repair-apply.txt
- measured/repeat-preview.txt
- measured/settled.txt
- measured/version.txt
- measured/workflow-drop-guard.txt
- measured/workflow-noop.txt
- original/rls.sql
- original/schema.hcl
- README.md
- snippets/deployment.json
- snippets/policy.hcl
- sql/catalog.sql
- sql/drift.sql
- sql/policy-state.sql
- TASKS-LICENSE.txt
- verified.json