Trust & Content Distribution

How trust relationships let an action spread beyond its immediate recipient, and how a container owner vouches for content it accepted so its members can accept it too.

Two Relay Mechanisms, Not One

Cloudillo has two ways of re-broadcasting someone else’s action, and both of them produce an APRV signed by the receiver. They are easy to confuse and they are not the same thing:

Wall-post relay Child relay
Trigger An approvable action addressed to us, from a connected peer, with auto-approve on An accepted child of a container we own that declares relay_children
Mechanism Auto-approve → APRV → follower broadcast Owner-vouched APRV → subscriber fan-out
Recipients Our followers The container’s subscribers
APRV shape aud = original issuer, sub = the action no aud, sub = the child, p = the container
Typical case A member posts to a community wall A member sends MSG into a CONV, or joins it with SUBS

The rest of this page takes them in turn. Both are implemented in cloudillo-action (post_store.rs, fanout.rs).

Auto-Approval for Trusted Connections

When an action arrives addressed to us and its issuer is trusted, the receiver can approve it automatically and then push the approval — with the content bundled — to its own followers. This is how a post lands on a community wall and reaches the community’s followers.

Auto-Approval Conditions

All of the following must hold (try_auto_approve in post_store.rs):

  1. Approvable type — the action type declares approvable=true (POST, MSG, REPOST).
  2. Addressed to us — action.audience == our_id_tag.
  3. Not our own — action.issuer != our_id_tag.
  4. Setting enabled — profile.auto_approve_actions is true.
  5. Connected issuer — the issuer’s local profile reports a bidirectional connection (connected.is_connected()), meaning both sides have sent CONN tokens.

Auto-approval runs only for inbound, non-sync actions.

Auto-Approval Flow

flowchart TD
    A[Inbound action stored] --> B{All five<br/>auto-approve<br/>conditions met?}
    B -- no --> C[Standard processing<br/>action waits for<br/>manual approval]
    B -- yes --> D[Set status to 'A']
    D --> E["Mint APRV<br/>aud = original issuer<br/>sub = action_id<br/>visibility = 'F'"]
    E --> F{Does the subject type<br/>declare broadcast=true?}
    F -- no --> G[Deliver APRV to the<br/>issuer only]
    F -- yes --> H[schedule_broadcast_delivery<br/>to every follower]
    H --> I["POST /api/inbox<br/>{token: APRV, related: [POST]}"]

The broadcast recipient set is every profile carrying the follower flag, minus the author; Suspended, Blocked and Banned profiles are already dropped by the query (helpers.rs).

Because the APRV carries the approved token as a related action, followers who never had a relationship with the original author still accept the content — the community’s signature vouches for it. See Communities for the same path told from the community’s side.

Child Relay: Owner-Vouched relay_children

A conversation is different. Its members are subscribers of a container, not followers of its owner, and a message from one member has to reach every other member — including members who have no relationship with the sender at all.

Containers that declare relay_children solve this by having the owner vouch for every child it accepts. Today CONV is the only type that declares it, alongside subscribable.

What the Owner Does

When a child action (a MSG, a join SUBS) is accepted on the container owner’s node, maybe_relay_child_to_subscribers (fanout.rs) runs:

  1. Cheap exit. No parent and no action-typed subject means there is no container to find — the common POST/REACT/FLLW case leaves immediately.
  2. Skip our own actions. A child issued by us is never vouched. This is the recursion guard: without it the owner’s own vouching APRV would itself be a child of the container and would be vouched again, forever. It also keeps our own STAT out, and leaves the owner’s own MSG/SUBS un-vouched — those are already accepted by the normal gates.
  3. Resolve the container. Walk the parent chain first (MSG → CONV), then an action-typed subject (SUBS → CONV). A 1:1 direct message resolves to nothing, so DM delivery stays on its ordinary audience path.
  4. Check the flags. The container’s type must declare relay_children, and we must actually own the container.
  5. Mint the vouch. An owner-signed APRV with sub = the child’s action ID, p = the container’s action ID, visibility = 'F' and no audience — it fans out to subscribers, not to one recipient.
