Jumentix
Integrations

Choose infrastructure per service, not per platform

Jumentix keeps technology decisions at the adapter boundary, where they can be tested and replaced.

Jumentix open-source mascot

Bun-powered tooling

The fast path is the default path

Jumentix standardizes on Bun as the pinned monorepo runtime, package manager, script runner, test runner and browser-spec bundler. That keeps local work, CI gates, package checks and website publishing on one toolchain.

Why Bun matters here

The repository uses Bun where it actually reduces friction: fast installs with workspaces, direct TypeScript execution, repeatable package scripts, focused branch gates, and browser-test bundling before Cypress runs against real IndexedDB and DOM APIs.

  • One pinned version, `bun@1.3.13`, protects every workspace from “works on my machine” drift.
  • `bun run --filter` lets package checks stay scoped while full gates remain available for release work.
  • Bun bundles Cana browser specs before Cypress, avoiding Cypress webpack fragility while preserving real-browser evidence.
  • The same CLI drives local dev, docs sync, package dry-runs, security checks and production website publishing.
bun install
bun run website:dev
bun run ci:affected
1runtime/package/test/bundle tool
1.3.13+pinned Bun version
3workspace roots: apps, packages, tooling
30xofficial Bun install-speed ceiling vs npm

Browser in-memory playgrounds

The package contracts run without servers

These runnable examples use Category and Task records across contract-compatible in-memory adapters and browser-native local storage. They execute entirely in the browser, so teams can inspect package behavior before adding Redis, RabbitMQ, databases, PM2 processes or a backend runtime.

100%browser execution
0servers required for the lab
35available code playgrounds
2apps represented: Service Management and Backend Template
Concept first

1. Read the boundary

Start with the request path and layer matrix so each playground has a place in the architecture.

MVP path

2. Run the smallest slice

Use the browser lab to see Category and Task flow through controller, use-case, adapter and client state.

Contracts

3. Compose domains

Move to Message Mediator, SDKs and storage ports when the example needs cross-domain data or a consumer contract.

Heavy data

4. Stress the system

Finish with the bulk mutex + DLQ playground to watch real lock rejection, replay and IndexedDB commits in the canvas.

Service Management UI

The full lab starts from a visual service model, validates the domain shape, then feeds runtime contracts from the same Category and Task vocabulary.

App surface

Backend Template

The request path mirrors controller, use-case, repository, mediator and client boundaries without exposing users to infrastructure setup.

App runtime

Persistence and cache

In-memory stores and key/value state keep Category filters, Task records and UI preferences in the same service-result shape as external adapters.

@jumentix/key-value-storage

Messaging and locks

Message Mediator, Mutex Service and Dead Letter Queue show request/response, events, write protection and recoverable rejection before durable infrastructure is introduced.

@jumentix/message-mediator

Bulk write recovery

The DLQ playground sends rejected bulk Task requests back through the controller workflow, so replay still reacquires the Category mutex before writing.

@jumentix/dead-letter-queue

Playground directory

Jump to any runnable package example

Every registered code playground is listed here with a direct link to the live widget below. Use it as a map across Cana, messaging, persistence, mutex, SDK and design examples.

Browser MVP lab

Complete product slices with Category and Task records, from first MVP to heavy-data recovery.

MVP path

Full Jumentix browser app

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP
Stress path

Bulk writes with mutex + DLQ

Concurrent writes, locks, DLQ replay and browser persistence in one heavy-data scenario.

Jumentix browser lab · Mutex + Dead Letter Queue + Cana
MVP path

REST MVP Day 1 — use-case on the in-memory adapter

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP
MVP path

REST MVP Day 2 — typed client over the same adapter

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP
MVP path

Realtime MVP Day 1 — live command with ack and broadcast

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP
MVP path

Realtime MVP Day 2 — REST fallback parity drill

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP
MVP path

SaaS MVP Day 1 — tenant policy on the in-memory adapter

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP
MVP path

Microservices MVP Day 2 — worker persisting real notifications

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP
MVP path

Microservices MVP Release — explicit failure with DLQ replay

A complete browser-only product slice using Category and Task records.

Jumentix browser lab · Zero to first MVP

Cana frontend data

Relational browser data, workers, event listeners, IndexedDB and UI state integration.

Frontend data

Getting started

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Schema upgrade

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Keys

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

CRUD

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Bulk operations

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Query + explain

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Transactions

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Change events

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Hooks

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Errors

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Storage assessment

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Operation ledger

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Export

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Backend selection

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Factory adapter

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

Worker client flow

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

SPA MVP Day 1 — offline records in Cana

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

SPA MVP Day 2 — UI state from Cana events and indexed queries

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture
Frontend data

SPA MVP Release — durability across reopen

Local relational state, workers, events and IndexedDB-backed UI examples.

Cana · Cana architecture

Coordination and state ports

Mediator, mutex and key/value examples that explain the architecture boundaries around shared work.

Consumers and design source

SDK clients and Designer Core examples that connect UI modeling to runnable consumers.

MVP path

Full Jumentix browser app

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

Full Jumentix browser app

Run Category and Task as separate domains that exchange messages through the mediator to compose a task board without a server.

const database = api.createInMemoryDatabase({
  stores: ['categories', 'tasks']
});
const keyValue = api.createKeyValueStorage();
const mutex = api.createMutex(keyValue);
const mediator = api.createMessageMediator();
const emittedEvents = [];

await database.connect();
await keyValue.connect();

const model = api.createServiceModel({
  app: 'service-management',
  domain: 'Tasks'
});
const designReport = api.validateDesign(model);

await database.stores.categories.create('work', {
  id: 'work',
  name: 'Work',
  color: '#2563eb'
});
await database.stores.categories.create('home', {
  id: 'home',
  name: 'Home',
  color: '#16a34a'
});

mediator.registerHandler('categories.get.v1', async (message) => {
  const category = await database.stores.categories.getOneById(message.payload.id);
  return {
    ok: Boolean(category.result),
    result: category.result,
    metadata: {
      domain: 'Categories',
      servedBy: 'categories.get.v1'
    }
  };
});

await mediator.subscribe('tasks.created', async (event) => {
  emittedEvents.push({
    title: event.payload.title,
    categoryId: event.payload.categoryId
  });
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const task = {
    ...message.payload,
    completed: false,
    createdAt: Date.now()
  };
  const lock = await mutex.lock('category', task.categoryId);
  if (!lock.result.locked) {
    return { ok: false, error: 'category is busy' };
  }
  try {
    await database.stores.tasks.create(task.id, task);
    await keyValue.set(`category:${task.categoryId}:lastTask`, task.id);
    await mediator.publish({ name: 'tasks.created', payload: task });
    return { ok: true, result: task };
  } finally {
    await mutex.unlock('category', task.categoryId);
  }
});

mediator.registerHandler('tasks.board.v1', async (message) => {
  const taskList = await database.stores.tasks.getAll(
    { completed: message.payload.completed },
    { page: 1, size: 20 }
  );
  const cards = await Promise.all(taskList.result.map(async (task) => {
    const category = await mediator.request({
      contract: 'categories.get.v1',
      payload: { id: task.categoryId },
      metadata: {
        sourceDomain: 'Tasks',
        reason: 'compose task board'
      }
    });
    return {
      id: task.id,
      title: task.title,
      completed: task.completed,
      category: category.result
        ? {
          id: category.result.id,
          name: category.result.name,
          color: category.result.color
        }
        : null
    };
  }));
  return {
    ok: true,
    result: {
      view: 'task-board',
      composedBy: ['Tasks', 'Categories'],
      cards
    }
  };
});

const restClient = api.createRestClient((request) => mediator.request({
  contract: 'tasks.create.v1',
  payload: request.body,
  metadata: { transport: 'rest' }
}));
const websocketClient = api.createWebSocketClient((request) => mediator.request({
  contract: 'tasks.create.v1',
  payload: request.input,
  metadata: { transport: 'websocket' }
}));

