The outbox needs a delivery policy

Preserving an offline action was not permission to send it later; D4U7 classified intent by freshness, authority, consequence, and safe retry behavior.

The first D4U7 outbox had two states: pending and sent.

It was technically capable. A contribution written without a network received a stable identifier, survived a reload in IndexedDB, and was delivered after connectivity returned. The server's receipt prevented a retry from creating a duplicate.

Then I queued a decision offline. Before the browser reconnected, the room deadline passed, another proposal changed, and the decision authority moved to a different room state. The outbox delivered the old choice automatically because “pending” contained no reason not to.

Preserving intent and executing intent were different promises. The outbox had implemented the first and assumed the second.

I added a delivery policy to every operation.

Time changes the meaning of an action

Some operations remain meaningful after delay. A draft saved locally is still the author's text tomorrow. An acknowledgement of an immutable document revision can still refer to that exact revision. A reaction represented as adding one identity to a set can often arrive later without overwriting another change.

Other operations depend on current context:

  • Select proposal B as the final decision.
  • Close the room for further review.
  • Accept an action whose deadline or scope may have changed.
  • Revise a contribution that another device has already revised.
  • Remove evidence currently used by a decision.
  • Invite a participant under a membership policy that may no longer apply.

Network delivery can be delayed by seconds or days. The product could not treat the operation as a timeless packet.

Every queued action therefore named the state on which it was based and the conditions under which that intent remained deliverable. Offline support became a domain policy, not a transport feature.

Three policies covered the first useful boundary

D4U7 began with three delivery policies.

Auto-deliver under named preconditions

The client may send when connectivity returns if the declared base and authority conditions still hold. The server remains the final evaluator.

Examples included adding a standalone piece of evidence to an open room or acknowledging one immutable revision. “Auto” meant no extra prompt under unchanged context, not bypassing validation.

Revalidate and ask when relevant state changed

The client fetches current state and explains the difference before delivery. Revising a proposal or accepting a follow-up action belonged here. If the base remained current, delivery could proceed; if not, the person reviewed the change.

Online-only

The product does not offer the consequential action without fresh server state and current authority. Recording a final decision and changing room membership used this policy. A person could draft the rationale offline, but the commit action waited.

The categories were conservative. I could loosen a policy after proving merge and authorization semantics. Recovering from an automatically delivered stale decision would be harder.

The operation carried its assumptions

A delivery policy without machine-checkable context would become a label the server had to interpret loosely.

The queued operation recorded:

type OutboxOperation = {
  operationId: string
  roomId: string
  kind: OperationKind
  payloadVersion: number
  payload: unknown
  baseRoomRevision: number
  baseContributionRevision?: string
  authorityVersion?: string
  policy: 'auto' | 'revalidate' | 'online-only'
  createdAt: string
  dependencies: string[]
}

The base room revision identified the broader state the person had seen. A contribution revision identified the document being changed. Authority version named the membership or permission decision when the operation depended on it.

These were preconditions, not locks. Other participants and devices continued working. When the server received the operation, it compared the assumptions with current state and returned a typed result.

The payload retained the author's original intent. Revalidation could create a new operation after review; it did not mutate history until an old packet happened to pass.

The first server response used HTTP success for accepted work and a generic conflict for everything else. The outbox could not explain what recovery required.

I defined domain outcomes:

  • accepted: the operation created a durable revision or transition and has a receipt.
  • already-accepted: the stable operation ID has the same prior receipt.
  • base-changed: relevant state advanced; current revisions are attached for review.
  • phase-changed: the room no longer allows this operation kind.
  • authority-changed: authentication succeeded but current permission does not authorize the action.
  • invalid: the payload violates a maintained domain rule.
  • unsupported-version: the server cannot interpret the operation shape safely.
  • temporarily-unavailable: no domain conclusion was reached; retry policy applies.

A timeout or lost response produced unknown outcome, not failure. The client queried the receipt or retried the same operation ID.

Typed outcomes turned a red outbox badge into recovery choices. A changed base opened comparison. Changed authority offered export or a request path. Unsupported version preserved the local payload for migration.

Stable identity made retries safe, not correct

X8B6 had taught me to assign every local operation a stable ID. If a request reached the server and the response disappeared, retrying returned the existing receipt instead of repeating the mutation.

That property solved duplicate delivery. It did not make a stale operation appropriate.

