Audit Service

audit-service is the platform’s immutable audit log. It is a Quarkus service backed by append-only PostgreSQL storage. Domain services never write to audit storage directly — they publish audit.event Kafka events, and audit-service consumes and persists them. This decoupling means audit recording never adds latency to the originating request.

Retention and Storage#

Audit records are retained for 7 years in PostgreSQL. There is no external archival tier — all records remain in the database for the full retention window.

The audit_events table is range-partitioned by occurred_at, with one partition per calendar month (84 partitions over 7 years). PostgreSQL partition pruning means queries with a time range only scan the relevant month partitions, keeping query performance predictable regardless of overall record volume.

A scheduled job running on the 1st of each month creates the next two months’ partitions as a rolling lookahead. No partition is ever dropped.

Kafka Event Schema#

Every state-mutating action in the platform publishes an audit.event Kafka message. The schema is defined in ss3-protos at io/shopstar/audit/v1/events.proto.

FieldTypeNotes
event_idUUIDGenerated by the publishing service
store_idUUIDRequired
actor_idstringUUID, ANONYMISED, or empty for SYSTEM actors
actor_typeenumSTAFF, CUSTOMER, or SYSTEM
actionstringSCREAMING_SNAKE_CASE — e.g. PRODUCT_UPDATED
target_typestringEntity type — e.g. PRODUCT, ORDER
target_idstringEntity primary key
service_namestringOriginating service
trace_idstringW3C OTel trace ID
occurred_atTimestamp
before_stateStructOptional; PII stripped by publisher
after_stateStructOptional; PII stripped by publisher

Action naming convention#

Actions follow the pattern {ENTITY_TYPE}_{PAST_TENSE_VERB} in SCREAMING_SNAKE_CASE:

PRODUCT_CREATED, PRODUCT_UPDATED, PRODUCT_DELETED, ORDER_PLACED, ORDER_CANCELLED, STAFF_ROLE_GRANTED, STAFF_ROLE_REVOKED, CUSTOMER_ERASED, FEATURE_TOGGLED, etc.

Publishing from Domain Services#

The ss3-quarkus extension provides an AuditEventPublisher CDI bean. Services inject it and call publish() after any state-mutating operation:

auditPublisher.publish(
    "PRODUCT_UPDATED",
    "PRODUCT",
    product.getId().toString(),
    previousProduct,    // before — @AuditExclude fields stripped automatically
    updatedProduct      // after  — @AuditExclude fields stripped automatically
);

The publisher extracts actor identity and trace ID from RequestContext automatically. Services do not construct the event directly.

PII in snapshots#

Before/after snapshots must not contain raw PII. Publishers annotate sensitive fields with @AuditExclude; the SnapshotBuilder utility (also in ss3-quarkus) strips them before the event is serialised.

public class Customer {
    public String id;
    @AuditExclude public String email;        // stripped from snapshots
    @AuditExclude public String phoneNumber;  // stripped from snapshots
    public String status;
}

GDPR Erasure#

On customer.erasure_requested, audit-service runs:

UPDATE audit_events
SET    actor_id = 'ANONYMISED'
WHERE  actor_id  = :customerId
AND    actor_type = 'CUSTOMER';

This runs across all 84 partitions. The action record, target entity reference, and snapshots are untouched — only the customer’s actor_id is replaced with the sentinel value.

Staff actor_id is never anonymised. Staff actions on financial records must remain attributable for compliance.

Query API#

The admin SPA queries audit history through a REST API on audit-service:

GET /api/v1/audit-events
  ?storeId=        required
  &targetType=     e.g. PRODUCT
  &targetId=       entity primary key
  &actorId=        staff or customer UUID
  &action=         e.g. PRODUCT_UPDATED
  &from=           ISO 8601 datetime
  &to=             ISO 8601 datetime
  &page=           default 0
  &size=           default 50, max 200

Query responses include an actorDisplayName and targetDisplayName field resolved lazily via gRPC (identity-service and the relevant domain service respectively) and cached in Redis with a 5-minute TTL. Both entity IDs are always present in the response regardless of display name resolution success.

Compliance Export#

Auditors and regulators can request a full export via:

GET /api/v1/audit-events/export?storeId=&from=&to=

The response streams NDJSON (one event per line) directly from PostgreSQL using a server-side cursor. No full result set is loaded into heap. The Content-Disposition header prompts a file download. This endpoint is gated to staff with the compliance role.

SIEM and Ops Alerting#

audit-service has no coupling to SIEM or alerting systems. External consumers — a SIEM Kafka connector and a platform ops alerting consumer — tail the audit.event topic directly in their own consumer groups:

Consumer groupPurpose
audit-service-consumerPersist to PostgreSQL
siem-connectorForward events to SIEM in real time
ops-alerting-consumerTrigger alerts on high-risk actions (mass delete, privilege escalation, bulk price change)

Kafka Topics#

Consumed#

TopicAction
audit.eventINSERT into audit_events
customer.erasure_requestedAnonymise actor_id for CUSTOMER actors

Published#

None — audit-service is a sink.

Vault Configuration#

ss3/kv/audit-service/
  db.url
  db.username
  db.password