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.
Overview
Section titled “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:
- An
RpcServer.Protocoltransport layer (see the RPC docs). - A
Storageservice (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.
EventLogServerEncrypted.layer
Section titled “EventLogServerEncrypted.layer”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 layerEventLogServerEncrypted.layerRpcHandlers
Section titled “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.
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
Section titled “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.
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>EventLogServerEncrypted.PersistedEntry
Section titled “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.
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.
EventLogServerUnencrypted.layer
Section titled “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.
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>EventLogServerUnencrypted.layerServer
Section titled “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.
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 tagEventLogServerUnencrypted.makeWrite
Section titled “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.
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 environmentEventLogServerUnencrypted.make
Section titled “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.
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted";
EventLogServerUnencrypted.make;// => Effect<EventLogServerUnencrypted["Service"], never, Storage | EventLog.Registry>EventLogServerUnencrypted.Storage
Section titled “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.
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 });EventLogServerUnencrypted.compactBacklog
Section titled “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.
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", ... }SQL-backed storage
Section titled “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 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) ) ]));Shared server pieces (EventLogServer)
Section titled “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
Section titled “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.
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
Section titled “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.
import * as EventLogServer from "effect/unstable/eventlog/EventLogServer";
EventLogServer.layerAuthMiddleware; // => Layer<EventLogAuthentication>EventLogServer.ChunkedMessageState
Section titled “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.
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 bytesEventLogSessionAuth.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>EventLogSessionAuth.SessionAuthPayload
Section titled “EventLogSessionAuth.SessionAuthPayload”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".
Constants
Section titled “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. |
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth";
EventLogSessionAuth.AuthPayloadContext; // => "eventlog-auth-v1"EventLogSessionAuth.Ed25519PublicKeyLength; // => 32EventLogSessionAuth.Ed25519SignatureLength; // => 64EventLogSessionAuth.SessionAuthChallengeLength; // => 32EventLogSessionAuth.SessionAuthChallengeTimeToLiveMillis; // => 30000Protocol RPCs (EventLogMessage)
Section titled “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
Section titled “EventLogRemoteRpcs”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 transportHelloRpc ("EventLog.Hello")
Section titled “HelloRpc ("EventLog.Hello")”Starts a session; the server replies with a HelloResponse (remoteId +
challenge).
AuthenticateRpc ("EventLog.Authenticate")
Section titled “AuthenticateRpc ("EventLog.Authenticate")”Sends an Authenticate payload (publicKey, signingPublicKey, signature,
algorithm: "Ed25519") proving control of the signing key. Errors with
EventLogProtocolError.
WriteSingleRpc ("EventLog.WriteSingle")
Section titled “WriteSingleRpc ("EventLog.WriteSingle")”Sends a complete encoded write payload in one frame. Requires an authenticated
identity (carries the EventLogAuthentication middleware).
WriteChunkedRpc ("EventLog.WriteChunked")
Section titled “WriteChunkedRpc ("EventLog.WriteChunked")”Sends one ChunkedMessage part of a large write payload; the server reassembles
parts by message id. Authenticated.
ChangesRpc ("EventLog.Changes")
Section titled “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
Section titled “EventLogAuthentication”The RpcMiddleware.Service that marks the write/changes RPCs as requiring an
authenticated EventLog.Identity, with EventLogProtocolError as its error.
EventLogProtocolError
Section titled “EventLogProtocolError”Schema.TaggedErrorClass returned by the RPCs: requestTag, optional
publicKey / storeId, a code
("Unauthorized" | "Forbidden" | "NotFound" | "InvalidRequest" | "InternalServerError"), and a message.
Message & payload schemas
Section titled “Message & payload schemas”SingleMessage/ChunkedMessage— transport framing.ChunkedMessagehas staticsplit(id, data),join(map, part),chunkSize(512000), andinitialJoinState().WriteEntries— encrypted write payload (publicKey,storeId,iv,encryptedEntries). Has staticencode/decode(msgpack).WriteEntriesUnencrypted— plaintext write payload (publicKey,storeId,entries). Has staticencode/decode.HelloResponse—remoteId+challenge.Authenticate— the authentication request fields.StoreId— branded store id schema (StoreId.make("...")).
import { ChunkedMessage } from "effect/unstable/eventlog/EventLogMessage";
ChunkedMessage.chunkSize; // => 512000const parts = ChunkedMessage.split(0, new Uint8Array(1_000_000)); // => [Chunked, Chunked]See also
Section titled “See also”- Event Log overview — the client side (
EventLogRemote), journals, and schemas. - Event Log reference — the full module index.
- RPC —
RpcServer,RpcServer.Protocoltransports, and serialization. - SQL — providing a
SqlClientlayer for SQL-backed storage.