Wiki
Permissions

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.

RoleCapabilities
viewerread
commenterread + comment
editorread + write pages/attachments/tags + comment
admineditor + 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

FileContents
packages/api/src/lib/access.tsPure resolvers (resolveSpaceRole, resolvePageRole, filterReadablePages) + async loaders. Auth-free, unit-tested.
packages/api/src/lib/authz.tsRouter-facing gates with the org-manager override: requireSpaceCapability, requirePageCapability, requireSpaceManage, requirePageManage, resolveMyPageAccess.
packages/api/src/routers/space-member.tsSpace member CRUD (spaceMembers.*) + myRole.
packages/api/src/routers/page-access.tsPage 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 panel

Space 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 visibility for that.

On this page