Communities

A community in Cloudillo is a shared space with its own identity, its own storage and its own signing keys. Members post into it, it vouches for what they post, and it relays the result to everyone who follows it.

A Community Is a Tenant

A community is not a container living inside somebody’s account. It is a tenant — the same first-class unit as a person’s profile. It has its own tn_id, its own id_tag, its own key pair, its own inbox at cl-o.<id_tag>, and its own database rows. Federation cannot tell the difference: a community is just another peer.

The only thing separating the two is one enum field on the tenant record:

pub enum ProfileType {
    #[serde(rename = "person")]
    Person,
    #[serde(rename = "community")]
    Community,
}

Tenant.typ carries it (crates/cloudillo-types/src/meta_adapter.rs:36-43, :188-214), and it serializes to the wire as the lowercase string "person" or "community".

Info

Because a community owns its keys, actions it approves are signed by the community. A member’s own signature is never replaced — see How a Community Relays a Wall Post below.

How It Differs From a Person Profile

Same tenant machinery, different behaviour at seven points:

Behaviour Person Community Source
Following Follows and is followed Followed, never follows. The follow implied by a CONN is applied only when the peer is a Person crates/cloudillo-action/src/native_hooks/mod.rs:41-63 (conn_follower_patch)
following flag on members Set on connect Never set — every CONN/INVT hook branch guards context.tenant_type == "community" and leaves the flag Undefined native_hooks/conn.rs:306-311, :369-374
Auto-approve Off by default Ships with profile.auto_approve_actions = true crates/cloudillo-profile/src/community.rs:244-249
Connection mode Unset (manual confirmation) Ships with profile.connection_mode = "A" (auto-accept) crates/cloudillo-profile/src/community.rs:250-260
Roles Column unused Members carry roles from the profile hierarchy crates/cloudillo-types/src/roles.rs:15-16
Content ownership The issuer owns it The audience owns it — a post addressed to a community belongs to the community crates/cloudillo-action/src/native_hooks/ownership.rs:30-35 (owns_subject)
Home feed Always merged in Can be hidden via profiles.hidden_in_home, which excludes its audience from the merged feed only crates/cloudillo-action/src/handler.rs:52-82

Permissions follow from the roles: ABAC short-circuits to allow for anyone carrying leader, and the default rules grant profile:admin to moderators and above. See ABAC Permissions for the full model (crates/cloudillo-core/src/abac.rs:513-518, :565-570).

Creation

PUT /api/profiles/{id_tag} creates a community (crates/cloudillo-profile/src/community.rs:28-350). The caller needs either the SADM role or a valid invite ref of type PROFILE_INVITE_REF_TYPE (:44-53).

The handler then, in order:

  1. Optionally registers the identity with an IdP, for typ == "idp" communities (:87-180).
  2. Creates a complete tenant — keys, certificate, profile row (:190-204).
  3. Updates that tenant to ProfileType::Community (:226-238).
  4. Sets the two settings that make it behave like a community: profile.auto_approve_actions = true and profile.connection_mode = "A" (:244-260).
  5. Issues a CONN from the creator to the community; the community’s connection_mode = "A" auto-replies with CONN:ACC (:262-282).
  6. Writes the creator’s profile row inside the community tenant with roles: ["leader"] (:305-322).

The Three Join Doors

How someone becomes a member depends on which door they came through, and the three doors give three different results:

Door Rests at Roles granted Can post? Source
Plain CONN, connection_mode = "A" (the community default) 'A' — auto-accepted, CONN:ACC sent back, profile Connected plus the follower flag none No — read only native_hooks/conn.rs:347-391
Plain CONN, connection_mode unset 'C' — pending until a leader calls POST /api/actions/{action_id}/accept. The follower flag is set immediately anyway, gated on privacy.allow_followers none yet No native_hooks/conn.rs:392-420
Invited CONN — a pending INVT with sub = "@{community}" exists 'A' — auto-accepted, and the INVT is retired so it does not linger as “pending” ["contributor"] Yes native_hooks/conn.rs:268-332
Plain CONN, connection_mode = "I" — the closed door 'D' — rejected, processing aborted none No native_hooks/conn.rs:335-346
Note

The auto-accept door and the invite door look alike from outside — both land at 'A' — but only the invite door grants a role. A plain auto-accepted connection produces a follower, not a contributor.

Roles

Profile roles are a strict hierarchy, lowest to highest (crates/cloudillo-types/src/roles.rs:15-16):

public → follower → supporter → contributor → moderator → leader

A role implies every role beneath it, and a role’s position in that array is its level. Two constants anchor the guards (crates/cloudillo-core/src/roles.rs:32-35): MODERATOR_LEVEL = 4 and LEADER_LEVEL = 5.

  • can_manage_member(actor, target) — the actor must be moderator or above and strictly outrank the target; leaders are the exception and may manage peer leaders (crates/cloudillo-core/src/roles.rs:37-42).
  • can_assign_role(role, actor_level) — leaders may assign any known role; everyone else is capped strictly below their own level, and unknown role names are never assignable (crates/cloudillo-core/src/roles.rs:52-59).