const firstTask = await restClient.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: {
    id: 'task-1',
    title: 'Publish in-memory playgrounds',
    categoryId: 'work'
  }
});
await websocketClient.connect();
const secondTask = await websocketClient.request({
  operationId: 'tasks.create',
  input: {
    id: 'task-2',
    title: 'Review browser contract flow',
    categoryId: 'home'
  }
});
await websocketClient.disconnect();

const tasks = await database.stores.tasks.getAll({}, { page: 1, size: 10 });
const lastWorkTask = await keyValue.get('category:work:lastTask');
const taskBoard = await mediator.request({
  contract: 'tasks.board.v1',
  payload: { completed: false },
  metadata: { source: 'browser-playground' }
});

return {
  designOk: designReport.ok,
  taskCount: tasks.total,
  createdByRest: firstTask.result.title,
  createdByWebSocket: secondTask.result.title,
  lastWorkTask: lastWorkTask.result,
  composedDomains: taskBoard.result.composedBy,
  taskBoard: taskBoard.result.cards,
  emittedEvents
};
Stress path

Bulk writes with mutex + DLQ

Concurrent writes, locks, DLQ replay and browser persistence in one heavy-data scenario.

Proves
Rejected requests are recoverable work, not lost work.
Watch
Red lock rejections, DLQ intake, Message Mediator replay, worker commits and IndexedDB events while the stream is still running.

Bulk writes with mutex + DLQ

Create many Task records for one Category, force lock contention, enqueue rejected controller requests in a dead-letter queue, then replay them through the controller workflow.

const taskSchema = {
  version: 1,
  stores: [
    {
      name: 'categories',
      keyPath: 'id',
      indexes: [{ name: 'byName', keyPath: 'name', unique: true }]
    },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' },
        { name: 'bySource', keyPath: 'source' },
        { name: 'byClient', keyPath: 'clientId' },
        { name: 'byWorker', keyPath: 'workerId' }
      ]
    }
  ]
};
const database = api.createCanaDatabaseClient({
  name: api.createCanaDatabaseName('bulk-mutex-dlq-workers'),
  schema: taskSchema,
  operationLedger: true
});
const keyValue = api.createKeyValueStorage();
const mutex = api.createMutex(keyValue);
const mediator = api.createMessageMediator();
const deadLetterQueue = api.createDeadLetterQueue({ maxAttempts: 3 });
const replayInbox = [];
const timeline = [];
let totalTimelineEvents = 0;
const canaEvents = [];
const storageUsageSamples = [];
const workerShards = [];
const React = api.React;
const BulkTaskImportContext = React.createContext(null);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const realtimeMetrics = {
  attempted: 0,
  processed: 0,
  rejected: 0,
  replayed: 0
};
const criticalTimelineSteps = new Set([
  'client-ingestion-stopped',
  'controller-replay-batch',
  'controller-replay',
  'react-component-render'
]);
function publishRealtimeMetrics(phase, request = {}) {
  if (typeof reportPlaygroundProgress === 'function') {
    reportPlaygroundProgress({
      ...realtimeMetrics,
      taskId: request.taskId,
      clientId: request.clientId,
      workerId: request.workerId,
      phase,
      timestamp: Date.now()
    });
  }
}
function recordTimeline(entry) {
  totalTimelineEvents += 1;
  if (timeline.length < 320) {
    timeline.push(entry);
  } else if (criticalTimelineSteps.has(entry.step)) {
    timeline.shift();
    timeline.push(entry);
  }
}
async function recordIndexedDbQuota(label, tasks = 0) {
  const estimate = navigator.storage && navigator.storage.estimate
    ? await navigator.storage.estimate()
    : {};
  const usage = Number(estimate.usage ?? 0);
  const quota = Number(estimate.quota ?? 0);
  const percent = quota > 0 ? (usage / quota) * 100 : 0;
  storageUsageSamples.push({ label, usage, quota, percent, tasks });
  return storageUsageSamples[storageUsageSamples.length - 1];
}
const stopCanaEvents = database.subscribe((event) => {
  canaEvents.push({
    cursor: event.cursor,
    type: event.type,
    store: event.store,
    key: event.key,
    taskId: event.store === 'tasks' && event.record ? event.record.id : undefined,
    categoryId: event.record && event.record.categoryId ? event.record.categoryId : event.key,
    clientId: event.record && event.record.clientId ? event.record.clientId : undefined,
    workerId: event.record && event.record.workerId ? event.record.workerId : undefined,
    source: event.record && event.record.source ? event.record.source : 'category-seed'
  });
});

function createCanaWorkerShard(id) {
  const channel = new MessageChannel();
  channel.port1.start?.();
  channel.port2.start?.();
  const shard = {
    id,
    status: 'starting',
    handledRequests: 0,
    events: 0,
    database: database.cana.name,
    channel,
    router: null,
    host: null,
    client: null
  };
  shard.router = api.createCanaRouter({
    port: channel.port1,
    timeoutMs: 5000,
    onBroadcast(event) {
      shard.events += 1;
      canaEvents.push({
        cursor: event.cursor,
        type: event.type,
        store: event.store,
        key: event.key,
        taskId: event.store === 'tasks' && event.record ? event.record.id : undefined,
        categoryId: event.record && event.record.categoryId ? event.record.categoryId : event.key,
        clientId: event.record && event.record.clientId ? event.record.clientId : undefined,
        workerId: id,
        source: event.record && event.record.source ? event.record.source : 'worker-broadcast'
      });
    }
  });
  shard.host = api.createCanaWorkerHost({
    port: channel.port2,
    name: database.cana.name,
    schema: taskSchema,
    originId: `docs-${id}`,
    retainedEvents: 50,
    operationLedger: true
  });
  shard.client = api.createCanaWorkerClient(shard.router);
  return shard;
}

function closeWorkerShard(shard) {
  return Promise.resolve()
    .then(() => shard.client.close())
    .catch(() => undefined)
    .then(() => {
      shard.router.dispose();
      shard.channel.port1.close();
      shard.channel.port2.close();
      return shard.host.dispose();
    });
}

function createBulkTaskImportProvider({ actorId, clientId }) {
  const state = {
    actorId,
    clientId,
    submittedBatches: 0,
    lastBatchSize: 0
  };
  const contextValue = {
    actorId,
    clientId,
    getState: () => ({ ...state }),
    submitBulkImport: async (tasks, options = {}) => {
      state.submittedBatches += 1;
      state.lastBatchSize = tasks.length;
      recordTimeline({
        step: 'react-context-submit',
        component: 'BulkTaskImportProvider',
        actorId,
        clientId,
        taskCount: tasks.length
      });
      const responses = await bulkImportController({
        body: { tasks },
        actorId,
        client: clientId,
        stopNewRequestsAfterMs: options.stopNewRequestsAfterMs ?? Number.POSITIVE_INFINITY
      });
      state.accepted = (state.accepted ?? 0) + responses.filter((response) => response.ok).length;
      state.rejected = (state.rejected ?? 0) + responses.filter((response) => response.deadLetterId).length;
      state.interrupted = (state.interrupted ?? 0) + responses.filter((response) => response.interrupted).length;
      recordTimeline({
        step: 'react-context-complete',
        component: 'BulkTaskImportProvider',
        clientId,
        accepted: state.accepted,
        rejected: state.rejected,
        interrupted: state.interrupted
      });
      return responses;
    }
  };
  return {
    Context: BulkTaskImportContext,
    value: contextValue
  };
}

