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.
Prerequisites
Section titled “Prerequisites”- A feature already running locally. The Walkthrough covers the first one.
- You’ve read Schemas as data so the field factories make sense.
The code
Section titled “The code”In your feature’s schema/ folder, define the new entity:
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:
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.
What you get for free
Section titled “What you get for free”The framework derives a SQL 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.
Common gotchas
Section titled “Common gotchas”- Entity names are kebab-case by convention. Multi-word entities
like
user-profilework;userProfiledoes not (the boot validator rejects it). r.entityand the screen registration must use the same name. Addingr.entity("tag", …)but registeringtagListScreenwithentity: "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
notefield withtype: "reference", the reference resolves through the framework, no manual join.
Live example
Section titled “Live example”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.crud("task", taskEntity, { write: editorWrite, read: openRead, verbs: { delete: false, restore: false }, }); r.writeHandler(defineEntityDeleteHandler("task", taskEntity, adminWrite)); r.writeHandler(defineEntityRestoreHandler("task", taskEntity, adminWrite));});Full source: samples/recipes/basic-entity.
See also
Section titled “See also”- 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.