rspace-online/docs/SPACE-ARCHITECTURE.md

34 KiB

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.


<username>.rspace.online — Identity-Provisioned Spaces

Every EncryptID registration auto-provisions a space:

Registration → EncryptID created → Space provisioned at <username>.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 networkalice.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 <anything>.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

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:

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:

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.

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 <username>.rspace.online
// In src/encryptid/server.ts — after successful registration

// Auto-provision user's space
const userSpace = await createCommunity(
  username,                    // name
  username,                    // slug (becomes <username>.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.

const DEFAULT_USER_MODULES = [
  'canvas',   // the canvas itself
  'notes',    // note-taking
  'files',    // file storage
  'wallet',   // identity-linked wallet
];

Space Settings Stored in Meta

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
}

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:

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
}
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

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

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

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

// 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.

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:

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)

interface CommunityDoc {
  meta: CommunityMeta;
  shapes: { [id: string]: ShapeData };
  members: { [did: string]: SpaceMember };
  nestedSpaces: { [refId: string]: SpaceRef };    // NEW
}

CommunityMeta

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

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 <username>.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 <username>.rspace.online
  • 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 (Approach B — Server-Mediated)

  • encrypted and encryptionKeyId flags on CommunityMeta — DONE
  • AES-256-GCM encryption at rest for Automerge documents — DONE
    • Custom file format: magic bytes rSEN + keyId length + keyId + IV + ciphertext
    • Transparent encrypt-on-save, decrypt-on-load in community-store
    • Key derivation via HMAC-SHA256 from server secret + keyId (placeholder for EncryptID L2)
  • API endpoints: GET/PATCH /api/spaces/:slug/encryption — DONE
  • Plaintext projection for nested views: server decrypts and serves via WS — inherent in Approach B
  • Future: EncryptID Layer 2 client-side key delegation (Approach C) for true E2E encryption

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 nestingRESOLVED: 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 <username>.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.