function BulkImportPanel({ provider, createNextTask, streamConfig }) {
  const previewTree = React.createElement(
    provider.Context.Provider,
    { value: provider.value },
    'BulkImportButton'
  );
  return {
    component: 'BulkImportPanel',
    previewElementType: previewTree.type === provider.Context.Provider
      ? 'BulkTaskImportContext.Provider'
      : 'unknown',
    startStream: async () => {
      recordTimeline({
        step: 'react-component-click',
        component: 'BulkImportPanel',
        clientId: provider.value.clientId,
        mode: 'concurrent-30s-stream',
        durationMs: streamConfig.durationMs,
        maxConcurrentRequests: streamConfig.maxConcurrentRequests
      });
      const responses = [];
      let stopped = false;
      let inFlight = 0;
      const startedAt = Date.now();

      await new Promise((resolve) => {
        const launchNext = () => {
          if (Date.now() - startedAt >= streamConfig.durationMs) {
            if (!stopped) {
              stopped = true;
              recordTimeline({
                step: 'client-ingestion-stopped',
                component: 'BulkImportPanel',
                clientId: provider.value.clientId,
                elapsedMs: Date.now() - startedAt,
                reason: '30 second stream window completed'
              });
            }
            if (inFlight === 0) resolve();
            return;
          }

          while (inFlight < streamConfig.maxConcurrentRequests && Date.now() - startedAt < streamConfig.durationMs) {
            const task = createNextTask(provider.value.clientId);
            inFlight += 1;
            provider.value.submitBulkImport([task])
              .then((batchResponses) => {
                responses.push(...batchResponses);
              })
              .finally(() => {
                inFlight -= 1;
                launchNext();
              });
          }
        };
        launchNext();
      });

      recordTimeline({
        step: 'react-component-render',
        component: 'BulkImportPanel',
        clientId: provider.value.clientId,
        state: provider.value.getState()
      });
      return responses;
    }
  };
}

await database.connect();
await keyValue.connect();
workerShards.push(
  createCanaWorkerShard('worker-a'),
  createCanaWorkerShard('worker-b'),
  createCanaWorkerShard('worker-c')
);
await Promise.all(workerShards.map(async (shard) => {
  await shard.client.open();
  shard.status = 'ready';
}));
await recordIndexedDbQuota('opened', 0);

await database.stores.categories.add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
await recordIndexedDbQuota('category seeded', 0);

await mediator.subscribe('dead-letter.enqueued', async (event) => {
  replayInbox.push(event.payload.recordId);
  recordTimeline({
    step: 'dead-letter-listener-received',
    taskId: event.payload.taskId,
    recordId: event.payload.recordId
  });
});

await mediator.subscribe('tasks.created', async (event) => {
  recordTimeline({
    step: 'task-created-event',
    taskId: event.payload.id,
    source: event.payload.source
  });
});

let committedTaskCount = 0;

async function recordWriteQuota(taskId) {
  committedTaskCount += 1;
  if (committedTaskCount <= 3 || committedTaskCount % 500 === 0) {
    await recordIndexedDbQuota(`${taskId}: ${committedTaskCount} tasks`, committedTaskCount);
  }
}

function createTaskRecord(input, source) {
  return {
    id: input.id,
    title: input.title,
    categoryId: input.categoryId,
    completed: false,
    clientId: input.clientId,
    workerId: input.workerId,
    source,
    createdAt: new Date().toISOString(),
    updatedAt: Date.now()
  };
}

async function writeTasksWithCanaWorkers(tasks, source) {
  const groups = new Map();
  for (const task of tasks) {
    const selectedWorker = workerShards.find((shard) => shard.id === task.workerId)
      ?? workerShards[groups.size % workerShards.length];
    const record = {
      ...task,
      workerId: selectedWorker.id,
      source
    };
    const current = groups.get(selectedWorker) ?? [];
    current.push(record);
    groups.set(selectedWorker, current);
  }

  const reports = [];
  for (const [worker, records] of groups.entries()) {
    worker.status = records.length > 1 ? 'bulk-writing' : 'writing';
    const write = records.length === 1
      ? await worker.client.add('tasks', records[0])
      : await worker.client.bulkAdd('tasks', records);
    worker.handledRequests += records.length;
    worker.status = 'ready';
    for (const task of records) {
      await recordWriteQuota(task.id);
    }
    const lastTask = records[records.length - 1];
    await keyValue.set(`category:${lastTask.categoryId}:lastTask`, lastTask.id);
    reports.push({
      workerId: worker.id,
      count: records.length,
      emittedEvents: write.events ? write.events.length : records.length
    });
  }
  return reports;
}

async function createTaskUseCase(input) {
  const lock = await mutex.lock('category', input.categoryId);
  if (!lock.result.locked) {
    const record = await deadLetterQueue.enqueue({
      entityName: 'Task',
      resourceId: input.categoryId,
      operation: 'tasks.create.v1',
      payload: input,
      actorId: input.requestedBy
    });
    await mediator.publish({
      name: 'dead-letter.enqueued',
      payload: {
        recordId: record.id,
        taskId: input.id,
        categoryId: input.categoryId
      },
      metadata: {
        source: 'tasks.create.use-case',
        reason: 'category resource is already locked'
      }
    });
    realtimeMetrics.rejected += 1;
    publishRealtimeMetrics('rejected', {
      taskId: input.id,
      clientId: input.clientId,
      workerId: input.workerId
    });
    return {
      ok: false,
      status: 409,
      error: 'category is locked; request queued for replay',
      deadLetterId: record.id
    };
  }

  try {
    recordTimeline({
      step: 'lock-acquired',
      taskId: input.id,
      categoryId: input.categoryId
    });
    publishRealtimeMetrics('accepted', {
      taskId: input.id,
      clientId: input.clientId,
      workerId: input.workerId
    });
    await sleep(input.processingMs);
    const task = createTaskRecord(input, input.source);
    const [write] = await writeTasksWithCanaWorkers([task], input.source);
    recordTimeline({
      step: 'cana-task-written',
      taskId: task.id,
      categoryId: task.categoryId,
      clientId: task.clientId,
      workerId: write.workerId,
      source: task.source,
      emittedEvents: write.emittedEvents
    });
    await mediator.publish({ name: 'tasks.created', payload: task });
    realtimeMetrics.processed += 1;
    publishRealtimeMetrics('processed', {
      taskId: task.id,
      clientId: task.clientId,
      workerId: write.workerId
    });
    return { ok: true, status: 201, result: task };
  } finally {
    await mutex.unlock('category', input.categoryId);
    recordTimeline({
      step: 'lock-released',
      taskId: input.id,
      categoryId: input.categoryId
    });
  }
}

async function createTaskController(request) {
  if (!request.replay) {
    realtimeMetrics.attempted += 1;
    publishRealtimeMetrics('submitted', {
      taskId: request.body.id,
      clientId: request.body.clientId,
      workerId: request.body.workerId
    });
  }
  recordTimeline({
    step: request.replay ? 'controller-replay' : 'controller-create',
    taskId: request.body.id,
    clientId: request.body.clientId,
    workerId: request.body.workerId
  });
  return createTaskUseCase({
    ...request.body,
    requestedBy: request.actorId,
    source: request.replay ? 'dead-letter-replay' : 'bulk-import'
  });
}

async function bulkImportController(request) {
  const startedAt = Date.now();
  let stopRecorded = false;
  recordTimeline({
    step: 'bulk-import-controller',
    component: 'BulkImportController',
    actorId: request.actorId,
    client: request.client,
    clientId: request.client,
    taskCount: request.body.tasks.length,
    stopNewRequestsAfterMs: request.stopNewRequestsAfterMs
  });
  return Promise.all(request.body.tasks.map(async (task) => {
    if (task.clientDelayMs > 0) {
      await sleep(task.clientDelayMs);
    }
    const elapsedMs = Date.now() - startedAt;
    if (elapsedMs > request.stopNewRequestsAfterMs) {
      if (!stopRecorded) {
        stopRecorded = true;
        recordTimeline({
          step: 'client-ingestion-stopped',
          component: 'BulkImportController',
          clientId: request.client,
          elapsedMs,
          reason: 'stop accepting new client requests'
        });
      }
      recordTimeline({
        step: 'client-request-interrupted',
        component: 'BulkImportController',
        taskId: task.id,
        clientId: request.client,
        workerId: task.workerId,
        elapsedMs
      });
      publishRealtimeMetrics('interrupted', {
        taskId: task.id,
        clientId: request.client,
        workerId: task.workerId
      });
      return {
        ok: false,
        status: 202,
        interrupted: true,
        error: 'client stopped sending new requests before controller admission',
        taskId: task.id
      };
    }
    return createTaskController({
      body: task,
      actorId: request.actorId
    });
  }));
}

