Token Format

The wire format of an action token: every claim it carries, how visibility and audience are resolved, what the status codes mean, and how tokens are addressed, keyed and signed. This page is the single reference for those rules — other pages in this section link here rather than restating them.

An action token is a JSON Web Token signed with ES384.

Standard JWT Claims

Field Type Required Description
iss identity * The identity that created the token.
aud identity The audience — the recipient for directed actions, or the container the action belongs to. See Audience Inheritance.
sub identity / action ID The subject: content or a profile referenced without creating hierarchy. See Parent vs Subject.
iat timestamp * Issue time.
exp timestamp Expiry.

Cloudillo Claims

Field Type Required Description
k string * Key ID of the signing key. See Signing and Key IDs.
t string * Action type, with optional subtype as TYPE:SUBTYPE (e.g. POST, POST:IMG, REACT:LIKE, CONN:DEL).
c string / object Content, specific to the action type.
p string Parent action ID — creates hierarchy (threading).
a string[] Attachment file IDs (f1~...).
v char Visibility level. See Visibility.
f string Capability flags: R/r reactions, C/c comments, O/o open. Uppercase enables, lowercase disables.
_ string Proof-of-work nonce (used by CONN for rate limiting).
Note

There is no separate subtype claim. The subtype is encoded in t after a colon and split during processing. root_id is likewise absent — see Root ID.

Parent vs Subject

Two claims reference other actions, and they mean different things:

  • p (parent) creates true hierarchy. A CMNT under a POST, a MSG under a CONV, a reply MSG under another MSG. The parent chain is what root_id is computed from, what subscriptions are inherited through, and what an audience is inherited from.
  • sub (subject) references an action or profile without creating hierarchy. REACT, REPOST, SUBS, INVT and APRV use it.

The distinction is not cosmetic. A REACT on a post must not become a child of that post: it would land in the comment thread, inherit the post’s audience, and be counted as thread content. Using sub keeps the reference while leaving the tree alone — reactions are aggregated into STAT instead.

Visibility

The v claim carries a single character, parsed by VisibilityLevel::from_char (crates/cloudillo-core/src/abac.rs:52-64):

v Level Who can read
P Public Anyone, including unauthenticated visitors
V Verified Any authenticated user from any federated instance
2 2nd degree Friend of a friend (reserved for the voucher token system)
F Follower Authenticated users who follow the owner
C Connected Users with a mutual connection to the owner
S Subscribed Any active subscriber of the action’s container
absent Direct Owner and explicit audience only

S is the group-content level. The owner and tenant pass the base check; subscribers pass via the audience bridge, enforced in crates/cloudillo-action/src/filter.rs. CONV sets default_visibility: Some('S') (crates/cloudillo-action/src/dsl/definitions.rs:1077), so the SUBS, INVT and MSG actions beneath a conversation inherit it and become readable by the conversation’s members without being public.

Resolution Chain

Visibility is rarely written explicitly. It is resolved in order (crates/cloudillo-action/src/task.rs:172-199):

  1. Explicit v on the token.
  2. Inherited from the parent, then from an action-typed subject.
  3. The type default — BehaviorFlags::default_visibility from the action’s DSL definition (this is where CONV contributes 'S').
  4. The user’s setting profile.default_visibility.
  5. 'F' (Followers) as the final fallback.

The result then passes through apply_open_flag_visibility: an action carrying the open flag (O) is promoted to Connected for discoverability — but a resolved 'S' is never promoted, because that would leak group content outside the group.

Audience Inheritance

When aud is absent, it is not simply left empty. During action creation (crates/cloudillo-action/src/task.rs:540-565) the audience is resolved from the parent via helpers::resolve_parent_audience (crates/cloudillo-action/src/helpers.rs:144-154), which returns the parent’s audience, falling back to the parent’s issuer.

This is what makes conversations federate. A MSG whose p points at a CONV hosted by a community is minted with aud set to that community — which is exactly what routes it to the container owner for relay, rather than to whoever happens to have created the conversation. The DSL definition says as much: audience: None, // Optional: for DMs. For CONV messages, inherits from CONV. (crates/cloudillo-action/src/dsl/definitions.rs:364).

Inheritance is skipped for types marked broadcast=true (such as STAT). For those, aud = None is the deliberate “post to my own wall, fan out to my followers” signal. Inheriting there would set aud equal to the issuer’s own identity, the broadcast branch of schedule_delivery would compute zero recipients, and the action would never federate at all.

Action Status Codes

Every stored action carries a status that governs whether it is listed, whether it awaits a decision, and whether it is still in flight. Six are declared in crates/cloudillo-types/src/action_types.rs:43-67; three more are set by the pipeline and never appear in that module.

