# 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.

## The two axes

[Section titled “The two axes”](#the-two-axes)

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.

## Author default (manifest)

[Section titled “Author default (manifest)”](#author-default-manifest)

The author declares a collection’s default retention in the manifest under `collections.<name>.retention`. See the [manifest reference](/agents/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.

## Owner override (runtime)

[Section titled “Owner override (runtime)”](#owner-override-runtime)

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

[Section titled “Effective retention”](#effective-retention)

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.

## The sweeper

[Section titled “The sweeper”](#the-sweeper)

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.