async function replayDeadLettersController() {
  const pendingBefore = await deadLetterQueue.pending();
  const replayableRecords = pendingBefore.filter((record) => record.operation === 'tasks.create.v1');
  const skippedRecords = pendingBefore.filter((record) => record.operation !== 'tasks.create.v1');
  recordTimeline({
    step: 'controller-replay-batch',
    component: 'ReplayDeadLettersController',
    taskCount: replayableRecords.length,
    mode: 'worker-bulk-add'
  });
  const replayTasks = replayableRecords.map((record) => createTaskRecord(
    {
      ...record.payload,
      processingMs: 0
    },
    'dead-letter-replay'
  ));
  const bulkReports = await writeTasksWithCanaWorkers(replayTasks, 'dead-letter-replay');
  await Promise.all(replayableRecords.map((record) => deadLetterQueue.settle(record.id, 'succeeded')));
  realtimeMetrics.replayed += replayableRecords.length;
  replayableRecords.slice(-900).forEach((record) => {
    publishRealtimeMetrics('replayed', {
      taskId: record.payload.id,
      clientId: record.payload.clientId,
      workerId: record.payload.workerId
    });
  });
  publishRealtimeMetrics('replayed');
  replayableRecords.slice(0, 80).forEach((record) => {
    recordTimeline({
      step: 'controller-replay',
      taskId: record.payload.id,
      clientId: record.payload.clientId,
      workerId: record.payload.workerId
    });
  });
  const report = {
    replayed: replayableRecords.map((record) => record.id),
    retried: [],
    abandoned: [],
    skipped: skippedRecords.map((record) => record.id)
  };
  const records = await deadLetterQueue.list();
  return {
    ok: true,
    status: 200,
    pendingBefore: pendingBefore.length,
    report,
    bulkReports,
    records: records.map((record) => ({
      id: record.id,
      taskId: record.payload.id,
      status: record.status,
      attempts: record.attempts
    }))
  };
}

const reactClientIds = ['react-client-a', 'react-client-b', 'react-client-c'];
const streamDurationMs = 30000;
const maxConcurrentRequestsPerClient = 12;
const requestPaceMs = 25;
let globalSequence = 0;
function createNextTask(clientId) {
  const sequence = globalSequence;
  globalSequence += 1;
  return {
    id: `task-${sequence + 1}`,
    title: `Concurrent Task ${sequence + 1}`,
    categoryId: 'work',
    clientId,
    workerId: workerShards[sequence % workerShards.length].id,
    sequence,
    processingMs: 12,
    clientDelayMs: requestPaceMs
  };
}

const reactClients = reactClientIds.map((clientId) => {
  const provider = createBulkTaskImportProvider({
    actorId: `${clientId}-controller`,
    clientId
  });
  return {
    id: clientId,
    provider,
    panel: BulkImportPanel({
      provider,
      createNextTask,
      streamConfig: {
        durationMs: streamDurationMs,
        maxConcurrentRequests: maxConcurrentRequestsPerClient
      }
    })
  };
});
const streamStartedAt = Date.now();
const bulkResponseGroups = await Promise.all(
  reactClients.map((client) => client.panel.startStream())
);
const actualRunDurationMs = Date.now() - streamStartedAt;
const bulkResponses = bulkResponseGroups.flat();
const pendingAfterBulk = await deadLetterQueue.pending();
const replay = await replayDeadLettersController();
const pendingAfterReplay = await deadLetterQueue.pending();
const taskRows = await database.stores.tasks.query({ index: 'byUpdatedAt' });
const categoryRows = await database.stores.categories.query({ index: 'byName' });
const lastTask = await keyValue.get('category:work:lastTask');
await recordIndexedDbQuota('final', taskRows.length);
const storage = await database.cana.storageState();
const workerShardSummary = workerShards.map((shard) => ({
  id: shard.id,
  database: shard.database,
  status: shard.status,
  handledRequests: shard.handledRequests,
  events: shard.events
}));
const databaseSnapshot = {
  adapter: 'Cana database adapter',
  backend: database.cana.backend,
  workerMode: 'createWorkerHost + createWorkerClient',
  storage,
  indexedDbDatabase: database.cana.name,
  stores: {
    categories: categoryRows.length,
    tasks: taskRows.length
  },
  categoryIds: categoryRows.map((category) => category.id),
  taskIds: taskRows.map((task) => task.id)
};

stopCanaEvents();
await Promise.all(workerShards.map(closeWorkerShard));
await database.disconnect();

const createdDuringBulk = bulkResponses.filter((response) => response.ok).length;
const submittedToController = bulkResponses.filter((response) => !response.interrupted).length;
const interruptedBeforeController = bulkResponses.filter((response) => response.interrupted).length;
const rejectedToDeadLetterQueue = bulkResponses.filter((response) => response.deadLetterId).length;
const deadLetterQueueFullyProcessed = pendingAfterReplay.length === 0
  && replay.records.every((record) => record.status === 'succeeded');
const jobsAccountedFor = taskRows.length + interruptedBeforeController;
publishRealtimeMetrics('complete');

return {
  databaseAdapter: 'Cana',
  databaseBackend: databaseSnapshot.backend,
  requestMode: 'concurrent-30s-stream',
  streamDurationMs,
  actualRunDurationMs,
  maxConcurrentRequestsPerClient,
  requestPaceMs,
  attemptedBulkCount: bulkResponses.length,
  createdDuringBulk,
  submittedToController,
  interruptedBeforeController,
  rejectedToDeadLetterQueue,
  shutdownReport: {
    stopNewRequestsAfterMs: streamDurationMs,
    pendingDeadLettersAfterReplay: pendingAfterReplay.length,
    deadLetterQueueFullyProcessed,
    jobsAccountedFor,
    noLostJobs: deadLetterQueueFullyProcessed && jobsAccountedFor === bulkResponses.length
  },
  pendingBeforeReplay: pendingAfterBulk.map((record) => ({
    id: record.id,
    taskId: record.payload.id,
    resourceId: record.resourceId,
    status: record.status
  })),
  replayInbox,
  replayReport: replay.report,
  finalTaskCount: taskRows.length,
  lastTaskInCategory: lastTask.result,
  reactClients: reactClients.map((client) => ({
    id: client.id,
    taskCount: client.provider.value.getState().accepted
      + client.provider.value.getState().rejected
      + client.provider.value.getState().interrupted,
    accepted: client.provider.value.getState().accepted,
    rejected: client.provider.value.getState().rejected,
    interrupted: client.provider.value.getState().interrupted
  })),
  workerShards: workerShardSummary,
  requestTimeline: timeline,
  totalTimelineEvents,
  canaEvents: canaEvents.slice(-500),
  totalCanaEvents: canaEvents.length,
  storageUsageSamples,
  databaseSnapshot,
  controllerLevelReplay: timeline
    .filter((entry) => entry.step.startsWith('controller'))
    .slice(0, 40)
    .map((entry) => entry.step),
  storedTasks: taskRows.slice(0, 20).map((task) => ({
    id: task.id,
    title: task.title,
    source: task.source,
    clientId: task.clientId,
    workerId: task.workerId
  })),
  omittedStoredTasks: Math.max(0, taskRows.length - 20),
  deadLetterRecords: replay.records
};
Live data flow

One merged canvas follows concurrent requests for 30 seconds: each recent request becomes its own token across the React client, Context API, controller, mutex, DLQ, Message Mediator, replay, Cana workers, IndexedDB commits, subscribe events, and the return path to the client. Lock rejections are red.

AcceptedRejected by lockInput stoppedReplayMessage MediatorCana workersCana eventsIndexedDB quota
React clients: 0Cana workers: 0Attempted: 0Admitted to controller: 0Accepted in bulk: 0Rejected by lock: 0Interrupted before controller: 0Replayed: 0DLQ drained: noNo lost jobs: noFinal tasks: 0Real events: 0Cana events: 0Live request tokens: 0IndexedDB quota: 0.0000%IndexedDB usage: 0 BQuota samples: 0Window: 30sConcurrency/client: 12
Realtime flow
processed 0rejected 0replayed 0
Pipeline comparison
Admission0.0%Lock pressure0.0%DLQ recovered0.0%Final persistence0.0%
acceptedlock rejectedinterruptedvia replay
Total input0 · 0.0%
Controller admitted0 · 0.0%
Created without retry0 · 0.0%
Rejected to DLQ0 · 0.0%
Recovered by replay0 · 0.0%
Persisted in IndexedDB0 · 0.0%

