Skip to main content

Audit trails (system trails)

Model City records every write operation as a system trail: an audit event with a common envelope and an event-type-specific payload. Trails are decentralized — each vertical owns its own event types, its own SystemTrailGenerator and its own <vertical>_trails table — and read-only for administrators.

How a trail is produced​

Write use cases call the vertical's SystemTrailGenerator (a subclass of AbstractSystemTrailGenerator in model-city-commons), which builds the envelope and persists it through the vertical's SystemTrailStore in the same transaction as the business change. There is no create/update/delete API for trails — they are a side effect of the write.

The correlation id comes from the MDC (see Observability), so a trail can be tied back to the exact request that produced it.

The envelope​

Built by AbstractSystemTrailGenerator as a NewSystemTrail and stored in the <vertical>_trails table:

FieldTypeNotes
eventIdUUIDUnique, immutable; generated at source.
eventTypeStringCanonical type, UPPER_SNAKE_CASE (e.g. USER_REGISTERED).
operationTypeenumCREATE, UPDATE or DELETE.
occurredAttimestampUTC instant of the business event.
correlationIdStringThe request's correlation id (distributed trace).
responsibleUserIdStringAuth0 sub of the actor; SYSTEM for webhook-triggered events.
responsibleUserRoleStringActor role (nullable).
neighbourhoodIdLongNeighbourhood scope (nullable).
zoneIdLongZone scope (nullable).
resourceTypeStringType of the affected resource (e.g. EVENT_TICKET).
resourceIdStringId of the affected resource (String — supports Auth0 subs).
payloadJSONBSelf-contained, event-specific detail (serialized JSON).

payload conventions: CREATE carries the created resource snapshot; UPDATE carries the new state (optionally a before/after); DELETE carries the last known snapshot.

Webhook actor

Stripe-webhook events (*_CONFIRMED / *_CANCELLED) have no authenticated caller, so they record the SYSTEM actor marker (AbstractSystemTrailGenerator.SYSTEM_ACTOR).

Zone / neighbourhood scope​

Only filled when the resource has a direct territorial relation: users (the user's neighbourhood), civic_questions and security_alerts (their zone + neighbourhood), and answers (inherited from their question). Other resources (city-places, events, cars, reservations, …) leave them null.

Event catalogue by vertical​

Core Module​

Event typeOperationResource
USER_REGISTEREDCREATEUSER
USER_UPDATEDUPDATEUSER
USER_STATUS_CHANGEDUPDATEUSER
USER_DELETEDDELETEUSER
AGENT_INVITEDCREATEAGENT_INVITATION
OPERATION_AUTHORIZATION_CREATEDCREATEOPERATION_AUTHORIZATION
OPERATION_AUTHORIZATION_VERIFIEDUPDATEOPERATION_AUTHORIZATION
OPERATION_AUTHORIZATION_BURNTUPDATEOPERATION_AUTHORIZATION

Engagement Module​

Event typeOperationResource
CIVIC_QUESTION_CREATEDCREATECIVIC_QUESTION
CIVIC_QUESTION_UPDATEDUPDATECIVIC_QUESTION
ANSWER_SUBMITTEDCREATECIVIC_ANSWER
SECURITY_ALERT_CREATEDCREATESECURITY_ALERT
SECURITY_ALERT_DELETEDDELETESECURITY_ALERT

Leisure Module​

Event typeOperationResource
CITY_PLACE_* / CITY_ROUTE_* (CREATED/UPDATED/DELETED)CREATE/UPDATE/DELETECITY_PLACE / CITY_ROUTE
PUBLIC_SPACE_* / RESERVABLE_RESOURCE_*CREATE/UPDATE/DELETEPUBLIC_SPACE / RESERVABLE_RESOURCE
SPACE_RESERVATION_CREATED / SPACE_RESERVATION_DELETEDCREATE/DELETESPACE_RESERVATION
EVENT_* (CREATED/UPDATED/DELETED)CREATE/UPDATE/DELETEEVENT
EVENT_TICKET_PURCHASEDCREATEEVENT_TICKET
EVENT_TICKET_CONFIRMED / CANCELLED (Stripe)UPDATEEVENT_TICKET
EVENT_TICKET_REFUNDED / AUTO_REFUNDEDUPDATEEVENT_TICKET

Mobility Module​

Event typeOperationResource
CAR_REGISTEREDCREATECAR
STREET_RESERVATION_CREATED / RENEWEDCREATESTREET_RESERVATION
STREET_RESERVATION_CONFIRMED / CANCELLED (Stripe)UPDATESTREET_RESERVATION
SANCTION_ISSUEDCREATESANCTION

Example payload​

{
"answerId": 5012,
"questionId": 42,
"citizenSub": "auth0|abc",
"vote": "YES",
"questionNeighbourhoodId": 7,
"questionZoneId": 3
}

The admin read API​

Each vertical exposes its own GET /system-trails (a SystemTrailController per vertical). All endpoints are admin-only (MODEL-CITY-PLATFORM-ADMIN) and reached under /api/<vertical>/system-trails.

Query paramTypeNotes
eventTypeStringExact type, one at a time (e.g. SANCTION_ISSUED).
responsibleUserIdStringActor's Auth0 sub — used for the per-user activity view.
from / toISO-8601Inclusive bounds on occurredAt.
pageint0-based; page size 20, ordered by occurredAt descending.

The response is a Spring Page<SystemTrailDto> (serialized via PageJacksonModule). Because each module owns its own events, types cannot be mixed across modules — the caller picks one module per query. This also powers the admin panel's per-user activity view by querying one or more modules with responsibleUserId={userId}.

Design notes

eventId (UUID) lets consumers deduplicate; occurredAt + eventId gives a total order. If a payload ever changes incompatibly, a schemaVersion field can be added to the envelope and consumers should tolerate unknown fields (open-world assumption).