decentrl.
Event Store

Publishing Events

Events are the atomic unit of communication in Decentrl — signed, encrypted, and dual-delivered.

Formal spec: DCTRL-0005 — Event Encryption and Storage defines event envelopes, dual delivery, encrypted tags, and the forward secrecy trade-off.

All communication and state changes in Decentrl are represented as cryptographically signed, encrypted events. Applications function as event processors: they publish events, query event streams, and reduce them into local application state.

Anatomy of an Event

An event has a type and a data payload:

{
  type: "chat.message",
  data: {
    id: "550e8400-e29b-41d4-a716-446655440000",
    chatId: "abc",
    content: "Hello Bob",
    sender: "did:decentrl:YWxpY2U:z6MkpT...",
    timestamp: 1699123456
  }
}

When published, the event is wrapped in a signed envelope, encrypted, and dual-delivered — one copy to the recipient's mediator, one copy to the sender's own mediator.

Event Flow: Alice Sends a Message

1. Alice publishes a chat.message event:
   - Encrypt with contract root secret → send to Bob's mediator (TWO_WAY_PRIVATE)
   - Encrypt with storage key → save on Alice's mediator with encrypted tags

2. Alice's local state updates immediately (optimistic update):
   - messages["abc"] = [...messages["abc"], newMessage]

3. Bob's client syncs (poll or WebSocket push):
   - Decrypt with root secret → verify signature → re-encrypt with storage key → store
   - Apply to local state: messages["abc"] = [...messages["abc"], newMessage]

Ephemeral Events

Some events don't need to be stored. Presence indicators, typing signals, and online status are transient — they matter right now but not later.

Events tagged with no tags (tags: []) are sent via TWO_WAY_PRIVATE for real-time delivery but are not stored on the sender's mediator. They exist only in transit and in the recipient's pending event queue — once acknowledged, they vanish.

chat.presence → tags: []   // ephemeral — not stored, just delivered
chat.message  → tags: ["chat.abc"]  // persistent — stored and queryable

Defining Events with the SDK

The SDK lets you define your event schema declaratively with Zod:

import { defineDecentrlApp } from '@decentrl/sdk'
import { z } from 'zod'

const app = defineDecentrlApp({
  events: {
    'chat.message': {
      schema: z.object({
        id: z.string().uuid(),
        chatId: z.string(),
        content: z.string(),
      }),
      tags: ['chat.${chatId}'],
    },
    'chat.reaction': {
      schema: z.object({
        messageId: z.string().uuid(),
        emoji: z.string(),
      }),
      tags: ['chat.${messageId}'],
    },
    'chat.presence': {
      schema: z.object({ timestamp: z.number() }),
      tags: [],   // empty tags = ephemeral
    },
  },
  state: { /* ... reducers ... */ },
})

Publishing is a single method call — the SDK handles signing, encryption, and dual-delivery:

const client = app.createClient({ mediatorDid: 'did:web:mediator.decentrl.io' })

await client.publish('chat.message', {
  id: crypto.randomUUID(),
  chatId: 'abc',
  content: 'Hello Bob',
})