An idempotently repeated decision can still be the wrong decision under new evidence. A contribution delivered exactly once can still overwrite a newer revision if the server ignores its base. A membership change can be deduplicated and unauthorized.

I wrote the distinction into the design:

  • Identity answers whether this is the same attempted operation.
  • Preconditions answer whether the operation applies to current state.
  • Authority answers whether the current actor may perform it.
  • Merge semantics answer whether concurrent intent can coexist.
  • Receipt answers what durable outcome occurred.

Safe retry was one layer of correctness, not a universal property inherited by every queued action.

That unresolved boundary became the most important next problem.

The word outbox suggests a line. Operations could depend on one another.

A person might create a proposal locally, then attach evidence to that proposal, then revise its title. The later operations referenced an entity the server did not know until the first was accepted. Delivering them concurrently could fail for an avoidable reason.

Each operation listed dependencies by stable operation or local entity ID. The delivery worker selected operations whose prerequisites had receipts. Independent operations could proceed without waiting for the whole queue.

If a parent operation needed attention, dependants became blocked with an explanation. They did not repeatedly hit the server. If the parent was deliberately abandoned, the interface offered to reattach or export dependant content where meaningful.

I avoided assuming creation order always defined dependency. A later acknowledgement of an existing decision could be independent of an earlier draft edit. Explicit edges allowed useful work to continue.

The result was a small directed graph with bounded operation types, not a general workflow engine. Its limitations were now visible enough to design against.

Cancellation could not unsend a request

The first outbox offered a Delete button for pending entries. If a delivery attempt was already in flight, deleting the local record could erase the only operation ID needed to discover its outcome.

I separated local withdrawal from server reversal.

Before any attempt, a queued operation could be abandoned after confirmation, with its draft retained or exported. Once an attempt began, the operation record stayed until the outcome was known. The person could request cancellation, but the interface explained that cancellation might race with acceptance.

If the server accepted a reversible operation, undo became a new domain operation with its own authority and receipt. It did not delete history. Some actions, such as recording and publishing a final decision, were amended rather than erased.

The state machine included:

draft → queued → attempting → accepted
                 ↘ unknown → checking receipt
queued → abandoned
attempting → cancel-requested → accepted | cancelled

The product stopped presenting deletion as control over packets that had already left the device.

Service workers and the Background Sync idea made it tempting to promise that queued work would send even after the page closed. Browser support and scheduling were not uniform, and the operating system could terminate background work.

D4U7 attempted delivery while an authenticated compatible client was active. Where a safe background opportunity existed, it could accelerate eligible operations. Correctness did not depend on it.

The outbox remained visible on the next open. An entry did not become failed merely because background execution never occurred. The interface said waiting to send when D4U7 is open and online instead of implying a guaranteed invisible worker.

Online-only and attention-required operations never ran in a background context that could not present current state to the person. Auto-deliver operations still passed server preconditions.

This kept progressive enhancement honest. The browser could make delivery more convenient without becoming the only place the product knew how to recover.

Authentication had its own blocked state

An expired session was common after sleep. The outbox originally counted unauthorized responses as failed attempts and increased backoff.

Authentication was neither a transient network failure nor a domain rejection. The operation moved to blocked-authentication, retained its identity, and stopped retrying until a person signed in or another safe credential path completed.

After authentication, the server re-evaluated current membership and operation preconditions. The outbox did not assume that the newly signed-in account was the one that had authored the draft. If identities differed, it offered export and an explicit reassignment path only where the domain allowed it.

Signing out prompted the person about local work. D4U7 could remove cached server projections immediately while retaining encrypted or protected drafts only under an explicit device policy. On a shared device, local intent needed a deliberate export or deletion decision.

The simple “retry after login” flow hid identity and privacy questions. The outbox made them visible before sending another person's text under the wrong account.

Current authority was checked at execution

Storing authorityVersion did not freeze permission. It identified what the person had reasonably believed when authoring the operation.

The server checked current authorization at delivery. Membership could have been revoked, a role could have changed, or the room could have entered a restricted phase. An old capability in IndexedDB did not remain valid because the device had once been trusted.

For a changed-authority result, D4U7 retained the local draft and explained which action was no longer permitted. It did not reveal new room state beyond what the current account could access. A revoked participant could export their unsent text without receiving the room's newer private content.

I avoided embedding long-lived bearer credentials in outbox entries. Authentication material followed its own browser security lifecycle. Operations carried identity and context, not reusable secrets.

