tags
Generic, host-agnostic tagging for any entity. Owns two event-sourced entities — the per-tenant tag catalog (read_tags, with optional color and scope) and tag-assignment join rows keyed by (entityType, entityId) (read_tag_assignments) — so tagging adds NO column to the host entity and needs no relational pivot or JOIN. Catalog screens are declarative (entityList + entityEdit) and use convention QNs tag:{create,update,delete}; TagManager/TagPicker keep create-tag/update-tag/delete-tag. Also: assign-tag (idempotent), remove-tag (idempotent) and list queries for the catalog and the assignments. Read which tags an entity has, or which entities carry a tag, by listing tag-assignment filtered on entityId or tagId and composing in the read-layer. A tag with empty scope is global; a scope of an entityType restricts it to that type in the picker. Every path uses one access rule — adopt the host’s model with createTagsFeature({ access: { openToAll: { reason } } }) or pin roles with createTagsFeature({ roles }). Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).
Quick example
Section titled “Quick example”From recipes-tags-basic — the smallest working mount:
// kumiko-feature-version: 1// Tags Basic Sample//// Shows the whole point of the `tags` bundle: tagging an entity needs ZERO// changes to that entity. The `note` entity below has no tag column, no// `wireTagsFor`, no awareness of tags at all — yet notes can be tagged and// grouped, because the tags feature owns its own tables (read_tags +// read_tag_assignments) and keys assignments by (entityType, entityId).//// Flow (see the integration test):// 1. App-author defines a plain `note` entity — nothing tag-specific.// 2. A tenant creates a tag via `tags:write:create-tag`.// 3. The tag is attached to a note via `tags:write:assign-tag`// with { tagId, entityType: "note", entityId: <noteId> }.// 4. "Which tags does this note have?" / "Which notes carry this tag?"// are read-layer compositions: list `tag-assignment` filtered by// entityId or tagId — no JOIN, no column on `note`.
import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";import { z } from "zod";
// --- Entity ---//// A plain entity. Note there is NOTHING here that mentions tags — that is the// feature's promise: any entity is taggable as-is.
export const noteEntity = createEntity({ table: "read_sample_tags_notes", fields: { title: createTextField({ required: true, maxLength: 200 }), },});
const noteTable = buildEntityTable("note", noteEntity);
const noteExecutor = createEventStoreExecutor(noteTable, noteEntity, { entityName: "note" });
// --- Feature ---
export const noteFeature = defineFeature("note-management", (r) => { // tags is non-optional for this recipe: the demo tags notes. The note // feature itself stays completely tag-agnostic — it only declares the // dependency so the bundle is mounted. r.requires("tags");
r.entity("note", noteEntity);
r.writeHandler({ name: "note:create", schema: z.object({ id: z.string(), title: z.string() }), access: { roles: ["TenantAdmin"] }, handler: async (event, ctx) => noteExecutor.create({ id: event.payload.id, title: event.payload.title }, event.user, ctx.db), });
r.queryHandler({ name: "note:list", schema: z.object({}), access: { roles: ["TenantAdmin"] }, handler: async (_query, ctx) => { const rows = await ctx.db.selectMany(noteTable); return { rows }; }, });});📄 On GitHub: samples/recipes/tags-basic/src/feature.ts
Live preview
Section titled “Live preview”
How it fits
Section titled “How it fits”The write commands this feature provides (Provides).
flowchart TB
n_tags["tags"]
subgraph how_provides["Provides"]
n_cmd_tags_write_assign_tag(["assign-tag"])
n_cmd_tags_write_create_tag(["create-tag"])
n_cmd_tags_write_delete_tag(["delete-tag"])
n_cmd_tags_write_remove_tag(["remove-tag"])
n_cmd_tags_write_tag_create(["create"])
n_cmd_tags_write_tag_delete(["delete"])
n_cmd_more(["+2 more"])
end
n_tags --> n_cmd_tags_write_assign_tag
n_tags --> n_cmd_tags_write_create_tag
n_tags --> n_cmd_tags_write_delete_tag
n_tags --> n_cmd_tags_write_remove_tag
n_tags --> n_cmd_tags_write_tag_create
n_tags --> n_cmd_tags_write_tag_delete
n_tags --> n_cmd_more
Provides — write commands this feature registers (dispatch them through the command bus):
tags:write:assign-tagtags:write:create-tagtags:write:delete-tagtags:write:remove-tagtags:write:tag:createtags:write:tag:deletetags:write:tag:updatetags:write:update-tag
Getting started
Section titled “Getting started”Start with recipes-tags-basic for a step-by-step walkthrough with runnable code and integration tests.
Dependencies
Section titled “Dependencies”- Requires: none
- Activation: always on (not toggleable)