---
test_id: example-nextjs-todos
run_date: 2026-06-16
run_time_utc: "17:06:51Z"
runner: claude-opus (main thread; tester agent's curl-only sandbox cannot launch docker/pnpm/pgpaw)
sandbox_or_env: "docker postgres:16 on 127.0.0.1:5433 (5432 busy), pgpaw serve :8080, next dev :3000"
duration_secs: 520
result: partial
exit_reason: "Data path verified end-to-end via API/SSE; real browser interaction not exercised (Playwright MCP disconnected). Two bugs found and fixed during the run."
related_commit: "46e4742"
---

## Summary

Brought the full stack up and exercised the example's end-to-end data path: a write through the Next.js API route lands in Postgres and returns a transaction id, and PgPaw's live SSE stream carries the matching `insert`/`update`/`delete` deltas plus `up-to-date`, each tagged with that `txid`. The optimistic-confirmation contract (`api → {txid}` ⇄ `sse delta {txid}`) is fully demonstrated. Two real defects were uncovered and fixed mid-run: (1) the example **500'd under Next SSR** because `useLiveQuery` has no `getServerSnapshot`; (2) the running `target/release/pgpaw` was **stale** (built before the txid feature) so deltas initially carried no `txid`. After fixing both, the loop is green. Browser-level rendering/clicks were NOT exercised — Playwright MCP was disconnected — so this is a `partial`, not a full pass.

## Steps Executed

### Step 1: Provision Postgres (port remapped 5432→5433)
**Action run**: `docker run -d --name pgpaw-todos-pg ... -p 5433:5432 postgres:16 -c wal_level=logical`
**Primary observation**: container id returned; `pg_isready` accepting connections in ~1s; `SHOW wal_level` → `logical`.
**Awareness check**: 5432 was already in use on the host → remapped host port to 5433 (Deviation). No prior `pgpaw-todos-pg` container existed.
**Judgment**: pass

### Step 2: Apply schema
**Action run**: `docker exec -i pgpaw-todos-pg psql -U postgres -d myapp < examples/nextjs-todos/schema.sql`
**Output excerpt**: `CREATE TABLE` / `GRANT`; `\dt` → `public|todos|table`.
**Judgment**: pass

### Step 3: pgpaw init
**Action run**: `echo y | target/release/pgpaw init --pg-port 5433 ...`
**Output excerpt**: `✓ wal_level = logical`, `✓ created publication "cache_server_pub" FOR ALL TABLES`, `✓ installed DDL event trigger`.
**Awareness check**: publication is **FOR ALL TABLES** → the notebook's separate `alter publication ... add table todos` Step is unnecessary and would error; skipped (Deviation). `init` is interactive (`Proceed? [y/N]`) — required piping `y`.
**Judgment**: pass

### Step 4: pgpaw serve + health
**Action run**: `target/release/pgpaw serve --pg-port 5433 --port 8080 --data-dir /tmp/pgpaw-todos-cache` (backgrounded), then `curl /healthz`.
**Output excerpt**: `{"status":"ok","watermark":26534928}`.
**Judgment**: pass

### Step 5: Build + link the library
**Action run**: `pnpm install --ignore-workspace` + `pnpm build` in `packages/tanstack-db` (ESM+CJS+dts built), then `pnpm link --global`.
**Primary observation**: build OK; **`pnpm link --global` FAILED** — `ERR_PNPM_NO_GLOBAL_BIN_DIR` (no `PNPM_HOME`).
**On weirdness**: fell back to a local dependency — edited the example's dep to `link:../../packages/tanstack-db` (Deviation; the example's `^0.1.0` registry spec would 404 anyway since the package is unpublished).
**Judgment**: pass (via fallback)

### Step 6: Install example + env + dev server
**Action run**: `pnpm install --ignore-workspace` in `examples/nextjs-todos` → symlinked `@pgpaw/tanstack-db`; wrote `.env` with `DATABASE_URL=...:5433`; `pnpm dev` (backgrounded).
**Output excerpt**: `✓ Ready in 240ms`.
**Awareness check**: Next 16 rewrote `tsconfig.json` `jsx → react-jsx` (expected). `sharp` build script ignored (irrelevant to this test).
**Judgment**: pass

