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
| Layer | What it tracks | Access |
|---|---|---|
version field | A monotonic counter on each record. Increments on every write. | Read directly from BaseModel\<T\> |
| Record history | A snapshot of data plus author and timestamp on each save. | GET /repository/history/{datatype}/{id} |
| Activity feed | Higher-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
datapayload at the moment of the write version— matches the version the record had after that writeauthor— who made the changemodifydate— timestampstate— workflow state at the time
List history for a record
/repository/history/{datatype}/{id}JWTcurl '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
/repository/history/restoreJWTSend 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.
/repository/trash-restoreJWTcurl -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
/crm/activity/by-resource/{datatype}/{id}JWTcurl 'https://appengine.appmint.io/crm/activity/by-resource/lead/lead-123' \
-H "orgid: my-org" -H "Authorization: Bearer $JWT"
Read by customer
/crm/activity/by-customer/{customerId}JWTAppend manually
/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:
/repository/activities/get/{datatype}/{id}JWTThis 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:
- Per record, fast:
GET /repository/history/{datatype}/{id}— returns the full version chain, each entry tagged withauthorandmodifydate. - 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/*requiresreadon the underlying datatype.POST /repository/history/restoreandtrash-restorerequireupdateandcreaterespectively.- ConfigAdmin and ContentAdmin pass these by default.