sequenceDiagram
    participant B as Bob (member)
    participant A as Alice (CONV owner)
    participant C as Charlie (member)
    B->>A: MSG (parent = CONV)
    Note over A: hooks accept the child<br/>resting status 'A'
    A->>A: mint APRV<br/>sub = MSG, p = CONV, vis 'F'
    Note over A: vouched = true<br/>raw fan-out suppressed
    A->>C: {token: APRV, related: [MSG]}
    A->>B: {token: APRV, related: [MSG]}
    Note over C: R3 accepts the APRV<br/>MSG stored pre-approved

Why the Raw Fan-Out Is Suppressed

process_after_store gates the vouch on the hook’s resting status: only a child that came to rest at 'A' is vouched, so a rejected child (a stranger’s SUBS resting at 'D') is never relayed. If the vouch was minted, schedule_subscriber_fanout is skipped entirely — the APRV now carries the child, and sending the raw token as well would duplicate it.

Note

A hook-rejected child is never vouched. This is what keeps the relay from laundering content the container itself refused.

What the Members Do

Two inbound rules make the receiving side work:

  • Rule R3 — accept an APRV whose parent is a container we hold that declares relay_children, when the APRV’s issuer owns that container. The JWT signature is verified before this point, so the vouch cannot be forged; the bundled related token is bound to APRV.subject and re-checked in process_related_actions. R3 keys on relay_children, not on a type list, so it stays generic (process.rs).
  • Pre-approved SUBS — a relayed SUBS is accepted on a member’s node even with no local INVT and even though the issuer is not the container’s creator. That is the whole point of the relay: every member learns the full roster. The owner only vouched it after its own hooks accepted it (native_hooks/subs.rs).

Roster Backfill

The vouching APRV pushes a new member forward to everyone already in the container. It does nothing about the other direction — the joiner has never seen the members who were there before.

So on an accepted join SUBS, the owner also runs backfill_roster_to_joiner (fanout.rs): for every existing active member it delivers that member’s vouching APRV bundled with their SUBS, to the joiner only. The joiner ends up with the complete pre-existing roster.

One member has no vouching APRV to deliver: the owner’s own SUBS, which was created locally and therefore never vouched (step 2 above). It is delivered raw and accepted through allow_unknown plus the creator branch in the SUBS hook.

Subscriber Fan-Out

The raw fan-out is still the path for subscribable containers that do not declare relay_children, and for every case where the vouch was not minted.

schedule_subscriber_fanout (fanout.rs) walks the parent chain to a subscribable root, checks that we own it — a remote owner does its own fan-out, we just deliver to it — lists the active ('A') SUBS rows against it, and schedules one delivery per subscriber, excluding ourselves and the sender.

flowchart TD
    A[Bob sends MSG on his own node] --> B{Is the CONV local?}
    B -- no --> C[Deliver to the CONV owner only]
    C --> D[Alice's node receives it at /api/inbox]
    D --> E[Alice runs process_after_store]
    E --> F{relay_children<br/>container we own?}
    F -- yes --> G[Owner-vouched APRV<br/>raw fan-out suppressed]
    F -- no --> H[schedule_subscriber_fanout<br/>one delivery per active SUBS]

Both mechanisms lean on the same envelope: an action may be delivered with other tokens attached, so the recipient has everything it needs to accept it in one request.

Delivery Payload

{
  "token": "eyJhbGciOi...MAIN_ACTION_TOKEN",
  "related": [
    "eyJhbGciOi...RELATED_ACTION_TOKEN_1",
    "eyJhbGciOi...RELATED_ACTION_TOKEN_2"
  ]
}

Bundling is driven by the deliver_subject behaviour flag. The two live uses are APRV + the approved action, and INVT + the subject CONV.

Processing

flowchart TD
    A[Inbox receives token + related] --> B["Store related tokens<br/>status 'W' (waiting)"]
    B --> C[Process the main action<br/>verify signature, check<br/>permissions, store, run hook]
    C --> D[process_related_actions]
    D --> E[For each related token:<br/>verify signature,<br/>skip the relationship gate,<br/>store]

Key point: related actions skip the relationship gate, never the signature check. Trust flows from the APRV’s issuer to the content it vouched for — which is exactly why the vouch is only minted for children the owner’s own hooks accepted.

See Also