# Space Architecture: Nested Spaces & Permission Cascade *Version 0.1 — February 2026* --- ## Core Principle: A Space Is a Space Is a Space There is no `type` field. There is no distinction between "personal" and "community" at the schema level. A space is the singular primitive. What varies between spaces is: - **Who owns it** — the `ownerDID` and admin set - **Who can see into it** — visibility + membership - **What it nests** — references to other spaces - **What apps are deployed on it** — enabled modules A space where one person is the sole admin with `members_only` visibility *behaves* like a personal space. A space with many members and `public_read` visibility *behaves* like a community. But they are the same data structure, the same Automerge document, the same API surface. This uniformity is what makes arbitrary nesting depth work without special cases. --- ## `.rspace.online` — Identity-Provisioned Spaces Every EncryptID registration auto-provisions a space: ``` Registration → EncryptID created → Space provisioned at .rspace.online ``` This space is not special. It's a space where: - The user is the sole `admin` - Visibility defaults to `members_only` (sovereign by default) - The user's chosen apps are enabled - It exists at a predictable, memorable URL ### What this gives the user 1. **A sovereign home on the network** — `alice.rspace.online` is Alice's address. Her canvas, her apps, her data. 2. **An app deployment surface** — Alice enables rWallet, rVote, rNotes on her space. They're live immediately. 3. **The root of her sharing tree** — When Alice shares content into a DAO, it originates from her space. When she revokes, it disappears from the DAO. Her space is always the canonical source. 4. **A URL she can give someone** — "Find me at `alice.rspace.online`." The equivalent of a homepage, but living. ### What this does NOT mean - It is NOT a different kind of space - It does NOT have special server-side logic beyond auto-provisioning - It CAN be reconfigured — Alice could make it `public`, add members, change its name - Other spaces CAN also be provisioned at `.rspace.online` — the subdomain is just a slug --- ## Nested Spaces Nesting is the mechanism for both sharing and composition. A space can reference other spaces, which appear as embedded regions on its canvas. ### SpaceRef: The Nesting Primitive ```typescript interface SpaceRef { id: string; // unique ref ID within this space sourceSlug: string; // the nested space's slug sourceDID?: string; // who created this nesting (provenance) // What to show from the nested space filter?: SpaceRefFilter; // Where it appears on the parent canvas x: number; y: number; width: number; height: number; rotation: number; // What the parent space's members can do with nested content permissions: NestPermissions; // Display collapsed?: boolean; // render as a card summary vs. full canvas label?: string; // override display name } interface SpaceRefFilter { // Show all shapes, or a subset? shapeTypes?: string[]; // only these shape types (e.g., ['folk-note', 'folk-budget']) shapeIds?: string[]; // only these specific shapes tags?: string[]; // only shapes with these tags (future) moduleIds?: string[]; // only content from these modules } interface NestPermissions { read: boolean; // can members of the parent space see this nested content? write: boolean; // can they edit shapes in the nested space? addShapes: boolean; // can they add new shapes to the nested space? deleteShapes: boolean; // can they delete shapes from the nested space? reshare: boolean; // can they nest this space further into other spaces? expiry?: number; // unix timestamp — auto-revoke after this time } ``` ### How Nesting Works at the CRDT Level Each space's `CommunityDoc` gains a `nestedSpaces` map alongside its existing `shapes` map: ```typescript interface CommunityDoc { meta: CommunityMeta; shapes: { [id: string]: ShapeData }; members: { [did: string]: SpaceMember }; nestedSpaces: { [refId: string]: SpaceRef }; // NEW } ``` When a client renders a space, it: 1. Renders all local `shapes` as before 2. For each entry in `nestedSpaces`: a. Opens a secondary WebSocket to the nested space's sync endpoint b. Receives that space's shapes (filtered by `SpaceRefFilter` if set) c. Renders them inside the `SpaceRef`'s bounding box on the canvas d. Applies `NestPermissions` — e.g., if `write: false`, nested shapes are non-editable This is the same mechanism planned for `folk-canvas` (Task-45), but elevated from a shape type to a first-class document field. The `folk-canvas` shape becomes the *renderer* for `SpaceRef` entries. ### Nesting Depth There is no hard limit on nesting depth. A space can nest a space that nests a space. Each boundary applies its own `NestPermissions`, and the **permission cascade** (see below) ensures the most restrictive permission always wins. ``` alice.rspace.online └── nestedSpaces: ├── ref-1 → dao.rspace.online (read + write) │ └── nestedSpaces: │ ├── ref-a → working-group (read + write) │ │ └── nestedSpaces: │ │ └── ref-x → alice.rspace.online/project-x (read-only) │ │ ↑ circular reference is fine — alice │ │ shared a filtered view of her own │ │ space back into the working group │ └── ref-b → treasury (read-only, rWallet shapes only) │ └── ref-2 → art-collab.rspace.online (read-only) ``` ### Circular References A space nesting itself (directly or through a chain) is permitted. The renderer detects cycles and displays the nested instance as a static snapshot rather than a live recursive embed, preventing infinite recursion. The canonical data still syncs — only the rendering is bounded. --- ## Permission Cascade Model ### The Rule: Most Restrictive Wins (Intersection) When a space is nested, the effective permissions at any depth are the **intersection** of all permissions along the nesting chain. No downstream nest can grant more access than its parent granted. ### Formal Definition ``` EffectivePermissions(depth N) = NestPermissions[1] ∩ NestPermissions[2] ∩ ... ∩ NestPermissions[N] ``` Where `∩` for each boolean field is logical AND: ```typescript function cascadePermissions(chain: NestPermissions[]): NestPermissions { return { read: chain.every(p => p.read), write: chain.every(p => p.write), addShapes: chain.every(p => p.addShapes), deleteShapes: chain.every(p => p.deleteShapes), reshare: chain.every(p => p.reshare), expiry: Math.min(...chain.filter(p => p.expiry).map(p => p.expiry!)) || undefined, }; } ``` ### Examples **Example 1: Permission narrowing across depth** ``` Alice's space └── nests DAO space (read: ✓, write: ✓, reshare: ✓) └── nests Working Group (read: ✓, write: ✓, reshare: ✗) └── nests Bob's space (read: ✓, write: ✗) Effective permissions at each level: Level 1 (DAO in Alice's view): read: ✓ write: ✓ reshare: ✓ Level 2 (WG in Alice's → DAO view): read: ✓ write: ✓ reshare: ✗ ← narrowed Level 3 (Bob in Alice → DAO → WG): read: ✓ write: ✗ reshare: ✗ ← narrowed further ``` **Example 2: Revocation propagates** ``` Alice revokes the DAO nest from her space → DAO content disappears from Alice's canvas → Working Group content (nested inside DAO) also disappears → Bob's content (nested inside WG) also disappears ``` One action, clean propagation. Alice doesn't need to know the nesting depth. **Example 3: reshare: false stops propagation** ``` Alice nests her project into DAO space with reshare: false → DAO members can see and interact with Alice's project → DAO members CANNOT nest Alice's project further into other spaces → If they try, the server rejects the SpaceRef creation ``` This is Alice's control over how far her content travels. ### Permission Enforcement Points | Point | What's checked | Who checks | |-------|---------------|------------| | **SpaceRef creation** | 1. Source space's `nestPolicy.consent` — is this requester allowed? 2. Source space's `nestPolicy.blocklist/allowlist` 3. Requested permissions capped by `nestPolicy.defaultPermissions` 4. Does the creator have `reshare` permission if nesting an already-nested space? 5. Does the creator have `admin` or `moderator` role in the target? | Server, at `POST /api/spaces/:slug/nest` or approval flow | | **WebSocket upgrade** | Does the connecting user have access to this space AND to all nested spaces in the chain? | Server, at WS handshake via `authenticateWSUpgrade()` | | **Shape write** | Is the effective `write` permission `true` across the full nesting chain? Does the user's role in the *source* space permit this action? | Server, in WS message handler | | **Shape read** | Is the effective `read` permission `true`? Does the user meet the source space's visibility requirements? | Server, when syncing nested space data | | **Revocation** | Only the `SpaceRef` creator, an admin of the nesting space, or an admin of the *source* space can remove a `SpaceRef`. Source space admins can always revoke — this is the "pull the plug" guarantee. | Server, at `DELETE /api/spaces/:slug/nest/:refId` | ### Dual Authority: Nesting Permission + Space Role A user's effective capability in a nested space is determined by TWO factors: 1. **The NestPermissions chain** — what the nesting allows at a structural level 2. **The user's role in the source space** — what the user is allowed to do in that space independently Both must permit the action. If the nesting grants `write: true` but the user is only a `viewer` in the source space, they still can't write. If the user is an `admin` in the source space but the nesting says `write: false`, they still can't write *through the nest*. ```typescript function canPerformAction( action: 'read' | 'write' | 'addShapes' | 'deleteShapes', nestChain: NestPermissions[], userRoleInSource: SpaceRole, ): boolean { const cascaded = cascadePermissions(nestChain); // Nesting must allow it if (!cascaded[action]) return false; // User's role in the source space must also allow it const requiredRole = actionToMinimumRole(action); return roleRank(userRoleInSource) >= roleRank(requiredRole); } function actionToMinimumRole(action: string): SpaceRole { switch (action) { case 'read': return 'viewer'; case 'write': return 'participant'; case 'addShapes': return 'participant'; case 'deleteShapes': return 'moderator'; default: return 'admin'; } } ``` --- ## Identity-Space Binding ### Auto-Provisioning at Registration When `POST /api/register/complete` succeeds in EncryptID, the server also: 1. Calls `createCommunity(username, username, did, 'members_only')` 2. Fires `onSpaceCreate(username)` for all registered modules 3. Enables the user's selected default modules (or a sensible default set) 4. The space is immediately available at `.rspace.online` ```typescript // In src/encryptid/server.ts — after successful registration // Auto-provision user's space const userSpace = await createCommunity( username, // name username, // slug (becomes .rspace.online) did, // ownerDID 'members_only', // sovereign by default ); // Enable default modules await setEnabledModules(username, DEFAULT_USER_MODULES); // Fire module hooks for (const mod of getAllModules()) { if (mod.onSpaceCreate) { await mod.onSpaceCreate(username); } } ``` ### Default Module Set New users get a sensible starting set. They can add or remove modules at any time. ```typescript const DEFAULT_USER_MODULES = [ 'canvas', // the canvas itself 'notes', // note-taking 'files', // file storage 'wallet', // identity-linked wallet ]; ``` ### Space Settings Stored in Meta ```typescript interface CommunityMeta { name: string; slug: string; createdAt: string; visibility: SpaceVisibility; ownerDID: string | null; enabledModules: string[]; // NEW — which modules are active in this space description?: string; // NEW — optional space description avatar?: string; // NEW — optional space avatar/icon URL } ``` --- ## Nest Consent & Notification ### The Problem Without consent controls, anyone who can access a space could nest it into another space without the owner knowing. The `reshare` flag on `NestPermissions` controls whether *already-nested* content can be nested further — but it doesn't govern the *initial* nesting. The space owner needs to control whether their space can be nested at all, by whom, and whether they're notified. ### NestPolicy: Per-Space Inbound Nesting Rules Every space has a `nestPolicy` in its metadata that governs how *other* spaces can nest it: ```typescript interface NestPolicy { // Who can nest this space into their space? consent: 'open' | 'members' | 'approval' | 'closed'; // Should the space owner/admins be notified of nesting events? notifications: NestNotifications; // Default permissions granted when this space is nested // (the nester can request less, but not more) defaultPermissions: NestPermissions; // Spaces that are always allowed to nest this one (bypass consent) allowlist?: string[]; // slugs // Spaces that are never allowed to nest this one blocklist?: string[]; // slugs } interface NestNotifications { onNestRequest: boolean; // notify when someone requests to nest this space onNestCreated: boolean; // notify when nesting is established onNestRevoked: boolean; // notify when a nesting is removed onReshare: boolean; // notify when nested content is re-nested further channel: 'inbox' | 'email' | 'both'; // where to send notifications } ``` ### Consent Levels | Level | Behavior | Default for | |-------|----------|-------------| | `open` | Anyone with access to the space can nest it. No approval needed. | Public community spaces | | `members` | Only members of this space can nest it into other spaces. | Collaborative workgroups | | `approval` | Nesting creates a pending request. Space admin must approve before the nest becomes active. | Auto-provisioned user spaces | | `closed` | No one can nest this space. It exists only at its own URL. | Sensitive/private spaces | ### How Consent Flows **`open` — no friction:** ``` Bob nests alice.rspace.online into dao.rspace.online → SpaceRef created immediately → Alice notified (if notifications.onNestCreated: true) ``` **`members` — membership gate:** ``` Bob attempts to nest alice.rspace.online into dao.rspace.online → Server checks: is Bob a member of alice.rspace.online? → YES → SpaceRef created, Alice notified → NO → 403 Forbidden ``` **`approval` — request/approve flow:** ``` Bob requests to nest alice.rspace.online into dao.rspace.online → Server creates a PendingNestRequest (not a live SpaceRef yet) → Alice notified via her preferred channel → Alice reviews: sees who's requesting, which space, what permissions → Alice approves → SpaceRef created, Bob notified → Alice denies → request deleted, Bob notified → Alice can also modify the permissions before approving (e.g., downgrade write: true to write: false) ``` **`closed` — hard block:** ``` Bob attempts to nest alice.rspace.online → 403 Forbidden, no request created ``` ### PendingNestRequest ```typescript interface PendingNestRequest { id: string; sourceSlug: string; // the space being nested (e.g., alice) targetSlug: string; // where it's being nested into (e.g., dao) requestedBy: string; // DID of requester requestedPermissions: NestPermissions; message?: string; // optional note from requester ("sharing my project notes") status: 'pending' | 'approved' | 'denied'; createdAt: number; resolvedAt?: number; resolvedBy?: string; // DID of admin who approved/denied modifiedPermissions?: NestPermissions; // if admin adjusted before approving } ``` ### Default Permissions Cap The `defaultPermissions` in `NestPolicy` acts as a ceiling. When someone nests the space, they can request permissions *up to* this level, but not beyond: ``` Alice's nestPolicy.defaultPermissions: read: true, write: false, addShapes: false, deleteShapes: false, reshare: false Bob requests to nest Alice's space with write: true → Server caps it: write: false (Alice's default ceiling) → Bob's SpaceRef gets read: true, write: false ``` This means Alice can set her policy once — "my space can be read but not written to when nested" — and every future nesting respects it without her reviewing each request (unless she wants to, via `approval` consent). ### Allowlist & Blocklist For spaces where `consent: 'approval'` is too much friction for trusted relationships but `open` is too permissive: - **Allowlist**: These slugs bypass consent entirely. If `climate-dao` is on Alice's allowlist, the DAO can nest her space without approval. - **Blocklist**: These slugs are always denied, even if consent is `open`. Overrides everything. Blocklist takes priority over allowlist. If a slug appears in both, it's blocked. ### Notifications in Practice Notifications are delivered through rSpace's existing channels: - **`inbox`**: Appears in the user's rSpace inbox (the rInbox module). Non-intrusive, checked at the user's pace. - **`email`**: Sent to the user's EncryptID-linked email via Mailcow. For important events. - **`both`**: Both channels. Example notification: ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔗 Nest Request @bob wants to nest your space into climate-dao.rspace.online Requested permissions: Read: ✓ Write: ✗ Reshare: ✗ Message: "Sharing your research notes with the funding working group" [Approve] [Approve with changes] [Deny] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` ### Updated CommunityMeta ```typescript interface CommunityMeta { name: string; slug: string; createdAt: string; visibility: SpaceVisibility; ownerDID: string | null; enabledModules: string[]; description?: string; avatar?: string; encrypted?: boolean; encryptionKeyId?: string; nestPolicy: NestPolicy; // NEW — governs inbound nesting } ``` ### Sensible Defaults ```typescript // Auto-provisioned user space: sovereign by default const DEFAULT_USER_NEST_POLICY: NestPolicy = { consent: 'approval', notifications: { onNestRequest: true, onNestCreated: true, onNestRevoked: false, onReshare: true, channel: 'inbox', }, defaultPermissions: { read: true, write: false, addShapes: false, deleteShapes: false, reshare: false, }, }; // Newly created community space: open collaboration const DEFAULT_COMMUNITY_NEST_POLICY: NestPolicy = { consent: 'members', notifications: { onNestRequest: false, onNestCreated: true, onNestRevoked: true, onReshare: false, channel: 'inbox', }, defaultPermissions: { read: true, write: true, addShapes: true, deleteShapes: false, reshare: true, }, }; ``` Note: these are not enforced by a `type` field. The auto-provisioned space gets `DEFAULT_USER_NEST_POLICY` at creation time, and a manually created space gets `DEFAULT_COMMUNITY_NEST_POLICY` — but the user can change either to anything. A "personal" space could be set to `open` and a "community" space could be set to `closed`. The defaults just encode sensible starting points. ### API Surface for Consent ``` GET /api/spaces/:slug/nest-policy Get space's nest policy PATCH /api/spaces/:slug/nest-policy Update nest policy (admin only) POST /api/spaces/:slug/nest-requests Create a pending nest request GET /api/spaces/:slug/nest-requests List pending requests (admin only) GET /api/spaces/:slug/nest-requests/:id Get a specific request PATCH /api/spaces/:slug/nest-requests/:id Approve/deny (admin only) ``` When a request is approved, the server automatically creates the `SpaceRef` in the target space's `nestedSpaces` map with the (potentially modified) permissions. --- ## How Sharing Actually Works ### Scenario: Alice shares her project notes into a DAO 1. Alice has shapes on her canvas at `alice.rspace.online` — notes, a budget, a timeline. 2. Alice is a member of `climate-dao.rspace.online`. 3. Alice opens her space settings or right-clicks a group of shapes → "Nest into space..." 4. She selects `climate-dao.rspace.online` and sets permissions: - `read: true` — DAO members can see the shapes - `write: false` — only Alice can edit them (they're her canonical data) - `reshare: false` — the DAO can't nest these further 5. Server creates a `SpaceRef` in `climate-dao`'s `nestedSpaces` map, pointing to `alice` with the chosen filter and permissions. 6. DAO members visiting `climate-dao.rspace.online/canvas` now see Alice's shapes rendered inside a bordered region on the canvas. 7. The shapes are live — if Alice updates them on her space, the DAO sees the update in real time. ### Scenario: Alice revokes 1. Alice opens her space settings → sees "Shared into: climate-dao" 2. Clicks revoke. 3. Server removes the `SpaceRef` from `climate-dao`'s `nestedSpaces` map. 4. The shapes vanish from the DAO canvas. The DAO's Automerge doc is unchanged — it never contained Alice's shapes directly, only a reference. 5. Alice's shapes remain on her own canvas, untouched. ### Scenario: DAO nests a working group 1. `climate-dao.rspace.online` creates a new space: `climate-dao-wg-funding` (or it could be at `funding.climate-dao.rspace.online` if wildcard subdomains are supported) 2. The DAO admin nests the working group into the DAO space with `read: true, write: true, reshare: true` 3. Working group members can also be members of the DAO — their role in each space is independent 4. Content created in the working group space appears on both the working group canvas AND the DAO canvas (via the nest) 5. Alice's content that was nested into the DAO with `reshare: false` does NOT appear in the working group — `reshare: false` blocked further nesting ### Scenario: Browsing nested spaces When viewing a canvas with nested spaces, the user sees: ``` ┌─────────────────────────────────────────────────────────────┐ │ climate-dao.rspace.online/canvas │ │ │ │ [folk-note] [folk-budget] [folk-poll] │ │ │ │ ┌──────────────────────────────┐ │ │ │ ≡ alice.rspace.online │ ← nested space border │ │ │ (shared by @alice, read-only) │ │ │ │ │ │ │ [folk-note] [folk-timeline] │ │ │ │ │ │ │ └──────────────────────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ ≡ funding working group │ │ │ │ (read + write) │ │ │ │ │ │ │ │ [folk-budget] [folk-note] │ │ │ │ │ │ │ └──────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` Each nested region is: - Visually bounded (border, header with source space name) - Scrollable/zoomable independently (it's a canvas within a canvas) - Badged with the source and permission level - Clickable to "enter" — navigating to the full nested space --- ## Encryption & Nested Spaces ### At-Rest Encryption Each space's Automerge document can optionally be encrypted at rest using the owner's EncryptID Layer 2 AES-256 key: ```typescript interface CommunityMeta { // ... existing fields ... encrypted: boolean; // is this space's document encrypted at rest? encryptionKeyId?: string; // which key was used (for key rotation) } ``` For the user's own space, this is straightforward — encrypt with their derived key, decrypt on their device. ### Sharing Encrypted Content via Nesting When an encrypted space is nested into another space, the nested content must be readable by the parent space's members. Two approaches: **Approach A: Re-encryption at the boundary** The space owner re-encrypts the shared subset of shapes with a shared key that the parent space's members can access. This key is distributed via the parent space's membership (each member's public key encrypts a copy of the shared key). **Approach B: Plaintext projection** The server (which must hold decryption capability for sync purposes, or the owner's client does) creates a plaintext projection of the filtered shapes and serves that to the parent space's WebSocket. The canonical encrypted version stays in the source space. **Approach C: Client-side decryption with delegated keys** The source space owner wraps their content key with each authorized viewer's public key. Authorized clients decrypt on their device. This is true E2E but adds complexity to the nesting chain. **Recommendation:** Start with Approach B (plaintext projection, server-mediated) for simplicity. Graduate to Approach C (delegated keys) for spaces that require E2E encryption. The `encrypted` flag on `CommunityMeta` determines which path is used. --- ## Schema Changes Summary ### CommunityDoc (Automerge) ```typescript interface CommunityDoc { meta: CommunityMeta; shapes: { [id: string]: ShapeData }; members: { [did: string]: SpaceMember }; nestedSpaces: { [refId: string]: SpaceRef }; // NEW } ``` ### CommunityMeta ```typescript interface CommunityMeta { name: string; slug: string; createdAt: string; visibility: SpaceVisibility; ownerDID: string | null; enabledModules: string[]; // NEW description?: string; // NEW avatar?: string; // NEW encrypted?: boolean; // NEW encryptionKeyId?: string; // NEW } ``` ### New Types ```typescript interface SpaceRef { id: string; sourceSlug: string; sourceDID?: string; filter?: SpaceRefFilter; x: number; y: number; width: number; height: number; rotation: number; permissions: NestPermissions; collapsed?: boolean; label?: string; createdAt: number; createdBy: string; // DID of who created this nesting } interface SpaceRefFilter { shapeTypes?: string[]; shapeIds?: string[]; tags?: string[]; moduleIds?: string[]; } interface NestPermissions { read: boolean; write: boolean; addShapes: boolean; deleteShapes: boolean; reshare: boolean; expiry?: number; } ``` ### No Changes To - `ShapeData` — shapes remain the same. They don't know or care whether they're being viewed directly or through a nest. - `SpaceMember` — membership is per-space, unchanged. - `SpaceVisibility` — the four levels remain. They govern direct access to the space; nesting adds a second access path that still must satisfy visibility. --- ## API Surface ### New Endpoints ``` POST /api/spaces/:slug/nest Create a SpaceRef (nest a space) GET /api/spaces/:slug/nest List all nested space refs GET /api/spaces/:slug/nest/:refId Get a specific SpaceRef PATCH /api/spaces/:slug/nest/:refId Update SpaceRef (permissions, filter, position) DELETE /api/spaces/:slug/nest/:refId Remove a SpaceRef (un-nest) GET /api/spaces/:slug/nested-in List all spaces this space is nested into (so the owner can see where their content appears) ``` ### WebSocket Protocol Additions ``` Message Type Direction Purpose ───────────────────────────────────────────────────────────── nest-sync Server→Client Sync data from a nested space nest-subscribe Client→Server Subscribe to a specific nested space's updates nest-unsubscribe Client→Server Unsubscribe from a nested space nest-permission-check Client→Server Check effective permissions for an action in a nest ``` ### Modified Endpoints ``` POST /api/register/complete Now also provisions .rspace.online GET /api/spaces Returns user's own space first, pinned ``` --- ## Implementation Phases ### Phase 1: Schema & Auto-Provisioning - Add `enabledModules`, `description`, `avatar`, `nestPolicy` to `CommunityMeta` - Add `nestedSpaces` map to `CommunityDoc` - Add auto-provisioning in EncryptID registration flow (with `DEFAULT_USER_NEST_POLICY`) - Add subdomain routing for `.rspace.online` ### Phase 2: Nesting CRUD & Consent - Implement `SpaceRef` creation, update, deletion endpoints - Implement `nested-in` reverse lookup - Implement `nestPolicy` CRUD (get/update) - Implement consent flow: `open` (immediate), `members` (membership gate), `approval` (request/approve/deny), `closed` (block) - Implement `PendingNestRequest` lifecycle (create, list, approve, deny) - Implement `defaultPermissions` ceiling on requested permissions - Implement allowlist/blocklist evaluation - Permission validation on nest creation (consent + reshare check + role check) - Notification dispatch (inbox module integration) ### Phase 3: Nested Space Rendering - Build `folk-canvas` shape (Task-45) as the renderer for `SpaceRef` entries - Implement secondary WebSocket connections for nested space data - Render nested spaces as bordered, labeled regions on the canvas - Handle collapsed vs. expanded views ### Phase 4: Permission Cascade - Implement `cascadePermissions()` across nesting chains - Enforce dual authority (nest permissions + space role) on all write operations - Implement revocation propagation - Add `reshare: false` enforcement ### Phase 5: Encryption Integration - Add `encrypted` flag to `CommunityMeta` - Implement plaintext projection for nested encrypted spaces - Implement module-level encryption opt-in (e.g., rWallet always encrypted) --- ## Open Questions 1. **Subdomain wildcards** — Should `working-group.climate-dao.rspace.online` be supported for hierarchical subdomain nesting? Or keep it flat with `climate-dao-wg-funding.rspace.online`? 2. **Offline nesting** — When offline, should the client cache nested space content from the last sync? How stale can nested content be before we show a warning? 3. ~~**Notification of nesting**~~ — **RESOLVED**: See *Nest Consent & Notification* section. Space owners configure consent level (`open`/`members`/`approval`/`closed`) and notification preferences in their space's `nestPolicy`. Auto-provisioned user spaces default to `approval` consent with inbox notifications. 4. **Billing/quotas** — Does each `.rspace.online` space count against storage? Are there limits on how many spaces a user can create or how deeply they can nest? 5. **Presence across nests** — When Alice is editing shapes in her space that are visible via nesting in the DAO space, do DAO members see Alice's presence cursor? Or only direct space visitors? 6. **Nest request expiry** — Should pending nest requests expire after a period of inactivity? (e.g., 30 days with no response → auto-denied and cleaned up) 7. **Bulk consent** — Should there be a way to approve/deny nest requests in bulk? DAOs with many members nesting content could generate a lot of requests if set to `approval`. --- *This document describes the target architecture. Implementation should proceed incrementally through the phases above, with each phase usable independently.*