HighX
Platform

Retention and data rights

Reclaiming operational storage, exporting a tenant, and deleting one.

Retention

Operational records — sagas, idempotency keys, subscription history — are reclaimed on a schedule, not when someone notices the storage bill.

highx/retention.ts
export const retentionPolicy: RetentionPolicy = {
  subscriptionHistoryLimit: 32,
  sagaRetentionDays: 30,
  idempotencyRetentionDays: 7,
};

Declare it in both places: highx.config.ts is what doctor reads, and createTenantObject is what actually sweeps. Declaring it in only one leaves a check that reads as enforced and is not.

Retention is opt-in. Omit the policy and no sweep is ever scheduled — fine for a prototype, not for anything that runs.

The tenant object schedules its own sweep on an alarm, because a quiet tenant still accumulates records and would otherwise never reclaim them. Archiving happens before deleting and a failed archive aborts the delete, so the failure mode is keeping a record too long rather than losing it. Each sweep is bounded; report.hasMore says whether to schedule another pass immediately rather than hold one alarm open.

Without an archive, expired records are deleted rather than kept — which doctor reports as a warning.

Collaborative op logs

A collaborative field keeps an append-only op log beside its snapshot. documentOpRetentionDays bounds it, defaulting to the idempotency window.

Reclaiming an op loses no work — the snapshot holds it. It only means a client returning from a long absence resyncs from a snapshot instead of replaying the ops it missed, so op logs can sit on a shorter leash than records that cannot be reconstructed.

Workflow runs

A workflow records every external result it obtained so a resumed run does not repeat the call. Those step records are the larger half of a run and they accumulate in any system that schedules work, so workflowRunRetentionDays bounds them, defaulting to the saga window.

Only finished runs are ever eligible. A sleeping or waiting run is never reclaimed however old it is — its records are what it will resume from, and deleting them would destroy work still owed rather than work already done.

Tenant data rights

const archive = exportTenant(database, tenantId);
const report = deleteTenantData(database, tenantId);
assertTenantDeleted(database, tenantId);

Export covers every declared table, including empty ones, so a reader can tell "absent" from "not exported". Deletion runs in one tenant transaction and reports what remains, so an incomplete erasure is visible rather than assumed.

Deleting a tenant also clears its operational state — connections, subscriptions, and retained idempotency results — so a deleted tenant cannot replay a key referring to erased data.

importTenant restores an export into an empty tenant, refusing to overwrite live data or accept a different schema version.

Backup

The two storage authorities are backed up separately, because they fail separately and nothing spans them transactionally.

Tenant storage

exportTenant and importTenant are the supported path, and the one to test. Restore into an empty tenant and compare, rather than restoring over live data — importTenant refuses that outright.

Point-in-time recovery of a Durable Object depends on platform tooling. Treat exports as the recovery mechanism you control.

Global storage

D1 is a derived read model: every row in it was materialized from tenant data by a saga, so the tenant objects are the source of truth and D1 can be rebuilt from them.

# Scheduled export, kept alongside tenant exports.
wrangler d1 export highx-global --remote --output=global-$(date +%F).sql

# Restore into a fresh database, then repoint the binding once verified.
wrangler d1 create highx-global-restored
wrangler d1 execute highx-global-restored --remote --file=global-2026-08-06.sql

D1 also keeps a rolling window of automatic bookmarks:

wrangler d1 time-travel info highx-global
wrangler d1 time-travel restore highx-global --timestamp=2026-08-06T12:00:00Z

Time travel covers a bounded window and is not a substitute for exports. Take scheduled exports for anything that must survive longer than that window.

Restoring D1 does not roll back tenant data, and no transaction spans the two. After any restore the read model may be missing rows tenant storage still has — re-publish them rather than hand-editing D1. The saga that materialized each row is idempotent, so re-running it converges; a hand-edited read model diverges from its source in a way nothing will detect.

A backup that has never been restored is a guess. The D1 half is a runbook rather than code, so it needs a scheduled rehearsal into a throwaway database.

For the generated operational routes and CLI commands, see the operations runbook. Tenant exports can be written with highx tenant export, verified in an empty tenant, and restored with highx tenant restore --yes only after migration status and schema version are confirmed.

On this page