Kafka-like durable pub/sub + request/reply backbone for Module Federation micro-frontends.
This monorepo provides:
@kierkegaard/contracts: shared envelope/topic/protocol contracts@kierkegaard/sdk: app-facing SDK package that wraps the broker client and topic helpers@kierkegaard/broker-remote: Module Federation remote exposing:broker/clientbroker/contractsbroker/devtools
@kierkegaard/server: Fastify + WebSocket + durable SQLite-backed broker server@kierkegaard/host-shell,remote-a,remote-b: demo MF host/remotes
The primary value is first-class request/reply for federated apps, with a unified client API that can run in:
memorymode (single-page runtime)servermode (durable + replayable)
- Topic strings + payload schemas are the coupling boundary.
- Broker API is imported as a federated remote (not shared source import), so remotes are loosely coupled.
- Broker singleton is guarded with
globalThis[Symbol.for("kierkegaard/broker")]to prevent accidental duplication. - Server ack guarantees durability: once a publish ACK is returned, the event has been persisted.
- Vite + React + TypeScript
- Module Federation plugin:
@originjs/vite-plugin-federation - Vitest + Testing Library
- Playwright (e2e smoke)
- Fastify +
@fastify/websocket - SQLite engine (
sql.js) persisted to.sqlitefile on disk - Effect.ts for service/layer orchestration in broker/client and server bootstrap paths
This section is for teams that already have a host + remotes and want to plug in Kierkegaard.
At minimum, you need:
- broker remote entry (exposes
broker/client) - optional durable server (required for durable mode + replay)
Local defaults used in examples:
- broker remote:
http://127.0.0.1:4173/assets/remoteEntry.js - broker server:
http://127.0.0.1:7777
// vite.config.ts
federation({
remotes: {
broker: "http://127.0.0.1:4173/assets/remoteEntry.js"
}
});import { createFederatedBrokerSdk } from "@kierkegaard/sdk";
// Keep loader in app source so MF runtime can rewrite import correctly.
export const broker = createFederatedBrokerSdk(() => import("broker/client"));const brokerModule = await import("broker/client");
const { setDriver } = brokerModule;
await setDriver({
type: "server", // use "memory" for single-runtime non-durable mode
wsUrl: "ws://127.0.0.1:7777/ws",
httpUrl: "http://127.0.0.1:7777"
});import { broker } from "./broker";
await broker.ready(); // recommended before subscribe/respond
const billing = broker.topic<{ id: string }>("billing.invoice_paid");
const math = broker.topic<never, { a: number; b: number }, { result: number }>("math.add");
billing.subscribe((payload) => {
console.log("invoice", payload.id);
});
math.respond(({ a, b }) => ({ result: a + b }));
const reply = await math.request({ a: 2, b: 3 });- Memory mode: fastest setup, single browser runtime, non-durable.
- Server mode: durable persistence + replay + cross-session recovery.
packages/
contracts/
sdk/
broker-remote/
server/
host-shell/
remote-a/
remote-b/
This section is for contributors developing the Kierkegaard library itself.
pnpm install
pnpm devpnpm dev runs all local development services:
- server (Fastify):
http://127.0.0.1:7777 - broker remote:
http://127.0.0.1:4173 - host shell:
http://127.0.0.1:4170 - remote-a:
http://127.0.0.1:4171 - remote-b:
http://127.0.0.1:4172
See .env.example.
Important variables:
BROKER_DRIVER=memory|serverBROKER_WS_URL=ws://127.0.0.1:7777/wsBROKER_HTTP_URL=http://127.0.0.1:7777BROKER_DB_PATH=./kierkegaard.sqliteVITE_BROKER_DRIVER=memory|server(host runtime selection)
Imported from broker/client:
publish(topic, payload, options?) -> Promise<Envelope>subscribe(topicPattern, handler, options?) -> Unsubscriberequest(topic, payload, options?) -> Promise<ReplyPayload>respond(topicPattern, handler, options?) -> Unsubscribereplay(topic, { fromOffset | lastN }) -> AsyncIterable<Envelope>stats()setDriver({ type: "memory" | "server", ... })
For existing remotes, prefer importing the SDK package instead of calling broker/client directly everywhere.
- One place to wire MF broker loading
- Simpler APIs in remotes (
topic()helpers) - Keeps remote code independent from broker internals
import { createFederatedBrokerSdk } from "@kierkegaard/sdk";
// Loader lives in app source, so MF plugin can rewrite it correctly.
const broker = createFederatedBrokerSdk(() => import("broker/client"));
const billing = broker.topic<{ id: string }>("billing.invoice_paid");
await billing.publish({ id: "inv-42" });If you prefer direct imports like publish() from the SDK:
import { configureFederatedBroker, publish } from "@kierkegaard/sdk";
configureFederatedBroker(() => import("broker/client"));
await publish("billing.invoice_paid", { id: "inv-42" });type Envelope<T = unknown> = {
id: string;
topic: string;
key?: string;
ts: number;
schemaVersion: number;
producer?: string;
correlationId?: string;
causationId?: string;
replyTo?: string;
kind: "event" | "request" | "reply" | "error";
payload: T;
offset?: number;
};- Exact:
billing.invoice_paid - Suffix wildcard:
billing.* - Global wildcard:
*
GET /healthGET /statsGET /events?topic=<topic>&fromOffset=<n>GET /events?topic=<topic>&lastN=<n>WS /wsfor publish/subscribe/request/reply frames
Server persistence stores:
- envelope metadata + payload
- per-topic monotonically increasing offset
- correlation metadata for request/reply
Run all unit/integration tests:
pnpm test
pnpm typecheck
pnpm buildRun e2e smoke test:
pnpm -F @kierkegaard/host-shell e2eCurrent automated coverage includes:
- topic matching + pattern validation
- envelope generation/schema validation
- memory driver pub/sub + request/reply + timeout + replay ring buffer
- server durability + per-topic offsets + replay
- websocket publish/subscribe integration
- server-driver request/reply correlation + timeout + replay
- host+two-remotes+broker e2e smoke (publish + request/reply)