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. ACMNTunder aPOST, aMSGunder aCONV, a replyMSGunder anotherMSG. The parent chain is whatroot_idis 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,INVTandAPRVuse 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):
- Explicit
von the token. - Inherited from the parent, then from an action-typed subject.
- The type default —
BehaviorFlags::default_visibilityfrom the action’s DSL definition (this is whereCONVcontributes'S'). - The user’s setting
profile.default_visibility. '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
CONNrequest: arrives asC→ user accepts →A(mutual connection).INVTto a conversation: arrives asC→ user accepts → aSUBSis created with statusA.- Mutual
CONN: both sides have sent one → auto-accepted →N. - A
DELsubtype: flips its target’s status toD.
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
- Action Lifecycle - How tokens are created, verified and delivered
- Content-Addressing & Merkle Trees - The full hash chain
- Access Control - How visibility levels are enforced
- Action DSL - Where
key_pattern,default_visibilityandbroadcastare declared