decentrl.
Event Store

Event Processors

Client-side reducers that transform encrypted event streams into application state.

Event Sourcing

Decentrl applications are pure event processors. There is no backend database, no server-side state, no REST API returning "current data." Instead:

  1. Events are the source of truth — stored encrypted on your mediator
  2. Application state is a derived projection — built locally by applying events through reducers
  3. Reducers are pure functions — given the same events, they always produce the same state

This is the same pattern used by databases (write-ahead logs), distributed systems (event sourcing), and state management libraries (Redux). Decentrl applies it to encrypted, decentralized communication.

Defining a Chat Application

A chat application defines event types and how they reduce into state:

Application: ChatApp

Events:
  chat.create    → { id, participants, createdBy, createdAt }
  chat.message   → { id, chatId, content, sender, timestamp }
  chat.reaction  → { id, messageId, chatId, emoji, sender, removed? }
  chat.presence  → { timestamp }                            // ephemeral
  chat.read      → { chatId, timestamp }

State slices:
  chats       → reduced from chat.create (append if not duplicate)
  messages    → reduced from chat.message (grouped by chatId)
  reactions   → reduced from chat.reaction (add/remove by sender+emoji)
  lastSeen    → reduced from chat.presence (latest timestamp per DID)
  readMarkers → reduced from chat.read (max timestamp per chatId)

Idempotent Reducers

Because events may arrive out of order or be replayed during state reconstruction, all reducers must be idempotent. The same event applied twice should produce the same result:

reduce chat.message:
  if messages[chatId] already contains event.id → return unchanged
  else → append event to messages[chatId]

reduce chat.reaction:
  if event.removed → filter out matching (sender, emoji)
  if reactions[messageId] already contains (sender, emoji) → return unchanged
  else → append reaction

reduce chat.read:
  readMarkers[chatId] = max(existing, event.timestamp)   // naturally idempotent

Duplicate detection by event ID handles messages arriving twice. Using max() for timestamps naturally handles out-of-order delivery.

Beyond Chat

The same pattern works for any application:

Social network:

post.create     → builds timeline
post.reaction   → aggregates likes/reactions
follow.create   → builds social graph
follow.remove   → updates social graph

Collaborative document:

doc.create      → initializes document
doc.edit        → applies operation (CRDT or OT)
doc.comment     → builds comment thread

Marketplace:

listing.create  → adds product listing
listing.update  → modifies price/availability
order.create    → initiates transaction
order.status    → tracks fulfillment

The protocol doesn't care what your events represent. It handles identity, encryption, routing, and storage. Your application defines the schema and reducers — everything else is automatic.