"I fixed this three weeks ago"
Findings recur because migrations recreate objects, generators regenerate schemas, and a fix applied in the dashboard is not a fix recorded in the repository.
"I definitely fixed this."
You almost certainly did. This is not one of those posts where the twist is that you were wrong.
The warning came up, you read it, you enabled row-level security on the table and wrote a policy, and you watched the advisor go quiet. Three weeks later the same warning is back with the same table name, and the natural conclusion is that the tooling is flaky.
The tooling is not flaky. The fix was real and it is gone, and the interesting question is which of a small number of mechanisms removed it.
"Nobody has touched that table since."
Probably true of the table and not true of the objects around it.
A policy is not a property of a table in the way a column is. It is a separate object attached to one, and it can be removed by operations that were aimed at something else entirely:
- A migration that drops and recreates the table — to change a column type, add a constraint, or reorder something — takes every policy on it along the way. The recreation script restores the table. It restores the policies only if somebody remembered to put them in it.
- A schema generator run again after a model change emits the tables it knows about. If policies were added outside the generator's view, they are not in its output, and applying it removes them.
- A restore from a backup or a branch reset returns the database to a state where the fix had not happened yet.
In each case the table looks untouched, because the table is untouched. The thing that vanished was attached to it.
"It is in the migrations."
This is the one worth checking rather than assuming, because it is the most common answer and it is wrong about half the time.
There is a difference between a fix that exists in your database and a fix that exists in your repository. If you resolved the original warning by clicking through the dashboard — enabling row-level security on the table, adding a policy in the editor — then the fix exists in exactly one place: that database. It is not in a migration, it is not in version control, and it will not be reapplied to anything.
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public' and tablename = 'bookings';
Run that against production and against a database built from your migrations alone. If the two disagree, you have found the mechanism, and the recurring warning is the database converging on what your repository actually says.
A fix applied in a dashboard is a fix that exists in one database and no history.
"Then something is undoing it."
Yes, and it is usually your own deployment pipeline behaving correctly.
This is the part that makes the pattern durable: every actor is doing its job. The migration recreates the table because that is what it was written to do. The generator emits the schema it was given. The pipeline applies what is in the repository, which is the entire point of having a pipeline. Nothing is malfunctioning — the repository and the database simply disagreed about what should exist, and the repository won, as it is designed to.
That is also why "be more careful" is not a fix here. The failure is a gap between two systems of record, not a lapse of attention.
"How do I make a fix stay fixed?"
Three things, in order of how much they help.
- Put it in a migration, always. Even when you fixed it in the dashboard to stop the bleeding. Write the migration afterwards, apply it to a fresh database, and confirm the state matches. A fix that is not in version control has not been made — it has been postponed.
- Make the migration idempotent, and include the policies with the table. If a migration recreates a table, the policies belong in the same file, immediately after it. They are part of the table's definition in every sense except the one Postgres uses.
- Watch for the transition, not the state. A check that reports every finding on every run gets ignored by March. A check that tells you a policy which existed last week does not exist now is one you will still be reading in six months — and a policy disappearing is a finding in its own right, which is the category most monitoring never thinks to look for.
The last point is the one that generalises past this problem. Almost everything worth alerting on in a database's security posture is a change rather than a state, and almost every tool reports the state.
RowShield is an independent product with no affiliation to, or endorsement from, Supabase.