Decision record

ADR-0013: A group decides which pages, a role decides what you may do; a restriction covers the branch; the repository is the source

Written by

ADR-0013: A group decides which pages, a role decides what you may do; a restriction covers the branch; the repository is the source

Status: accepted · 2026-09-16

Context

Phase 16 asked three questions that each had a cheap answer and a right one.

Who may read a page. Phase 1 built canReadPage principal-based — a caller is a user plus, eventually, group ids — and the page record already carried visibility: 'restricted' and a restrictedTo list. Nothing populated the group ids and nothing wrote the list. The cheap answer was permissions on groups: a group with a role, the shape Confluence and Notion teams expect. The right answer had to fit the roles that already exist and the six surfaces that answer “which pages”: the tree, a single read, comments, the review queue, MCP, and search — the last with an index that cannot ask Firestore at query time.

Whether a restriction reaches a page’s children. A page is a node in a tree. Restricting only the node leaves its children at the root of the tree for anyone outside the list, parent missing; restricting the branch means a child inherits a list it does not carry, which the search index has to know.

Which direction a repository connection runs. docs/ in a repository and pages in a workspace are the same documents in two places. Two-way sync is what a demo wants. It is also what the Google Docs rule (CLAUDE.md: never bidirectional) exists to refuse, for the same reason: two writers, two histories, one merge nobody asked for.

Decision

A group is a principal, not a role. A group is a named set of members and nothing else. It appears where a person appears — in a page’s allow-list — and a member’s role still says what they may do on the pages the list lets them read. Groups are managed by whoever may invite, because putting someone in a group is the same class of decision as putting them in the workspace.

A restriction covers the branch. A page under a restricted page reads by the nearest restricted ancestor’s list unless it carries a list of its own (effectiveRestriction). The tree hides a branch whole. The search index stores the effective list per row, plus the creator and a role:admin principal, so that its filter says yes in exactly the three ways a read does; restricting or moving a page reindexes its descendants. The creator and every admin always keep access; the person restricting is always on the list; a public or link-shared page is unpublished first, and a restricted page cannot be published.

The repository is the source. A GitHub connection is one way: pushes to the branch upsert pages by github:owner/repo:path under a service member the connection creates; files gone from the tree archive their pages; nothing writes back. The connection reuses the folder importer’s planner, so a repository’s docs/ and a zip of the same folder produce the same tree.

Assumptions

Invalidation triggers

Blast radius

Alternatives rejected

Escape plan

Consequences

What happened

Written and maintained by , who made this decision — about · GitHub.

← All decisions