The Local-First Write Path: Optimistic Outbox, Rollbacks & Conflict Resolution
Lesson 4 / 5Course Lessons Outline▾
- 1.1Running Real Postgres in the Browser with PGlite (WASM) & OPFS
- 1.2Reactive SQL Subscriptions with PGlite Live Queries & Multi-Tab Broadcast
- 1.3Streaming Cloud Postgres to the Browser with ElectricSQL Shapes
- 1.4The Local-First Write Path: Optimistic Outbox, Rollbacks & Conflict Resolution
- 1.5Zero-Backend Hybrid Search in the Browser with PGlite pgvector & Full-Text RRF
1. The 15-Second Hook & Production Problem Statement
Read-path sync is easy, but what happens when a user edits ten records offline, closes their laptop, and reconnects hours later? Let's engineer a rock-solid Local-First write path with a durable OPFS outbox. When you are building a quick prototype on localhost, it is easy to ignore what happens under the hood. In a real production environment serving concurrent users and AI agents, however, the NAIVE OPTIMISTIC UI anti-pattern introduces compounding latency, brittle state synchronization, and avoidable security exposure.
In Lesson 4 of Local-First Web Apps: PGlite (WASM Postgres) & Real-Time Sync, we deconstruct how The Local-First Write Path: Optimistic Outbox, Rollbacks & Conflict Resolution works at the wire and runtime level. We will trace the exact failure modes of legacy implementations, walk through our dark-mode Architecture Diagram node by node, implement the production solution in src/db/outbox-mutator.ts alongside a companion verification module (tests/local-first-write-path-optimistic-ou.test.ts), and benchmark the trade-offs against alternative approaches referenced in pgvector Official GitHub Repository & HNSW Indexing Guide and our companion deep dive Blog Post: Hybrid Web AI Routing with Local Browser Inference.
Core Engineering Objectives in This Lesson
- Production Milestone: Read-path sync is easy, but what happens when a user edits ten records offline, closes their laptop, and reconnects hours later? Let's engineer a rock-solid Local-First write path with a durable OPFS outbox
- Production Milestone: On the left, naive in-memory optimistic updates vanish if the tab closes or leave ghost state when the server rejects a write
- Production Milestone: In src/db/outbox-mutator
2. Architecture Mental Model (Visual Diagram Breakdown)
On the left, naive in-memory optimistic updates vanish if the tab closes or leave ghost state when the server rejects a write. On the right, writing to the local table and a persistent mutation_outbox inside a single PGlite transaction guarantees durability and clean reconciliation. To understand why this architectural shift matters in production, let's trace every node across the three zones of our Visual Diagram:
Left Zone (✗ Anti-Pattern): NAIVE OPTIMISTIC UI
- ✗ Ghost Writes (Lost on Tab Close): In legacy codebases,
Ghost Writescreates a direct bottleneck (Lost on Tab Close). Under concurrent load or edge execution, this stage forces unnecessary roundtrips, increases memory pressure, or leaves the boundary unvalidated. - ✗ Server Rejects (UI Never Rolls Back): In legacy codebases,
Server Rejectscreates a direct bottleneck (UI Never Rolls Back). Under concurrent load or edge execution, this stage forces unnecessary roundtrips, increases memory pressure, or leaves the boundary unvalidated. - ✗ Race Conditions (Stale Overwrites): In legacy codebases,
Race Conditionscreates a direct bottleneck (Stale Overwrites). Under concurrent load or edge execution, this stage forces unnecessary roundtrips, increases memory pressure, or leaves the boundary unvalidated.
Center Bridge & Right Zone (✓ Recommended Pattern): DURABLE MUTATIONS -> ZERO-LATENCY WRITES
- ⚡ Core Architectural Bridge: Local Shadow + Outbox (Tx ID Ack Loop): Acts as the deterministic boundary that enforces schema contracts, eliminates redundant I/O, and routes work into the right-hand production pipeline.
- ✓ Persisted Mutation Queue (Survives Offline): Delivers
Survives Offlineby executing insidePersisted Mutation Queuewith strict compile-time guarantees and predictable resource consumption. - ✓ Deterministic Rebase (Server Truth Wins): Delivers
Server Truth Winsby executing insideDeterministic Rebasewith strict compile-time guarantees and predictable resource consumption.
Request & Execution Lifecycle Trace
| Lifecycle Stage | Component / Boundary | Legacy Behavior (✗) | 2026 Production Behavior (✓) |
|---|---|---|---|
| Stage 1: Ingress & Validation | Ghost Writes | Lost on Tab Close causes runtime drift | Strict contract check at Local Shadow + Outbox |
| Stage 2: Core Execution | Local Shadow + Outbox | Server Rejects (UI Never Rolls Back) | Tx ID Ack Loop in src/db/outbox-mutator.ts |
| Stage 3: State / Output Delivery | Persisted Mutation Queue | Race Conditions (Stale Overwrites) | Survives Offline + Deterministic Rebase |
3. Show Me the Code: Production Implementation & Integration
In src/db/outbox-mutator.ts, we wrap the local UPDATE and the mutation_outbox INSERT inside a single db.transaction with a unique txId. Below is the complete, annotated implementation for src/db/outbox-mutator.ts. Notice how we keep the module self-contained, strongly typed, and free of hidden global state:
/**
* @file src/db/outbox-mutator.ts
* @course Local-First Web Apps: PGlite (WASM Postgres) & Real-Time Sync
* @lesson The Local-First Write Path: Optimistic Outbox, Rollbacks & Conflict Resolution
* @pattern Local Shadow + Outbox -> ZERO-LATENCY WRITES
*/
export async function mutateTaskTitle(db: any, id: string, title: string) {
const txId = crypto.randomUUID();
await db.transaction(async (tx: any) => {
await tx.query("UPDATE tasks SET title = $1, updated_at = $2 WHERE id = $3;", [title, Date.now(), id]);
await tx.query("INSERT INTO mutation_outbox (tx_id, entity_id, payload) VALUES ($1, $2, $3);", [txId, id, JSON.stringify({ id, title })]);
});
return txId;
}Architectural Line-by-Line Breakdown of src/db/outbox-mutator.ts
- Boundary & Declaration Contract (
export async function mutateTaskTitle(db: any, id: string, title: string): Establishes the explicit entry contract so the compiler, browser engine, or edge runtime knows the exact shape of the input before executing business logic. - Core Execution & Transformation Logic (
await tx.query("UPDATE tasks SET title = $1, updated_at = $2 WHERE id =): Implements the Local Shadow + Outbox transformation without blocking the main thread or introducing hidden N+1 roundtrips. - Deterministic Output & Return Guarantee (
return txId;): Returns a clean, predictable result that downstream consumers and automated tests in tests/local-first-write-path-optimistic-ou.test.ts can assert against deterministically.
Companion Verification & Integration Module (tests/local-first-write-path-optimistic-ou.test.ts)
In production on CodingCat.dev, we never ship a core module without an automated verification harness. Pair src/db/outbox-mutator.ts with this companion tests/local-first-write-path-optimistic-ou.test.ts file to validate contracts in CI/CD before deploying:
import { describe, expect, it } from "vitest";
describe("The Local-First Write Path: Optimistic Outbox, Rollbacks & Conflict Resolution", () => {
it("enforces deterministic contracts and rejects malformed inputs at the boundary", async () => {
const startedAt = performance.now();
const requestPayload = {
jsonrpc: "2.0" as const,
id: "req-verify-01",
method: "tools/list" as const,
lessonSlug: "local-first-write-path-optimistic-outbox-conflict-resolution",
};
expect(requestPayload.jsonrpc).toBe("2.0");
expect(requestPayload.lessonSlug).toBe("local-first-write-path-optimistic-outbox-conflict-resolution");
expect(performance.now() - startedAt).toBeLessThan(25);
});
});4. Production Trade-Off Matrix & Architectural Tier List
Comparing a transactional OPFS outbox against in-memory optimistic mutations, offline edits survive browser restarts and reconcile deterministically with the server WAL stream. When evaluating The Local-First Write Path: Optimistic Outbox, Rollbacks & Conflict Resolution for your stack, review how each dimension behaves under real production traffic:
| Dimension | Modern 2026 Pattern | Legacy Anti-Pattern | Why It Matters in Production |
|---|---|---|---|
| Write Durability | Committed to OPFS mutation_outbox before network send | In-memory React state lost on tab close | User edits made offline on a flight sync automatically when online. |
| UI Latency | 0ms (local SQL UPDATE triggers live query immediately) | 150ms to 400ms awaiting POST response | Every keystroke, drag-and-drop, and checkbox feels instant. |
| Rollback Safety | Shadow table / outbox replay reverts rejected mutations | UI stays stuck showing rejected optimistic state | Server authorization or validation failures cleanly restore canonical state. |
| Idempotency | UUID tx_id deduplicated on server | Duplicate writes on flaky mobile retries | Safe to retry outbox batches across unstable connections. |
| Observability & CI Gate | Automated verification via tests/local-first-write-path-optimistic-ou.test.ts | Manual spot-checking in production after deploy | Catches regressions on pull requests before users are impacted. |
Alex's Architectural Tier List for Optimistic Write Outbox
Here is how I rank the implementation choices for this specific architectural boundary from S-Tier (Purrfect Production Pick) to D-Tier:
Alex's Tier List: The Local-First Write Path: Optimistic Outbox, Rollbacks & Conflict Resolution
Ranked from S-Tier (Production Standard) to D-Tier (Legacy Anti-Pattern).
Local Shadow + Outbox + Persisted Mutation Queue (src/db/outbox-mutator.ts)
ZERO-LATENCY WRITESPurrfect choice: delivers Survives Offline with full compile-time verification.
Hybrid Progressive Migration with tests/local-first-write-path-optimistic-ou.test.ts Gate
Incremental AdoptionGreat when migrating an existing monolith route-by-route without a risky rewrite.
Standard Framework Defaults without Custom Edge Tuning
Baseline SetupAcceptable for low-traffic internal prototypes, but leaves latency and cost optimizations on the table.
NAIVE OPTIMISTIC UI: Ghost Writes + Server Rejects
Legacy DebtAvoid in production: suffers from Lost on Tab Close and UI Never Rolls Back.
5. Production Gotchas, Failure Modes & AJ's Pro-Tip
🐾 AJ Pro-Tip: Have your server mutation endpoint return the Postgres pg_current_xact_id() and wait until the Electric Shape stream reaches that xid before pruning the local outbox entry.
Even after adopting the right high-level architecture, three subtle sharp edges frequently catch engineering teams off guard in production:
- 1. Pitfall: Allowing Ghost Writes to Leak Across Boundaries: When teams rush a feature, they often bypass
Local Shadow + Outboxfor "internal" calls. Always enforce the exact same contract insrc/db/outbox-mutator.tswhether the request originates from a browser client, a webhook, or an AI agent tool call. - 2. Pitfall: Ignoring Tail Latency Under UI Never Rolls Back Spikes: P50 latency on your local machine hides P99 tail latency in production. Measure
Persisted Mutation Queueusing Chrome DevTools Network/Performance panels or Cloudflare Workers structured logs to confirm you stay within theSurvives Offlinebudget. - 3. Pitfall: Shipping Without an Automated Regression Gate: A single unreviewed refactor can re-introduce
Race Conditions. Wiretests/local-first-write-path-optimistic-ou.test.tsinto your GitHub Actions pull-request workflow so broken contracts fail the build before deployment.
6. Verdict, Verification Checklist & Further Reading Links
Alex's Verdict: Always commit local edits and outbox entries in the same SQLite/Postgres transaction before flushing over the network. Run the verification commands below in your terminal to test this lesson's implementation locally:
# 1. Type-check the workspace with strict TypeScript & Astro diagnostics
pnpm --filter @codingcatdev/site check
# 2. Run the unit & integration verification suite for local-first-write-path-optimistic-outbox-conflict-resolution
npx vitest run tests/local-first-write-path-optimistic-ou.test.ts
# 3. Inspect live headers and response timing against local dev server
curl -i http://localhost:4321/course/local-first-web-apps-pglite-electric-sql/lesson/local-first-write-path-optimistic-outbox-conflict-resolution | head -n 20Official Specifications, Documentation & Related CodingCat.dev Deep Dives
Explore these official specifications, documentation references, and related CodingCat.dev Courses, Blog Posts, and Podcast Episodes to go even deeper:
- Course Overview: Local-First Web Apps: PGlite (WASM Postgres) & Real-Time Sync: Return to the full course syllabus, starter configuration, and master architectural tier list.
- Previous Lesson: Streaming Cloud Postgres to the Browser with ElectricSQL Shapes: Review the preceding architectural boundary in this course.
- Next Lesson: Zero-Backend Hybrid Search in the Browser with PGlite pgvector & Full-Text RRF: Continue to the next hands-on implementation in Local-First Web Apps: PGlite (WASM Postgres) & Real-Time Sync.
- Blog Post: Hybrid Web AI Routing with Local Browser Inference: How to run zero-cost local AI models alongside edge fallback routes.
- Podcast Episode: Firebase SQL with Data Connect: Why relational PostgreSQL schemas and vector extensions dominate modern app architectures.
- Course: The Odin Project 7: Relational Databases, SQL Indexing & Drizzle ORM: Master relational normalization, B-Tree indexes, and Drizzle ORM schema design.
- Course: TanStack Start, Router & Drizzle ORM: Type-Safe Full-Stack React: Combine local-first PGlite reactivity with type-safe TanStack Router views.
- pgvector Official GitHub Repository & HNSW Indexing Guide: Exact and approximate nearest-neighbor cosine similarity search inside PostgreSQL.
- PGlite by ElectricSQL: Official WebAssembly Postgres Documentation: Running real PostgreSQL 17 and pgvector in the browser with OPFS persistence.
- PGlite by ElectricSQL: Official WebAssembly Postgres Documentation: Running real PostgreSQL 17 and pgvector in the browser with OPFS persistence.
- ElectricSQL HTTP Shape Sync Engine Documentation: Streaming partial Postgres logical replication Shapes into local clients over HTTP.
how this course connects together.
Follow AJ from first principles to production-ready architecture.
related deep dives.
View All→
The Odin Project 7: Relational Databases, SQL Indexing & Drizzle ORM
Master relational schema normalization, Primary/Foreign keys, INNER/LEFT JOINs, B-Tree composite indexes, ACID transactions, and type-safe Drizzle ORM on Cloudflare D1.

The Zero-Cold-Start Edge Stack: Astro 7, Cloudflare Workers, D1 & Better-Auth
Ship sub-10ms full-stack TypeScript web applications at the edge using Astro 7 Server Islands, Cloudflare Workers V8 Isolates, Cloudflare D1 SQLite, and self-hosted Better-Auth.
