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:

  1. Blob Data → hashed to create Variant IDs (b1~...)
  2. File Descriptor → hashed to create File IDs (f1~...)
  3. Action Token JWT → hashed to create Action IDs (a1~...)
  4. Parent References → create immutable chains between actions
  5. Attachment References → bind files to actions cryptographically
  6. 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_id is 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_id by 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 root

Database 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:

  1. 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"
}
  1. Task Scheduling

Server creates a task and waits for dependencies (e.g., file attachments to be processed).

  1. 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)
  1. 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 client

Action Verification Pipeline

Federated Action Reception

When a remote instance sends an action (federation), it arrives at /api/inbox:

  1. Inbound Request
POST /api/inbox
Content-Type: application/json

{
  "token": "eyJhbGc..."
}
  1. 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 locally

Complete 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:

  1. Signature Verification: Verify the action was created by the claimed author using their public key
  2. Action ID Verification: Recompute action_id and verify it matches (proves no tampering)
  3. Parent Chain Verification: Recursively verify parent actions exist and are valid
  4. 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:

  1. Signature: Proves the issuer created the action
  2. Key Ownership: Public key fetched from issuer’s /api/me/keys
  3. Expiration: Optional exp claim prevents token replay
  4. Audience: Optional aud claim ensures intended recipient

Spam Prevention

Multiple mechanisms prevent spam:

  1. Relationship Requirements: Only receive actions from connected/followed users
  2. Rate Limiting: Limit actions per user per time period
  3. Proof of Work: (Future) Optional PoW for anonymous actions
  4. 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:

  1. Verify LIKE action signature and action_id
  2. Verify parent POST action (signature + action_id)
  3. Verify each file attachment (file_id = SHA256(descriptor))
  4. 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