Security
Role-based access control: roles in their own table, rules in the database
Most access-control bugs we are called in to fix have the same root cause: the rule lived in the front end. Hide the menu item, hide the button, and hope nobody types the URL. GoGee's model puts the rule where the data is.
GoGee feature series · 29 of 33
How it's actually built
- Role storage
- Dedicated roles table, one row per user and role
- Never on the profile
- Roles are not a column users can update about themselves
- Role checks
- Security-definer functions such as has_role()
- Enforcement point
- Row-level security policies on every protected table
- Scoped roles
- Branch-scoped staff and manager records where a project is multi-branch
- Client trust
- None, the browser cannot assert its own role
- 01Input
Role storage
Dedicated roles table, one row per user and role
- 02Deterministic
Never on the profile
Roles are not a column users can update about themselves
- 03Deterministic
Role checks
Security-definer functions such as has_role()
- 04Deterministic
Enforcement point
Row-level security policies on every protected table
- 05Output
Scoped roles
Branch-scoped staff and manager records where a project is multi-branch
Role-based access control, data flow, generated from the shared GoGee feature diagram template.
Why roles live in their own table
If a role is a column on the user's profile, and users can edit their profile, then users can edit their role. Storing roles as separate rows removes that entire class of privilege escalation, and it allows one user to hold several roles at once, a branch manager who is also a head-office reviewer, for instance.
- One row per user and role, so multiple roles are natural
- Profile edits can never change what a user is allowed to do
- Roles are readable in an admin view without guesswork
Checks run inside the policy
Role checks are wrapped in security-definer functions and called from row-level security policies. The practical result is that the rule applies to every access path, the site, an admin screen, a server function, a direct API call, because Postgres itself enforces it.
In multi-branch projects the same pattern carries branch scope: helper functions establish whether the current user is staff or a manager at the branch that owns the row, and head office sees across branches while a branch sees only its own.
What this does not include
To be precise: a generic before-and-after audit trail across every table is not implemented as a platform-wide feature. Purpose-built event logs exist where they matter, document signing events and affiliate invite auditing, for example, and additional logging is added deliberately per project rather than assumed.
Questions we get asked
Can a user grant themselves admin rights?
No. Roles are stored in a separate table that users cannot write to, and checks run inside database policies.
Can a user hold more than one role?
Yes. Roles are rows, so multiple roles per user are supported.
Is every table change logged automatically?
No. Specific event logs exist where they are required; a blanket CRUD audit trail is scoped per project.
Read next
Platform
Member sections & client logins
GoGee's member area gives each customer their own orders, invoices, vouchers, addresses, saved baskets and bus…
Franchise
Franchise & multi-branch hub
GoGee's franchise model uses branch-scoped roles and row-level security so head office works globally while fr…
Multi-branch
Stock availability across branches
GoGee holds stock per branch with reserved quantities and low-stock thresholds, ranks the nearest branch by di…

