Notes History
A bundled feature that turns any entity’s single overwritable notes field into a chronological, authored history — without adding a column, a join table, or a migration. Every note is a fresh, timestamped, authored entry; nothing is ever overwritten.
Status: ✅ Stable
What: A host-agnostic, append-only note-entry per (entityType, entityId). Each entry carries a body, and the author + timestamp come for
free from the framework’s base columns (insertedById / insertedAt) — no
custom author/date fields to manage. Read an entity’s history by listing
entries filtered on entityId, sorted by insertedAt.
When you reach for it: A record has a single “Notes” textarea today and you need to know who wrote what and when — call logs, case notes, internal comments — without building a comment/activity-log table per feature.
How it works
Section titled “How it works”One event-sourced entity, no relational pivot and no JOIN:
note-entry(read_note_entries) — host-agnostic rows keyed by(entityType, entityId). The framework projects the table from its own create events, so attaching a note history adds no column to the host entity.
Cross-entity views compose in the read-layer — this entity’s note
history lists entries filtered on entityId, sorted by insertedAt. No
JOIN, no pivot.
Strictly append-only
Section titled “Strictly append-only”Only two handlers are registered: add-note (create) and the list query.
There is no update, no delete. A correction is a new entry, not an edit —
that’s the entire reason this bundle exists instead of a single overwritable
textarea. The author is never client-supplied: add-note always attributes
the note to the authenticated caller, so a note can’t be authored as someone
else.
No deterministic aggregate-id
Section titled “No deterministic aggregate-id”Unlike tags’ tag-assignment, note-entry
has no dedup key — an entity legitimately carries many notes, so every
add-note is an ordinary random-id stream. There’s nothing to make
idempotent.
Web UI
Section titled “Web UI”Mount the client plugin once (for the component + i18n), then drop
<NotesSection> where you need it:
import { runDevApp } from "@cosmicdrift/kumiko-dev-server";import { createNotesHistoryFeature } from "@cosmicdrift/kumiko-bundled-features/notes-history";import { notesHistoryClient } from "@cosmicdrift/kumiko-bundled-features/notes-history/web";
// server: mount the feature (roles default to TenantAdmin/TenantMember)await runDevApp({ features: [createNotesHistoryFeature()] });
// client: register the component + translations oncecreateKumikoApp({ clientFeatures: [notesHistoryClient()] });NotesSection — the note history
Section titled “NotesSection — the note history”Shows every note for an entity newest-first (author + timestamp), plus a
textarea to append a new one. It owns its own state — not part of a host
form’s save. Mount it standalone or as an entityEdit extension section:
import { NotesSection, NOTES_SECTION_EXTENSION_NAME } from "@cosmicdrift/kumiko-bundled-features/notes-history/web";
// standalone — drop it into any screen:<NotesSection entityName="contact" entityId={contactId} />
// or as an extension section in an entityEdit screen schema:{ kind: "extension", title: "Notes", component: { react: { __component: NOTES_SECTION_EXTENSION_NAME } } }The list is capped at 200 entries (newest-first) — there is no “load more”
yet. An entity with a longer history only shows its 200 most recent notes;
upgrade to cursor pagination (the query already supports cursor) if that
limit is ever hit in practice.
Handlers
Section titled “Handlers”The feature is driven entirely by dispatching its handler — nothing is wired into the noted entity.
| Handler | Payload | Notes |
|---|---|---|
notes-history:write:add-note | { entityType, entityId, body } | Author is always the authenticated caller, never the payload |
Reads are the one list query — notes-history:query:note-entry:list
(filter on entityId, sort by insertedAt).
// Append a note to any entity by (type, id) — no column on that entity{ "entityType": "contact", "entityId": "<contact-uuid>", "body": "Called about renewal." }Access control
Section titled “Access control”Every path uses one access rule. The default is
{ roles: ["TenantAdmin", "TenantMember"] }. Apps with a different role
vocabulary adopt their own model — otherwise the handler is access_denied
for their users:
// pin roles (e.g. a global SystemAdmin operator also writes notes) …createNotesHistoryFeature({ roles: ["TenantAdmin", "TenantMember", "SystemAdmin"] });
// … or adopt the host's whole modelcreateNotesHistoryFeature({ access: { openToAll: true } });GDPR — export and erasure
Section titled “GDPR — export and erasure”body is annotated userOwned (owner: authorId) on the entity, so a
mounted crypto-shredding KMS encrypts it per-author and erases it on forget
by destroying that author’s subject key — without needing a physical delete,
which would otherwise break the history for other people who read it.
That coverage isn’t automatic: mount the companion feature
notes-history-user-data alongside user-data-rights for a user’s authored
notes to appear in GDPR exports. Without a mounted KMS, userOwned fields
fall back to plaintext storage framework-wide (a property of the
crypto-shredding design, not specific to this bundle) — apps needing real
Art. 17 coverage for notes must mount one.
import { createNotesHistoryFeature } from "@cosmicdrift/kumiko-bundled-features/notes-history";import { notesHistoryUserDataFeature } from "@cosmicdrift/kumiko-bundled-features/notes-history-user-data";
await runDevApp({ features: [createNotesHistoryFeature(), notesHistoryUserDataFeature] });When not to use notes-history
Section titled “When not to use notes-history”- You need to edit or retract a note — this bundle is deliberately append-only. If mutable notes-with-history is genuinely the requirement, this isn’t it.
- The note drives business logic — that’s a first-class field on the entity, not free-text history.
- You need rich text / attachments —
bodyis a plainlongTextfield; there’s no formatting or file-attachment support.
See also
Section titled “See also”- Bundled-features overview
tags— the sibling bundle this one’s(entityType, entityId)pattern is modeled on.- Recipe
samples/recipes/notes-history-basic— the full append → read flow against a plaintaskentity. It has no docs page yet.