Audit Log Module
Overview
The audit-log module provides a write-once, append-only audit trail for entity mutations across the FlowPOS system. It records who changed what, when, and captures the before/after state of any tracked entity.
Architecture
The module follows hexagonal architecture with strict layer separation:
audit-log/
├── audit-log.module.ts
├── domain/
│ ├── audit-log.domain.ts # CreateAuditLogInput type
│ └── audit-log-repository.domain.ts # IAuditLogRepository port + sortable keys
├── application/
│ └── audit-log.service.ts # Use cases: log, getByEntity, getAll
├── infrastructure/
│ └── audit-log.repository.ts # Kysely adapter (implements IAuditLogRepository)
└── interfaces/
├── audit-log.controller.ts # HTTP layer (GET endpoints)
└── query/
└── paginate-audit-log.query.ts # Query params DTO
Layer Responsibilities
| Layer | Responsibility |
|---|---|
domain/ | Defines CreateAuditLogInput (pure TS) and IAuditLogRepository contract. No framework imports. |
application/ | Orchestrates use cases: create entry, query by entity, paginate all entries. |
infrastructure/ | Kysely implementation of IAuditLogRepository. All SQL lives here. |
interfaces/ | Thin HTTP controller. Maps query params to service calls, handles errors uniformly. |
Database Schema
Table: audit_log
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key (auto-generated) |
entity | text | Entity type, e.g. accounts_payable_bill |
entity_id | UUID | ID of the audited record |
action | text | Action performed: create, update, delete, auto_paid, etc. |
old | jsonb | Snapshot of the entity before the action (null for creates) |
new | jsonb | Snapshot of the entity after the action (null for deletes) |
user_id | UUID | FK to user.id (nullable, ON DELETE SET NULL) |
created_at | timestamptz | Timestamp of the event |
Indexes:
idx_audit_log_entity_entity_id— composite(entity, entity_id)for fast entity history queriesidx_audit_log_created_at— for time-range queries and default sort
Domain Concepts
CreateAuditLogInput
Input shape consumed by AuditLogService.log():
interface CreateAuditLogInput {
entity: string; // Entity type string
entityId: string; // UUID of the entity instance
action: string; // Action label: create | update | delete | ...
oldValue?: JsonValue; // Before-state snapshot (null for creates)
newValue?: JsonValue; // After-state snapshot (null for deletes)
userId?: string; // Actor UUID (nullable for system actions)
}
Immutability
Audit log records are never updated or deleted. The repository only exposes create, findByEntity, and findAll. This is intentional — audit data must be tamper-proof.
Use Cases
1. Log an entity mutation
Used by other modules to record a change. Typically called inside a Kysely transaction:
// Inside a transactional service method
await this.auditLogService.log(
{
entity: "accounts_payable_bill",
entityId: bill.id,
action: "update",
oldValue: previousSnapshot,
newValue: updatedSnapshot,
userId: actorId,
},
trx, // Pass the Kysely transaction for atomicity
);
2. Query entity history
Returns all audit entries for a specific record, newest first:
const history = await this.auditLogService.getByEntity(
"accounts_payable_bill",
billId,
);
3. Paginated global query
Returns a filtered, sorted, paginated list:
const page = await this.auditLogService.getAll({
page: 1,
size: 20,
orderBy: "createdAt",
order: SortOrder.Desc,
filter: { "auditLog.entity": "accounts_payable_bill" },
});
API Endpoints
Base path: /audit-log
GET /audit-log
Returns a paginated list of audit entries.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
entity | string | No | Filter by entity type |
entityId | UUID | No | Filter by entity instance UUID |
userId | UUID | No | Filter by actor UUID |
search | string | No | Search entity type and action fields |
page | number | No | Page number (default: 1) |
size | number | No | Page size (default: 10; 0 = no limit) |
orderBy | enum | No | createdAt | entity | action |
order | enum | No | asc | desc (default: asc) |
Example request:
GET /audit-log?entity=accounts_payable_bill&page=1&size=20&orderBy=createdAt&order=desc
Authorization: Bearer <token>
Response: IOffsetPagination<SelectableAuditLog>
GET /audit-log/:entity/:entityId
Returns all audit entries for a specific entity instance.
Path parameters:
| Parameter | Description |
|---|---|
entity | Entity type string, e.g. accounts_payable_bill |
entityId | UUID of the entity instance |
Example request:
GET /audit-log/accounts_payable_bill/3fa85f64-5717-4562-b3fc-2c963f66afa6
Authorization: Bearer <token>
Response: SelectableAuditLog[] ordered by createdAt desc
Transaction Safety
The log() method accepts an optional trx (Kysely transaction) parameter. Always pass the active transaction when the audit log is part of a mutation:
await this.database.transaction().execute(async (trx) => {
const updated = await this.billRepository.update({ id, entity, trx });
await this.auditLogService.log({ ...auditPayload, userId }, trx);
});
This guarantees that if the entity update fails, the audit record is also rolled back — preventing orphaned audit entries.
Currently Audited Entities
| Module | Entity Key | Actions |
|---|---|---|
| Accounts Payable Bills | accounts_payable_bill | create, update, auto_paid |
| Accounts Payable Payments | accounts_payable_payment | create, update |
| Accounts Receivable Receipts | accounts_receivable_receipt | create, update |
To add auditing to a new module, import AuditLogModule and inject AuditLogService.
Bruno API Collection
Located at: api-client/flowpos/collections/audit-log/
| File | Description |
|---|---|
audit logs.yml | Paginated list with all optional filters |
audit log by entity.yml | Entity-specific history (uses {{auditEntityType}} and {{auditEntityId}} vars) |
Set auditEntityType and auditEntityId in your Bruno environment variables before running the entity history request.