Skip to content

Golden path

The smallest complete Kumiko feature: one entity, one write handler that emits an event, one projection, one screen, and an integration test that writes through HTTP and reads the result back. Every other recipe is a variation on this shape. The walkthrough is in docs/golden-path.md.

  • Entity — ticketEntity declares title and isClosed. r.crud registers ticket:create, ticket:list and ticket:detail for it; update and delete are switched off with verbs.
  • Write handler — ticket:close checks that the ticket exists and emits the ticket-closed event. It never touches the table.
  • Event — r.defineEvent declares ticket-closed with a Zod payload; emitEvent appends it to the ticket’s stream in the handler’s transaction.
  • Projection — ticket-status folds ticket-closed into the ticket row (isClosed: true), in the same transaction as the write.
  • Screen — ticket-list is a declarative entityList; the renderer builds the UI from it.
  • Test — setupTestStack boots the real server and database; stack.http.writeOk and queryOk go over HTTP. Each test runs in its own tenant.
src/feature.ts entity, event, handler, projection, screen
src/__tests__/feature.integration.test.ts write → read back, access, not found

The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):

import {
createBooleanField,
createEntity,
createEntityExecutor,
createTextField,
defineFeature,
emitEvent,
setFields,
} from "@cosmicdrift/kumiko-framework/engine";
import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
import { z } from "zod";
export const ticketEntity = createEntity({
table: "read_golden_path_tickets",
fields: {
title: createTextField({ personal: false, reason: "is_business_data", required: true }),
isClosed: createBooleanField({ default: false }),
},
});
export const ticketingFeature = defineFeature("ticketing", (r) => {
const { table: ticketTable, executor: ticketExecutor } = createEntityExecutor(
"ticket",
ticketEntity,
);
r.crud("ticket", ticketEntity, {
write: { access: { roles: ["Admin", "User"] } },
read: { access: { roles: ["Admin", "User"] } },
verbs: { update: false, delete: false },
});
const ticketClosed = r.defineEvent("ticket-closed", z.object({ resolution: z.string().min(1) }), {
piiFields: "none",
});
r.writeHandler({
name: "ticket:close",
schema: z.object({ id: z.uuid(), resolution: z.string().min(1) }),
access: { roles: ["Admin"] },
handler: async (event, ctx) => {
const ticket = await ticketExecutor.detail({ id: event.payload.id }, event.user, ctx.db);
if (!ticket) {
return failNotFound("ticket", event.payload.id);
}
if (ticket["isClosed"]) return failUnprocessable("ticket_already_closed");
await emitEvent(ctx, ticketClosed, {
aggregateId: event.payload.id,
aggregateType: "ticket",
payload: { resolution: event.payload.resolution },
});
return { isSuccess: true as const, data: { id: event.payload.id } };
},
});
r.projection({
name: "ticket-status",
source: "ticket",
table: ticketTable,
apply: {
[ticketClosed.name]: setFields(ticketTable, { isClosed: true }),
},
});
r.screen({
id: "ticket-list",
type: "entityList",
entity: "ticket",
columns: ["title", "isClosed"],
});
});

📄 On GitHub: samples/recipes/golden-path/src/feature.ts