Actions & Action Tokens
An Action Token represents a user action within Cloudillo. Examples of actions include creating a post, adding a comment, leaving a like, or performing other interactions.
Each Action Token is:
- Cryptographically signed by it’s creator.
- Time-stamped with an issue time.
- Structured with relevant metadata about the action.
Action tokens are implemented as JSON web tokens (JWTs).
Action Token Fields
| Field | Type | Required | Description |
|---|---|---|---|
| iss | identity | * | The identity of the creator of the Action Token. |
| aud | identity | The audience of the Action Token. | |
| sub | identity | The subject of the Action Token. | |
| iat | timestamp | * | The time when the Action Token was issued. |
| exp | timestamp | The time when the Action Token will expire. | |
| k | string | * | The ID of the key the identity used to sign the Token. |
| t | string | * | The type of the Action Token. |
| c | string / object | The content of the Action Token (specific to the token type). | |
| p | string | The ID of the parent token (if any). | |
| a | string[] | The IDs of the attachments (if any). |
Merkle Tree Structure
Cloudillo’s action system implements a merkle tree structure where every action, file, and attachment is content-addressed using SHA-256 hashing. This creates cryptographic proof of authenticity and immutability through a six-level hierarchy:
- Blob Data → hashed to create Variant IDs (
b1~...) - File Descriptor → hashed to create File IDs (
f1~...) - Action Token JWT → hashed to create Action IDs (
a1~...) - Parent References → create immutable chains between actions
- Attachment References → bind files to actions cryptographically
- Complete DAG → forms a verifiable directed acyclic graph
Each level is tamper-evident: modifying any content changes all parent hashes, making tampering immediately detectable.
See Content-Addressing & Merkle Trees for complete details on how this creates proof of authenticity for all resources.
Attachment and Token IDs
Attachment and token IDs are derived using SHA256 hashes of their content, creating a multi-level content-addressing hierarchy:
All identifiers use SHA-256 content-addressing:
- Action IDs (a1~): Hash of entire JWT token →
a1~8kR3mN9pQ2vL6xW... - File IDs (f1~): Hash of descriptor string →
f1~Qo2E3G8TJZ2HTGh... - Variant IDs (b1~): Hash of raw blob bytes →
b1~abc123def456ghi...
See Content-Addressing & Merkle Trees for detailed hash computation.
Verification Chain
This creates a verifiable chain of hashes:
Action (a1~8kR...)
├─ Signed by user (ES384)
├─ Content-addressed (SHA256 of JWT)
└─ Attachments: [f1~Qo2...]
└─ File (f1~Qo2...)
├─ Content-addressed (SHA256 of descriptor)
└─ Descriptor: "d1~tn:b1~abc...,sd:b1~def..."
├─ Variant tn (b1~abc...)
│ └─ Content-addressed (SHA256 of blob)
├─ Variant sd (b1~def...)
│ └─ Content-addressed (SHA256 of blob)
└─ Variant md (b1~ghi...)
└─ Content-addressed (SHA256 of blob)Properties:
- Immutable: Content cannot change without changing all IDs
- Verifiable: Anyone can recompute hashes to verify integrity
- Deduplicate: Identical content produces identical IDs
- Tamper-evident: Any modification breaks the hash chain
Overriding Action Tokens
- Each token type is linked to a database key, allowing previous tokens to be overridden where applicable.
- The database key always contains the “iss” (issuer) field and may include other relevant fields.
- Example: A REACT token (representing a reaction to a post) uses a key composed of “iss” and “p” (parent post ID). If a user reacts to the same post multiple times, the latest reaction replaces the previous one.
Root ID Handling
Important: The root_id field is NOT included in the action token JWT.
root_idis stored in the database for query optimization- It is a computed field, derived by traversing the parent chain to find the root action
- It is NOT cryptographically signed (not in the JWT payload)
- Recipients must compute
root_idby following parent references
Why Root ID is Computed
This design choice has several benefits:
- Smaller JWT payload: Keeps tokens compact and efficient
- Avoids redundancy: Root ID can be derived from the parent chain
- Maintains flexibility: Thread structure can be recomputed if needed
- No signature burden: No need to sign derived data
Finding the Root
To find the root of an action thread:
find_root_id(action_id):
action = fetch_action(action_id)
if action.parent_id exists:
return find_root_id(action.parent_id) // Recursive
else:
return action.id // Found rootDatabase Optimization
The computed root_id is stored in the database to enable efficient thread queries:
-- Find all actions in a thread
SELECT * FROM actions
WHERE root_id = 'a1~abc123...'
ORDER BY created_at;
-- Without root_id, this would require recursive parent traversal!Action Creation Pipeline
Local Action Creation
When a user creates an action (e.g., posting, commenting), the following process occurs:
- Client Request
POST /api/actions
Authorization: Bearer <access_token>
Content-Type: application/json
{
"type": "POST",
"content": "Hello, Cloudillo!",
"attachments": ["f1~abc123..."],
"audience": "alice.example.com"
}- Task Scheduling
Server creates a task and waits for dependencies (e.g., file attachments to be processed).
- JWT Creation & Signing
Build JWT with claims:
iss: user's identity
t: action type (POST, CMNT, etc.)
c: content payload
p: parent action ID (if reply/reaction)
a: file attachment IDs
iat: timestamp
k: signing key ID
Sign JWT with ES384 (ECDSA P-384 curve)- Storage & Response
Return the action ID to the client:
{
"action_id": "a1~xyz789...",
"token": "eyJhbGc..."
}Complete Flow Diagram
Client
↓ POST /api/actions
Server creates ActionCreatorTask
↓
Scheduler checks dependencies
↓
Wait for FileIdGeneratorTask (if attachments)
↓
ActionCreatorTask runs
├─ Build JWT claims
├─ Fetch private key from AuthAdapter
├─ Sign JWT (ES384)
├─ Compute action ID (SHA256)
└─ Store in MetaAdapter
↓
Response to clientAction Verification Pipeline
Federated Action Reception
When a remote instance sends an action (federation), it arrives at /api/inbox:
- Inbound Request
POST /api/inbox
Content-Type: application/json
{
"token": "eyJhbGc..."
}- Verification Steps
Decode JWT (extract issuer ID)
↓
Fetch issuer's public key
GET https://cl-o.{issuer}/api/me/keys
↓
Verify JWT signature (ES384)
↓
Check permissions:
- Comments/reactions: verify parent ownership
- Connections/follows: verify audience matches
- Posts: verify following/connected status
↓
Sync attachments (if any)
GET https://cl-o.{issuer}/api/file/{id}
↓
Store action locallyComplete Verification Flow
Remote Instance
↓ POST /api/inbox
Create ActionVerifierTask
↓
Decode JWT (unverified)
↓
Fetch issuer's public keys
↓ GET https://cl-o.{issuer}/api/me/keys
Verify JWT signature (ES384)
↓
Check expiration
↓
Verify permissions
├─ Following/Connected status
├─ Audience matches
└─ Parent ownership (for replies)
↓
Sync attachments (if any)
↓ GET https://cl-o.{issuer}/api/file/{id}
Store in MetaAdapter
↓
Trigger hooks (notifications, etc.)Action Retrieval
GET /api/actions
Retrieve actions owned by or visible to the authenticated user:
Request:
GET /api/actions?type=POST&limit=50&offset=0
Authorization: Bearer <access_token>Response (200 OK):
{
"actions": [
{
"id": "a1~xyz789...",
"type": "POST",
"issuer": "alice.example.com",
"content": "Hello, Cloudillo!",
"attachments": ["f1~abc123..."],
"created_at": 1738483200,
"token": "eyJhbGc..."
}
],
"total": 150,
"limit": 50,
"offset": 0
}GET /api/actions/:id
Retrieve a specific action by ID:
Request:
GET /api/actions/a1~xyz789...
Authorization: Bearer <access_token>Response (200 OK):
{
"id": "a1~xyz789...",
"type": "POST",
"issuer": "alice.example.com",
"content": "Hello, Cloudillo!",
"attachments": ["f1~abc123..."],
"parent": null,
"created_at": 1738483200,
"token": "eyJhbGc..."
}Federation & Distribution
Outbound Distribution
Determine recipients based on action type:
- POST: send to all followers
- CMNT/REACT: send to parent action owner
- CONN/FLLW: send to audience
For each recipient:
POST https://cl-o.{recipient}/api/inbox
Body: {"token": "eyJhbGc..."}The /api/inbox endpoint is public (no authentication required) because the action token itself contains the cryptographic proof of authenticity.
Security Considerations
Action Token Immutability
Action tokens are content-addressed using SHA-256: action_id = SHA256(entire_jwt_token).
This includes:
- Header (algorithm, type)
- Payload (issuer, content, attachments, timestamps, etc.)
- Signature (cryptographic proof of authorship)
Immutability properties:
- Tokens cannot be modified without changing the ID
- Duplicate actions are automatically deduplicated
- References to actions are tamper-proof
- Parent references create immutable chains
- Attachment references are cryptographically bound
Merkle Tree Verification
The content-addressing system creates a merkle tree that can be verified at multiple levels:
- Signature Verification: Verify the action was created by the claimed author using their public key
- Action ID Verification: Recompute action_id and verify it matches (proves no tampering)
- Parent Chain Verification: Recursively verify parent actions exist and are valid
- Attachment Verification: Verify file descriptors and blob variants match their hashes
See Content-Addressing & Merkle Trees for complete verification examples.
Complete Verification
After all levels are verified:
- ✅ Author identity confirmed (signature)
- ✅ Action content confirmed (action_id hash)
- ✅ Parent references confirmed (recursive verification)
- ✅ File attachments confirmed (file and variant hashes)
- ✅ Complete merkle tree verified
See Content-Addressing & Merkle Trees for detailed verification examples.
Signature Verification
Every federated action undergoes cryptographic verification:
- Signature: Proves the issuer created the action
- Key Ownership: Public key fetched from issuer’s
/api/me/keys - Expiration: Optional
expclaim prevents token replay - Audience: Optional
audclaim ensures intended recipient
Spam Prevention
Multiple mechanisms prevent spam:
- Relationship Requirements: Only receive actions from connected/followed users
- Rate Limiting: Limit actions per user per time period
- Proof of Work: (Future) Optional PoW for anonymous actions
- Reputation: (Future) Trust scores based on user behavior
Action Token Types
User Relationships
- CONN - Connect
- Represents one side of a connection between two profiles. A connection is established when both parties issue a connection token to each other.
- FLLW - Follow
- Represents a follow relationship (a profile follows another profile).
Content
- POST - Post
- Represents a post created by a profile. Can include text, images, videos, or other attachments.
- REPOST - Repost/Share
- Represents the reposting/sharing of another user’s content to your profile.
- CMNT - Comment
- Represents a comment attached to another token (post, comment, etc.).
- REACT - Reaction
- Represents a reaction (like, emoji, etc.) to another token.
- ENDR - Endorsement
- Represents an endorsement or recommendation of another user or content.
Communication
- MSG - Message
- Represents a direct message sent from one profile to another.
Metadata
- STAT - Statistics
- Represents statistics about another token (number of reactions, comments, views, etc.).
- ACK - Acknowledge
- Represents an acknowledgment of a token issued to another profile. It is issued by the audience of the original token.
TODO
Permissions
- Public
- Followers
- Connections
- Tags
Flags
- Can react
- Can comment
- With permissions?
Tokens
- Review
- Represents a review post with a rating, attached to something
- Patch
- Represents a patch of a token (modify the content, or flags for example)
- Resource
- Represents a resource that can be booked (e.g. a conference room)
- Book
- Represents a booking of a resource.
Complete Example: LIKE → POST → Attachments → Variants
This example demonstrates the complete merkle tree structure from a LIKE action down to the individual image blob bytes.
Example Data
LIKE Action (Bob reacts to Alice's post)
├─ Action ID: a1~m9K7nP2qR8vL3xWpYzT4BjN...
├─ Type: REACT:LIKE
├─ Issuer: bob.example.com
├─ Parent: a1~8kR3mN9pQ2vL6xW... (Alice's POST)
└─ Created: 2025-01-02T10:30:00Z
POST Action (Alice's post with 3 images)
├─ Action ID: a1~8kR3mN9pQ2vL6xWpYzT4BjN...
├─ Type: POST:IMG
├─ Issuer: alice.example.com
├─ Content: "Check out these amazing photos from our trip!"
├─ Attachments:
│ ├─ f1~Qo2E3G8TJZ2HTGhVlrtTDBpvBGOp6gfGhq4QmD6Z46w (Image 1)
│ ├─ f1~7xW4Y9K5LM8Np2Qr3St6Uv8Xz9Ab1Cd2Ef3Gh4Ij5 (Image 2)
│ └─ f1~9mN1P6Q8RS2Tu3Vw4Xy5Za6Bc7De8Fg9Hi0Jk1Lm2 (Image 3)
└─ Created: 2025-01-02T09:15:00Z
Image 1 File Descriptor
├─ File ID: f1~Qo2E3G8TJZ2HTGhVlrtTDBpvBGOp6gfGhq4QmD6Z46w
├─ Descriptor: d1~tn:b1~abc123...:f=AVIF:s=4096:r=150x150,
│ sd:b1~def456...:f=AVIF:s=32768:r=640x480,
│ md:b1~ghi789...:f=AVIF:s=262144:r=1920x1080
└─ Variants:
├─ tn: b1~abc123def456ghi789... (4KB, 150×150px)
├─ sd: b1~def456ghi789jkl012... (32KB, 640×480px)
└─ md: b1~ghi789jkl012mno345... (256KB, 1920×1080px)
Image 2 File Descriptor
├─ File ID: f1~7xW4Y9K5LM8Np2Qr3St6Uv8Xz9Ab1Cd2Ef3Gh4Ij5
└─ Variants: tn, sd, md (similar structure)
Image 3 File Descriptor
├─ File ID: f1~9mN1P6Q8RS2Tu3Vw4Xy5Za6Bc7De8Fg9Hi0Jk1Lm2
└─ Variants: tn, sd, md, hd (4K image, has HD variant)Merkle Tree Visualization
flowchart TB
subgraph "Action Layer"
LIKE[LIKE Action<br/>a1~m9K7nP2qR8vL3xW...<br/>Type: REACT:LIKE<br/>Issuer: bob.example.com]
POST[POST Action<br/>a1~8kR3mN9pQ2vL6xW...<br/>Type: POST:IMG<br/>Issuer: alice.example.com<br/>Content: Check out these photos!]
end
subgraph "Parent Reference"
LIKE -->|parent_id| POST
end
subgraph "Attachment References"
POST -->|attachments[0]| FILE1
POST -->|attachments[1]| FILE2
POST -->|attachments[2]| FILE3
end
subgraph "File Descriptor Layer"
FILE1[File 1<br/>f1~Qo2E3G8TJZ2...]
FILE2[File 2<br/>f1~7xW4Y9K5LM8...]
FILE3[File 3<br/>f1~9mN1P6Q8RS2...]
end
subgraph "File 1 Variants"
FILE1 --> V1TN[tn variant<br/>b1~abc123def456...<br/>AVIF, 4KB<br/>150×150px]
FILE1 --> V1SD[sd variant<br/>b1~def456ghi789...<br/>AVIF, 32KB<br/>640×480px]
FILE1 --> V1MD[md variant<br/>b1~ghi789jkl012...<br/>AVIF, 256KB<br/>1920×1080px]
end
subgraph "File 2 Variants"
FILE2 --> V2TN[tn variant<br/>b1~jkl012mno345...<br/>AVIF, 4KB<br/>150×150px]
FILE2 --> V2SD[sd variant<br/>b1~mno345pqr678...<br/>AVIF, 28KB<br/>640×480px]
FILE2 --> V2MD[md variant<br/>b1~pqr678stu901...<br/>AVIF, 248KB<br/>1920×1080px]
end
subgraph "File 3 Variants"
FILE3 --> V3TN[tn variant<br/>b1~stu901vwx234...<br/>AVIF, 4KB<br/>150×150px]
FILE3 --> V3SD[sd variant<br/>b1~vwx234yza567...<br/>AVIF, 35KB<br/>640×480px]
FILE3 --> V3MD[md variant<br/>b1~yza567bcd890...<br/>AVIF, 280KB<br/>1920×1080px]
FILE3 --> V3HD[hd variant<br/>b1~bcd890efg123...<br/>AVIF, 1.2MB<br/>3840×2160px]
end
subgraph "Hash Computation"
COMP1[Action ID = SHA256 of JWT token]
COMP2[File ID = SHA256 of descriptor string]
COMP3[Blob ID = SHA256 of blob bytes]
end
style LIKE fill:#ffcccc
style POST fill:#ccffcc
style FILE1 fill:#ccccff
style FILE2 fill:#ccccff
style FILE3 fill:#ccccff
style V1TN fill:#ffffcc
style V2TN fill:#ffffcc
style V3TN fill:#ffffcc
style V1SD fill:#ffeecc
style V2SD fill:#ffeecc
style V3SD fill:#ffeecc
style V1MD fill:#ffddcc
style V2MD fill:#ffddcc
style V3MD fill:#ffddcc
style V3HD fill:#ffcccc
Verification Steps
To verify this complete chain:
- Verify LIKE action signature and action_id
- Verify parent POST action (signature + action_id)
- Verify each file attachment (file_id = SHA256(descriptor))
- Verify all variants for each file (blob_id = SHA256(blob_data))
Complete verification example: see Content-Addressing & Merkle Trees.
Result: ✅
- Bob’s LIKE signature verified
- LIKE action_id verified
- Alice’s POST signature verified
- POST action_id verified
- All 3 file IDs verified
- All 10 blob IDs verified (3+3+4 variants)
- Complete merkle tree authenticated
Properties of This Structure
Immutability:
- Cannot change Bob’s reaction without changing LIKE action_id
- Cannot change Alice’s post content without changing POST action_id
- Cannot swap images without changing file_ids
- Cannot modify image bytes without changing blob_ids
Verifiability:
- Anyone can recompute all hashes
- No trusted third party needed
- Pure cryptographic proof of authenticity
Deduplication:
- If Alice uses the same image in another post, same file_id is reused
- If Bob also posts the same image, same blob_ids are reused
- Storage and bandwidth savings across the network
Federation:
- Remote instances can verify the complete chain
- Cannot tamper with any level without detection
- Trustless content distribution
See Also
- Content-Addressing & Merkle Trees - Complete details on the merkle tree structure
- File Storage & Processing - How file variants are generated and stored
- System Architecture - Task system and scheduler
- Identity System - Profile keys for action signing
- Access Control - Token validation and permissions
- Action Flow Implementation - Detailed implementation walkthrough