Walkthrough — A Conversation in a Community
Three people on three different servers hold a conversation inside a community that lives on a fourth. Nobody has to know anybody. This page follows every token from the first invitation to the last message, naming the real action type and the real code path at each step.
The cast.
| Who | Identity | Role here |
|---|---|---|
| The community | devs.cloudillo.net |
Hosts the conversation |
| Alice | alice.cloudillo.net |
Community leader; creates the conversation |
| Bob | bob.example.com |
Invited to the community, then to the conversation |
| Carol | carol.example.org |
Joins after Bob |
Four separate instances, so every arrow below is a real federation hop.
Scene 0 — The Community Exists
devs.cloudillo.net is a community, and in Cloudillo a community is its own tenant: its own tn_id, its own signing keys, its own id_tag, its own inbox, distinguished only by Tenant.typ == ProfileType::Community (crates/cloudillo-types/src/meta_adapter.rs:42). That one fact is what makes everything below work — the community is a full federation peer, not a folder inside somebody’s account.
Membership is a CONN relationship plus a roles value on the profile, never a subscription. See Communities for what a community is, the three ways in, and what the roles mean.
Scene 1 — Bob and Carol Join the Community
There are three doors into a community; we take the one that grants contributor, the invitation. (The other two — open connection and the wall-post follow — are in Communities.)
Alice, a leader, issues an INVT whose subject is an identity reference, sub = @devs.cloudillo.net (crates/cloudillo-action/src/subject_ref.rs, SubjectRef::Identity), with aud = bob.example.com. An identity subject routes the token into the community branch, native_hooks/invt.rs:85 → on_create_community() at :140.
Note
The @ prefix is load-bearing. The community-home lookup queries the subject as @{id_tag} (native_hooks/conn.rs:38, :272); a bare tag matches nothing and the invitation is never found. A test pins exactly this: adapters/meta-adapter-sqlite/tests/crud_tests.rs.
Only a moderator or above may invite on the community’s behalf. The invitation is delivered to two places at once: Bob’s instance, where it rests at status 'C' (confirmation) so his client can show an accept/reject prompt, and the community itself, where it rests at 'A' so the pending-invitation lookup can find it later (native_hooks/invt.rs:219-252).
Bob accepts. native_hooks/invt.rs:326 on_accept_community() marks the community profile Connected on Bob’s own node, then federates a plain CONN to the community. The community’s native_hooks/conn.rs:278 finds the outstanding invitation (has_pending_invitation), auto-accepts, replies CONN:ACC, sets roles = ["contributor"] (conn.rs:314) and retires the invitation (conn.rs:35, retire_community_invitations). Carol joins the same way.
Bob and Carol are now members of the community. Neither of them has any relationship with the other, and that never changes.
Scene 2 — Alice Creates the Conversation
Alice issues a CONV with aud = devs.cloudillo.net. The audience is the whole point: ownership of a container is decided by owns_subject() — audience if set, otherwise issuer (native_hooks/ownership.rs:30). So the community owns this conversation, not Alice, even though Alice signed it.
native_hooks/conv.rs:25 on_create auto-creates Alice’s own SUBS with x.role: "admin" and audience = the CONV's audience (conv.rs:40-46), so her subscription federates to the community rather than staying home.
The CONV’s defaults come from the DSL (dsl/definitions.rs:1073-1076): default_flags: "rco" — closed, and no reactions or comments on the CONV token itself — plus default_visibility: 'S', so children are readable by members, and relay_children: true, which is the flag Scene 4 turns on.
sequenceDiagram
participant A as alice.cloudillo.net
participant D as devs.cloudillo.net
participant B as bob.example.com
A->>B: INVT (sub=@devs.cloudillo.net)
A->>D: INVT (community home copy, status 'A')
Note over B: rests at 'C' — awaiting confirmation
B->>D: CONN (accept)
D->>B: CONN:ACC
Note over D: roles = ["contributor"], invitation retired
A->>D: CONV (aud=devs.cloudillo.net)
A->>D: SUBS (sub=CONV, x.role=admin)
Note over D: community owns the CONV (owns_subject)
Scene 3 — Bob Joins the Conversation
Alice invites Bob to the conversation with a second INVT — this time the subject is an action reference, the CONV’s action_id, not an identity. Like the community invitation it is delivered twice: to Bob, and to the conversation’s home so the invitation is on record there when Bob’s subscription arrives (deliver_to_subject_owner, dsl/definitions.rs:1178, resolved in task.rs:866-900). The conversation’s home is its owner — the community.
Bob accepts. native_hooks/invt.rs:293-311 creates his SUBS on his behalf, with sub = the CONV and x.role: "member". Its audience is the owner of the subject action, which for a community-hosted conversation is the community, so the subscription federates there.
At the community, native_hooks/subs.rs:59 on_receive walks the auto-accept decision tree: is the target open? is there an invitation on record? is the issuer the target’s creator? is the token pre-approved? Bob’s hits the second branch (subs.rs:95-106) — the invitation Alice filed in the CONV home. The subscription is accepted, rests at status 'A', and the invitation is retired.
Warning
Known divergence. In the current implementation both of the “deliver to the owner” steps in this scene resolve the owner as the subject action’s issuer (native_hooks/invt.rs:299, task.rs:889) rather than through owns_subject(). For a personal conversation the two are the same; for a community-hosted one they are not, and the tokens go to Alice instead of the community. The behaviour described above is the intended one.
Scene 4 — The Roster Spreads
Now the part that makes the whole arrangement work.
The CONV declares relay_children, and the community owns it, so Bob’s accepted subscription is vouched: the community mints an owner-signed APRV whose subject is Bob’s SUBS and whose parent is the CONV (crates/cloudillo-action/src/fanout.rs:206). That approval fans out to every subscriber with Bob’s SUBS bundled alongside it as the related token.
Alice’s instance accepts the APRV under rule R3 — an approval from a peer we have a relationship with, vouching for content we would otherwise refuse (crates/cloudillo-action/src/process.rs:679-694) — and accepts the bundled SUBS through the pre_approved branch of the same decision tree (native_hooks/subs.rs:117-124). Alice now sees Bob in the roster, without ever having connected to him.
The other direction is backfill_roster_to_joiner() (fanout.rs:286), which walks the container’s active subscriptions and delivers each existing member’s APRV + SUBS pair to Bob, so Bob learns who is already there. Two members are skipped: Bob’s own subscription, which reaches him via the vouch just minted, and the owner’s own — the community never vouches for its own actions (fanout.rs:224-231), so if it holds a subscription of its own it is delivered raw and accepted through the creator branch (fanout.rs:315-332).
The mechanics of both directions are in Child Relay: Owner-Vouched relay_children and Roster Backfill.
Scene 5 — Carol Joins
Exactly the same sequence, and this is where it starts to pay. The backfill now carries two members to Carol rather than one, and Carol’s vouch fans out to both Alice and Bob. Each new member costs the community one APRV and n deliveries; it costs the existing members nothing but an inbound token they can already verify.
sequenceDiagram
participant A as alice.cloudillo.net
participant D as devs.cloudillo.net
participant B as bob.example.com
participant C as carol.example.org
B->>D: SUBS (sub=CONV)
Note over D: invitation on record → accepted, status 'A'
D->>D: mint APRV (sub=Bob's SUBS, p=CONV)
D->>A: APRV + related: Bob's SUBS
Note over A: R3 → APRV accepted, SUBS accepted pre_approved
D->>B: backfill: APRV + Alice's SUBS
C->>D: SUBS (sub=CONV)
D->>A: APRV + related: Carol's SUBS
D->>B: APRV + related: Carol's SUBS
D->>C: backfill: Alice's and Bob's SUBS
Scene 6 — The Actual Conversation
Bob sends a MSG with p = the CONV’s action_id. Sending it requires an active subscription — requires_subscription: true on MSG (dsl/definitions.rs:387), enforced by check_subscription_permissions (process.rs:707-717).
Bob’s instance walks the parent chain looking for a container it owns (fanout.rs:39). It finds the CONV, which is not local, so there is no local fan-out to do; the message is simply delivered to the container’s owner, the community. The message’s audience is inherited from its parent rather than set by Bob — see Audience Inheritance.
The community accepts the message, mints a vouching APRV exactly as it did for the subscriptions, and fans that out to Alice and Carol with Bob’s MSG bundled as the related token (Related Action Bundling). Neither Alice nor Carol needs any relationship with Bob. The signature they verify is Bob’s; the permission they act on is the community’s.
On the authoritative node, native_hooks/msg.rs:43 recomputes the CONV’s comments / comments_ts counters from its live children and schedules a coalesced STAT broadcast, so the unread badge moves for everyone at once. Push and email notification is gated by should_push_notify("MSG") (forward.rs:196), with the deep link /~/app/messages/{conv_id} (forward.rs:221).
sequenceDiagram
participant B as bob.example.com
participant D as devs.cloudillo.net
participant A as alice.cloudillo.net
participant C as carol.example.org
Note over B: active SUBS required (requires_subscription)
B->>D: MSG (p=CONV)
Note over D: parent chain → CONV not local → deliver to owner
D->>D: accept, mint APRV (sub=MSG, p=CONV)
D->>A: APRV + related: Bob's MSG
D->>C: APRV + related: Bob's MSG
Note over D: msg.rs recomputes comments counters → STAT
Scene 7 — Carol Leaves
Carol issues SUBS:DEL. A deletion is always accepted (native_hooks/subs.rs:190-204) — there is no gate to pass, since nobody can be forced to stay — her invitation is retired, and she drops out of the status='A' subscriber query that drives every fan-out above. No further message reaches her, and no other member has to do anything.
Every Token in One Table
| Token | Issuer | Audience | Parent / subject | What it accomplishes |
|---|---|---|---|---|
INVT |
Alice | Bob | sub = @devs.cloudillo.net |
Invites Bob into the community |
CONN |
Bob | community | — | Bob accepts; community sets roles = ["contributor"] |
CONV |
Alice | community | — | Creates the conversation; community becomes its owner |
SUBS |
Alice | community | sub = CONV |
Alice’s own admin subscription, auto-created |
INVT |
Alice | Bob | sub = CONV |
Invites Bob into the conversation |
SUBS |
Bob | community | sub = CONV |
Bob joins; accepted on the invitation branch |
APRV |
community | each subscriber | sub = Bob’s SUBS, p = CONV |
Vouches for Bob so members accept him |
MSG |
Bob | inherited from CONV | p = CONV |
The message itself |
APRV |
community | each subscriber | sub = Bob’s MSG, p = CONV |
Vouches for the message |
SUBS:DEL |
Carol | community | sub = CONV |
Carol leaves |
Why It Is Built This Way
The community is a single authoritative node, and that is what removes the n² problem. Members never need pairwise relationships with each other: each of them has exactly one relationship, with the community, and the community’s approval is what makes a stranger’s token acceptable. Add a hundred members and nobody’s connection list grows by more than one.
The vouch adds permission, not authorship. Every relayed token still carries its original author’s signature, and every receiving node still verifies it. What the APRV supplies is the answer to “why should I accept this from someone I don’t know?” — because a peer I do know, and who owns the container this belongs to, already accepted it.
See Also
- Communities - What a community is, and how membership works
- Trust & Content Distribution - Both relay mechanisms in full
- Conversation Token - CONV reference
- Subscription Token - SUBS reference and the auto-accept tree
- Invitation Token - INVT reference
- Message Token - MSG reference
- Token Format - Fields, audience inheritance, content addressing