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: { reason: "every signed-in tenant member may read and add notes on shared records" }, },});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
recipes-notes-history-basic: the full append → read flow against a plaintaskentity.