Skip to content

Event Log Sync Server

The sync server is the counterpart to the EventLogRemote 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 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.

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).
  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 primitives are only needed for custom transports.

Encrypted server (common, privacy-preserving)

Section titled “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.

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.

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

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

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.

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)
);

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.

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

Section titled “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.

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>

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.

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)

Section titled “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.

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.

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.

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

Section titled “EventLogServerUnencrypted.layerNoRpcServer”

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

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

Section titled “EventLogServerUnencrypted.layerRpcHandlers”

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

import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
EventLogServerUnencrypted.layerRpcHandlers;
// => Layer<RpcHandlers | EventLogAuthentication, never,
// Storage | StoreMapping | EventLogServerAuthorization | EventLog.Registry>

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.

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)

Section titled “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.

import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
EventLogServerUnencrypted.EventLogServerUnencrypted; // the service tag

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.

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

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.

import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
EventLogServerUnencrypted.make;
// => Effect<EventLogServerUnencrypted["Service"], never, Storage | EventLog.Registry>

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.

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

Section titled “EventLogServerUnencrypted.makeStorageMemory / layerStorageMemory”

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

import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
EventLogServerUnencrypted.makeStorageMemory; // => Effect<Storage["Service"], never, Scope>
EventLogServerUnencrypted.layerStorageMemory; // => Layer<Storage>

EventLogServerUnencrypted.StoreMapping + layerStoreMappingStatic

Section titled “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.

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

Section titled “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.

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
}
);

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.

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

Section titled “EventLogServerUnencrypted.EventLogServerStoreError”

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

import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
new EventLogServerUnencrypted.EventLogServerStoreError({
reason: "NotFound",
message: "No provisioned store"
});
// => EventLogServerStoreError { _tag: "EventLogServerStoreError", reason: "NotFound", ... }

EventLogServerUnencrypted.EventLogServerAuthError

Section titled “EventLogServerUnencrypted.EventLogServerAuthError”

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

import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
new EventLogServerUnencrypted.EventLogServerAuthError({
reason: "Forbidden",
publicKey: "pk"
});
// => EventLogServerAuthError { _tag: "EventLogServerAuthError", reason: "Forbidden", ... }

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

SqlEventLogServerUnencrypted.layerStorage / makeStorage

Section titled “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.

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

Section titled “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.

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:

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)
)
])
);

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.

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.

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>

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.

import * as EventLogServer from "effect/unstable/eventlog/EventLogServer";
EventLogServer.layerAuthMiddleware; // => Layer<EventLogAuthentication>

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.

import * as EventLogServer from "effect/unstable/eventlog/EventLogServer";
EventLogServer.ChunkedMessageState; // Context.Reference<Map<number, {...}>>

Session authentication (EventLogSessionAuth)

Section titled “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

Section titled “EventLogSessionAuth.makeSessionAuthChallenge”

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

import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";
EventLogSessionAuth.makeSessionAuthChallenge;
// => Effect<Uint8Array, EventLogSessionAuthError> (32 random bytes)

EventLogSessionAuth.encodeSessionAuthPayload

Section titled “EventLogSessionAuth.encodeSessionAuthPayload”

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

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

Section titled “EventLogSessionAuth.decodeSessionAuthPayload”

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

import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";
declare const bytes: Uint8Array;
EventLogSessionAuth.decodeSessionAuthPayload(bytes);
// => Effect<SessionAuthPayload, EventLogSessionAuthError>

EventLogSessionAuth.signSessionAuthPayloadBytes

Section titled “EventLogSessionAuth.signSessionAuthPayloadBytes”

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

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

Section titled “EventLogSessionAuth.signSessionAuthPayload”

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

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

Section titled “EventLogSessionAuth.verifySessionAuthPayloadBytes”

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

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

Section titled “EventLogSessionAuth.verifySessionAuthPayload”

Convenience: encodes a SessionAuthPayload and verifies the supplied signature.

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

Section titled “EventLogSessionAuth.verifySessionAuthenticateRequest”

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

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>

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

EventLogSessionAuth.EventLogSessionAuthError

Section titled “EventLogSessionAuth.EventLogSessionAuthError”

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

ConstantValueMeaning
AuthPayloadContext"eventlog-auth-v1"Domain-separation string embedded in the canonical payload.
Ed25519PublicKeyLength32Required byte length of raw Ed25519 public keys.
Ed25519SignatureLength64Required byte length of Ed25519 signatures.
SessionAuthChallengeLength32Number of random bytes in a challenge.
SessionAuthChallengeTimeToLiveMillis30_000TTL (ms) for a pending challenge.
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

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

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

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

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

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

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

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

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.

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

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

  • 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.
  • HelloResponseremoteId + challenge.
  • Authenticate — the authentication request fields.
  • StoreId — branded store id schema (StoreId.make("...")).
import { ChunkedMessage } from "effect/unstable/eventlog/EventLogMessage";
ChunkedMessage.chunkSize; // => 512000
const parts = ChunkedMessage.split(0, new Uint8Array(1_000_000)); // => [Chunked, Chunked]
  • Event Log overview — the client side (EventLogRemote), journals, and schemas.
  • Event Log reference — the full module index.
  • RPCRpcServer, RpcServer.Protocol transports, and serialization.
  • SQL — providing a SqlClient layer for SQL-backed storage.