Skip to content

Collection retention

A collection can be told to keep only its most recent rows. Retention has two layers: the author default, declared in the manifest and shipped with the app, and the owner override, set at runtime per collection without touching the manifest. What is actually enforced is the effective retention, computed per axis.

Retention has two independent axes, either or both of which may be set:

  • maxRows: keep only the newest N live rows in the collection.
  • maxAgeDays: keep only rows younger than N days (by creation time).

When both are set, a row is kept only if it satisfies both; a row over either bound is pruned.

The author declares a collection’s default retention in the manifest under collections.<name>.retention. See the manifest reference for the exact retention key shape and its validation (for example, maxRows may not exceed the platform row cap). These defaults ship with the app and are re-materialized on every deploy.

The app owner can tighten or loosen retention on any collection at runtime, per axis, without a redeploy, through the owner surface:

Terminal window
# Show the effective retention now
homespun data <app> <collection> retention --show
# Keep only the newest 500 rows (sets the maxRows override)
homespun data <app> <collection> retention --max-rows 500
# Revert the rows axis to whatever the manifest declares
homespun data <app> <collection> retention --clear-rows

The same control is available over HTTP as GET and PATCH /v1/apps/:id/collections/:name/retention (owner cookie or owning-agent key), and as getCollectionRetention / setCollectionRetention in the SDK.

Effective retention is computed per axis as override ?? authorDefault: the owner override wins on its axis when set, otherwise the author default applies. Clearing an override on an axis (sending null) reverts that axis to the author default. There is deliberately no way to express “no retention” through an override, so a collection an ingest hook writes into keeps its author-required bound no matter what the owner does.

Effective maxRows is capped at the account’s MAX_ROWS_PER_APP: an override above the platform’s own row cap could never take effect, so it is rejected. The override is owner-owned: a manifest redeploy rewrites the author default but never the override, so an owner’s setting survives every redeploy.

The read and write responses report the effective bounds, the authorDefault, the raw override, and wouldPrune: how many existing live rows the effective bounds would prune on the next sweep, so a tightening’s impact is visible before it happens.

A background sweeper enforces effective retention on a slow cadence. Its behaviour:

  • Feed-silent hard delete. Over-retention rows are hard-deleted with no tombstone and no feed entry, so pruning does not double the on-disk bytes or spam every watcher. A watching client is told to resync instead: each pass that prunes for an app stamps a prune watermark and broadcasts a resync once, so a stale cursor is caught and re-snapshots.
  • Seed rows are exempt. A seeded starter row (author kind system) ships with the app and is never counted or pruned; retention bounds user-generated growth only.
  • Batched. At most a bounded number of rows per collection per pass, so a large first prune is spread over passes rather than done in one statement.
  • Meters released. The freed rows and bytes are decremented from the app and account usage counters as each batch is deleted.
  • Detached collections are skipped. A collection removed from the manifest (detached) is left alone, which is why an override on a detached collection is rejected.