Skip to main content
A device that forwards a frame it did not originate holds it for about five seconds. If no neighbour can take it in that time, the frame is dropped. Custody, new in v0.28.0, lets a device hold one class of frame for hours instead and deliver it when the recipient appears. It is off by default, and a device that never enables it, or never meets a custodian, behaves as before.

What custody carries

Custody v1 carries only sealed document replication frames: deltas, snapshots, version offers and blob-removal reports from the DataStore. Direct messages, media chunks and requests for a snapshot or a blob are never deposited.
  1. The depositor asks. When a device offers its own replication frame to neighbours because the recipient is out of reach, it adds a one-hop custody request. It can do this only while it retains the plaintext, so a frame offered after a restart carries no request.
  2. Forwarders strip the request. Every device removes the request from a third-party frame it transmits, so a deposit is always one hop, from the frame’s own sender over the link that proved it.
  3. The custodian holds what it could not forward. A frame with a request is forwarded normally first. Custody starts only when that forward would be abandoned.
  4. The custodian redelivers. When the recipient appears, the held frame goes straight to it, at most once per neighbour per hold. Meanwhile it can be re-originated toward other neighbours.
  5. A receipt settles nothing. The custodian may send the depositor a signed receipt. Only the recipient’s acknowledgement settles a message, so the depositor keeps its own outbox entry and retries unchanged.
Records are sealed on the custodian’s disk and restored at launch. They expire in wall time at the end of the hold.

Enable it

Custody is applied when the protocol is constructed; there is no runtime update. In React Native:
In Python, pass a CustodyConfig as ProtocolConfig(custody=...):
Omit a field to keep the core default. Python and Rust use the same fields in snake case. The core validates the bounds at construction and rejects, for example, a hold that is not shorter than the outbox lifetime.

Inspect and erase

getCustodyStats() returns what this device is holding and what it has done as a custodian and as a depositor. held and heldBytes are gauges; the rest are cumulative since start or the last erase, with one counter per refusal reason, so “custody is off” can be told from “nobody asked”. eraseCustody() drops every held frame and resets the counters. The DataStore wipeAll() erases custody as well. In Python the methods are get_custody_stats() and erase_custody() on pm.protocol. See the networking API reference for the exact signatures.

Risks to accept

A custodian stores other people’s ciphertext and the routing metadata around it, on its own disk and battery. The release threat model records three residual risks: custody-borne re-key pressure (R19), a custodian retaining routing metadata about third parties (R20) and deposit spam (R21). Keep the stranger tier closed unless you need it, size the quotas for the device’s storage, and treat custody as a latency improvement, not a delivery guarantee. The full contract is the custody chapter.