Skip to content

Audit service ​

Audit owns immutable change history inside the modular monolith. Admin and Organization own all audit HTTP routes. Audit has no HTTP presentation layer, route registration, or deployment of its own.

Recorded history ​

Working organization changes, admin membership creation, organization membership creation/removal, team creation, active-context changes, authorization changes, and admin-driven Identity provisioning append history in the same PostgreSQL transaction as their authoritative write. Failed audit validation or insertion rolls that transaction back. An append failure aborts the transaction even when its error is ignored.

Records preserve an actor user ID, optional membership identity, actor-name snapshot, event/resource, approved field-level before/after values, history scope, affected organization, request/operation correlation, transport peer IP, and user agent. NewOperation uses the verified user ID as the actor name when the supplied name is empty or whitespace; supplied display names remain unchanged. They do not include credentials, hashes, tokens, raw request bodies, or arbitrary metadata. No-op business changes produce no entry. Names stay as recorded even after a profile rename or deletion.

Admin-originated changes remain in admin history, including delegated organization changes. Organization-originated changes belong only to that organization's history. Context switches that affect several memberships record each changed membership in its own organization's history, without including the other organizations' identifiers.

Account provisioning and membership creation retain separate commits. A failed membership step can leave an existing, audited Identity account; the successful account and membership steps share an operation ID. Ordinary registration, login, read access, failed requests, exports, and unimplemented 501 workflows are not covered.

HTTP owners and grants ​

OwnerEndpointsPermission
AdminGET /api/v1/admin/audit-events, GET /api/v1/admin/audit-events/{eventId}admin.audit.read
OrganizationGET /api/v1/organizations/{organizationId}/audit-events, GET /api/v1/organizations/{organizationId}/audit-events/{eventId}org.audit.read

Protected scope owners inherit these explicit catalog permissions. Other users require grants. A platform grant does not satisfy an organization history check. List and detail queries are scoped in SQL; IDs from another history return 404 after scope authorization.

Lists return {items, next_cursor} with summaries. Detail returns the approved changes and request context. Query filters are actor_user_id, event_name, resource_type, resource_id, operation_id, from, and to; Admin additionally accepts organization_id for affected organizations. from is inclusive and to exclusive, using RFC 3339 event timestamps. A resource ID requires a resource type. Limit defaults to 20 and is capped at 100. Cursors are versioned and bound to the caller, scope, and filters, ordered by database recording time and ID descending. Pagination is a traversal of committed rows, not a frozen database snapshot.

Public Go capabilities ​

services/audit/public exposes operation/event DTOs, Recorder, TransactionalRecorderFactory, EventLister, and EventReader. Persistent mutation commands carry server-constructed operation data. Legacy adapters pass that same verified data through context. Only infrastructure adapters bind a recorder to pgx.Tx; recorders never open or commit transactions. Business use cases receive transaction-bound repositories through owner-local mutation runners. Audit readers evaluate grants through narrow Authorization capabilities.

Construct the writer before provider modules and configure read permissions after Authorization is available. Missing write dependencies fail closed. Nil-database, in-memory provider construction exists for documentation and unit tests; it does not supply persistent audit guarantees.

Storage and operations ​

Apply migrations with go run ./tools/cli database migrate. Audit uses audit_events and its own goose_audit_version; Authorization adds its read permissions in a separate catalog migration. Deploy migrations before the application. History begins at rollout; the old auth_audit_logs table is neither imported nor removed.

Retain records indefinitely. Historical references have no foreign keys to live business rows. Triggers reject update, delete, and truncate. Database administrators remain trusted and can maintain the database. The development CLI's existing local/environment guards protect cleanup; its audit-trigger bypass is scoped to a cleanup transaction and is restored before commit. Runtime has no purge or mutation capability.

Back up audit data with PostgreSQL. An older application version can leave the additive schema in place but does not retain the new write guarantee. Append failure logs contain event/source/resource and operation references without event values; existing request logs and tracing provide read/write timing.

Verification ​

Run race-enabled short tests across Audit, Admin, Organization, Authorization, Identity, API, and CLI. Set AUDIT_TEST_DATABASE_URL to a dedicated loopback PostgreSQL test database whose name includes test, then run:

sh
go test -race ./services/audit/...
go test -race ./services/api/cmd/server -run '^TestAuditHTTPIntegration$' -count=1

These integration tests create isolated schemas, exercise the real migrations and runtime router, and remove their test schemas afterward. They cover append-only controls, swallowed append errors, atomic rollback, concurrent snapshots, scoped pagination, actor-name preservation, request peer attribution, authorization revocation, and partial provisioning. Existing Authorization integration tests now supply the mandatory audit writer and trusted operation context.

See ADR-018 and the public Go reference.

Documentation follows the source. Planned contracts are marked explicitly.