# Auditing *Layer: Backend · a resource-level opt-in flag* Flip `audit: true` on a resource whose model is an append-only / EAV dimension and codegen mounts a read-only `{slug}_activity` child resource — one row per version of a record, with field-level before/after diffs — plus the FE table and a changeset drawer to browse it. No log table, view, or proxy model by hand. ```{note} Auditing is not yet merged to `main` — it lives on the `commenting-audit` branch (checked out at `fsh-5`). The config surface, the `x-fsh-audit` extension, and the activity resource shape may still change. ``` ## How it works The append-only / EAV factories auto-install an `ActivityViewPlugin` that builds a `{table}_activity` SQL view (one folded row per changeset). You own the parent model's factory and the `audit: true` flag; codegen's `AuditScaffold` walks each flagged resource, emits an ORM proxy over the view and an in-memory [activity resource](resources.md) that flows through the normal pipeline (schemas, routes, serializers, openapi), stamping `x-fsh-audit` so the FE auto-detects it and scaffolds the table + changeset catalog. ## The config you write **You write** — `be/config/inventory_resources/category.jsonnet`: ```jsonnet { model: "inventory.models.Category", pk: { name: "id", type: "uuid" }, route_prefix: "/categories", representation_roles: { default: "default", opa: "default" }, // The entire opt-in. Mounts a read-only `/categories-activity` resource over // the changeset view: get + a searchable list ref-filtered by category id, // ordered by changed_at. Silent no-op unless the model's `__factory__` is an // audit dimension (below) — that's what creates the view it reads. audit: true, operations: [ /* get, list, create, update, delete — the parent's own CRUD */ ], } ``` **You write** — `inventory/models/category.py`: the parent must opt into an audit dimension factory. ```python from codegen_database.factory import CodegenDatabaseAppendOnly # or CodegenDatabaseEAV class Category(Base): __tablename__ = "inventory_categories" __table_args__ = {"schema": "public"} # Installs the ActivityViewPlugin -> creates `inventory_categories_activity`. # Any other factory makes `audit: true` a no-op. __factory__ = CodegenDatabaseAppendOnly ``` ## The frontend codegen emits The scaffold appends the activity `ResourceConfig` in-memory — read-only, over the view's columns (`id`, `{parent}_id`, `changed_at`, `actor` (NULL for historical rows), `change_type` `'create' | 'update'`, `fields` jsonb diff) — and stamps `audit_parent` as `x-fsh-audit` on every route. The FE keys off that extension (`is_activity_list_op`) and emits an ordinary bound table, no FE config: **Generated** — `fe/src/_generated/tables/inventory/category_activity.gen.ts` (do not edit): ```typescript import { useFSHTableController } from "@fsh/components-library"; import { listCategoryActivity } from "../../sdk/inventory/category_activity.gen"; // Detected via x-fsh-audit; the ref filter is the parent's id. export const categoryActivityTableFilters = [ { id: "category_id", label: "Category" }, ] as const satisfies readonly FilterDefinition[]; // useTable hook baked with the SDK fn, cache key, and pagination — // a view only declares columns. See table-configuration.md. ``` The one hand-editable artifact is the changeset catalog, scaffolded once (`if_exists="skip"`). Codegen can't read tracked column names out of the JSONB `fields` diff, so you list them by hand — one entry per tracked column: **Scaffolded, safe to edit** — `fe/src/fields/inventory/categoryActivityChangeset.tsx`: ```typescript import type { ChangesetFieldDef } from "@fsh/components-library"; // { field, label?, render?(value) } — feeds the FSHChangesetDrawer diff. // Add an entry when you add a tracked column, or it renders without a label. export const categoryActivityChangesetFields: ChangesetFieldDef[] = [ { field: "name", label: "Name" }, { field: "description", label: "Description" }, ]; ```