0 requests were recovered after lock rejection; 0 reached the database.

Throughput
0 req/s0 requests in 30.0s
Request outcomes
Created directly0
Lock -> DLQ0
Interrupted0
Replayed0
React clients
Waiting for run0
Cana workers
Waiting for workers0
IndexedDB quota
0.0000% · 0 B
Shutdown health
DLQ drainedno
No lost jobsno
MVP path

REST MVP Day 1 — use-case on the in-memory adapter

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

REST MVP Day 1 — use-case on the in-memory adapter

Run the create task use-case against the real in-memory database adapter and prove the 201/400/404 rules.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

async function createTaskUseCase(input) {
  if (!input.title || !String(input.title).trim()) {
    return { status: 400, body: { error: 'title is required' } };
  }
  const category = await database.stores.categories.getOneById(input.categoryId);
  if (!category.result) {
    return { status: 404, body: { error: 'category not found' } };
  }
  const task = {
    id: crypto.randomUUID(),
    title: input.title,
    categoryId: input.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  return { status: 201, body: task };
}

const created = await createTaskUseCase({ title: 'Ship the MVP', categoryId: 'work' });
const invalid = await createTaskUseCase({ title: ' ', categoryId: 'work' });
const orphan = await createTaskUseCase({ title: 'No owner', categoryId: 'missing' });
const listed = await database.stores.tasks.getAll({}, { page: 1, size: 10 });

return {
  created: created.status,
  invalid: invalid.status,
  unknownCategory: orphan.status,
  storedTotal: listed.total,
  firstTask: listed.result[0].title
};
MVP path

REST MVP Day 2 — typed client over the same adapter

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

REST MVP Day 2 — typed client over the same adapter

Route OpenAPI operationIds through a REST client into a handler backed by the in-memory adapter.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

const client = api.createRestClient(async (request) => {
  if (request.operationId === 'createTask') {
    const input = request.body ?? {};
    if (!input.title || !String(input.title).trim()) {
      return { ok: false, status: 400, error: 'title is required' };
    }
    const category = await database.stores.categories.getOneById(input.categoryId);
    if (!category.result) {
      return { ok: false, status: 404, error: 'category not found' };
    }
    const task = { id: crypto.randomUUID(), ...input, completed: false };
    await database.stores.tasks.create(task.id, task);
    return { ok: true, status: 201, result: task };
  }
  if (request.operationId === 'listTasks') {
    const tasks = await database.stores.tasks.getAll({}, { page: 1, size: 20 });
    return { ok: true, status: 200, result: tasks.result };
  }
  return { ok: false, status: 404, error: 'unknown operationId' };
});

const created = await client.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: { title: 'Publish first REST MVP', categoryId: 'work' }
});
const rejected = await client.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: { title: '', categoryId: 'work' }
});
const listed = await client.request({ operationId: 'listTasks', method: 'GET', path: '/tasks' });

return {
  created: created.status,
  rejected: rejected.status,
  listed: listed.status,
  total: listed.result.length,
  firstTask: listed.result[0].title
};
MVP path

Realtime MVP Day 1 — live command with ack and broadcast

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

Realtime MVP Day 1 — live command with ack and broadcast

Wire a WebSocket client to a mediator handler that persists through the in-memory adapter and broadcasts the created event.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
const mediator = api.createMessageMediator();
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

