@cosmicdrift/kumiko-framework
v0.5.2
Published
Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.
Maintainers
Readme
@cosmicdrift/kumiko-framework
Framework core for Kumiko — engine, pipeline, API, DB, event-store, and every other bit that makes Kumiko go.
Multi-tenant, command-based, event-sourced app framework for Bun + Hono + Drizzle. Define features, register entities, write commands — the framework wires dispatch, persistence, projections, async subscribers, and realtime delivery.
See the monorepo root README for the broader pitch, the docs/plans directory for architecture, and samples/ for runnable examples of every feature.
Install
yarn add @cosmicdrift/kumiko-framework
# peers you probably already have:
yarn add drizzle-orm hono ioredis zodBun is the intended runtime. Node 20+ works for the CLI and tests.
At-a-glance
import { defineFeature, createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
export const taskEntity = createEntity({
fields: {
title: createTextField({ required: true, searchable: true }),
done: createTextField(),
},
softDelete: true,
});
const taskTable = buildDrizzleTable("task", taskEntity);
const taskExecutor = createEventStoreExecutor(taskTable, taskEntity, { entityName: "task" });
export const taskFeature = defineFeature("tasks", (r) => {
r.entity("task", taskEntity);
// Write handlers go through createEventStoreExecutor — events + projection in one TX,
// optimistic locking, access control, all explicit.
r.writeHandler(
"task:create",
z.object({ title: z.string() }),
async (event, ctx) => taskExecutor.create(event.payload, event.user, ctx.db),
{ access: { roles: ["User"] } },
);
r.writeHandler(
"task:update",
z.object({ id: z.uuid(), version: z.number(), changes: z.object({ title: z.string().optional() }) }),
async (event, ctx) => taskExecutor.update(event.payload, event.user, ctx.db),
{ access: { roles: ["User"] } },
);
r.writeHandler(
"task:delete",
z.object({ id: z.uuid() }),
async (event, ctx) => taskExecutor.delete(event.payload, event.user, ctx.db),
{ access: { roles: ["Admin"] } },
);
r.queryHandler(
"task:list",
z.object({}),
async (query, ctx) => taskExecutor.list(query.payload, query.user, ctx.db),
{ access: { openToAll: true } },
);
r.queryHandler(
"task:detail",
z.object({ id: z.uuid() }),
async (query, ctx) => taskExecutor.detail(query.payload, query.user, ctx.db),
{ access: { openToAll: true } },
);
// Read-model fed from task events, rebuildable via the CLI
r.projection({
name: "tasks-per-day",
source: "task",
table: tasksPerDayTable,
apply: {
"task.created": async (event, tx) => {
/* count++ */
},
},
});
// Async consumer — runs after commit via the event-dispatcher,
// cursor-based, at-least-once, per-consumer dead-letter semantics.
// Omit `table` for pure side-effect handlers (mail, webhooks, ...).
r.multiStreamProjection({
name: "notify-new-task",
apply: {
"task.created": async (event) => {
// e.g. push to an external notification service
},
},
});
});Package exports
| Entry | What's in it |
|---|---|
| @cosmicdrift/kumiko-framework/engine | defineFeature, createEntity, field helpers, access rules, registry |
| @cosmicdrift/kumiko-framework/db | Drizzle re-exports, createEventStoreExecutor, table builders, tenant-db |
| @cosmicdrift/kumiko-framework/event-store | events table, append, loadAggregate, loadAggregateAsOf |
| @cosmicdrift/kumiko-framework/pipeline | Dispatcher, event-dispatcher (AsyncDaemon), projection-rebuild, SSE + search consumers |
| @cosmicdrift/kumiko-framework/api | buildServer, auth middleware, SSE route, error contract |
| @cosmicdrift/kumiko-framework/auth | JWT helper, password hashing, session users |
| @cosmicdrift/kumiko-framework/search | SearchAdapter interface, in-memory adapter, Meili wrapper |
| @cosmicdrift/kumiko-framework/jobs | BullMQ-backed job runner, cron scheduling |
| @cosmicdrift/kumiko-framework/files | Signed-URL upload/download, tenant-scoped storage |
| @cosmicdrift/kumiko-framework/i18n | i18next setup, per-feature translation registration |
| @cosmicdrift/kumiko-framework/ui | React hooks (Zustand stores, SSE subscription, optimistic mutations) |
| @cosmicdrift/kumiko-framework/testing | setupTestStack, createTestDb, request helpers |
| @cosmicdrift/kumiko-framework/utils | Safe JSON, qualified-name helpers |
| @cosmicdrift/kumiko-framework/errors | Error classes, writeFailure, reason contracts |
Core concepts
- Feature as unit of deployment.
defineFeatureregisters entities, write/query handlers, projections, post-event subscribers, lifecycle hooks, access rules, and translations. - Commands in, state out. Writes are commands dispatched through HTTP; the dispatcher validates, enforces access, runs lifecycle hooks, persists events, and triggers projections in a single TX.
- Event-sourced by default. Every write goes through
createEventStoreExecutorand appends a domain event to the aggregate stream. Auto-generated CRUD events (<entity>.created/updated/deleted/restored) for record writes, explicitctx.appendEventfor domain events with intent. Projections feed off the stream for same-TX read-after-write consistency. - Async side-effects via cursor. SSE broadcast, search indexing, and
feature-registered
r.multiStreamProjectionconsumers run on a single cursor-based dispatcher (AsyncDaemon pattern). Per-consumer checkpoints, halt-on-poison, dead-letter after N retries. - Multi-tenant scoping. Every event, entity, projection, and search
index carries
tenantId.TenantDbis a TX-scoped wrapper that refuses writes outside the current tenant. - Optimistic concurrency.
UNIQUE(aggregate_id, version)on events gives atomic append + conflict detection;VersionConflictErrorsurfaces races as a first-class value. - Idempotency + dedup. Request-id backed unique index on the events
table turns retries into replays.
IdempotencyGuardcaches write outcomes per tenant.
Status
This framework is pre-1.0 and evolves fast. Every feature has a runnable
sample under samples/; the roadmap lives in docs/plans/uebersicht.md.
License
BUSL-1.1 — see LICENSE.
