Identity and access

Four roles, one table

administrator, editor, viewer and pending — with the permission table transcribed once in code and everything else a projection of it.

RoleRead publishedRead draftsWritePublishDeleteManage users
administratoryesyesyesyesyesyes
editoryesyesyesyesyesno
vieweryesyesnononono
pendingnononononono

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.