Time Machine
Time Machine records every change to every record and lets you query or restore any point in time. All schemas have time machine enabled by default.
How It Works
When you create or update a record, two things happen in a single transaction:
- The main table is updated
- A history entry is written (INSERT into history table)
This is atomic: either both succeed or neither.
-- Before: main table has PO-001 with status=draftUPDATE purchase_order_v1 SET status = 'approved' WHERE id = 'PO-001';
-- Atomically:INSERT INTO purchase_order_v1_history (entity_id, operation, old_value, new_value, actor, timestamp)VALUES ('PO-001', 'UPDATE', '{"status":"draft",...}', '{"status":"approved",...}', 'ravi.kumar', now());Both statements complete together, or neither executes.
Storage Tiers
Hot Tier (0-90 days)
Data lives in Postgres {table}_history partitioned by occurred_at. Instant query, full fidelity.
# Query hot tiercurl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/history \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'at=2026-05-19T14:30:00Z'Partition strategy: monthly. Partitions older than 90 days are automatically moved to S3 (warm tier).
Warm Tier (90 days - 5 years)
Data lives in S3 Parquet format, queryable via the warm-reader (DataFusion). Slightly slower, but still interactive.
Queries transparently fall back to the warm tier if the hot query doesn’t find data:
# Query warm tier (automatically)curl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/history \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'at=2025-12-01T00:00:00Z' # 5+ months agoThe API server queries the warm-reader RPC internally. To you, it’s transparent.
Cold Tier (5+ years)
Data moved to Glacier or Deep Archive. Restore required (deferred to Phase 10).
Operations
List History
Get all changes to a record:
curl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/history \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'limit=50' \ --data-urlencode 'offset=0'Response:
{ "data": [ { "event_id": 1, "timestamp": "2026-05-19T14:32:00Z", "actor": "ravi.kumar", "operation": "CREATE", "old_value": null, "new_value": {"id":"PO-001","status":"draft","amount":50000} }, { "event_id": 2, "timestamp": "2026-05-19T14:35:00Z", "actor": "anita.sharma", "operation": "UPDATE", "old_value": {"status":"draft"}, "new_value": {"status":"approved"} } ], "pagination": { "limit": 50, "offset": 0, "total": 42 }}Point-in-Time Query
Reconstruct the state of a record at a specific moment:
curl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/history \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'at=2026-05-19T14:33:00Z'Response: the record’s state at 2026-05-19 14:33:00 (between the CREATE and UPDATE):
{ "data": { "id": "PO-001", "status": "draft", "amount": 50000, "created_at": "2026-05-19T14:32:00Z", "version": 1 }}Diff
Compare record state between two timestamps:
curl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/diff \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'from=2026-05-19T14:32:00Z' \ --data-urlencode 'to=2026-05-19T14:35:00Z'Response:
{ "data": { "added": { "approval_date": "2026-05-19T14:35:00Z" }, "changed": { "status": {"from":"draft","to":"approved"} }, "removed": {} }}Restore
Write a new event that restores a record to a past state:
curl -X POST https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/restore \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "at": "2026-05-19T14:33:00Z", "reason": "Approval was incorrect; customer dispute. Reverting to draft per stakeholder request." }'Response (201 Created):
{ "data": { "id": "PO-001", "status": "draft", "version": 3, "restored_at": "2026-05-19T14:40:00Z" }}Note: Restore writes a new event. It does not delete the UPDATE event; it creates a RESTORE event. The full history is preserved.
History after restore:
Event 1: CREATE (draft)Event 2: UPDATE (approved)Event 3: RESTORE (back to draft, reason: "Approval was incorrect...")Replay
Stream all events for a record (Server-Sent Events):
curl -N https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/replay \ -H "Authorization: Bearer $TOKEN"Response (text/event-stream):
data: {"event_id":1,"timestamp":"2026-05-19T14:32:00Z","operation":"CREATE",...}data: {"event_id":2,"timestamp":"2026-05-19T14:35:00Z","operation":"UPDATE",...}data: {"event_id":3,"timestamp":"2026-05-19T14:40:00Z","operation":"RESTORE",...}Snapshots
Take a point-in-time snapshot of all records in a schema:
curl -X POST https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/history/snapshot \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "at": "2026-05-01T00:00:00Z", "reason": "Monthly snapshot for compliance archive" }'Response (202 Accepted):
{ "data": { "snapshot_id": "snap-abc123xyz", "schema": "acme/supply-chain/procurement/purchase-order/v1", "timestamp": "2026-05-01T00:00:00Z", "status": "in_progress", "expires_at": "2026-06-01T00:00:00Z" }}Snapshots are exported to S3 and are queryable for compliance audits.
Configuration
Customize time-machine retention per schema:
apiVersion: velocity.sh/v1kind: SchemaDefinitionmetadata: name: purchase-orderspec: timeMachine: enabled: true hotRetention: 90d # How long to keep in Postgres warmRetention: 5y # How long to keep in S3 coldRetention: none # Glacier (deferred) snapshotRetention: 7d # Keep snapshots for 7 daysRetention Policies
- Hot (0-90 days): Partitioned Postgres table, instant query
- Warm (90d-5y): S3 Parquet, queryable via warm-reader (DataFusion)
- Cold (5y+): Glacier Deep Archive, restore required (Phase 10)
After 90 days, the operator automatically exports the Postgres history partition to S3 and drops the Postgres partition. The warm-reader takes over.
Redaction in History
Sensitive fields are redacted in history entries per the schema’s field configuration:
spec: fields: - name: credit_card type: string sensitivity: pii masking: strategy: partial visibleChars: 4When you query history, the credit_card field in old_value/new_value shows as ****1234 (not plaintext).
Audit of Time-Machine Operations
Every restore is recorded in the audit log:
SELECT actor, operation, entity_id, reason FROM platform.audit_logWHERE operation = 'RESTORE'ORDER BY timestamp DESC;Output:
actor operation entity_id reason─────────────────────────────────────────────────────────────────ravi.kumar RESTORE PO-001 Approval was incorrect...Use Cases
Accidental Data Deletion Recovery
User accidentally soft-deletes a record:
# Check when it was goodcurl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/history \ -H "Authorization: Bearer $TOKEN"
# Restore itcurl -X POST https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/restore \ -H "Authorization: Bearer $TOKEN" \ -d '{"at":"2026-05-19T14:30:00Z","reason":"Accidental delete; customer follow-up required"}'Dispute Investigation
Customer claims a value was different on a date:
# Check historycurl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/history \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'at=2026-04-19T00:00:00Z'
# Compare with nowcurl -G https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/diff \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode 'from=2026-04-19T00:00:00Z' \ --data-urlencode 'to=2026-05-19T00:00:00Z'Provide the diff and audit trail to dispute resolution team.
Compliance Snapshots
Take monthly snapshots for regulatory audit:
for month in {01..12}; do curl -X POST https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/history/snapshot \ -H "Authorization: Bearer $TOKEN" \ -d "{\"at\":\"2026-${month}-01T00:00:00Z\",\"reason\":\"Monthly compliance snapshot\"}"doneSnapshots are immutable and exportable for audit.
Operational Diagnostics
Trace how a state was reached:
# Full replay of PO-001curl -N https://api.velocity.acme.com/api/acme/supply-chain/procurement/purchase-order/v1/PO-001/replay \ -H "Authorization: Bearer $TOKEN" | jq '.'Shows exact sequence of who did what when. Invaluable for debugging production incidents.
Limitations & Deferred Features
Deferred
- Cold-tier restore (Glacier): Phase 10. For now, data in Glacier is immutable.
- Selective restore (restore some fields only): Phase 10. Restore is all-or-nothing.
- Warm-tier cross-dataset joins: Phase 4 revision. Warm-reader is single-table only.
Current Behavior
- History is write-once, read-many. You cannot edit history (immutable by design).
- Restore is a new event, not a rollback. All previous events remain visible.
- RLS applies to history queries. You can only see history for records you can read now.
Performance Notes
- Hot query (< 90 days): < 10ms (Postgres index scan)
- Warm query (90d-5y): 100-500ms (S3 + DataFusion scan)
- Large snapshots (> 1M records): Background job, 202 Accepted response
For frequently-accessed warm-tier data, consider archiving selectively (don’t archive high-frequency records).