sveltekit-admin does not ship an AuditLog table or a dedicated log UI. It
provides an opt-in audit callback after every successful create, update,
or delete. The callback receives a redacted AuditEvent; persistence and
viewing are application concerns.
Reads (GET), logout, and _search are not audited. A POST that fails
validation or the database write does not call audit. If audit is omitted,
there are no extra reads or calls.
A production-oriented Prisma model
This model keeps the event envelope queryable while storing snapshots in JSON. actorId is a string so an application can use either numeric or string user
IDs; the callback below normalizes the value. Add a tenant key when the same
database contains more than one tenant.
Use a compound index that matches the viewer’s most common filter (for
example, [tenantId, at]). Keep the log append-oriented: avoid exposing the
JSON snapshots to ordinary application code, and decide whether the database
should enforce retention, partitioning, or archival before the table grows.
Persist the callback
The exact callback type is AuditEvent. Its action discriminant determines
which fields exist:
Put the signed-in administrator in event.locals in your auth handle (see Authentication). Then persist the event without
reconstructing or bypassing its redaction:
The library strips fields whose names contain password, hash, secret, or token, as well as fields listed in models[].hidden, from values, before, after, and changes. Do not add raw request data or query the
original row again in the callback to “fill in” omitted fields: that defeats
the redaction boundary. Treat event.locals as your actor source and store
only the identity your authorization layer has already established.
The callback is awaited before the 303 redirect, so a successful prisma.auditLog.create commits before the response. There is no transaction
between the adapter write and your sink. If the callback throws, the mutation
still redirects and the handler logs [sveltekit-admin] audit callback failed:; monitor that error if audit records
are compliance-critical.
A scoped, read-only viewer in the admin
Leave AuditLog out of exclude if you want it to appear as a normal admin
model. Mark every editable scalar as readonly, select only useful list
columns, and apply both authorization and tenant scoping:
The example intentionally admits only tenant-bound admins/auditors. A global
administrator needs a separate explicit policy (for example, a dedicated
cross-tenant viewer); do not silently treat a missing tenantId as “all
tenants.”
models[].scope is the authorization boundary for this model: it is composed
into list, count, detail, edit, delete, and mutation queries. It must return a
non-empty condition for every request; an empty object is rejected rather than
being treated as “no scope.” models[].listWhere is different: it scopes only
the list view (including its search, filters, count, and pagination). Do not
use listWhere alone to protect tenant data.
The example’s scope assumes tenantId is present in event.locals for every
authorized request. Adapt the field and locals path to your schema. Never fall
back to an unscoped query when the tenant is missing. If logs are global,
replace the tenant scope with an explicit policy rather than accidentally
making them visible to every authenticated user.
readonly prevents field editing, but it does not make a model an immutable,
append-only store or remove the model’s delete action. If administrators must
never delete audit records, use exclude: ['AuditLog'] and provide a separate
viewer with a deny-delete policy, or enforce retention and delete permissions
outside this package. Excluding the model also means its writes cannot recurse
through the admin UI; keep the callback guard when other code can write it.
Retention, indexing, and security checklist
- Persist only the redacted event fields; never log raw
FormData, request headers, cookies, tokens, passwords, or a second unredacted ORM snapshot. - Resolve the actor from the authenticated
event.locals, and store a stable ID plus a display label only if your privacy policy permits it. - Require an explicit tenant scope for tenant-owned logs. Test missing,
expired, and cross-tenant sessions; fail closed instead of returning
{}. - Restrict the viewer with
authCheck, mark columnsreadonly, and avoid putting large JSON snapshots inlistFieldsif the list becomes costly. - Decide whether audit records themselves are sensitive: protect JSON snapshots, database backups, exports, and support tooling accordingly.
- Index the time range and the filters the viewer uses (
tenantId,model,recordId, oractorId); do not index every JSON property by default. - Define retention, archival, and deletion controls before production. Database-level partitioning or an external immutable sink may be more appropriate for compliance workloads.
- Monitor callback failures. The business write remains successful when the callback throws, so a failed sidecar must be observable and recoverable.