Skip to content

Add an entity to a feature

You already have a feature with one entity. You want to add a second one — same defaults, full CRUD, a list screen, an edit screen, and a nav entry.

  • A feature already running locally. The Walkthrough covers the first one.
  • You’ve read Schemas as data so the field factories make sense.

In your feature’s schema/ folder, define the new entity:

src/features/notes/schema/tag.ts
import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
export const tagEntity = createEntity({
fields: {
name: createTextField({ required: true, sortable: true, searchable: true }),
color: createTextField({ default: "#888888" }),
},
});

Wire it into the feature alongside the existing entity:

src/features/notes/feature.ts
import {
defineEntityCreateHandler,
defineEntityDeleteHandler,
defineEntityDetailHandler,
defineEntityListHandler,
defineEntityUpdateHandler,
} from "@cosmicdrift/kumiko-framework/engine";
import { tagEntity } from "./schema/tag";
export const notesFeature = defineFeature("notes", (r) => {
// existing note entity stays
r.entity("note", noteEntity);
// new tag entity
r.entity("tag", tagEntity);
r.writeHandler(defineEntityCreateHandler("tag", tagEntity));
r.writeHandler(defineEntityUpdateHandler("tag", tagEntity));
r.writeHandler(defineEntityDeleteHandler("tag", tagEntity));
r.queryHandler(defineEntityListHandler("tag", tagEntity));
r.queryHandler(defineEntityDetailHandler("tag", tagEntity));
r.nav({ id: "tags", label: "notes:nav.tags", order: 20, screen: "tag-list" });
});

Add list and edit screens (same shape as the note ones), translations for the new keys, and you’re done. Hot-reload picks it up.

The framework derives a Drizzle table, a Zod schema, the default access rules, list pagination, sortable columns, and a search index entry — all from the entity definition. You wrote the schema; the framework wrote the rest.

If you need custom behaviour on save (a slug from a name, validation across multiple fields), drop in a hook:

r.hook("preSave", "tag", async (ctx, { changes, data }) => {
if (changes.name && !changes.color) {
return { ...data, color: stringToHexColor(data.name) };
}
return data;
});

The hook runs in the same transaction as the write — if it throws, the whole thing rolls back. See Lifecycle and hooks for the contract.

  • Entity names are kebab-case by convention. Multi-word entities like user-profile work; userProfile does not (the boot validator rejects it).
  • r.entity and the screen registration must use the same name. Adding r.entity("tag", …) but registering tagListScreen with entity: "tags" (plural) gives you a “no such entity” boot error.
  • Cross-feature lookups go through the dispatcher. If your tag entity is referenced by a note field with type: "reference", the reference resolves through the framework — no manual join.

A complete feature wiring one entity with default CRUD plus soft-delete/restore — the smallest useful end-to-end:

export const taskFeature = defineFeature("tasks", (r) => {
r.entity("task", taskEntity);
// Writes append CRUD-style events onto the task stream and update the
// projection row in the same TX (the executor inside the helper takes care
// of both). Custom logic? Replace any single line with an explicit
// r.writeHandler.
r.writeHandler(defineEntityCreateHandler("task", taskEntity, editorWrite));
r.writeHandler(defineEntityUpdateHandler("task", taskEntity, editorWrite));
r.writeHandler(defineEntityDeleteHandler("task", taskEntity, adminWrite));
r.writeHandler(defineEntityRestoreHandler("task", taskEntity, adminWrite));
// Reads served from the projection table.
r.queryHandler(defineEntityListHandler("task", taskEntity, openRead));
r.queryHandler(defineEntityDetailHandler("task", taskEntity, openRead));
});

Full source: samples/recipes/basic-entity.

  • Schemas as data — what the entity definition is and what gets derived.
  • Commands and queries — the pipeline around the auto-generated CRUD handlers.
  • Walkthrough — the full first-feature tutorial if you don’t yet have a feature to extend.