Skip to content

Phase 2.5 Step 3.5: flip write-path KV authorization reads to D1 (prerequisite for #730) #736

@whoabuddy

Description

@whoabuddy

Context

Surfaced during Step 3.4 phase-executor full-path import audit. The Step 3.4 spec (#729) treated `getMessage` and `getReply` as obsolete read helpers after Steps 3.1–3.3 flipped the GET handlers to D1. The audit found these helpers are still live in WRITE-path authorization reads (PATCH mark-read, POST reply):

  • `app/api/outbox/[address]/route.ts:198` — POST handler: `getMessage(kv, messageId)` to fetch original message for auth
  • `app/api/outbox/[address]/route.ts:318` — POST handler: `getReply(kv, messageId)` to check for duplicate replies
  • `app/api/outbox/[address]/route.ts:343` — POST handler: `getMessage(kv, messageId)` in partial-write recovery path
  • `app/api/inbox/[address]/[messageId]/route.ts:203` — PATCH handler: `getMessage(kv, messageId)` to verify ownership before mark-read

These are authorization reads embedded in write flows, not display reads. Step 3.2 (#731) flipped the GET path only.

Why this is a prerequisite for #730 (Step 4)

Step 4 removes KV writes on POST/PATCH handlers. Once writes are gone, KV is no longer kept in sync — KV reads on those same handlers would race against stale data. Step 4's cutover requires the entire POST/PATCH path (read + write) to be on D1.

Doing the auth-read flip as a separate step also isolates the variable: this PR is reversible (D1 already has the data via dual-write since Step 1; flipping reads doesn't impact writes), whereas Step 4 is the irreversible write removal.

Scope

In:

  • Flip POST `/api/outbox/[address]` to use D1 for the original-message auth lookup (`getMessageFromD1` from feat(inbox): flip GET /api/inbox/[address]/[messageId] to D1 reads #731)
  • Flip POST `/api/outbox/[address]` to use D1 for the duplicate-reply check (needs new helper or use `fetchRepliesForMessages` with single-element array)
  • Flip PATCH `/api/inbox/[address]/[messageId]` to use D1 for the mark-read ownership check (`getMessageFromD1` from feat(inbox): flip GET /api/inbox/[address]/[messageId] to D1 reads #731)
  • Adopt the D1-throws fallback pattern (503 + Retry-After) on all flipped paths
  • Preserve the existing tenant-discriminator security gate (the `to_btc_address = ?` + `from_btc_address = ?` predicates already enforce this in D1 SQL)
  • Tests covering: auth path on real-message hit, auth path on missing-message miss, tenant-discriminator (caller's BTC address must match), D1-throws-503

Out:

Why not roll this into #730

#730 is already the irreversible cutover with a 60min smoke window. Bundling the auth-read flip would conflate two cutovers into one PR and a single smoke. Splitting them lets each smoke isolate one variable (LOOP_PROMPT PR-scoping discipline).

Acceptance criteria

  • All four call sites flipped to D1
  • D1-throws fallback returns 503 + Retry-After: 5
  • Tenant-discriminator SQL gate preserved
  • Test coverage per spec
  • Smoke window CLEAN post-merge (POST + PATCH paths are revenue-affecting; +30min smoke)
  • Worker-logs error baseline check on POST/PATCH paths

References

Sign-off context

Authorized under whoabuddy's 2026-05-11T01:40Z directive ("go all the way; Step 4 is important to reducing the bill"). Step 4 (#730) still requires the explicit double-checkpoint per LOOP_PROMPT; this Step 3.5 is incremental and inherits the series authorization.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestprod-gradeProduction-grade standards gap

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions