Four roles, one table
administrator, editor, viewer and pending — with the permission table transcribed once in code and everything else a projection of it.
| Role | Read published | Read drafts | Write | Publish | Delete | Manage users |
|---|---|---|---|---|---|---|
administrator | yes | yes | yes | yes | yes | yes |
editor | yes | yes | yes | yes | yes | no |
viewer | yes | yes | no | no | no | no |
pending | no | no | no | no | no | no |
That table exists once in the codebase. The database policies, the API's checks and the grants payload the Studio receives are all projections of it, so there is no way for them to disagree.
Why pending exists
It is the column default, and it grants nothing whatsoever.
A user can arrive by routes the application never sees: a direct sign-up call, the Supabase dashboard's "add user", the Auth admin API. If the default were viewer, every such arrival would immediately be able to read drafts — unpublished work, embargoed announcements, half-written pages. Landing with no capability at all is the one mitigation that survives someone changing an auth setting.
Anonymous access
Not a role. An anonymous request has no session at all, and gets published content only — enforced by the database, not by a filter in application code.
Grants and the Studio
The Studio receives a Sanity-shaped grants payload so its interface reflects what the user may do — no publish button for a viewer. That payload is a projection of the table above and not a third enforcement point: the API and the database are where permission is decided.
