diff --git a/docs/design/2026-10-12-collectives-architecture.md b/docs/design/2026-10-12-collectives-architecture.md new file mode 100644 index 0000000..cee138d --- /dev/null +++ b/docs/design/2026-10-12-collectives-architecture.md @@ -0,0 +1,112 @@ +# Collectives architecture — participation, rooms, subcollectives, diffusion + +Status: design for build (phase 1 dispatch in flight) +Date: 2026-10-12 +Decides: landing flow, collective model, room matrix, provisioning, diffusion + +## Product shape + +`app.occupymedia.cc` lands on the **participation manual** (Spanish-first, English +toggle) with three paths: + +1. **Join an existing collective** — invite link (`/join/[token]`, exists). +2. **Start a collective** — creates the collective + its room set + onboarding. +3. **Solo user** — account + onboarding room (channels + guide bot explains how to + plug in); can join or start a collective later. + +## Collective model (maps to what exists) + +A **collective is a group** in the Hub's group tree — `groups.parent_id` becomes +self-referencing (top-level groups = collectives, children = subcollectives, +recursive). This rides the existing groups/RBAC/files machinery +(`group_files`, `group_members`, `chat_media_archive` are all keyed +workspace+group) instead of fighting workspace isolation with cross-tenant +writes. Workspace-per-collective stays available later if isolation demands it. + +- Root group = collective. Children = subcollectives ("report to the top one"). +- Files: each collective/subcollective keeps its own group folder in the files + app (already how `group_file_shares` is laid out: `/`). +- Chat: room set per collective (below), bound to the group via the existing + `ROOM_MAPPINGS`-style immutable mapping. + +## Rooms per collective + +| Room | Visibility | Encryption | Federation | Purpose | +|---|---|---|---|---| +| Open | Public, listed | **Never** (bot-readable) | **Yes** | Outsiders (any Matrix server) talk to the collective | +| Closed (production) | Invite-only | **E2EE default** | No | Internal production | +| Onboarding | Invite-only, per user | No | No | Guide bot posts the participation manual, ES/EN | + +Open rooms are **federated**: outsiders join from their own Matrix accounts on +any server — no accounts minted on ours for outsiders (registration stays +token-gated; outsiders never get local credentials). Closed rooms are +local-only + E2EE: ciphertext on the server, nothing readable by seizure. +Archive mirroring applies to Open rooms only (bot-readable by definition); +closed-room material stays in the room (members' clients = the backup) until +the phase-B encrypted-export flow gives admins self-service copies. + +## Provisioning + +A **provisioner bot** account (distinct from the archive bot; both +crypto-blind) creates the room set on collective creation: + +- creates Open room (public, federated, `history_visibility: shared`), sets + power levels (collective admins = PL 100, members = PL 0 + post), posts the + room address into the Hub group record +- creates Closed room (invite-only, **`m.room.encryption` state set at + creation** — the bot sets ciphertext it can never read, invites the founding + members) +- creates the founder's Onboarding room + posts the manual sequence (ES first) +- records everything in the Hub DB (group ↔ room mapping table — extends the + existing archive mapping pattern) + +Encrypted-by-default is thus **structural**: every production room is born +encrypted; the only unencrypted rooms are Open (by product definition) and +Onboarding (bot-posted guide, disposable). + +## Diffusion (push content upward) + +Subcollectives **push** content to the parent's diffusion queue; the parent +**releases**. Maps onto the existing publishing pipeline +(drafts → approvals → schedule → dispatch): + +- New table `diffusion_submissions`: id, from_group_id, to_group_id (parent), + draft_id or file_ref, submitted_by, status + (`submitted|approved|released|rejected`), approver, timestamps. +- Submission = the existing approval request, cross-group: the parent + collective's approvers see it in their Approvals queue with origin labeling + ("from subcollective X"). +- Release = the existing publish dispatch. No new publish machinery. + +Recursive trees compose naturally: a sub-subcollective pushes to its parent; +that parent can push its own (and forwarded) content further up. + +## Participation manual + +Spanish-first static page at the app root (`/`), English toggle. Structure: +what this platform is → the three paths → room etiquette (open vs closed) → +safety model summary (E2EE, token-gated accounts, redaction behavior) → +how to get help (onboarding room). Source content: the Acampada 26S kit copy +where applicable; written new where not. Lives in the app (i18n dictionaries), +not a PDF. + +## Build phases + +1. **P1 (dispatched)**: `groups.parent_id` migration + collective create/join + APIs + landing page (manual + three paths) + provisioner bot (room set + + onboarding rooms) + onboarding guide content (ES/EN). +2. **P2**: files UI shows the collective tree; open/closed room addresses + surfaced per collective; Element spaces mapping (one Element space per + collective: open + closed rooms together). +3. **P3**: diffusion submissions (push/approve/release across the tree) with + origin labeling in Approvals. +4. **P4**: phase-B encrypted export bundles (in flight separately) extended to + per-collective admin keys. + +## Security posture recap + +- Outsiders: federated into Open rooms only; zero local credentials. +- Production rooms: E2EE at birth; server stores ciphertext. +- Onboarding rooms: disposable, bot-posted guide only. +- Seizure: E2EE ciphertext + admin-held export keys (phase B) reconstruct the + archive; Open-room content was public by definition.