Skip to content

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).

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

tags feature preview

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):

Start with recipes-tags-basic for a step-by-step walkthrough with runnable code and integration tests.

  • Requires: none
  • Activation: always on (not toggleable)