const liveCards = [];
await mediator.subscribe('tasks.created', async (event) => {
  liveCards.push(event.payload.title);
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const category = await database.stores.categories.getOneById(message.payload.categoryId);
  if (!category.result) {
    return { ok: false, error: 'category not found' };
  }
  const task = {
    id: crypto.randomUUID(),
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  await mediator.publish({ name: 'tasks.created', payload: task });
  return { ok: true, result: task };
});

const socket = api.createWebSocketClient((request) => mediator.request({
  contract: 'tasks.create.v1',
  payload: request.input,
  metadata: { transport: 'websocket' }
}));

await socket.connect();
const ack = await socket.request({
  operationId: 'tasks.create',
  input: { title: 'Show realtime status', categoryId: 'work' }
});
const rejected = await socket.request({
  operationId: 'tasks.create',
  input: { title: 'No owner', categoryId: 'missing' }
});
await socket.disconnect();

return {
  ack: ack.ok,
  createdTask: ack.result.title,
  rejectedError: rejected.error,
  broadcastedToUi: liveCards
};
MVP path

Realtime MVP Day 2 — REST fallback parity drill

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

Realtime MVP Day 2 — REST fallback parity drill

Kill the socket handler and prove the REST fallback returns the same business result through the same in-memory adapter.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

async function createTaskUseCase(input) {
  const task = {
    id: crypto.randomUUID(),
    title: input.title,
    categoryId: input.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  return { ok: true, result: task };
}

const downSocket = api.createWebSocketClient(async () => {
  throw new Error('socket unavailable');
});
const restFallback = api.createRestClient((request) => createTaskUseCase(request.body));

async function createTaskWithFallback(input) {
  try {
    const live = await downSocket.request({ operationId: 'tasks.create', input });
    return { transport: 'websocket', result: live };
  } catch {
    const fallback = await restFallback.request({
      operationId: 'createTask',
      method: 'POST',
      path: '/tasks',
      body: input
    });
    return { transport: 'rest', result: fallback };
  }
}

const response = await createTaskWithFallback({ title: 'Fallback parity task', categoryId: 'work' });
const stored = await database.stores.tasks.getAll({}, { page: 1, size: 10 });

return {
  transportUsed: response.transport,
  ok: response.result.ok,
  storedTotal: stored.total,
  storedTitle: response.result.result.title
};
MVP path

SaaS MVP Day 1 — tenant policy on the in-memory adapter

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

SaaS MVP Day 1 — tenant policy on the in-memory adapter

Prove the tenant guard: org-1 writes its own task, org-2 is denied, and the store only holds the legitimate record.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

async function createTenantTask(context, input) {
  if (context.organizationId !== input.organizationId) {
    return { ok: false, status: 403, error: 'tenant access denied' };
  }
  const task = {
    id: crypto.randomUUID(),
    organizationId: input.organizationId,
    title: input.title,
    categoryId: input.categoryId
  };
  await database.stores.tasks.create(task.id, task);
  return { ok: true, status: 201, result: task };
}

const own = await createTenantTask(
  { organizationId: 'org-1', userId: 'user-1' },
  { organizationId: 'org-1', title: 'Tenant scoped task', categoryId: 'work' }
);
const denied = await createTenantTask(
  { organizationId: 'org-2', userId: 'user-2' },
  { organizationId: 'org-1', title: 'Cross-tenant write', categoryId: 'work' }
);
const org1Tasks = await database.stores.tasks.getByRelation('organizationId', 'org-1');
const org2Tasks = await database.stores.tasks.getByRelation('organizationId', 'org-2');

return {
  ownWrite: own.status,
  crossTenantWrite: denied.status,
  org1Sees: org1Tasks.result.map((task) => task.title),
  org2Sees: org2Tasks.result.length
};
MVP path

Microservices MVP Day 2 — worker persisting real notifications

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

Microservices MVP Day 2 — worker persisting real notifications

Subscribe a notification worker to the mediator and persist each delivery in its own in-memory store — no fake HTTP endpoint.

const database = api.createInMemoryDatabase({ stores: ['tasks', 'notifications'] });
const mediator = api.createMessageMediator();
await database.connect();

await mediator.subscribe('tasks.created.v1', async (event) => {
  await database.stores.notifications.create(crypto.randomUUID(), {
    template: 'task-created',
    taskId: event.payload.id,
    title: event.payload.title
  });
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const task = {
    id: crypto.randomUUID(),
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  await mediator.publish({ name: 'tasks.created.v1', payload: task });
  return { ok: true, result: task };
});

const created = await mediator.request({
  contract: 'tasks.create.v1',
  payload: { title: 'Notify assignee', categoryId: 'work' }
});
const sent = await database.stores.notifications.getAll({}, { page: 1, size: 10 });

return {
  task: created.result.title,
  notificationsDelivered: sent.total,
  firstNotification: sent.result[0]
};
MVP path

Microservices MVP Release — explicit failure with DLQ replay

A complete browser-only product slice using Category and Task records.

Proves
The same architecture can start with in-memory adapters before external infrastructure exists.
Watch
Controller boundaries, service results, mediator messages and client state updates.

Microservices MVP Release — explicit failure with DLQ replay

Route a poison message to the dead-letter queue and replay it, proving the failure mode is explicit and measured.

const deadLetterQueue = api.createDeadLetterQueue({ maxAttempts: 2 });
const mediator = api.createMessageMediator();

mediator.registerHandler('tasks.create.v1', async (message) => {
  if (!message.payload.title) {
    const record = await deadLetterQueue.enqueue({
      entityName: 'Task',
      resourceId: message.payload.categoryId ?? 'unknown',
      operation: 'tasks.create.v1',
      payload: message.payload
    });
    return { ok: false, error: 'queued for replay', deadLetterId: record.id };
  }
  return { ok: true, result: { id: crypto.randomUUID(), ...message.payload } };
});

const failed = await mediator.request({
  contract: 'tasks.create.v1',
  payload: { categoryId: 'work' }
});
const report = await deadLetterQueue.replay({
  'tasks.create.v1': async (record) => {
    if (!record.payload.title) throw new Error('title is required');
    return record.id;
  }
});
const after = await deadLetterQueue.find(failed.deadLetterId);

return {
  firstAttempt: failed.error,
  replayReport: report,
  statusAfterReplay: after.status,
  lastError: after.lastError
};
Frontend data

Getting started

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Getting started

Open a client, create Category and Task records, then read them back.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Write the Cana tutorial',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
return {
  backend: client.backend,
  category: await client.table('categories').get('work'),
  task: await client.table('tasks').get('task-1')
};
Frontend data

Schema upgrade

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Schema upgrade

Start with Category, then raise the version and add the Task table.

const v1 = cana.createClient({
  name: dbName,
  schema: {
    version: 1,
    stores: [{ name: 'categories', keyPath: 'id' }]
  }
});
await v1.open();
await v1.table('categories').add({ id: 'work', name: 'Work' });
await v1.close();

const v2 = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await v2.open();
await v2.table('tasks').add({
  id: 'task-1',
  title: 'Created after upgrade',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
return {
  categories: await v2.table('categories').query(),
  tasks: await v2.table('tasks').query()
};
Frontend data

Keys

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Keys

Use stable ids in Category and Task records.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({
  id: 'docs',
  name: 'Docs',
  color: '#0f766e',
  createdAt: 1,
  updatedAt: 1
});
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Document stable keys',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 2,
  updatedAt: 2
});
return {
  categoryKey: 'docs',
  taskKey: 'docs-1',
  task: await client.table('tasks').get('docs-1')
};
Frontend data

CRUD

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

CRUD

Create, read, update and delete one Task.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({ id: 'work', name: 'Work', color: '#2563eb' });
const tasks = client.table('tasks');
await tasks.add({
  id: 'task-1',
  title: 'Draft the tutorial',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: 1,
  updatedAt: 1
});
await tasks.update('task-1', { completed: true, updatedAt: 2 });
const afterUpdate = await tasks.get('task-1');
await tasks.delete('task-1');
return { afterUpdate, afterDelete: await tasks.get('task-1') };
Frontend data

Bulk operations

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Bulk operations

Seed Category and Task records with bulk operations.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
  { id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
  { id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
  {
    id: 'task-1',
    title: 'Write the Cana tutorial',
    categoryId: 'work',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  },
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: true,
    priority: 'medium',
    createdAt: now,
    updatedAt: now + 1
  }
]);
const put = await client.table('tasks').bulkPut([
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: false,
    priority: 'high',
    createdAt: Date.now(),
    updatedAt: Date.now()
  },
  {
    id: 'task-3',
    title: 'Publish the example app',
    categoryId: 'work',
    completed: false,
    priority: 'medium',
    createdAt: Date.now(),
    updatedAt: Date.now()
  }
]);
return {
  put,
  categories: await client.table('categories').query({ index: 'byName' }),
  tasks: await client.table('tasks').query({ index: 'byUpdatedAt' })
};
Frontend data

Query + explain

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Query + explain

Run an indexed Task query by Category and inspect the plan.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
  { id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
  { id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
  {
    id: 'task-1',
    title: 'Write the Cana tutorial',
    categoryId: 'work',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  },
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: true,
    priority: 'medium',
    createdAt: now,
    updatedAt: now + 1
  }
]);
const { records, plan } = await client.table('tasks').explain({
  index: 'byCategory',
  equals: 'work'
});
return { records, plan };
Frontend data

Transactions

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Transactions

Create one Category and its first Task in a single commit.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
  const now = Date.now();
  await scope.table('categories').put({
    id: 'ops',
    name: 'Operations',
    color: '#f97316',
    createdAt: now,
    updatedAt: now
  });
  await scope.table('tasks').put({
    id: 'ops-1',
    title: 'Created with the category',
    categoryId: 'ops',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  });
  return 'ok';
});
return {
  outcome: tx.outcome,
  result: tx.result,
  categories: await client.table('categories').query(),
  tasks: await client.table('tasks').query()
};
Frontend data

Change events

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Change events

Subscribe and collect committed Task events.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const seen = [];
const stop = client.subscribe((event) => {
  seen.push({ cursor: event.cursor, type: event.type, store: event.store, key: event.key });
});
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Listen to Cana events',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});
await client.table('tasks').update('docs-1', { completed: true, updatedAt: 2 });
stop();
return { events: seen };
Frontend data

Hooks

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Hooks

Run beforeWrite and afterCommit hooks around Task writes.

const trail = [];
const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  hooks: {
    beforeWrite: (ctx) => { trail.push('before:' + ctx.store + ':' + ctx.type); },
    afterCommit: (events) => { trail.push('commit:' + events.length); }
  }
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Passes through hooks',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});
return { trail, task: await client.table('tasks').get('docs-1') };
Frontend data

Errors

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Errors

Detect duplicate Category ids with isCanaErrorCode.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
try {
  await client.table('categories').add({ id: 'docs', name: 'Duplicada', color: '#dc2626' });
  return { unexpected: 'no error' };
} catch (error) {
  return {
    isCanaError: cana.isCanaError(error),
    constraint: cana.isCanaErrorCode(error, 'ConstraintViolation'),
    code: error && error.code
  };
}
Frontend data

Storage assessment

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Storage assessment

Read storageState and durabilityAssessment after writing Task data.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Durable data',
  categoryId: 'docs',
  completed: false,
  priority: 'high',
  createdAt: 1,
  updatedAt: 1
});
const storage = await client.storageState();
const durability = await client.durabilityAssessment();
return { backend: client.backend, storage, durability };
Frontend data

Operation ledger

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Operation ledger

Resolve a committed Task write with the operation ledger enabled.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  operationLedger: true
});
await client.open();
const tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
  const now = Date.now();
  await scope.table('categories').put({ id: 'docs', name: 'Docs', color: '#0f766e' });
  await scope.table('tasks').put({
    id: 'docs-1',
    title: 'Reconcile uncertain write',
    categoryId: 'docs',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  });
  return 'wrote';
});
const resolved = await client.resolveWrite(tx.correlationId, tx.attemptedAt);
return { outcome: tx.outcome, resolved, task: await client.table('tasks').get('docs-1') };
Frontend data

Export

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Export

Export Category and Task stores as plain data.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
  { id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
  { id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
  {
    id: 'task-1',
    title: 'Write the Cana tutorial',
    categoryId: 'work',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  },
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: true,
    priority: 'medium',
    createdAt: now,
    updatedAt: now + 1
  }
]);
const dump = await client.exportAll();
return dump;
Frontend data

Backend selection

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Backend selection

Show client.backend after opening the task database.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  fallback: 'localStorage'
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
return { backend: client.backend, categories: await client.table('categories').query() };
Frontend data

