# Event Log

The **event log** is Effect's building block for local-first and offline-capable
applications. Instead of mutating shared state directly, your app defines a
catalog of typed **events** — durable facts like `TodoCreated` or `TodoCompleted`
— and writes them through a runtime that persists each one to a **journal** and
runs a handler that updates your projections. Because the journal is the source
of truth, the same events can be replayed locally on startup and synchronized to
a remote server so multiple devices converge on the same log.
**Unstable module:** The event log lives under `effect/unstable/eventlog`. Every const Todo = Schema.Struct({
  id: Schema.String,
  text: Schema.String
})

// A fact: a todo was created. The payload IS the persisted record.
const TodoCreated = Event.make({
  tag: "TodoCreated",
  primaryKey: (payload) => payload.id, // group all entries for this todo
  payload: Todo
})

// A fact: a todo was completed. Payload only needs the id.
const TodoCompleted = Event.make({
  tag: "TodoCompleted",
  primaryKey: (payload) => payload.id,
  payload: Schema.Struct({ id: Schema.String })
})

// A fact: a todo was deleted.
const TodoDeleted = Event.make({
  tag: "TodoDeleted",
  primaryKey: (payload) => payload.id,
  payload: Schema.Struct({ id: Schema.String })
})
```

Each event can also declare a `success` schema (the typed value its handler
returns to the writer) and an `error` schema (the typed failures a handler may
raise). When omitted, payload and success default to `Schema.Void` and error
defaults to `Schema.Never`.

<Aside type="caution" title="Keep tags and payload schemas stable">
Once entries are persisted, a tag and its payload schema are part of your
durable contract. Don't repurpose a tag or make a payload change that can't
decode historical entries — prefer a new tag or a backward-compatible schema.
Keep `primaryKey` deterministic so entries group consistently across local
journals and remote replicas.

## Group events

An `EventGroup` is the catalog of events for one domain. Start from
`EventGroup.empty` and chain `.add(...)`. You can define events inline in `.add`
(it takes the same options as `Event.make`) or build them separately and re-add
their options.

```ts
const Todo = Schema.Struct({ id: Schema.String, text: Schema.String })

const TodosGroup = EventGroup.empty
  .add({
    tag: "TodoCreated",
    primaryKey: (payload: { id: string }) => payload.id,
    payload: Todo
  })
  .add({
    tag: "TodoCompleted",
    primaryKey: (payload: { id: string }) => payload.id,
    payload: Schema.Struct({ id: Schema.String })
  })
  .add({
    tag: "TodoDeleted",
    primaryKey: (payload: { id: string }) => payload.id,
    payload: Schema.Struct({ id: Schema.String })
  })
```

Use `.addError(schema)` to attach a shared error to **every** event in the group
(handy for a `StoreFull` or `Forbidden` error common to the whole domain):

```ts
class StoreFull extends Schema.TaggedErrorClass<StoreFull>()("StoreFull", {}) {}

const GuardedTodos = TodosGroup.addError(StoreFull)
// every event's handler may now fail with StoreFull
```

## Build the schema and a client

`EventLog.schema(...groups)` combines one or more groups into an
`EventLogSchema`. `EventLog.makeClient(schema)` returns an Effect that yields a
typed write function. Calling `client(tag, payload)` is fully inferred: the
success type comes from the event's `success` schema and the error type is the
event's error union plus `EventJournalError`.

```ts
const TodosSchema = EventLog.schema(TodosGroup)

const program = Effect.gen(function* () {
  const client = yield* EventLog.makeClient(TodosSchema)

  // Effect<void, EventJournalError, never>  (success defaults to Schema.Void)
  yield* client("TodoCreated", { id: "1", text: "Buy milk" })
  yield* client("TodoCompleted", { id: "1" })
})
// program requires the EventLog service — provided by EventLog.layer below
```

The client is just a thin wrapper over the `EventLog` service's `write` method,
so it requires `EventLog` in context. That service is installed by a layer.

## Implement handlers

Handlers are where an event becomes an effect on your projection. Register them
with `EventLog.group(group, (handlers) => ...)`. The builder's `.handle` is
checked at the type level: **every event tag in the group must be handled**, or
the return type becomes a compile-time error string (`ValidateReturn`).

Each handler receives `{ storeId, payload, entry, conflicts }`:

- `storeId` — the logical store this write belongs to (see `CurrentStoreId`),
- `payload` — the decoded payload for this tag,
- `entry` — the journal `Entry` (has `id`, `idString`, `createdAt`, `primaryKey`),
- `conflicts` — for remote replay, other entries for the same primary key.

Because the entry commits **only if the handler succeeds**, the handler is the
right place to update a projection transactionally.

```ts
interface TodoView {
  readonly id: string
  readonly text: string
  readonly completed: boolean
}

