Space & page access
Content authorization via space roles and optional per-page ACLs.
Content lives in spaces (which hold pages). Who can see and edit it is governed by per-space roles, optionally narrowed by per-page ACLs — a layer distinct from organization RBAC.
Space roles
Every spaceMember row carries a role. The effective role is the strongest
of: an org manager (→ admin), the space creator (→ admin), a direct or team
spaceMember row, or a viewer baseline on public spaces.
| Role | Capabilities |
|---|---|
viewer | read |
commenter | read + comment |
editor | read + write pages/attachments/tags + comment |
admin | editor + manage the space (settings, visibility, members) |
Visibility still gates read:
public— any org member reads; writing needs a role.private— members only.restricted— creator + members.
Private stays private
Org managers only reach spaces they can already read. A private space stays private even for
owners/admins who aren't members.
Page ACLs (optional override)
A page's visibility is NULL by default — it inherits the space. Setting it
(public / private / restricted) plus pageMember rows creates a per-page
override that can only narrow access:
effective page role = min(spaceRole, pageRole)A space admin is never locked out. Child pages do not auto-inherit a parent page's override — each page inherits the space unless it has its own ACL (v1).
Where it lives
| File | Contents |
|---|---|
packages/api/src/lib/access.ts | Pure resolvers (resolveSpaceRole, resolvePageRole, filterReadablePages) + async loaders. Auth-free, unit-tested. |
packages/api/src/lib/authz.ts | Router-facing gates with the org-manager override: requireSpaceCapability, requirePageCapability, requireSpaceManage, requirePageManage, resolveMyPageAccess. |
packages/api/src/routers/space-member.ts | Space member CRUD (spaceMembers.*) + myRole. |
packages/api/src/routers/page-access.ts | Page ACL (pageAccess.*): get, setVisibility, member CRUD, myRole. |
Backend usage
// Gate a content write on the page (respects space + page ACL):
const existing = await loadPage(db, id);
await requirePageCapability(db, context, headers, existing, "write");
// Gate on the space (bare space content, e.g. creating a page):
await requireSpaceCapabilityById(db, context, headers, spaceId, "write");Frontend usage
Gate content affordances on the effective role, not org RBAC:
const { data: access } = useQuery(orpc.pageAccess.myRole.queryOptions({ input: { pageId } }));
// access.canWrite → show edit / publish
// access.canManage → show the sharing panelSpace settings (visibility + members) live on the space page; page sharing lives
on the page view — both gated on canManage.
Known limitations (v1)
- Search filters by space access but not per-page overrides (the page list and page tree
do). A restricted page can still surface in search to an excluded member. - Attachment delete
and space-level tag CRUD gate on space role, not per-page. - Page ACL management is
limited to space admins and the page's author (not every editor). - Draft / archived status is
not access control — use
visibilityfor that.