FabricFabricPlatform
Composable application standard

Offline and recovery

Capturing an action while disconnected, replaying it honestly, and what happens when the world changed while it waited.

Cached templates are template availability. They are not action durability. A collector in a basement can still see a screen; the question is what happens to the payment they capture.

What may be captured

Only an action whose capability declares connectivity: "offline-capturable", and only an event the render plan marks offline-eligible. Eligibility is refused at capture time rather than at replay, because a queue full of actions that can never be replayed is worse than a refusal in front of the operator.

Every capture requires a stable idempotency key. Replay without one is a duplicate payment waiting for a flaky network.

Authority is re-checked when it executes

An action's authorityMoment is capture, execution or both. An offline capture is never standing permission: an approval that lapsed while the capture sat in a queue does not fund a loan when connectivity returns.

The replay carries the marker and the real capture instant:

{ executionReason: "offline_replay", capturedAt: "2026-08-31T09:14:22Z", … }

Both travel with the request rather than by adapter convention. An adapter that had to remember to set them would eventually forget, and capture-authority evidence stamped at receipt time attests a moment nobody was there for.

A gateway that accepts replay must declare maxOfflineReplayAgeMs, and refuses replay outright without one. Deferring the lower bound to the platform looks safe and is not: the Host checks staleness only when the capture-authority adapter supplied an expiresAt, and skips the execution-time expiry check entirely for actions governed at capture time. A replay with no capturedAt is refused rather than defaulted, for the same reason.

Five dispositions, not two

Replay resolves each capture to one of these, and collapsing them is the common mistake.

OutcomeMeaningWhat happens
CompletedDelivered and progressing: the platform holds the invocationRemoved from the queue
ParkedDelivered, waiting on a human decisionRemoved from the queue
ReconciledDelivered and needing attention, or never deliverableRemoved, surfaced with the status and reason
FailedNot delivered, worth another attemptStays queued
ExpiredOutlived its TTL or its plan before it could be sentRemoved, with the records

Delivery is what the queue is responsible for. Once the platform returns an outcome it holds the invocation durably, so the capture has done its job and leaves the queue either way; what differs is whether a person needs to hear about it. An adapter that returns nothing has not delivered — the capture stays queued and the adapter fault surfaces as transient, rather than draining the queue into unknown over a missing return statement. A reconciled entry carries certainty, because an adapter whose outcome could not be determined is not the same as a refusal — telling an operator the charge was refused would be as wrong as telling them it succeeded.

A refusal is a returned outcome, not an exception. The platform treats a lapsed authority as a business result and returns reconciliation_required with the reason, so a queue that inspects only the exception path deletes the capture and reports it as applied. Read the outcome.

Replaying a repeated idempotency key is the mechanism working: the platform returns the original invocation without complaint. An idempotency conflict is the opposite — the key was reused for a materially different command, so this capture was never applied. It is settled, not transient: retrying yields the same conflict forever, so it is reconciled to a person who has the operator's work in the record and can resubmit it under a fresh key.

Reconciled also carries certainty. A refusal decided before any effect — a policy block, a validation failure, an authority check on first delivery — is refused. An authority check that failed on a recovery re-execution is unknown, because the lease worker re-runs authorization but skips adapter steps that already succeeded, so the external effect may have landed before the binding expired.

A reconciled capture must reach a person. Retrying a settled refusal only delays the moment somebody is told, and the operator is the one who can act on it.

A thrown failure that is not recognised stays transient, which errs toward retrying rather than discarding a capture. A returned outcome that is not recognised does the opposite and is surfaced for attention, because the platform answered and the answer was not one we understand — treating that as success is how a refusal gets reported as applied.

Parked and Expired carry their records too. A bare count leaves nobody able to say which action somebody now has to approve, or whose work was dropped.

Reading offline

A projection is readable offline only if its view declares offlineUsable, and only within its freshness bound. A stale cache fails closed with PROJECTION_CACHE_STALE rather than serving data that quietly stopped being true.

Storage

The queue is storage-agnostic; you supply IndexedDB, SQLite or AsyncStorage. Production requires an encrypted-at-rest store — captured parameters are business data sitting on a device that gets left in a car.

On this page