The baseline on join is contributor, not member. Both the invite-backed CONN path and the INVT path spell this out in the code: a non-canonical role string such as "member" is not in the hierarchy, so it expands to no permissions at all and renders in the UI as “Follower” (native_hooks/conn.rs:285-291, native_hooks/invt.rs:362-368).

Enforcement lives in two places, both outside ABAC:

  • Member removal (CONN:DEL addressed at another member) is gated outbound in crates/cloudillo-action/src/handler.rs:263-296 via can_manage_member_by_roles. Self-leave — where the audience is the community itself — is skipped.
  • Role re-assignment (PATCH /admin/profile/{idTag}) is gated in crates/cloudillo-profile/src/update.rs:148-215: name and status are leader-only, nobody may change their own roles, and clearing roles with {"roles": null} counts as a change and is gated the same way.
Warning

These profile roles are not conversation subscription roles. A SUBS token’s x.role is a separate, four-value scale — observer < member < moderator < admin (crates/cloudillo-action/src/helpers.rs:225-232) — that lives server-side, applies to a single conversation, and never appears in the JWT. See Subscription Tokens.

How a Community Relays a Wall Post

A member posts to the community’s wall by issuing a POST with audience = <community>. What follows is the auto-approve + follower broadcast path.

sequenceDiagram
    participant M as Member node<br/>(alice.cloudillo.net)
    participant C as Community node<br/>(devs.cloudillo.net)
    participant F as Follower nodes

    M->>C: POST /inbox {token: POST (audience=devs)}
    Note over C: schedule_delivery: POST is broadcast=true<br/>but audience is set → "specific audience only"
    Note over C: try_auto_approve: approvable, addressed to us,<br/>issuer is not us, auto_approve_actions=true,<br/>issuer is Connected → all five pass
    Note over C: POST status → 'A'<br/>mint community-signed APRV (subject = POST)
    Note over C: APRV branch: subject is broadcast=true<br/>→ schedule_broadcast_delivery
    C->>F: POST /inbox {token: APRV, related: [member's POST JWT]}
    Note over F: APRV accepted — they follow the community
    Note over F: process_related_actions enforces<br/>APRV.subject == related id, then<br/>processes the POST as pre-approved

Step by step, with the code:

  1. Delivery to the community only. POST has broadcast = true, but the follower fan-out branch requires audience_tag.is_none() (crates/cloudillo-action/src/post_store.rs:470). With an audience set, it falls through to standard delivery, whose recipient list is just the audience (post_store.rs:533-540). The member’s own followers see nothing yet.

  2. Auto-approval. The community runs try_auto_approve (post_store.rs:731-869). Five conditions must hold: the action type is approvable; the audience is us; the issuer is not us; profile.auto_approve_actions is enabled; and the issuer’s profile is Connected. A community satisfies the last two by construction — it ships with auto_approve_actions = true, and connection_mode = "A" is what made the member Connected in the first place.

  3. The vouch. The community sets the POST to status 'A' and mints its own APRV with subject = <the POST's action id> and visibility = 'F' (post_store.rs:840-853).

  4. Fan-out. The APRV branch of schedule_delivery sees the subject is broadcast = true and calls schedule_broadcast_delivery (post_store.rs:509-532, :672-728). The recipient set is helpers::broadcast_recipient_tags (helpers.rs:303-315) — every profile carrying the follower flag, i.e. every member and follower of the community, minus the author.

  5. The wire form is the APRV token plus the original, bundled: {"token": <community APRV>, "related": [<member's original POST JWT>]} (crates/cloudillo-action/src/delivery.rs:98-115).

  6. Acceptance. Recipients accept the APRV because they follow the community. process_related_actions (crates/cloudillo-action/src/process.rs:1415-1474) then enforces the APRV→subject binding — a bundled token whose id is not the APRV’s declared subject is dropped — and processes the POST as pre-approved.

Info

The member’s signature is preserved verbatim. The community adds a vouch on top; it never re-signs the content. What followers receive and verify is the member’s own token, wrapped in the community’s approval.

Not the Same as Relaying Conversation Children

There is a second relay mechanism, and it is easy to confuse with this one. Children of a conversation — the MSG and SUBS actions hanging under a CONV — travel via relay_children: the container owner vouches for each child with an APRV and fans it out to the conversation’s subscribers, not to the community’s followers. Both mechanisms produce an APRV signed by the container, but for different reasons and over different recipient sets. Wall posts go to followers; conversation children go to subscribers.

See Trust Flows for both mechanisms side by side.

See Also