Factory adapter

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Factory adapter

Use createCanaDatabaseClient with Category and Task stores.

const adapter = cana.createCanaDatabaseClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await adapter.connect();
await adapter.stores.categories.add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await adapter.stores.tasks.add({
  id: 'docs-1',
  title: 'Created through the adapter',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});
const task = await adapter.stores.tasks.get('docs-1');
await adapter.disconnect();
return {
  backend: adapter.cana.backend,
  stores: Object.keys(adapter.stores),
  task
};
Frontend data

Worker client flow

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

Worker client flow

Drive Cana through createWorkerHost, createRouter and createWorkerClient using MessageChannel.

const channel = new MessageChannel();
channel.port1.start?.();
channel.port2.start?.();

const broadcasts = [];
const router = cana.createRouter({
  port: channel.port1,
  timeoutMs: 5000,
  onBroadcast(event) {
    broadcasts.push({
      cursor: event.cursor,
      type: event.type,
      store: event.store,
      key: event.key
    });
  }
});

const host = cana.createWorkerHost({
  port: channel.port2,
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  originId: 'docs-worker-host',
  retainedEvents: 20,
  operationLedger: true
});

const workerClient = cana.createWorkerClient(router);

try {
  await workerClient.open();
  const now = Date.now();
  await workerClient.put('categories', {
    id: 'work',
    name: 'Work',
    color: '#2563eb',
    createdAt: now,
    updatedAt: now
  });
  await workerClient.add('tasks', {
    id: 'task-worker-1',
    title: 'Persist through the worker boundary',
    categoryId: 'work',
    completed: false,
    priority: 'medium',
    createdAt: now,
    updatedAt: now
  });
  const tasks = await workerClient.query('tasks', {
    index: 'byCategory',
    equals: 'work'
  });
  const count = await workerClient.count('tasks');
  return {
    ping: await workerClient.ping(),
    count,
    tasks,
    broadcasts
  };
} finally {
  await workerClient.close();
  router.dispose();
  await host.dispose();
  channel.port1.close();
  channel.port2.close();
}
Frontend data

SPA MVP Day 1 — offline records in Cana

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

SPA MVP Day 1 — offline records in Cana

Open a real Cana IndexedDB client, seed Category, and create the first Task — the whole workflow runs in the browser.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();

const now = Date.now();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Offline task',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: now,
  updatedAt: now
});

return {
  backend: client.backend,
  category: await client.table('categories').get('work'),
  task: await client.table('tasks').get('task-1')
};
Frontend data

SPA MVP Day 2 — UI state from Cana events and indexed queries

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

SPA MVP Day 2 — UI state from Cana events and indexed queries

Subscribe to committed change events, write and update a Task, then read it back through the byCategory index.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();

const seen = [];
const stop = client.subscribe((event) => {
  seen.push({ type: event.type, store: event.store, key: event.key });
});

const now = Date.now();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Listen to local changes',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').update('task-1', { completed: true, updatedAt: now + 1 });
stop();

const workTasks = await client.table('tasks').query({
  index: 'byCategory',
  equals: 'work'
});

return {
  events: seen,
  workTasks: workTasks.map((task) => ({ title: task.title, completed: task.completed }))
};
Frontend data

SPA MVP Release — durability across reopen

Local relational state, workers, events and IndexedDB-backed UI examples.

Proves
Frontend components can listen to data events instead of polling or duplicating storage logic.
Watch
Schema setup, table operations, event listeners, worker flow and state-management integration points.

SPA MVP Release — durability across reopen

Close the client and reopen the same database: the offline records survive, proving durable local state.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();

const now = Date.now();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Survives reload',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: now,
  updatedAt: now
});
await client.close();

const reopened = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await reopened.open();
const tasksAfterReopen = await reopened.table('tasks').query();
const categoriesAfterReopen = await reopened.table('categories').query();

return {
  backend: reopened.backend,
  categoriesAfterReopen: categoriesAfterReopen.length,
  tasksAfterReopen: tasksAfterReopen.map((task) => task.title)
};
Domain messaging

In-memory mediator

Request/response and event composition across independent domain modules.

Proves
Modules can exchange data through contracts without importing each other directly.
Watch
Subjects, payloads, handlers, composed reads and the return path to the caller.

In-memory mediator

Exchange messages between Category and Task domains, then compose a read model through mediator request/response.

const domainMessages = [];
const events = [];
const mediator = api.createInMemory();

const categories = new Map([
  ['work', { id: 'work', name: 'Work', color: '#2563eb' }],
  ['home', { id: 'home', name: 'Home', color: '#16a34a' }]
]);
const tasks = [];

await mediator.subscribe('tasks.created', async (event) => {
  events.push({
    title: event.payload.title,
    categoryId: event.payload.categoryId
  });
});

mediator.registerHandler('categories.get.v1', async (message) => {
  domainMessages.push({
    from: message.metadata.sourceDomain,
    to: 'Categories',
    contract: 'categories.get.v1',
    categoryId: message.payload.id
  });
  return {
    ok: true,
    result: categories.get(message.payload.id) ?? null
  };
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const task = {
    id: message.payload.id,
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  tasks.push(task);
  await mediator.publish({ name: 'tasks.created', payload: task });
  return { ok: true, result: task };
});

mediator.registerHandler('tasks.board.v1', async () => {
  const cards = await Promise.all(tasks.map(async (task) => {
    const category = await mediator.request({
      contract: 'categories.get.v1',
      payload: { id: task.categoryId },
      metadata: {
        sourceDomain: 'Tasks',
        reason: 'compose task board read model'
      }
    });
    return {
      id: task.id,
      title: task.title,
      categoryName: category.result?.name ?? 'Uncategorized',
      categoryColor: category.result?.color ?? '#64748b'
    };
  }));
  return {
    ok: true,
    result: {
      readModel: 'TaskBoard',
      composedFrom: ['Tasks', 'Categories'],
      cards
    }
  };
});

const response = await mediator.request({
  contract: 'tasks.create.v1',
  payload: {
    id: 'task-1',
    title: 'Wire mediator events',
    categoryId: 'work'
  },
  metadata: { source: 'browser-playground' }
});
const board = await mediator.request({
  contract: 'tasks.board.v1',
  payload: {},
  metadata: { source: 'task-board-page' }
});

return {
  createdTask: response.result,
  board: board.result,
  domainMessages,
  events
};
Domain messaging

Microservices MVP Day 1 — contract over the in-memory mediator

Request/response and event composition across independent domain modules.

Proves
Modules can exchange data through contracts without importing each other directly.
Watch
Subjects, payloads, handlers, composed reads and the return path to the caller.

Microservices MVP Day 1 — contract over the in-memory mediator

Prove request/response and publish/listen on the real in-memory mediator, including the explicit error for an unknown contract.

const mediator = api.createInMemory();
const receivedEvents = [];

await mediator.subscribe('tasks.created.v1', async (event) => {
  receivedEvents.push(event.payload.title);
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const task = {
    id: crypto.randomUUID(),
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  await mediator.publish({ name: 'tasks.created.v1', payload: task });
  return { ok: true, result: task };
});

const created = await mediator.request({
  contract: 'tasks.create.v1',
  payload: { title: 'Notify assignee', categoryId: 'work' }
});
const unknown = await mediator.request({
  contract: 'tasks.unknown.v1',
  payload: {}
});

return {
  createdTask: created.result.title,
  eventDelivered: receivedEvents,
  unknownContractError: unknown.error
};
Local state port

In-memory key/value

Small state records with the same contract shape used by replaceable adapters.

Proves
Infrastructure can remain swappable while callers keep stable service results.
Watch
Create/read/update/delete behavior and predictable error envelopes.

In-memory key/value

Cache UI preferences for the Task list with the same service-result shape used by package adapters.

const client = api.createInMemory();
await client.connect();

await client.set('ui:selected-category', {
  id: 'work',
  name: 'Work',
  visibleTaskIds: ['task-1', 'task-3']
});
await client.set('ui:last-sort', 'priority-desc');

const selectedCategory = await client.get('ui:selected-category');
const lastSort = await client.get('ui:last-sort');
await client.del('ui:last-sort');
const deletedSort = await client.get('ui:last-sort');
await client.disconnect();

return {
  selectedCategory: selectedCategory.result,
  lastSort: lastSort.result,
  deletedSort: deletedSort.result
};
Concurrency guard

Mutex with in-memory KV

Category-scoped locks around Task workflows.

Proves
A use-case can protect shared work without coupling the domain to storage internals.
Watch
Lock acquisition, rejection, release and the service-result returned to the controller.

Mutex with in-memory KV

Protect a Category update while two Task writers compete for the same resource.

const keyValue = api.createKeyValueStorage();
const mutex = api.create(keyValue);

const firstWriter = await mutex.lock('category', 'work');
const secondWriter = await mutex.lock('category', 'work');
const lockedBeforeRelease = await mutex.isLocked('category', 'work');
await mutex.unlock('category', 'work');
const lockedAfterRelease = await mutex.isLocked('category', 'work');

return {
  firstWriter: firstWriter.result,
  secondWriter: secondWriter.result,
  lockedBeforeRelease: lockedBeforeRelease.result,
  lockedAfterRelease: lockedAfterRelease.result
};
Consumer contract

REST client with mock fetch

A client-side consumer calling the same Task behavior through a stable API surface.

Proves
Consumers can depend on generated contracts instead of handwritten transport details.
Watch
Request shape, response shape, error handling and typed integration points.

REST client with mock fetch

Call Task OpenAPI operations with a browser-safe mock client.

const client = api.createMockClient();

const created = await client.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: {
    id: 'task-1',
    title: 'Generate REST SDK example',
    categoryId: 'work',
    completed: false
  }
});
const listed = await client.request({
  operationId: 'listTasks',
  method: 'GET',
  path: '/tasks?categoryId=work'
});

