Skip to content

Audit in Dynamic Record API ​

Per-table Controls ​

In RecordTableType:

  • disableAuditLog: disable built-in audit for the table
  • customAuditLog: custom handler for create/update/delete operations
  • columnHiddens: sensitive columns (e.g. password, remember_token) that are unconditionally stripped (unset) from sp_audit_logs.old_data, sp_audit_logs.new_data, metadata.field_changes, update recap messages, and field timeline/stats endpoints. Hidden columns are never displayed in changed-field lists on UI audit feeds.

Security & Sensitive Field Sanitization ​

When audit records are generated, sensitive attributes configured under columnHiddens (along with audit.excluded_attributes) are automatically removed:

  • Mutations affecting hidden fields still generate an audit log entry so the mutation history is preserved, but values are omitted from persisted data payloads.
  • Recap summaries exclude hidden fields.
  • AuditLogService::getFieldTimeline() and AuditLogService::getFieldStats() return empty data for hidden fields.
  • Real-time broadcasting (RecordMutated event), outgoing webhook deliveries (WebhookTrigger), and Eloquent model audit logs (AuditableTrait) also strip hidden columns by default.

Global Config Dependencies ​

  • audit.enabled
  • audit.queue_enabled
  • audit.log_relationships

Request Columns (user_id, ip_address, user_agent, request_id) ​

audit.queue_enabled is the only switch that decides whether an audit entry is queued — for HTTP CRUD and for RecordService::executeCreate/Update/Delete (the MCP tools, the AI SDK record tools, attachments and your own service calls) alike. With it off, every row is written during the request, whatever your QUEUE_CONNECTION is. With it on, AuditLogJob is queued only after the surrounding transaction commits, so a write that is rolled back leaves no audit entry.

Each row records who made the request and from where. With audit.queue_enabled, the row is written later by a queue worker, which has no HTTP request and no authenticated user. The package captures these values when the entry is queued, carries them with the job through Laravel's Context (hidden, so they never appear in logs), and writes them in the worker. The same values fill metadata.change_summary and metadata.user_id / user_name. A custom audit.job_class needs no change, because Context travels with every queued job.

Row Narrative (title, subject, recap) ​

These three human-readable columns are never stored empty. A value you supply always wins; blanks are filled from the event and entity:

ColumnFilled withFallback when that is empty
titlegetAuditTitle(event, entity) — e.g. Updated Settingsthe entity label
subjectthe first present audit.subject_fields value{Entity} #{entity_id}
recapgenerateRecap() — e.g. Updated Settings: Namethe resolved title

recap falls back most often on a record's first update: the previous state is read from the last audit row rather than the live row, so there is nothing to diff against and the generated recap is empty. Later updates diff normally and name the changed fields.

Tenant IDs may be int or string throughout this path — record.id_type of integer is fully supported.

Handler Context ​

Custom handler receives event/entity/audit data plus runtime context (request, table, operation, record_context).

Optional Global Mutation Policy ​

audit.filter defaults to null; configure it in published config/sp-audit.php as an AuditLogFilterInterface class or [ClassName::class, 'method']. The container resolves instance dependencies. A false result skips the built-in submission before audit preparation/dispatch; true continues normal processing. Invalid config/results or exceptions retain logging with a sanitized warning.

The five arguments are event enum, table, incoming audit payload, nullable actor, and runtime context. Context includes request, operation, authoritative tenant_id, record_context, request_context, and source (record). Snapshot/diff completeness is not guaranteed. Keep the policy stateless for long-running workers.

Existing custom logger return values are ignored: callable means handled, even for void/null/false. Global filtering does not suppress custom callback invocation. Table/global triggers and broadcasting continue when the built-in submission is rejected. Existing custom-handler early-return behavior is unchanged.

Normal post-write logic and the separate bulk-upsert wrapper both use the policy. Existing bulk upsert also audits through its inner update path; each submission gets its own decision. Authentication and direct low-level processing/trait calls are outside this hook. The package's record lifecycle listener (LogRecordAuditListener, behind RecordService::executeCreate/Update/Delete) is not queued: it calls log during the request, so the policy sees the originating request and actor. Only audit.queue_enabled moves the row write to a worker, and that happens after the policy has decided.

See audit policy configuration and coverage.