---
title: Audit Log
description: 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.

```bash
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.

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

```bash
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`.

```bash
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.

```bash
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.

```ts
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.
