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. Provides write-handlers create-tag, update-tag (optimistic-locked rename/recolor/re-scope), delete-tag (cascades over assignments), 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: true } }) 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”What this feature needs to run (Requires, top) and the write commands it provides (Provides, bottom).
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_update_tag(["update-tag"])
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_update_tag
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: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)