Skip to content
PharmaFlow Knowledgebase
Esc
↑↓navigate↵open⌘Jpreview
On this page

Audit Log

Query the append-only audit trail by entity, action, branch, user, search text, and date window.

You trace every sensitive change through the audit_log table in packages/database/schema.sql. Writers call logAction from apps/server/src/core/audit/audit.ts, or insertAuditEntry from apps/server/src/core/audit/outbox.ts when the row is written inside a transaction, and you read rows through the admin-only routes in apps/server/src/features/audit/route.ts.

What gets logged

You will find LOGIN and PASSWORD_CHANGE from apps/server/src/features/auth/service.ts, user CREATE, UPDATE, and RESET_PASSWORD from apps/server/src/features/users/service.ts, medicine IMAGE_UPDATE and org LOGO_UPDATE from apps/server/src/features/images/service.ts, changelog CREATE from apps/server/src/features/ops-visibility/service.ts, and sale REPLAY from apps/server/src/features/sync/service.ts when an offline sale is pushed. Each row stores the actor, branch, entity, action, entity id, before and after JSON, IP, and timestamp.

Reading the log

Both audit routes require authentication plus the admin role. You list entries newest first with pagination defaulting to page 1 and 50 rows.

curl "http://localhost:8000/audit?page=1&page_size=50" \
  -H "Authorization: Bearer $TOKEN"

You get items, total, page, and page_size. Every item joins the actor full_name as user_name for display.

Filtering by entity and action

You narrow the trail with entity, action, branch_id, and user_id query parameters. Combine them freely to answer questions like who reset passwords on a branch this week.

curl "http://localhost:8000/audit?entity=user&action=RESET_PASSWORD&branch_id=BRANCH_ID" \
  -H "Authorization: Bearer $TOKEN"
curl "http://localhost:8000/audit?user_id=USER_ID&action=LOGIN" \
  -H "Authorization: Bearer $TOKEN"

Searching and date windows

You full-text search with search, which matches entity, entity id, action, actor name, and both JSON snapshots. You bound the window with from_date and to_date.

curl "http://localhost:8000/audit?search=DEMO10&from_date=2026-01-01&to_date=2026-12-31" \
  -H "Authorization: Bearer $TOKEN"

Note the date handling in apps/server/src/features/audit/route.ts: from_date shifts back 3 hours and to_date shifts forward 21 hours, so a plain calendar date covers the full local day.

Reading one entry

You fetch a single row with GET /audit/:auditId. Unlike the list, this endpoint parses before_json and after_json into objects for you, falling back to raw text when a snapshot is not valid JSON.

curl http://localhost:8000/audit/AUDIT_ID \
  -H "Authorization: Bearer $TOKEN"

A missing id returns 404 with Audit entry not found.

Writing audit rows from your code

You call logAction with the actor, branch, entity triplet, and optional snapshots. Keep before and after small and free of secrets, since any admin can read them and search matches against them. A failed insert is logged and swallowed by logAction, so a broken audit write never blocks the operation it trails.

import { logAction } from "../../core/audit/audit";

await logAction({
	userId: currentUser.sub,
	branchId: currentUser.branch_id,
	entity: "medicine",
	action: "PRICE_CHANGE",
	entityId: medicineId,
	before: { unit_price: oldPrice },
	after: { unit_price: newPrice },
});

Next steps

See who the actors are in /platform/authentication-and-users, how branches scope the trail in /platform/branches, and the full endpoint catalog in /api.

Last updated on October 20, 2026