Spaces: rooms with collaborators, below the org
A space names part of an org — a project, a wiki, a folder, a document, a session — and has its own members (from this org, from others, link guests), roles that compile to permits, per-resource grants and an org policy that caps them. One model drives the gate, documents and files.
Status: the model and its enforcement landed (2026-11-01, #196) in
architect-spaces(features/spaces). Invites by email, invite links, share links and guest lanes are #197; the share sheet and members panel are #198.
An organization is the unit people belong to. A space is the unit they are invited into: a project’s subtree, a wiki, a folder of files, one document, a Studio session. Task used to hand-roll this three ways (share links, cross-org Sharing Grants, wiki Editors); Studio needs the same for sessions. Spaces are the one system both use.
The model#
features/spaces/src/model.rs, wasm-clean.
- A
Spacelives in one org and names what it covers by the permission resources the org’s services already check, org-relative: itsscope(vault/Alpha/,doc/<id>,files/<root>/Mixes/) and itsexcludecarve-outs (vault/Alpha/Invoices/). - A
Memberis aSubject: aUser(by identity server id, whether or not they are in the org) or aGuest(a share link’s holder,Principal::Guest, minted by #197). A member has an optional role, any number ofSpaceGrants (actions on one resource inside the scope), and an optional expiry. - A
SpaceRoleis a named set of actions held over the whole scope. Every space hasviewer(read),commenter(read, comment),editor(read, comment, write, download) andmanager(*, soadmin: changes the space and its members); a space may define its own or replace a built-in for itself. Space::rules_forcompiles a member to permissionRules: the role’s actions over each scope pattern, each grant, andread(plusadminfor a manager) on the space’s own resourcespaces/<id>.- The org’s
SpacePolicycaps what a space may give by how a member stands to the org (Standing): people from other orgs are capped atexternal_cap(default read, comment, write, download: they work in a space but do not manage it), guests atguest_cap(read, comment), and either can be turned off. The org’s own members are not capped: a viewer of the org made an editor of a space writes in that space.
Effective access is the org role ∪ the space role, capped by the
policy. OrgSpaces::allows is the whole decision.
Enforcement: one model, every plane#
features/spaces/src/server/mod.rs Spaces keeps every org’s spaces in
memory (read once from a SpaceStore: MemoryStore, or FileStore under
<data>/org/<id>/spaces/), writes each change through after appending it
to the audit trail, and is three things a server mounts:
- The gate.
Spaces::engineis aPermissionEngine(features/spaces/src/server/engine.rsSpacesEngine) that allows only what a space gives. It sits beside the org’s engine:CompositeEngine::new().push(confined).push(spaces.engine()). The org is the resource’s (org/<id>/…) or the lane’s (current_org), exactly as forConfinedEngine. Documents ride the same engine:DocPermissions(whatDocRegistry::with_permissionsandDocsHost::with_permissionsask) checksdoc/<id>through it, so a space over a document is enough. - Files.
Spacesis aGrantSource(features/files/src/server/mod.rsGrantSource,FilesServer::with_grants): a scope patternfiles/<root>/<path>becomes a filesGranton that folder for each member who holds actions there, generated on every call (read→ Read + History,write→ Write,admin→ Share). Nobody hand-makes those grants, and nothing has to clean them up. - The service.
SpacesService(features/spaces/src/service.rs): create, list, read, update, delete a space; set and remove members; grant and revoke; define roles; the caller’seffectiverules (what a UI greys out by); the space’saudittrail; the org’spolicy. The org comes from the lane, the caller from the gate. A space’s managers and the org’s owners and admins change it; its members see it; a space someone may not see isNotFound.
Revoke is retroactive. Every check reads the state as it is, so a removed member, a deleted space, a narrowed scope or a tightened policy binds on the next call — on the same connection.
The gate’s contract, again#
The gate is method-level: it checks a permit’s coarse resource
(doc/{doc} is asked as doc/**). SpacesEngine answers a coarse
question by overlap — allowed if the caller holds the action on
something under it — so a commenter of one document gets past the gate
on comment, and not on write. A service a space reaches must then
check the exact resource in its handler (PermissionEngine::check with
caller()), which DocPermissions and the files feature already do. A
handler that skips it hands a space member every resource behind the
method.
Designed for invites and links (#197) and the UI (#198)#
- A link guest is already a
Subject::Guest; a guest lane’sStaticPrincipal(Principal::Guest { link_id })resolves to it, and the policy caps it. #197 adds the link rows and lanes, not a new model. - Accepting an email invite is
set_memberfor the person who signed in; an invite link carries a role and adds the holder as aUser. effective,auditandSpaceSummaryare the share sheet’s and the members panel’s data.
Limits#
- A files grant is a whole folder, so a carve-out may not sit inside a
files scope pattern (
validate_reachrefuses it). - A document sync session already attached keeps running after a removal
until it reattaches (
DocRegistrychecks on attach). - Membership’s standing comes from
Members, which the gate’sMembersResolverfills as people sign in; the policy is applied at check time, when the caller has signed in.