return {
  created,
  listed
};
Consumer contract

WebSocket client with fake socket

A client-side consumer calling the same Task behavior through a stable API surface.

Proves
Consumers can depend on generated contracts instead of handwritten transport details.
Watch
Request shape, response shape, error handling and typed integration points.

WebSocket client with fake socket

Use the realtime client contract to create and list Task records in the browser.

const client = api.createFakeClient();
const status = await client.connect();

const created = await client.request({
  operationId: 'tasks.create',
  input: {
    id: 'task-1',
    title: 'Render realtime updates',
    categoryId: 'home',
    completed: false
  }
});
const listed = await client.request({
  operationId: 'tasks.list',
  input: { categoryId: 'home' }
});
const closed = await client.disconnect();

return {
  status,
  created,
  listed,
  closed
};
Design model

Validate a design

Domain shape, entities and relationships before runtime code.

Proves
The UI model can become executable contracts and examples.
Watch
Category and Task modeling, validation output and generated contract-ready data.

Validate a design

Normalize the sample model and collect validation issues (real designer-core API).

const raw = api.buildSampleModelPayload();
const state = api.normalizeStatePayload(raw);
const issues = api.collectModelIssues(state);
const errors = issues.filter((issue) => issue.severity === 'error');
return {
  ok: errors.length === 0,
  issueCount: issues.length,
  errorCount: errors.length,
  sample: issues.slice(0, 3)
};

PM2 operations

Bun-built services get supervised runtime profiles

Jumentix uses PM2 where a long-running VM or container needs process supervision: environment-specific profiles, logs, status, metrics, reloads and startup recovery. The ecosystem files keep REST, WebSocket, gRPC and Service Management processes explicit.

4named process families
3dev/staging/production profiles
bunPM2 interpreter in ecosystem files
reloadzero-downtime path for compatible services

Why PM2 belongs in the Jumentix story

PM2 is not the only deployment option, but it is a strong operational bridge for teams that run Node/Bun services on persistent machines. It daemonizes apps, restarts failed processes, exposes logs and metrics, supports cluster mode and preserves process lists across restarts.

  • Profiles are environment-specific instead of hidden in ad hoc shell history.
  • REST fallback, realtime and gRPC can be started, inspected and restarted independently.
  • Bun remains the interpreter, so the same runtime choice powers dev scripts and supervised services.
  • The PM2 model complements Docker, serverless and cloud functions without changing domain code.
bun run pm2:start:dev:restapi
bun run pm2:start:staging:websocket-rest
bun run pm2:start:prod:grpc-rest

Adapter catalog

The practical integration surface

Each adapter sits outside the domain. The application selects ports and contracts; the composition root decides which technology runs in each environment.

HTTP runtimes

Express, Fastify, Restify, AWS Lambda, Cloudflare Workers, Vercel Functions, LoopBack, Sails, Feathers, Derby, AdonisJS, and Total.js.

Realtime

Socket.IO, Redis Streams, cluster adapter, gRPC, REST fallback, AsyncAPI documents, and generated realtime clients.

SQL

PostgreSQL, MySQL, SQL Server, Oracle, SQLite, Aurora DSQL, and RDS through shared persistence contracts.

NoSQL

MongoDB, DynamoDB, Cassandra, Firebase, key-value storage, and in-memory adapters for local tests and prototypes.

Messaging

In-memory Message Mediator for local/browser flows, with RabbitMQ and BullMQ-compatible contracts for durable Node workers.

Deployment

PM2, Docker, Serverless, AWS, Azure, Google Cloud, Vercel, Cloudflare, and environment-specific runtime profiles.

Change the driver, preserve the application

Bootstrap adapters read environment configuration, compile the selected database and key-value clients, and inject contracts into the application composition root. That keeps business code independent from Sequelize, Postgres.js, Mongo clients, queues, or HTTP framework request objects.

  • Local test profile can use in-memory adapters without changing use-cases.
  • Production profile can choose a relational or NoSQL adapter per service.
  • Realtime, REST, and function deployments reuse the same application contracts.
const database = await compileDatabaseClient({
  driver: process.env.JUMENTIX_DATABASE_DRIVER,
});

const taskStore = database.stores.Tasks;
await taskStore.create(task);

Selection guide

How to pick the right adapter

The goal is not to support every technology for its own sake. The goal is to let each service choose the smallest reliable infrastructure that fits its data, latency, and operations profile.

DecisionPrefer this whenJumentix mechanismAvoids
Express/Fastify/RestifyYou want a long-running Node service with standard REST semantics and mature middleware.HTTP adapter maps request/response to controller methods and OpenAPI validation.Framework-specific request objects leaking into use-cases.
Cloudflare/Vercel/LambdaTraffic is bursty, globally distributed, or owned by platform function routing.Function adapter wraps the same operation contracts used by REST controllers.A separate business implementation for serverless.
PostgreSQL/MySQL/SQL ServerYou need relational constraints, transactions, reporting, or familiar operations.Database client factory composes repository adapters behind store contracts.SQL decisions coupled to domain entities.
MongoDB/DynamoDB/CassandraData shape, throughput, distribution, or access patterns fit document/key-value/wide-column storage.External persistence adapters implement the same repository contract expected by use-cases.A NoSQL rewrite of the application layer.
RabbitMQ/BullMQCommands or events must survive process restarts and cross service boundaries.Message Mediator contracts define subjects, payloads, request/response, and publish/listen behavior.Consumers importing producer implementations.

Package map

Integration code is packaged, not scattered

@jumentix/adapter-runtime-bootstrap

Centralizes runtime and infrastructure wiring so services avoid one-off bootstraps.

Runtime composition

@jumentix/database-client-factory

Compiles the configured database client and keeps selection rules out of business modules.

Database selection

@jumentix/persistence-contracts

Defines stable store/repository contracts shared by in-memory and external adapters.

Ports

@jumentix/message-mediator

Coordinates commands, requests, responses, and events across modules or services.

Messaging

@jumentix/sdk-rest-client

Reads canonical OpenAPI contracts and exposes typed REST client behavior.

Client SDK

@jumentix/cana

Provides local relational state, event listening, and browser persistence for frontend examples and future apps.

Frontend data

Build the product. Keep the architecture.

Explore the source, run the factory locally, and turn your next Node.js service into a repeatable platform capability.