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:
- Optionally registers the identity with an IdP, for
typ == "idp"communities (:87-180). - Creates a complete tenant — keys, certificate, profile row (
:190-204). - Updates that tenant to
ProfileType::Community(:226-238). - Sets the two settings that make it behave like a community:
profile.auto_approve_actions = trueandprofile.connection_mode = "A"(:244-260). - Issues a
CONNfrom the creator to the community; the community’sconnection_mode = "A"auto-replies withCONN:ACC(:262-282). - 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 → leaderA 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:DELaddressed at another member) is gated outbound incrates/cloudillo-action/src/handler.rs:263-296viacan_manage_member_by_roles. Self-leave — where the audience is the community itself — is skipped. - Role re-assignment (
PATCH /admin/profile/{idTag}) is gated incrates/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:
-
Delivery to the community only.
POSThasbroadcast = true, but the follower fan-out branch requiresaudience_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. -
Auto-approval. The community runs
try_auto_approve(post_store.rs:731-869). Five conditions must hold: the action type isapprovable; the audience is us; the issuer is not us;profile.auto_approve_actionsis enabled; and the issuer’s profile isConnected. A community satisfies the last two by construction — it ships withauto_approve_actions = true, andconnection_mode = "A"is what made the memberConnectedin the first place. -
The vouch. The community sets the POST to status
'A'and mints its ownAPRVwithsubject = <the POST's action id>andvisibility = 'F'(post_store.rs:840-853). -
Fan-out. The APRV branch of
schedule_deliverysees the subject isbroadcast = trueand callsschedule_broadcast_delivery(post_store.rs:509-532,:672-728). The recipient set ishelpers::broadcast_recipient_tags(helpers.rs:303-315) — every profile carrying thefollowerflag, i.e. every member and follower of the community, minus the author. -
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). -
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
- Identity System & User Profiles - The tenant and profile model communities share
- Communities REST API - Endpoints for creating and managing communities
- ABAC Permissions - How roles become permissions
- Community Conversation Walkthrough - Three people, one conversation, every token named