# Event Log Sync Server

**Unstable:** The event-log modules live under `effect/unstable/eventlog/...`. Unstable APIs
ship with `effect` but may change between minor releases. Pin your version and
read the [changelog](https://github.com/Effect-TS/effect) before upgrading.

The sync **server** is the counterpart to the [`EventLogRemote`](https://effect.plants.sh/event-log/)
client. A replica (browser, mobile app, or another service) holds a local
journal and periodically pushes its entries to a server and pulls back changes
made by other replicas. The server is what those replicas sync against: it
authenticates each replica, stores its entries, and streams changes back.

Everything the server does runs over the `EventLogRemoteRpcs` RPC protocol on
top of an [`RpcServer.Protocol`](https://effect.plants.sh/rpc/) transport (WebSocket, HTTP, stdio,
worker, ...). You bring the transport and a **storage** service; the event-log
layers wire the handshake, authentication, write, and change-streaming handlers.

## Overview

There are two server flavours, and the choice is a privacy/feature tradeoff:

- **Encrypted** (`EventLogServerEncrypted`) — the server stores opaque
  ciphertext, initialization vectors, entry ids, and sequence numbers. It never
  sees plaintext events and holds no encryption keys. This is the
  privacy-preserving, offline-first, multi-device replication case. The server
  cannot read, project, or compact events.
- **Unencrypted** (`EventLogServerUnencrypted`) — the server stores plaintext
  entries, runs your registered event handlers, detects conflicts, and can
  **compact** the backlog before replaying it. Use this for trusted
  deployments, local development, tests, and server-side event producers. The
  payloads are plaintext, so protect the store and transport at the
  infrastructure level.

Both flavours need the same two ingredients:

1. An `RpcServer.Protocol` transport layer (see the [RPC docs](https://effect.plants.sh/rpc/)).
2. A `Storage` service (in-memory for tests, SQL for production).

Authentication is identical for both: a replica calls `Hello` to receive a
short-lived random challenge, then `Authenticate` to bind its event-log public
key to an **Ed25519 signing key** by signing the challenge. The server persists
one signing-key binding per public key (so a key cannot silently rebind to a
different signing key), and `layerAuthMiddleware` rejects any later request
without an authenticated identity. The client `EventLogRemote` performs this
handshake automatically; the [`EventLogSessionAuth`](#session-authentication-eventlogsessionauth)
primitives are only needed for custom transports.
**Note:** Session bindings are part of the trust boundary. Durable storage **must**
preserve them across restarts (the SQL and built-in memory stores do), and you
should run the endpoint over TLS.

## Encrypted server (common, privacy-preserving)

This is the typical setup for syncing untrusted clients. Wire three layers:
`EventLogServerEncrypted.layer` (installs the RPCs + handlers), a `Storage`
layer, and an `RpcServer.Protocol` transport.

```ts
import { Layer } from "effect";
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";
import { EventLogRemoteRpcs } from "effect/unstable/eventlog/EventLogMessage";
import * as RpcServer from "effect/unstable/rpc/RpcServer";
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";

// 1. Storage: in-memory for tests, SQL for production (see below).
const StorageLive = EventLogServerEncrypted.layerStorageMemory;

// 2. Transport: any RpcServer.Protocol. Here, a WebSocket endpoint.
const ProtocolLive = RpcServer.layerProtocolWebsocket({ path: "/sync" }).pipe(
  Layer.provide(RpcSerialization.layerMsgPack)
);

// 3. The encrypted server. `layer` installs EventLogRemoteRpcs on the protocol
//    and wires the encrypted RPC handlers. It requires Protocol + Storage.
const ServerLive = EventLogServerEncrypted.layer.pipe(
  Layer.provide([StorageLive, ProtocolLive])
);
// ServerLive: Layer<never, never, HttpRouter>
//   (RpcSerialization is already supplied to the protocol above; the remaining
//    HttpRouter requirement comes from the WebSocket protocol transport)
```

The server stores ciphertext keyed by `publicKey / storeId`; clients decrypt
locally with their identity private key. The server can never read events, so
there is no event registry, no handlers, and no compaction.

### `EventLogServerEncrypted.layer`

Installs `EventLogRemoteRpcs` on the provided `RpcServer.Protocol` and wires the
encrypted handlers. Requires `RpcServer.Protocol | Storage`.

```ts
import { Layer } from "effect";
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";

const ServerLive = EventLogServerEncrypted.layer.pipe(
  Layer.provide(EventLogServerEncrypted.layerStorageMemory)
);
// => still needs RpcServer.Protocol from the transport layer
```

### `EventLogServerEncrypted.layerRpcHandlers`

The handler layer **without** installing an `RpcServer.Protocol`. Use it when
the protocol is installed elsewhere (e.g. you already run an `RpcServer` for
other groups). Provides the RPC handlers + `EventLogAuthentication`; requires
`Storage`.

```ts
import { Layer } from "effect";
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";
import { EventLogRemoteRpcs } from "effect/unstable/eventlog/EventLogMessage";
import * as RpcServer from "effect/unstable/rpc/RpcServer";

// Equivalent to `layer`, spelled out:
const ServerLive = RpcServer.layer(EventLogRemoteRpcs).pipe(
  Layer.provide(EventLogServerEncrypted.layerRpcHandlers),
  Layer.provide(EventLogServerEncrypted.layerStorageMemory)
);
```

### `EventLogServerEncrypted.Storage`

The `Context.Service` defining durable encrypted persistence. It provides the
server remote id, session-auth bindings, encrypted writes, and an encrypted
change stream keyed by `publicKey` + `storeId`.

```ts
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";

// Shape (read-only reference):
//   getId: Effect<RemoteId>
//   getOrCreateSessionAuthBinding(publicKey, signingPublicKey): Effect<Uint8Array>
//   write(publicKey, storeId, entries: PersistedEntry[]): Effect<EncryptedRemoteEntry[]>
//   changes(publicKey, storeId, startSequence): Stream<EncryptedRemoteEntry>
EventLogServerEncrypted.Storage;
```

### `EventLogServerEncrypted.makeStorageMemory` / `layerStorageMemory`

Process-local, volatile in-memory storage. `makeStorageMemory` is the scoped
effect; `layerStorageMemory` is the ready-made layer. Useful for tests and
examples, not for multi-process or restart-safe servers.

```ts
import { Effect, Layer } from "effect";
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";

const storage = EventLogServerEncrypted.makeStorageMemory; // => Effect<Storage["Service"], never, Scope>
const StorageLive = EventLogServerEncrypted.layerStorageMemory; // => Layer<Storage>
```

### `EventLogServerEncrypted.PersistedEntry`

`Schema.Class` for the rows the encrypted store persists: an `entryId`, the
AES-GCM `iv`, and the `encryptedEntry` ciphertext. Its `entryIdString` getter
stringifies the UUID entry id.

```ts
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";

declare const entry: EventLogServerEncrypted.PersistedEntry;
entry.entryIdString; // => "1b4e28ba-2fa1-11d2-883f-0016d3cca427"
```

## Unencrypted server (server-side compaction & projection)

When the server is trusted, the unencrypted server unlocks typed event handling,
conflict detection, and **backlog compaction**. You must additionally provide a
`StoreMapping` (which client store ids are allowed), an
`EventLogServerAuthorization` (read/write/identity gating), and an event-group
handler layer.

```ts
import { Layer } from "effect";
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
import { StoreId } from "effect/unstable/eventlog/EventLogMessage";
import * as RpcServer from "effect/unstable/rpc/RpcServer";
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";

declare const schema: any; // your EventLog.EventLogSchema<Groups>
declare const HandlersLive: Layer.Layer<any>; // EventGroup.ToService<Groups>
declare const AuthLive: Layer.Layer<EventLogServerUnencrypted.EventLogServerAuthorization>;

const ServerLive = EventLogServerUnencrypted.layer(schema, HandlersLive).pipe(
  Layer.provide([
    EventLogServerUnencrypted.layerStorageMemory,
    EventLogServerUnencrypted.layerStoreMappingStatic({
      storeId: StoreId.make("default")
    }),
    AuthLive,
    RpcServer.layerProtocolWebsocket({ path: "/sync" }).pipe(
      Layer.provide(RpcSerialization.layerMsgPack)
    )
  ])
);
```

On each incoming batch the server resolves the store via `StoreMapping`,
authorizes the write, sorts entries, drops duplicates, computes conflicts, runs
your registered handlers inside the storage transaction, then commits. Change
streams replay the requested backlog (running registered compactors where they
apply) and continue with live entries.

### `EventLogServerUnencrypted.layer`

Builds a complete unencrypted RPC server for a schema and its handler layer.
Installs `EventLogRemoteRpcs`, the handlers, and `layerServer`, leaving only the
infrastructure services (`Storage`, `StoreMapping`, `EventLogServerAuthorization`,
`RpcServer.Protocol`) to provide.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

declare const schema: any;
declare const HandlersLive: any;
const ServerLive = EventLogServerUnencrypted.layer(schema, HandlersLive);
// => Layer<never, E, EventLogServerAuthorization | RpcServer.Protocol | Storage | StoreMapping>
```

### `EventLogServerUnencrypted.layerNoRpcServer`

Same handlers as `layer` but **without** installing an `RpcServer.Protocol`.
Provides the RPC handlers + `EventLogAuthentication`; use when the protocol is
mounted elsewhere.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

declare const schema: any;
declare const HandlersLive: any;
const HandlersOnly = EventLogServerUnencrypted.layerNoRpcServer(schema, HandlersLive);
// => Layer<RpcHandlers | EventLogAuthentication, E,
//           EventLogServerAuthorization | Storage | StoreMapping>
```

### `EventLogServerUnencrypted.layerRpcHandlers`

The lowest-level handler layer (no `layerServer`, no protocol). Provides the RPC
handlers + `EventLogAuthentication`; requires
`Storage | StoreMapping | EventLogServerAuthorization | EventLog.Registry`.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

EventLogServerUnencrypted.layerRpcHandlers;
// => Layer<RpcHandlers | EventLogAuthentication, never,
//           Storage | StoreMapping | EventLogServerAuthorization | EventLog.Registry>
```

### `EventLogServerUnencrypted.layerServer`

Provides the `EventLogServerUnencrypted` service and an event-log `Registry`
from a configured `Storage`, without any RPC wiring. Use this when you want
server-side writes (`makeWrite`) but not a remote endpoint.

```ts
import { Layer } from "effect";
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

const ServiceLive = EventLogServerUnencrypted.layerServer.pipe(
  Layer.provide(EventLogServerUnencrypted.layerStorageMemory)
);
// => Layer<EventLogServerUnencrypted | EventLog.Registry, never, never>
```

### `EventLogServerUnencrypted.EventLogServerUnencrypted` (service)

The `Context.Service` that produces typed server-side writers. Its single method
`makeWrite(schema)` returns a write function for the events in that schema.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

EventLogServerUnencrypted.EventLogServerUnencrypted; // the service tag
```

### `EventLogServerUnencrypted.makeWrite`

Accessor that pulls the typed server-side write function out of the service
environment. Each write encodes the payload with the event schema, runs the
registered handler, and persists the entry inside a storage transaction. It
fails with `EventLogServerStoreError` (or your event's typed error) and **dies**
if the event tag is not in the schema.

```ts
import { Effect } from "effect";
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
import { StoreId } from "effect/unstable/eventlog/EventLogMessage";

declare const schema: any; // EventLog.EventLogSchema<Groups>

const program = Effect.gen(function* () {
  const write = yield* EventLogServerUnencrypted.makeWrite(schema);
  yield* write({
    storeId: StoreId.make("default"),
    event: "TodoCreated",
    payload: { id: "t1", text: "Buy milk" }
  });
});
// requires EventLogServerUnencrypted in the environment
```

### `EventLogServerUnencrypted.make`

The constructor effect behind `layerServer`. Builds the
`EventLogServerUnencrypted` service from a provided `Storage` and an event-log
`Registry`. Prefer `layerServer` unless you supply the service yourself.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

EventLogServerUnencrypted.make;
// => Effect<EventLogServerUnencrypted["Service"], never, Storage | EventLog.Registry>
```

### `EventLogServerUnencrypted.Storage`

The `Context.Service` for durable plaintext persistence. It exposes the server
remote id, session-auth bindings, `entriesAfter`, `write`, a compacting
`changes` stream, and a `withTransaction` boundary.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

// Shape (read-only reference):
//   getId: Effect<RemoteId>
//   getOrCreateSessionAuthBinding(publicKey, signingPublicKey): Effect<Uint8Array>
//   entriesAfter(storeId, entry): Effect<Entry[]>
//   write(storeId, entries): Effect<RemoteEntry[]>
//   changes({ storeId, startSequence, compactors }): Stream<RemoteEntry>
//   withTransaction(effect): Effect<...>
EventLogServerUnencrypted.Storage;
```

### `EventLogServerUnencrypted.makeStorageMemory` / `layerStorageMemory`

Process-local in-memory `Storage` (per-store journals, semaphore-serialized
transactions, live `PubSub` changes). For tests and examples only.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

EventLogServerUnencrypted.makeStorageMemory; // => Effect<Storage["Service"], never, Scope>
EventLogServerUnencrypted.layerStorageMemory; // => Layer<Storage>
```

### `EventLogServerUnencrypted.StoreMapping` + `layerStoreMappingStatic`

`StoreMapping` is the service that resolves a client-requested store id to a
server store id (and checks `hasStore`). `layerStoreMappingStatic` is the
trivial implementation that accepts exactly one configured store id and rejects
everything else as `NotFound`.

```ts
import { Effect } from "effect";
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
import { StoreId } from "effect/unstable/eventlog/EventLogMessage";

const MappingLive = EventLogServerUnencrypted.layerStoreMappingStatic({
  storeId: StoreId.make("default")
});

// resolve succeeds for the configured id and fails (NotFound) otherwise:
const program = Effect.gen(function* () {
  const mapping = yield* EventLogServerUnencrypted.StoreMapping;
  yield* mapping.resolve({ publicKey: "pk", storeId: StoreId.make("default") });
  // => StoreId("default")
}).pipe(Effect.provide(MappingLive));
```

### `EventLogServerUnencrypted.EventLogServerAuthorization`

The `Context.Service` gating plaintext writes, reads, and identities. Each
method returns `Effect<void, EventLogServerAuthError>`. There is no built-in
layer — provide your own policy.

```ts
import { Effect, Layer } from "effect";
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

const AuthLive = Layer.succeed(
  EventLogServerUnencrypted.EventLogServerAuthorization,
  {
    authorizeWrite: () => Effect.void, // allow all (replace with your policy)
    authorizeRead: () => Effect.void,
    authorizeIdentity: () => Effect.void
  }
);
```

### `EventLogServerUnencrypted.compactBacklog`

Runs the registered compactors over a backlog of `RemoteEntry` values before
replay. Contiguous entries handled by the same compactor may be collapsed into
fewer entries when the result maps back to increasing remote sequence numbers;
otherwise the originals are kept. The built-in `changes` streams call this for
you — use it directly only in a custom `Storage`.

```ts
import { Effect } from "effect";
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

declare const remoteEntries: ReadonlyArray<any>; // RemoteEntry[]
const program = EventLogServerUnencrypted.compactBacklog({
  remoteEntries,
  compactors: new Map() // empty map => entries returned unchanged
});
// => Effect<readonly RemoteEntry[]>
```

### `EventLogServerUnencrypted.EventLogServerStoreError`

`Data.TaggedError` raised by storage and store-mapping operations. `reason` is
`"NotFound" | "PersistenceFailure"`, with optional `publicKey`, `storeId`, and
`message`.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

new EventLogServerUnencrypted.EventLogServerStoreError({
  reason: "NotFound",
  message: "No provisioned store"
});
// => EventLogServerStoreError { _tag: "EventLogServerStoreError", reason: "NotFound", ... }
```

### `EventLogServerUnencrypted.EventLogServerAuthError`

`Data.TaggedError` raised when authorization rejects an identity or operation.
`reason` is `"Unauthorized" | "Forbidden"`, with a required `publicKey` and
optional `storeId` / `message`.

```ts
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";

new EventLogServerUnencrypted.EventLogServerAuthError({
  reason: "Forbidden",
  publicKey: "pk"
});
// => EventLogServerAuthError { _tag: "EventLogServerAuthError", reason: "Forbidden", ... }
```

## SQL-backed storage

For restart-safe, replicated, transactionally-ordered persistence, swap the
memory `Storage` for a SQL implementation. Both flavours expose `makeStorage`
(the constructor effect) and `layerStorage` (the layer), requiring a
[`SqlClient`](https://effect.plants.sh/sql/) layer. Tables are created for you in a dialect-specific way
(pg, mysql, mssql, sqlite/orElse).

Shared options for both: `entryTablePrefix` (default `"effect_events"`),
`remoteIdTable` (default `"effect_remote_id"`), and `insertBatchSize`
(default `200`).
**Caution:** Live change notifications are **process-local** after the initial SQL backlog
read. Multi-process deployments need request routing, reconnect/backfill
behavior, or an external notification channel for writes made on other nodes.

### `SqlEventLogServerUnencrypted.layerStorage` / `makeStorage`

Plaintext SQL storage for `EventLogServerUnencrypted`. Creates tables for the
remote id, per-store sequence state, plaintext entries, and session-auth
bindings. Requires `SqlClient`.

```ts
import { Layer } from "effect";
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
import * as SqlEventLogServerUnencrypted from "effect/unstable/eventlog/SqlEventLogServerUnencrypted";

declare const SqlLive: Layer.Layer<any>; // a SqlClient layer, e.g. PgClient.layer({...})

const StorageLive = SqlEventLogServerUnencrypted.layerStorage({
  entryTablePrefix: "myapp_events"
}).pipe(Layer.provide(SqlLive));
// => Layer<EventLogServerUnencrypted.Storage, SqlError, never>

// makeStorage is the underlying effect:
SqlEventLogServerUnencrypted.makeStorage();
// => Effect<Storage["Service"], SqlError, SqlClient | Scope>
```

### `SqlEventLogServerEncrypted.layerStorage` / `makeStorage` / `layerStorageSubtle`

Encrypted SQL storage for `EventLogServerEncrypted`. Persists the remote id,
session-auth bindings, and per-`publicKey`/`storeId` encrypted entry tables
(named from a hash of the scope key). `makeStorage` / `layerStorage` additionally
require an `EventLogEncryption` service (used to hash scope keys);
`layerStorageSubtle` supplies the default Web Crypto encryption layer for you.

```ts
import { Layer } from "effect";
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";
import * as SqlEventLogServerEncrypted from "effect/unstable/eventlog/SqlEventLogServerEncrypted";

declare const SqlLive: Layer.Layer<any>; // a SqlClient layer

// Simplest: layerStorageSubtle bundles the Web Crypto encryption layer.
const StorageLive = SqlEventLogServerEncrypted.layerStorageSubtle({
  entryTablePrefix: "myapp_events"
}).pipe(Layer.provide(SqlLive));
// => Layer<EventLogServerEncrypted.Storage, SqlError, never> (SqlClient provided)

// layerStorage requires EventLogEncryption to be provided separately:
SqlEventLogServerEncrypted.layerStorage();
// => Layer<Storage, SqlError, SqlClient | EventLogEncryption>

// makeStorage is the underlying effect:
SqlEventLogServerEncrypted.makeStorage();
// => Effect<Storage["Service"], SqlError, SqlClient | EventLogEncryption | Scope>
```

Wiring a full encrypted SQL server:

```ts
import { Layer } from "effect";
import * as EventLogServerEncrypted from "effect/unstable/eventlog/EventLogServerEncrypted";
import * as SqlEventLogServerEncrypted from "effect/unstable/eventlog/SqlEventLogServerEncrypted";
import * as RpcServer from "effect/unstable/rpc/RpcServer";
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";

declare const SqlLive: Layer.Layer<any>;

const ServerLive = EventLogServerEncrypted.layer.pipe(
  Layer.provide([
    SqlEventLogServerEncrypted.layerStorageSubtle().pipe(Layer.provide(SqlLive)),
    RpcServer.layerProtocolWebsocket({ path: "/sync" }).pipe(
      Layer.provide(RpcSerialization.layerMsgPack)
    )
  ])
);
```

## Shared server pieces (`EventLogServer`)

Both flavours build on the shared `EventLogServer` module, which implements the
handshake, chunked-write reassembly, and change framing. You normally consume it
indirectly, but the pieces are public.

### `EventLogServer.layerRpcHandlers`

The generic handler factory both flavours call. It takes a `remoteId`, a
`getOrCreateSessionAuthBinding` callback, an `onWrite` callback (receives
reassembled payload bytes), and a `changes` callback (returns a stream of
encoded payload bytes). It manages hello challenges, verifies authentication,
reassembles chunked writes, and frames large change payloads into chunks.

```ts
import * as EventLogServer from "effect/unstable/eventlog/EventLogServer";

EventLogServer.layerRpcHandlers({
  remoteId: undefined as any, // RemoteId from storage.getId
  getOrCreateSessionAuthBinding: (_publicKey, signingPublicKey) =>
    undefined as any, // Effect<Uint8Array>
  onWrite: (_data) => undefined as any, // Effect<void, EventLogProtocolError>
  changes: (_opts) => undefined as any // Stream<Uint8Array, unknown>
});
// => Layer<RpcHandlers | EventLogAuthentication>
```

### `EventLogServer.layerAuthMiddleware`

Provides the `EventLogAuthentication` middleware: it reads the authenticated
`EventLog.Identity` from the client annotations and rejects requests without one
with a `Forbidden` `EventLogProtocolError`. Merged into `layerRpcHandlers`
automatically.

```ts
import * as EventLogServer from "effect/unstable/eventlog/EventLogServer";

EventLogServer.layerAuthMiddleware; // => Layer<EventLogAuthentication>
```

### `EventLogServer.ChunkedMessageState`

A `Context.Reference` holding per-client partial-chunk assembly state (a `Map`
keyed by chunk message id). Populated during `Authenticate` and used by
`WriteChunked` to reassemble large writes. Default value is an empty `Map`.

```ts
import * as EventLogServer from "effect/unstable/eventlog/EventLogServer";

EventLogServer.ChunkedMessageState; // Context.Reference<Map<number, {...}>>
```

## Session authentication (`EventLogSessionAuth`)

These are the low-level Ed25519 challenge-response primitives behind the
handshake. The client `EventLogRemote` and the server handlers use them
automatically — reach for them directly only when implementing a custom
transport or testing the protocol.

The signed payload is a deterministic, length-prefixed canonical byte sequence
binding the remote id, the challenge, the event-log public key, and the signing
public key, prefixed with the `AuthPayloadContext` domain separator.

### `EventLogSessionAuth.makeSessionAuthChallenge`

Generates a fresh random challenge (`SessionAuthChallengeLength` bytes) via
`globalThis.crypto`.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

EventLogSessionAuth.makeSessionAuthChallenge;
// => Effect<Uint8Array, EventLogSessionAuthError>  (32 random bytes)
```

### `EventLogSessionAuth.encodeSessionAuthPayload`

Encodes a `SessionAuthPayload` (`remoteId`, `challenge`, `publicKey`,
`signingPublicKey`) into the canonical byte format. Validates the signing public
key length.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

EventLogSessionAuth.encodeSessionAuthPayload({
  remoteId: "remote-1",
  challenge: new Uint8Array(32),
  publicKey: "pk",
  signingPublicKey: new Uint8Array(32)
});
// => Effect<Uint8Array, EventLogSessionAuthError>
```

### `EventLogSessionAuth.decodeSessionAuthPayload`

Inverse of `encodeSessionAuthPayload`. Validates the context field, UTF-8
fields, signing-key length, and rejects truncated or trailing bytes.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

declare const bytes: Uint8Array;
EventLogSessionAuth.decodeSessionAuthPayload(bytes);
// => Effect<SessionAuthPayload, EventLogSessionAuthError>
```

### `EventLogSessionAuth.signSessionAuthPayloadBytes`

Signs already-encoded canonical payload bytes with a PKCS#8 Ed25519 private key.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

declare const payload: Uint8Array;
declare const signingPrivateKey: Uint8Array; // PKCS#8 bytes
EventLogSessionAuth.signSessionAuthPayloadBytes({ payload, signingPrivateKey });
// => Effect<Uint8Array, EventLogSessionAuthError>  (64-byte signature)
```

### `EventLogSessionAuth.signSessionAuthPayload`

Convenience: encodes a `SessionAuthPayload` and signs it in one step.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

declare const signingPrivateKey: Uint8Array;
EventLogSessionAuth.signSessionAuthPayload({
  remoteId: "remote-1",
  challenge: new Uint8Array(32),
  publicKey: "pk",
  signingPublicKey: new Uint8Array(32),
  signingPrivateKey
});
// => Effect<Uint8Array, EventLogSessionAuthError>
```

### `EventLogSessionAuth.verifySessionAuthPayloadBytes`

Verifies an Ed25519 signature over canonical payload bytes. Validates payload,
signing-key, and signature lengths first.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

declare const payload: Uint8Array;
declare const signingPublicKey: Uint8Array;
declare const signature: Uint8Array;
EventLogSessionAuth.verifySessionAuthPayloadBytes({ payload, signingPublicKey, signature });
// => Effect<boolean, EventLogSessionAuthError>
```

### `EventLogSessionAuth.verifySessionAuthPayload`

Convenience: encodes a `SessionAuthPayload` and verifies the supplied signature.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

declare const signature: Uint8Array;
EventLogSessionAuth.verifySessionAuthPayload({
  remoteId: "remote-1",
  challenge: new Uint8Array(32),
  publicKey: "pk",
  signingPublicKey: new Uint8Array(32),
  signature
});
// => Effect<boolean, EventLogSessionAuthError>
```

### `EventLogSessionAuth.verifySessionAuthenticateRequest`

The exact check the server performs on `Authenticate`: requires `algorithm` to
be `"Ed25519"`, then verifies the signature over the canonical payload.

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

declare const signature: Uint8Array;
EventLogSessionAuth.verifySessionAuthenticateRequest({
  remoteId: "remote-1",
  challenge: new Uint8Array(32),
  publicKey: "pk",
  signingPublicKey: new Uint8Array(32),
  signature,
  algorithm: "Ed25519"
});
// => Effect<boolean, EventLogSessionAuthError>
```

### `EventLogSessionAuth.SessionAuthPayload`

The interface for the fields that are canonicalized and signed: `remoteId`,
`challenge`, `publicKey`, `signingPublicKey`.

### `EventLogSessionAuth.EventLogSessionAuthError`

`Data.TaggedError` for encode/decode/sign/verify/challenge failures. `reason` is
one of `"InvalidPayload" | "InvalidContext" | "InvalidAlgorithm" |
"InvalidSigningPublicKeyLength" | "InvalidSignatureLength" |
"InvalidSigningPrivateKey" | "CryptoUnavailable" | "CryptoFailure"`.

### Constants

| Constant | Value | Meaning |
| --- | --- | --- |
| `AuthPayloadContext` | `"eventlog-auth-v1"` | Domain-separation string embedded in the canonical payload. |
| `Ed25519PublicKeyLength` | `32` | Required byte length of raw Ed25519 public keys. |
| `Ed25519SignatureLength` | `64` | Required byte length of Ed25519 signatures. |
| `SessionAuthChallengeLength` | `32` | Number of random bytes in a challenge. |
| `SessionAuthChallengeTimeToLiveMillis` | `30_000` | TTL (ms) for a pending challenge. |

```ts
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";

EventLogSessionAuth.AuthPayloadContext; // => "eventlog-auth-v1"
EventLogSessionAuth.Ed25519PublicKeyLength; // => 32
EventLogSessionAuth.Ed25519SignatureLength; // => 64
EventLogSessionAuth.SessionAuthChallengeLength; // => 32
EventLogSessionAuth.SessionAuthChallengeTimeToLiveMillis; // => 30000
```

## Protocol RPCs (`EventLogMessage`)

The wire protocol. You rarely touch these directly — they back the layers above
— but they are the contract a custom transport must speak.

### `EventLogRemoteRpcs`

The `RpcGroup` containing all five protocol RPCs. This is what `RpcServer.layer`
and the client install.

```ts
import { EventLogRemoteRpcs } from "effect/unstable/eventlog/EventLogMessage";
import * as RpcServer from "effect/unstable/rpc/RpcServer";

RpcServer.layer(EventLogRemoteRpcs); // installs the protocol on a Protocol transport
```

### `HelloRpc` (`"EventLog.Hello"`)

Starts a session; the server replies with a `HelloResponse` (`remoteId` +
`challenge`).

### `AuthenticateRpc` (`"EventLog.Authenticate"`)

Sends an `Authenticate` payload (`publicKey`, `signingPublicKey`, `signature`,
`algorithm: "Ed25519"`) proving control of the signing key. Errors with
`EventLogProtocolError`.

### `WriteSingleRpc` (`"EventLog.WriteSingle"`)

Sends a complete encoded write payload in one frame. Requires an authenticated
identity (carries the `EventLogAuthentication` middleware).

### `WriteChunkedRpc` (`"EventLog.WriteChunked"`)

Sends one `ChunkedMessage` part of a large write payload; the server reassembles
parts by message id. Authenticated.

### `ChangesRpc` (`"EventLog.Changes"`)

Streams remote changes for a `publicKey` / `storeId` from a `startSequence`.
Responses are `SingleMessage` or `ChunkedMessage` values. Authenticated; this is
a streaming RPC. Its static `encodeEncrypted` / `decodeEncrypted` /
`encodeUnencrypted` / `decodeUnencrypted` helpers convert between
`RemoteEntry`/`EncryptedRemoteEntry` arrays and msgpack bytes.

### `EventLogAuthentication`

The `RpcMiddleware.Service` that marks the write/changes RPCs as requiring an
authenticated `EventLog.Identity`, with `EventLogProtocolError` as its error.

### `EventLogProtocolError`

`Schema.TaggedErrorClass` returned by the RPCs: `requestTag`, optional
`publicKey` / `storeId`, a `code`
(`"Unauthorized" | "Forbidden" | "NotFound" | "InvalidRequest" |
"InternalServerError"`), and a `message`.

### Message & payload schemas

- `SingleMessage` / `ChunkedMessage` — transport framing. `ChunkedMessage` has
  static `split(id, data)`, `join(map, part)`, `chunkSize` (512000), and
  `initialJoinState()`.
- `WriteEntries` — encrypted write payload (`publicKey`, `storeId`, `iv`,
  `encryptedEntries`). Has static `encode` / `decode` (msgpack).
- `WriteEntriesUnencrypted` — plaintext write payload (`publicKey`, `storeId`,
  `entries`). Has static `encode` / `decode`.
- `HelloResponse` — `remoteId` + `challenge`.
- `Authenticate` — the authentication request fields.
- `StoreId` — branded store id schema (`StoreId.make("...")`).

```ts
import { ChunkedMessage } from "effect/unstable/eventlog/EventLogMessage";

ChunkedMessage.chunkSize; // => 512000
const parts = ChunkedMessage.split(0, new Uint8Array(1_000_000)); // => [Chunked, Chunked]
```

## See also

- [Event Log overview](https://effect.plants.sh/event-log/) — the client side (`EventLogRemote`),
  journals, and schemas.
- [Event Log reference](https://effect.plants.sh/event-log/reference/) — the full module index.
- [RPC](https://effect.plants.sh/rpc/) — `RpcServer`, `RpcServer.Protocol` transports, and serialization.
- [SQL](https://effect.plants.sh/sql/) — providing a `SqlClient` layer for SQL-backed storage.