T-HUB/docs/design/2026-10-12-collectives-architecture.md
glass beadian 4a7e78b28d Collectives P1: participation landing (ES/EN), group tree, room provisioning APIs
- 030: groups.parent_id tree + kind discriminator + group_rooms mapping
- /collectives module: create/tree/join APIs, MatrixProvisioner (open room
  public+federated, closed room E2EE-at-creation, onboarding room), NullProvisioner
  fail-soft when ARCHIVE_PROVISIONER_* unset
- web root becomes the public participation manual (ES-first + EN toggle,
  three paths: join by invite, start collective, solo account)
- participation manual content in core
2026-10-12 13:53:40 +00:00

8.5 KiB

Collectives architecture — participation, rooms, subcollectives, diffusion

Status — P1 shipped (implementation note). The three scope pillars of P1 are in: groups.parent_id/groups.kind/group_rooms (migration 030_collectives.sql), the participation-manual landing page at the app root (apps/web/src/app/page.tsx, copy in packages/core/src/participation-manual.ts), and the provisioner port + Matrix implementation (apps/api/src/collectives/). Join-by-invite is unchanged: an invite token claims workspace membership exactly as before; GET /collectives-public/for-invite/:token adds names-only collective labelling to the join card. Room addresses in the UI, directory, and the UI/state re-parenting surface remain P2+.

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.

Account tiers, signup, membership (amended 2026-10-12)

Self-serve accounts are now part of the product. The site lets anyone create an account without belonging to any collective. Posture reconciliation: the homeserver keeps registration token-gated; the Hub mediates account creation (the Hub holds the registration capability and provisions the Matrix account server-side after its own signup checks). Direct registration on chat.occupymedia.cc stays closed — the Hub is the only door, and it can add rate-limiting / verification later without touching the homeserver.

  • Solo account: gets the platform's storage (R2-backed files space) and PeerTube access — the full media-maker toolkit, no collective.
  • Membership is many-to-many: one media maker can belong to several collectives at once (group_members already models this); solo users can be invited into collectives later, or request to join.
  • Account is the unit of identity; collective membership is a set, not a home.

Contribution policy (per collective, disclosed at join time)

Every collective defines what happens to members' contributions:

  • Default and stated plainly at join: deleting your account does NOT delete your contributions. Content already submitted to a collective stays in its library/archive, attributed, under the collective's retention rules.
  • Collectives may override (e.g., "contributions are removed on request", "contributions are anonymized after 90 days") — the policy is a field on the collective, shown on the join screen and in the collective's settings.
  • Enforcement is schema-level: contributions must not ON DELETE CASCADE from the user row. User deletion detaches attribution per policy; it never silently rips content out of a collective archive.

Telegram access to open rooms

Open rooms bridge to Telegram (mautrix-telegram, already scaffolded in the chat CT's compose): a collective's open room can attach a Telegram group so outsiders participate from Telegram with no Matrix account at all — same bridge pattern as Matrix federation for open rooms, different door. Bridged rooms are verified for encryption state (never treated as plaintext by assumption); archiving follows the same open-room rules.

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: <ws>/<group>).
  • Chat: room set per collective (below), bound to the group via the existing ROOM_MAPPINGS-style immutable mapping.

Rooms per collective

Collective creation includes the Open (public) room by default — a visible toggle lets the founder opt out. A collective that opts out is fully private from day one; one that keeps it gets the outsider door (federated Matrix + optional Telegram bridge, below).

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.