3
3 Comments

A practical way to debug a UI role / RLS mismatch

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:

  1. Log the actor, workspace, team, and intended parent IDs at the capability check and insert.
  2. Resolve visible role labels from the same source used by the policy decision.
  3. Add a canCreateProject capability endpoint or equivalent preflight.
  4. Contract-test empty workspace, invited member, owner, lead, and returning-account states.
  5. Exercise every visible creation entry point against the same authorization path.
  6. Translate policy rejection into a typed domain error with a recovery action.
  7. Preserve form input when the capability changes or the insert fails.

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?

on August 5, 2026
  1. 1

    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.

    1. 1

      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?

      1. 1

        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.