Offline-first architecture does not mean offline authority. Consequential permission is a current server decision unless the product deliberately issues a bounded offline capability and accepts its risk. D4U7 did not need that complexity.

Conflict was an attention state, not an error bucket

When a contribution's base revision changed, the outbox could not automatically merge arbitrary Markdown with confidence.

It moved the operation to attention and prepared three inputs: shared base, current server revision, and local draft. A three-way merge suggested non-overlapping changes. The person reviewed the result before creating a new operation against current state.

The old operation remained in history as not applied due to base change. The new operation linked it. This preserved why the draft had been rewritten and prevented a retry of the old packet later.

Short structured fields used different merge semantics. Adding a unique evidence reference could be set-like. A changed deadline required explicit choice. An acknowledgement of an immutable revision could coexist with unrelated room changes.

The outbox did not contain one generic conflict resolver. Operation types carried their own concurrency policy.

“Needs your review” was more accurate and less alarming than “sync failed.” The system had succeeded in detecting that only a person could decide what the delayed intent now meant.

A chronological list of queued packets made sense to the delivery worker and not to the writer.

The outbox UI grouped entries:

  • Saved on this device: durable drafts not yet queued.
  • Waiting for connection: auto-deliver operations whose preconditions were last known valid.
  • Sign in to continue: delivery blocked on authentication.
  • Review what changed: base, phase, or authority changed.
  • Checking outcome: an attempt may have succeeded and needs its receipt.
  • Delivered: recent receipts with links to server revisions.

Every item showed the human object—a proposal title or evidence description—rather than only an operation kind and ID. Technical details remained available for diagnosis.

The page offered export before destructive cleanup. It never instructed a person to clear IndexedDB to repair sync. Deleting rebuildable room snapshots was an internal recovery option; deleting irreplaceable intent required an explicit decision.

The interface made the delivery policy part of the product promise.

Retention had to include abandoned local work

Outbox entries could accumulate when a person stopped returning to a room. Keeping them forever on a device was not automatically respectful.

D4U7 showed aged local drafts and asked for review. Auto-deletion applied only to rebuildable caches. Irreplaceable drafts required a visible device-level policy: retain, export, or delete.

Accepted and abandoned operation metadata could be compacted after their receipt or recovery value expired, while the server-side domain revision followed the room's retention policy. Unknown-outcome records stayed until they could be resolved or deliberately waived.

The browser did not send draft-age analytics. Retention was enforced locally and surfaced to the person. On shared devices, sign-out handling took precedence.

The outbox was not merely a queue implementation. It was storage for unpublished human intent, with privacy and lifecycle consequences.

Q2F8's question returned: useful someday is not a purpose. Even a protective local copy needed a defined end.

I built a matrix around operation kind, base change, phase change, authority change, and transport outcome:

  • Auto-deliver evidence under unchanged room state.
  • Receive the request, lose the response, then return the same receipt on retry.
  • Change the contribution base before delivering a revision.
  • Close the room before a queued operation reconnects.
  • Expire authentication while membership remains valid.
  • Revoke membership without exposing newer room state.
  • Accept a parent creation, then release dependant operations.
  • Leave a parent in attention and keep dependants blocked.
  • Request cancellation while acceptance races.
  • Upgrade an older outbox record without losing its payload.

The tests asserted server state, local state, and explanation. An operation that stayed safe in IndexedDB but appeared as an unrecoverable red error was not a successful product outcome.

Manual browser tests covered sleep, storage limits, service-worker updates, and multiple tabs. A per-device delivery lease prevented two open tabs from attempting the same local operation simultaneously; stable server identity remained the final deduplication boundary.

The scenario matrix made “offline works” too vague to use as a release claim.

Fewer actions went offline

After adding delivery policy, D4U7 supported fewer automatic offline actions than the first prototype.

That was progress.

Reading provisioned rooms, writing and preserving drafts, adding bounded evidence, and preparing responses remained useful without a network. Final decisions, permission changes, and lifecycle transitions required current state. Several operations could be authored offline and committed only after review.

The product promise became:

Your work will survive interruption. The system will deliver it automatically only when its assumptions remain valid, and it will help you review it when they do not.

This was less magical than a universal outbox and more trustworthy.

An outbox is often described as a reliability mechanism. The deeper responsibility is semantic. Time passes between intent and execution. State, authority, and consequence can change while a packet waits.

Preserving the operation is the beginning. Delivery policy decides whether preserving it also preserves what the person meant.

Reliability without meaning is only persistence.