// A simple in-memory projection keyed by todo id.
const makeTodosHandlers = (state: Ref.Ref<ReadonlyMap<string, TodoView>>) =>
  EventLog.group(TodosGroup, (handlers) =>
    handlers
      .handle(
        "TodoCreated",
        Effect.fn("TodoCreated")(function* ({ payload }) {
          yield* Ref.update(state, (map) =>
            new Map(map).set(payload.id, {
              id: payload.id,
              text: payload.text,
              completed: false
            })
          )
        })
      )
      .handle(
        "TodoCompleted",
        Effect.fn("TodoCompleted")(function* ({ payload }) {
          yield* Ref.update(state, (map) => {
            const next = new Map(map)
            const todo = next.get(payload.id)
            if (todo) next.set(payload.id, { ...todo, completed: true })
            return next
          })
        })
      )
      .handle(
        "TodoDeleted",
        Effect.fn("TodoDeleted")(function* ({ payload }) {
          yield* Ref.update(state, (map) => {
            const next = new Map(map)
            next.delete(payload.id)
            return next
          })
        })
      )
  )
```
**Commit-only-on-success:** If a handler fails, the journal does **not** commit the entry — there is no
durable fact and nothing to replicate. This makes the handler your validation
and projection boundary in one place: reject bad writes by failing, and the log
stays consistent with your projection.

## Wire up the runtime with layers

The runtime needs three things in its layer stack:

1. an **`EventJournal`** (start with `EventJournal.layerMemory`),
2. an **`Identity`** (a keypair that names this replica's log), and
3. the **handler layer** combined with the runtime via `EventLog.layer`.

`EventLog.layer(schema, handlerLayer)` provides `EventLog | Registry` and still
requires `EventJournal` and `Identity`. Generate an identity with
`EventLog.makeIdentity` (requires `EventLogEncryption`), or persist one as a
string with `encodeIdentityString` / `decodeIdentityString`.

```ts
// 1. The handler layer. We allocate the projection Ref inside the layer's
//    construction effect, so the handlers close over it. `Layer.unwrap`
//    turns an Effect-that-produces-a-Layer into a Layer.
const handlerLayer = Layer.unwrap(
  Effect.gen(function* () {
    const state = yield* Ref.make<ReadonlyMap<string, TodoView>>(new Map())
    return makeTodosHandlers(state)
  })
)

// 2. Combine the handler layer with the EventLog runtime for the schema.
//    => Layer<EventLog | Registry, never, EventJournal | Identity>
const RuntimeLayer = EventLog.layer(TodosSchema, handlerLayer)

// 3. Provide an Identity (generated here) and a journal underneath.
const IdentityLayer = Layer.effect(EventLog.Identity, EventLog.makeIdentity).pipe(
  Layer.provide(EventLogEncryption.layerSubtle)
)