### Step 7: Load the page  — FAIL then FIXED
**Action run**: `curl http://127.0.0.1:3000/`
**Primary observation**: **HTTP 500**. Dev log: `Error: Missing getServerSnapshot, which is required for server-rendered content` at `app/page.tsx:9` (the `useLiveQuery` call).
**Fix applied**: split the live-query UI into `app/todo-list.tsx` and made `app/page.tsx` a client wrapper that loads it via `next/dynamic` with `{ ssr: false }`. Re-fetch → **HTTP 200**, no error digest, dev log `GET / 200`.
**Judgment**: fail → pass after fix (bug #1, see Root Cause)

### Step 8: Write via the example API
**Action run**: `curl -X POST :3000/api/todos -d '{"id":"545C...","title":"buy milk","completed":false}'`
**Output excerpt**: `{"txid":747}`. psql: `545C...|buy milk|f`.
**Judgment**: pass

### Step 9: Toggle + capture live delta — FAIL (no txid) then FIXED
**Action run**: open `curl -N POST :8080/query?live=true`, then `PATCH :3000/api/todos/<id> -d '{"completed":true}'` mid-stream.
**Output excerpt (initial, stale binary)**:
```
data: {"type":"snapshot","url":"/q/77ea9db18c04104e/26836296","version":26836296}
data: {"key":"545C...","op":"update","row":{"completed":true,...}}
data: {"op":"up-to-date"}
```
**Primary observation**: live update delta arrives after the API write (CDC→SSE works), psql confirms `completed=t`, PATCH returns `{"txid":748}` — BUT the delta and `up-to-date` carried **no `txid`**.
**Root cause**: `target/release/pgpaw` mtime `Jun 16 01:20` predates the txid commit `cfc5ff3` (`Jun 16 22:31 +0700`) — a STALE binary without the txid feature.
**Fix applied**: `cargo build --release --bin pgpaw` (227 crates, 35s), killed + restarted serve on a fresh data-dir.
**Judgment**: fail → pass after rebuild (bug #2, see Root Cause)

### Step 10: Re-verify delta carries txid (fresh binary)
**Action run**: open live stream, `POST :3000/api/todos -d '{"id":"E29...","title":"ship it"}'` mid-stream.
**Output excerpt**:
```
data: {"type":"snapshot","url":"/q/77ea9db18c04104e/0","version":0}
data: {"key":"E29...","op":"insert","row":{...},"txid":749}
data: {"op":"up-to-date","txid":749}
```
**Primary observation**: API returned `{"txid":749}`; the `insert` delta AND `up-to-date` both carry `"txid":749` — the awaitTxId confirmation loop closes. Public snapshot is the `/q/{hash}/{version}` pointer.
**Judgment**: pass

### Step 11: Delete via the example API
**Action run**: `curl -X DELETE :3000/api/todos/545C...`
**Output excerpt**: `{"txid":750}`; psql `count` → 1 (only "ship it" remains).
**Judgment**: pass

## Environment Observations

- Host port 5432 occupied (pre-existing Postgres) → docker remapped to 5433.
- `pnpm link --global` unusable without `PNPM_HOME`; `link:` dependency is the working local-dev path.
- Publication is FOR ALL TABLES — `todos` auto-covered; no per-table add.
- The `tester` agent could not have run this: its sandbox allows only curl + file probes, not docker/pnpm/pgpaw/psql. Executed by the main thread instead.
- Playwright MCP disconnected → no real browser. Browser Steps were satisfied at the HTTP/API layer (page returns 200; writes/deltas via API + SSE).

## Evidence

- Health: `{"status":"ok","watermark":26534928}` then `...26837064` after rebuild.
- Live capture with txid: `/tmp/sse2.out` (insert delta + up-to-date both `"txid":749`).
- API responses: insert `{"txid":747}`, toggle `{"txid":748}`, insert `{"txid":749}`, delete `{"txid":750}`.
- psql state transitions: insert → `buy milk|f`; toggle → `buy milk|t`; insert → `ship it|f`; delete → count 1.
- Page: HTTP 500 (pre-fix) → HTTP 200 (post-fix), dev-log digest `2380209593` gone.

## Root Cause Analysis

**Bug #1 — example 500 under SSR (FIXED in example).** `useLiveQuery` uses `useSyncExternalStore` without a `getServerSnapshot`; Next App Router server-renders client components, so SSR threw. Fix: render the live-query subtree client-only via `next/dynamic({ ssr: false })`. The `nextjs-project-board` example has the identical pattern and the same fix was applied.

**Bug #2 — stale release binary (FIXED by rebuild; process gap, not a code bug).** The committed txid feature (`live.rs`) was never compiled into `target/release/pgpaw` this session (only debug `cargo build`/`cargo test` were run). The deployed binary silently lacked txid on the wire, which would make the example's `awaitTxId` hang for 30s on every optimistic write. Lesson: the release binary must be rebuilt after backend changes before an example E2E run — worth a precondition (`pgpaw` binary newer than last src commit) in the notebook.

## Deviations

- Postgres host port 5432 → 5433 (and `.env`/`--pg-port` updated).
- `pnpm link --global` → `link:` local dependency (example `package.json` edited; reverted in cleanup).
- Skipped the `alter publication ... add table` Step (publication is FOR ALL TABLES).
- Browser Steps (navigate/snapshot/type/click) executed via the documented API/SSE fallback — Playwright MCP unavailable.
- Run executed by main thread, not the `tester` agent (sandbox cannot launch infra).

## Next Actions

- [x] Fix example SSR crash (both examples) — `next/dynamic({ ssr: false })`.
- [ ] Re-run with Playwright connected to validate real browser render + click interactions (upgrade partial → full pass).
- [ ] Add a precondition to both notebooks: `target/release/pgpaw` is newer than the latest `src/**` commit (guards against the stale-binary trap).
- [ ] Consider documenting `pnpm setup` (PNPM_HOME) or a `file:`/`link:` dep in the example READMEs for the unpublished-package phase.
