- 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
167 lines
8.5 KiB
Markdown
167 lines
8.5 KiB
Markdown
# 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.
|