const AppLayer = RuntimeLayer.pipe(
  Layer.provide(EventJournal.layerMemory),
  Layer.provide(IdentityLayer)
)
```

`CurrentStoreId` is a `Context.Reference` that defaults to the branded store id
`"default"`. Override it to run multiple logical stores against one journal.

Under `EventLog.layer` sit two lower-level pieces you rarely wire by hand:
`EventLog.layerEventLog` installs the `EventLog` service and `Registry` given a
journal and identity, and `EventLog.layerRegistry` provides just the in-memory
`Registry` that collects handlers, compactors, remotes, and reactivity keys.

## Adding remote sync (overview)

To synchronize a local log with a server, add an `EventLogRemote` to the layer
stack. `EventLogRemote.layerEncrypted` (the default for untrusted networks) and
`EventLogRemote.layerUnencrypted` (trusted transports / tests) each register a
remote with the `Registry`. Once registered, sync runs automatically for the
current `Identity` + `CurrentStoreId`: local writes are pushed and remote changes
are streamed in and replayed through your handlers.

```ts
// Encrypted remote — requires an RpcClient.Protocol layer for the transport.
const RemoteLayer = EventLogRemote.layerEncrypted
// const RemoteLayer = EventLogRemote.layerUnencrypted // plaintext alternative
```

Both layers require an `RpcClient.Protocol` (WebSocket, HTTP, worker, ...) plus
the `Registry` from the runtime. See the
[sync server guide](https://effect.plants.sh/event-log/sync-server/) for the server side and
[the reference](https://effect.plants.sh/event-log/reference/) for `makeEncrypted` / `makeUnencrypted`,
`whenAuthenticated`, and the encryption identity model.

## Reactivity and compaction (overview)

`EventLog.groupReactivity(group, keys)` registers
[reactivity](https://effect.plants.sh/reactivity/) keys to invalidate when events from the group are
written or replayed, so derived views recompute. Pass a single key list (applied
to every event) or a per-tag map.

```ts
const TodosReactivity = EventLog.groupReactivity(TodosGroup, ["todos"])
// invalidates the "todos" reactivity key on any todo event, by primary key
```

`EventLog.groupCompaction(group, effect)` collapses history during remote replay:
matching entries are grouped by primary key and handed to your effect, which can
write replacement entries. Use it to discard superseded facts (e.g. keep only the
latest state for a key) before they hit the journal. See the
[reference](https://effect.plants.sh/event-log/reference/) for the full signatures.

---

## The grab bag

The moves you reach for most when wiring an event log. Each links to the
[full reference](https://effect.plants.sh/event-log/reference/) for the complete signatures.

### Define an event and group it

```ts
// Inline in a group: tag + primaryKey + payload (success/error optional)
const Todos = EventGroup.empty.add({
  tag: "TodoCreated",
  primaryKey: (p: { id: string }) => p.id,
  payload: Schema.Struct({ id: Schema.String, text: Schema.String }),
  success: Schema.String // handler returns the new id
})
```

### Build a schema and a typed client

```ts
const schema = EventLog.schema(Todos)

Effect.gen(function* () {
  const write = yield* EventLog.makeClient(schema)
  const id = yield* write("TodoCreated", { id: "1", text: "Buy milk" })
  // => Effect<string, EventJournalError, EventLog>
})
```

### Start in memory, swap to IndexedDB in the browser

```ts
EventJournal.layerMemory // throwaway, lost when scope ends
EventJournal.layerIndexedDb({ database: "my_app" }) // local-first web app
```

### Generate and persist an identity

```ts
// EventLog.makeIdentity : Effect<Identity, never, EventLogEncryption>
const str = EventLog.encodeIdentityString(identity) // => "eyJ..." (base64url)
const back = EventLog.decodeIdentityString(str) // restore from localStorage
```

### Invalidate reactivity keys when events are written

```ts
EventLog.groupReactivity(Todos, ["todos"]) // any todo event -> "todos"
EventLog.groupReactivity(Todos, { TodoCreated: ["todos", "counts"] })
```

### Partition stores in one journal

```ts
// CurrentStoreId is a Context.Reference defaulting to StoreId.make("default")
Effect.provideService(program, EventLog.CurrentStoreId, StoreId.make("ws-42"))
```

## In this section

- **[Reference](https://effect.plants.sh/event-log/reference/)** — the exhaustive client-side API across
  every module: `Event`, `EventGroup`, `EventLog`, `EventJournal` (in-memory,
  IndexedDB, and SQL), `EventLogRemote`, and `EventLogEncryption`, with all type
  helpers, identity APIs, and protocol types.
- **[Sync server](https://effect.plants.sh/event-log/sync-server/)** — stand up an `EventLogServer` so
  replicas synchronize: session authentication, encrypted and unencrypted
  storage, server-side compaction/projection, and SQL-backed persistence.