I recently captured a project-creation flow where the UI labeled the user as workspace Owner and team Lead, enabled the Create button, and then surfaced a row-level-security error on insert.
The replay does not expose the policy, so this is not a root-cause claim. It is a debugging plan based on the observable contract mismatch:
The evidence also included an earlier circular prerequisite: project creation required a team, while the empty team page instructed the user to create a project first. That belongs in the same state-machine test suite, not a separate screenshot test.
Full case with replay and screenshots:
https://www.indiehackers.com/post/8-tests-2-blockers-0-projects-created-137a9fcbcd
How do you keep frontend role labels and database authorization rules from drifting apart?
Point 2 is the fix that actually addresses root cause rather than symptoms, resolving visible role labels from the same source the policy decision reads from is what prevents the UI from ever confidently lying about what a user can do. Everything else on the list (logging, contract tests, typed errors) helps you catch the drift faster; only that one stops the drift from being possible in the first place.
The circular prerequisite is the more interesting bug of the two, honestly, because it's a state machine problem hiding inside what looks like two separate onboarding messages. "Create a project, but you need a team first" and "create a team, but go make a project first" pointing at each other means there's no valid entry state for a brand-new account, that's not a UI copy issue, it's a missing initial-state case in whatever logic decides what to show an empty workspace.
On your question: the only approach I've seen actually hold up is generating the UI's permission-check function directly from the same policy definitions the database enforces, rather than maintaining two independent implementations that happen to agree today. Anything short of a single source of truth is a synchronization problem waiting for someone to update one side during a refactor and forget the other.
That distinction between detection and prevention is useful. The UI role label should probably be treated as a projection of an authorization capability, not as an independent claim that happens to agree with the policy today.
The circular prerequisite also suggests the empty workspace needs to be an explicit state with exactly one valid next action, then every instruction and entry point can be contract-tested against it.
Have you found policy-to-UI code generation maintainable in practice, or would you rather expose a capability API that both the UI and the tests consume?
Honestly I haven't tried generation in practice, that was the theoretically clean answer, not a tested one. Your skepticism is probably right.
A generator becomes its own maintenance burden the moment it produces something subtly wrong. A capability API (canCreateProject() exposed as a real endpoint) seems more durable, one source of truth that's just a function call, not a build step, and it means your contract tests hit the actual production path instead of a shadow copy of it.
Betting on capability API over generation if a team has to live with it for a year.