Code Name Meaning
A Active Active, accepted or approved. The normal operational state — client listings filter on this.
C Confirmation Awaiting a user decision. CONN requests without a mutual, FSHR file shares, INVT.
N Notification Auto-processed, informational only. Mutual CONN auto-accepts, REACT notifications.
D Deleted Rejected or deleted. Excluded from most queries.
R Draft Saved but not published. Editable.
S Scheduled A draft with a future publish time; auto-publishes when due.
P Pending The meta-adapter’s default on insert, shared with outbound-pending rows.
V Verifying An inbound row whose attachment sync has not completed (crates/cloudillo-action/src/process.rs:936-950). Filtered out of client listings and the federation outbox; activation V → A is deferred until attachment sync and the post-store steps succeed. This is why a freshly received action does not reach connected clients immediately.
F Failed All retries exhausted while verifying or activating an inbound action (crates/cloudillo-action/src/task.rs:1216-1230). The row never reached A, so the descriptor invariant holds; it simply stops appearing anywhere.

Typical transitions:

flowchart LR
    P[P pending] --> V[V verifying]
    V --> A[A active]
    V --> F[F failed]
    C[C confirmation] -->|user accepts| A
    C -->|user rejects| D[D deleted]
    A -->|user deletes| D
    R[R draft] --> A
    S[S scheduled] --> A
  • CONN request: arrives as C → user accepts → A (mutual connection).
  • INVT to a conversation: arrives as C → user accepts → a SUBS is created with status A.
  • Mutual CONN: both sides have sent one → auto-accepted → N.
  • A DEL subtype: flips its target’s status to D.

Content Addressing

Every identifier in Cloudillo is the hash of the thing it names. The hasher (crates/cloudillo-types/src/hasher.rs) computes SHA-256, encodes it with base64url without padding, and prefixes it with a one-letter kind plus a version:

Prefix Identifies Hash of
a1~ Action The complete JWT — header, payload and signature
f1~ File The file descriptor string
b1~ Blob variant The raw variant bytes

So an action ID is "a" + "1~" + base64url_nopad(SHA256(token)). Because the signature is inside the hashed bytes, a token cannot be altered without changing its ID: references to actions are tamper-proof, duplicates deduplicate for free, and any recipient can recompute the ID to check what it received.

These identifiers chain into a merkle tree — an action binds its attachments’ file IDs, and each file descriptor binds its variants’ blob IDs. See Content-Addressing & Merkle Trees for the full structure and a worked verification example.

Database Keys

Alongside its content-addressed ID, an action may carry a database key derived from a key_pattern in its DSL definition. The key is not part of the token; it exists so that a newer action can override an older one instead of accumulating alongside it.

Patterns interpolate {type}, {issuer}, {audience}, {subject}, {parent}, and dotted content fields such as {content.idTag}. Examples from crates/cloudillo-action/src/dsl/definitions.rs:

Type Pattern Effect
REACT {type}:{subject}:{issuer} One reaction per user per action — reacting again replaces the previous reaction
SUBS {type}:{subject}:{issuer} One subscription per user per subscribable action
INVT {type}:{subject}:{audience} One outstanding invitation per invitee per action
STAT {type}:{parent} One statistics record per action

Types with key_pattern: None — POST, CMNT, CONV among them — are always unique; every one of them is a new row.

Root ID

root_id is not in the token. It is a computed field, stored in the database for query optimisation and derived by walking the parent chain to the root action. Recipients compute it themselves.

Keeping it out of the JWT keeps tokens compact, avoids signing redundant data, and lets thread structure be recomputed if it ever needs to be. Its main use is subscription inheritance: a subscription to the root action grants permission to interact with every nested reply beneath it.

Signing and Key IDs

The k claim names which of the issuer’s keys signed the token. Key IDs are generated as %y%m%d — six digits, e.g. 250827 for a key created on 2025-08-27 (adapters/auth-adapter-sqlite/src/profile_key.rs:44). Signing selects the newest key with ORDER BY k.key_id DESC LIMIT 1 (adapters/auth-adapter-sqlite/src/auth.rs:283-292).

Warning

There is no kid in the JWS header. The header is the bare {"alg":"ES384","typ":"JWT"} (adapters/auth-adapter-sqlite/src/crypto.rs:176-190); key selection happens through the payload k claim. A verifier that looks for kid will find nothing.

A verifier fetches the issuer’s public keys from https://cl-o.{issuer}/api/me, picks the one whose key_id matches k, and checks the ES384 signature.

See Also