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