Documentation

History and audit

Versioned records, change history, restore, and the activity audit trail.

Every record in AppEngine carries a version field and a change log. The platform records who changed what, when, and lets you restore prior versions or recover deleted records. This page covers the surface for reading and managing that history.

Three audit layers

LayerWhat it tracksAccess
version fieldA monotonic counter on each record. Increments on every write.Read directly from BaseModel\<T\>
Record historyA snapshot of data plus author and timestamp on each save.GET /repository/history/{datatype}/{id}
Activity feedHigher-level events ("contact created", "lead assigned", "status changed")./crm/activity/*, /repository/activities/get

The first two are global (every datatype). The third is opt-in per domain — CRM uses it heavily; storefront uses the order audit log; banking uses ledger entries.

The version field

Every BaseModel\<T\> includes:

{
  version: number,        // 0 at create, +1 on each successful update
  modifydate: string,     // ISO timestamp of last write
  author?: string,        // email or username of last writer
  // ...
}

Use it for optimistic concurrency:

curl -X POST https://appengine.appmint.io/repository/update/SKU-1 \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"data":{"price":24.99},"version":7}'

If the server's stored version is no longer 7, the write is rejected with 409. Refetch, merge, retry.

Record history

Each successful write also appends a BaseModel<History> snapshot in a parallel history collection. The snapshot includes:

  • The full data payload at the moment of the write
  • version — matches the version the record had after that write
  • author — who made the change
  • modifydate — timestamp
  • state — workflow state at the time

List history for a record

GET/repository/history/{datatype}/{id}JWT
curl 'https://appengine.appmint.io/repository/history/product/SKU-1' \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT"

Returns an array sorted newest-first. Each entry is itself a BaseModel<History> so you can read the wrapped data payload.

Restore a prior version

POST/repository/history/restoreJWT

Send the history record's id back, optionally with state to control whether the restored record reverts to draft or stays published.

curl -X POST https://appengine.appmint.io/repository/history/restore \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"datatype":"product","id":"SKU-1","historyId":"history-abc-123"}'

The restore creates a new write — the next version is current + 1, not the historical number. The old version stays in history.

Trash and restore

Deletes are soft by default: the record's state flips to deleted and it's hidden from normal queries. A POST /repository/trash-restore brings it back.

POST/repository/trash-restoreJWT
curl -X POST https://appengine.appmint.io/repository/trash-restore \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"datatype":"product","ids":["SKU-1","SKU-2"]}'

A scheduled PurgeScheduler runs nightly and hard-deletes records that have been in trash beyond the org's retention window (default 30 days). After hard-delete, history is preserved for audit but the record itself is gone.

Activities

The activity feed is a structured event log used by CRM and other domains to render timelines ("Lead created → Email sent → Reply received → Demo booked"). Activities are first-class records of datatype activity.

Read by resource

GET/crm/activity/by-resource/{datatype}/{id}JWT
curl 'https://appengine.appmint.io/crm/activity/by-resource/lead/lead-123' \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT"

Read by customer

GET/crm/activity/by-customer/{customerId}JWT

Append manually

POST/crm/activity/manageJWT
{
  "action": "create",
  "data": {
    "type": "note",
    "subject": "Called the lead, left voicemail",
    "resourceType": "lead",
    "resourceId": "lead-123"
  }
}

Most activities are emitted automatically by domain controllers — you only call /activity/manage when adding human notes or recording an external action (e.g., a SMS sent from a third-party tool).

Generic activities endpoint

There is also a generic activities endpoint on the repository:

GET/repository/activities/get/{datatype}/{id}JWT

This returns the embedded activities[] on the record itself. Most callers use the CRM activity service instead — it has richer filtering and pagination.

Querying who-changed-what

Two patterns:

  1. Per record, fast: GET /repository/history/{datatype}/{id} — returns the full version chain, each entry tagged with author and modifydate.
  2. Across records, ad-hoc: query the history collection directly through /dynamic-query/find:
{
  "collection": "history",
  "filter": {
    "data.author": "[email protected]",
    "data.targetDatatype": "product",
    "modifydate": { "$gte": "2026-04-01T00:00:00Z" }
  },
  "options": { "sort": { "modifydate": -1 }, "limit": 100 }
}

The exact field names on the history record (targetDatatype, targetId, previousData) match the snapshots stored by RepositoryService.update. Confirm against your environment if your audit pipeline depends on these.

What history does NOT capture

  • File contents — uploads/deletes are events but the file binary itself is not snapshotted.
  • Fields the server strips before write (large blobs, computed fields).
  • Reads — there is no read audit by default. Enable per-org "read audit" if you have a regulatory requirement; talk to platform support.

Permissions

  • GET /repository/history/* requires read on the underlying datatype.
  • POST /repository/history/restore and trash-restore require update and create respectively.
  • ConfigAdmin and ContentAdmin pass these by default.