Skip to content

Data retention

PhyCloud keeps every event in its hot store (PostgreSQL or SQLite) until a retention policy says otherwise. One robot streaming at 5 Hz writes about 1 GB a day, so most fleets need a policy.

A retention policy belongs to one tenant. It says how long events stay in the hot store and what happens when they age out:

Action What happens
archive_then_delete Events are written to the tenant's data lake and verified. Only then are they removed from the hot store.
delete Events are permanently erased from the hot store.

A tenant without a policy keeps every event. That is the default, so upgrading PhyCloud never deletes data.

This page explains how retention works. Operators should also read the operator guide. Endpoints are listed in the API reference.

The policy

Field Meaning
hot_retention Maximum age of an event in the hot store, such as 30d or 12h. Units: s, m, h, d, w. Minimum 1h.
max_events Optional. Keep at most this many events.
max_bytes Optional. Keep at most this many payload bytes.
action archive_then_delete or delete.
lake Where to archive. Required for archive_then_delete.

An event ages out when it is older than hot_retention, or when keeping it would exceed a cap. Age is measured from ingested_at, the time PhyCloud received the event. Robot clocks do not affect it.

Retention always removes the oldest events first, as a contiguous run of sequence numbers. It never leaves holes in the middle of the chain.

Risky changes wait

A change that would make the next run delete more data is staged rather than applied:

  • adding a policy to a tenant that has none;
  • shortening hot_retention;
  • adding a cap, or lowering one;
  • switching from archive_then_delete to delete.

A staged change takes effect after a delay, 72 hours by default, or as soon as a super-admin confirms it. It can be cancelled until then. Lengthening retention, removing a cap, switching to archive_then_delete, changing the lake and removing the policy all apply at once.

Every change, staged or applied, is written to the audit log.

Your hash chain stays verifiable

Each tenant's events form a hash chain: every event's previous_hash is the content_hash of the event before it. When retention removes the oldest events, PhyCloud records a retention checkpoint: the last removed sequence number and its hash.

GET /api/v1/verify starts at the checkpoint. The first remaining event must link to the checkpoint hash, so the chain still verifies, and the response shows where it starts:

{
  "verified": true,
  "events": 41022,
  "chain_start": 2588101,
  "checkpoint": { "sequence_number": 2588100, "content_hash": "9f2c…", "retained_events": 0 }
}

For an archived tenant, GET /api/v1/verify/archive proves the whole chain: it reads every archived segment from the lake, checks its digest and every hash and link, and then walks the hot store from the checkpoint. See Verifying the full chain.

A legal hold stops retention from deleting events, whatever the policy says. A hold covers a whole tenant or one source.

  • A tenant-level hold stops retention runs for that tenant.
  • A source-level hold keeps that source's events. The rest of the tenant's events age out as usual.
  • Under archive_then_delete, held events are still archived, so the lake has a complete chain.

When a hold is released, the next run removes the events it was keeping. Under an archive policy, it removes only events that were archived.

Evidence lockers keep their own copy of each event when they are sealed, so sealed evidence survives retention without a hold.

What you can see as a tenant

Any key with events:read can read its own tenant's retention state:

# Your policy, and any staged change
phyware cloud retention get

# Events, bytes, oldest event, checkpoint and archive totals
phyware cloud storage

# Prove the full chain across the lake and the hot store
phyware cloud retention verify-archive

Setting policies, running retention and managing holds need a super-admin key with admin:tenants.

Verifying the full chain

phyware cloud retention verify-archive (or GET /api/v1/verify/archive) returns:

Field Meaning
verified Nothing checked was tampered with or inconsistent.
complete The walk covered every sequence number from verified_from to the newest event, with no range that was deleted without archiving.
verified_from First sequence number checked. It is above 1 when the tenant used delete before switching to archiving.
archived_events, archived_segments, archive_end What was read from the lake.
unarchived_ranges Ranges removed from the hot store without being archived.
problem The first problem found, with its kind and the object or sequence number involved.
hot The hot-store walk, as GET /api/v1/verify returns it.

Problem kinds:

kind Meaning
segment_missing A segment or manifest is missing from the lake.
segment_corrupt A segment's size or SHA-256 does not match its manifest.
manifest_mismatch The manifest in the lake differs from PhyCloud's copy.
segment_mismatch A segment's events do not match its manifest.
content_hash_mismatch An archived event's data does not match its hash.
chain_link_broken An archived event does not link to the one before it.
checkpoint_mismatch The archive does not end at the retention checkpoint.
no_lake, lake_unavailable The lake could not be read.

The verifier reads every archived segment, so it can take a while for a large archive.

Archive format

Archived events are stored as NDJSON compressed with zstd, one segment per source per UTC day, in Hive-style partitions that DuckDB, Spark and Athena read directly:

<prefix>/tenant=acme/source=robot-1/date=2026-10-27/seg-00000000000000000001-00000000000000004321.ndjson.zst
<prefix>/tenant=acme/source=robot-1/date=2026-10-27/seg-00000000000000000001-00000000000000004321.manifest.json

Each line is one stored event, including sequence_number, content_hash, previous_hash and the original event_data text. You can read a segment with standard tools:

zstdcat seg-….ndjson.zst | jq -c '{sequence_number, event_type, captured_at}'

The manifest next to each segment records its sequence range, first and last hash, event count, size and SHA-256. PhyCloud keeps the same manifest.

Further reading