Skip to content

P0 MVP Final Review

Written 2026-07-27.

This document records what was confirmed to actually work in the P0 scope and the defects that surfaced along the way. What passed and what did not are given equal weight.

1. Conclusion

P0 feature Status Evidence
Wallet sign-in Confirmed Browser E2E → PostgreSQL session row created
XMTP 1:1 Confirmed A↔B, B↔C round trips. Verified through the app UI
XMTP groups Confirmed A/B/C three-person group send and receive. Verified through the adapter path
Dojang badges and inbox routing Confirmed Live GIWA Sepolia lookups. Restored in this pass (§3.2)
On-chain evidence (use case) Confirmed A real GIWA Sepolia transaction
On-chain evidence (full browser path) Unconfirmed §4

Evidence passed the use-case chain from Node against the real backend and chain. There is no record of passing end to end with two browser instances. The reason is in §4.

2. Automated tests

Target Passing
frontend unit and integration 211
backend unit and PostgreSQL integration 132
contracts unit, fuzz, fork 41

typecheck, build, scripts/check-secrets.sh, and scripts/check-secrets.test.sh all pass.

Browser E2E runs against the real XMTP dev network and GIWA Sepolia.

Spec Coverage
app-wallet-login.spec.ts Wallet A sign-in → inbox → Dojang badge in settings
app-three-users.spec.ts Three sign-ins, A-B / B-C 1:1 round trips, request-inbox routing and evidence blocking for unverified peers
messaging-three-users.spec.ts 1:1 and three-person groups via direct adapter calls
app-evidence.spec.ts Two app instances: proposal → both signatures → on-chain record

3. Defects found in this review

3.1 Every evidence API call returned 401 in the browser

The page was on 127.0.0.1:4173 and the API on localhost:3111. Those are different sites, so the SameSite=Lax session cookie was not attached to fetch. The results were:

  • GET /v1/evidence-requests returned 401 right after sign-in — and was silently swallowed by a catch
  • startEvidence() failed with 401, so the evidence screen never opened

The earlier judgement of "full evidence path passing" rested on integration tests that managed cookies directly from Node. The browser path was not under test, which is why this went unnoticed. The default in playwright.config.ts had the same trap, so it was changed to 127.0.0.1.

3.2 Dojang-based inbox routing was switched off

classifyConversation was looking only at the spam flag and sending everything else to the main inbox. The request inbox was always empty, which failed plan §3.1, design §6.2, and completion criteria §11.2. The rule was restored along with its tests.

3.3 Four defects already fixed in a separate session

The following were fixed in other work on the same repository and confirmed in code during this review.

Defect Symptom
fetcher bound to this Evidence API calls died in the browser with Illegal invocation
Evidence list not fetched at sign-in The app never noticed proposals created by a peer, so signing was impossible
messageRefs ignored Hashing all local messages caused content hash mismatches between participants
No stream resubscribe, no past messages Broken connections never recovered; opening a conversation showed an empty history

4. The full browser path for evidence

An earlier version of this section blamed the wrong cause. Corrected here.

Each attempt surfaced a different defect.

Attempt Where it stalled Resolution
1 Every evidence API call 401 Fixed the host mismatch (§3.1)
2 Sign button unresponsive The evidence screen was hiding the error. Surfaced it via notice
3 All 40 conversation messages targeted Range selection was unwired. Wired it
4 Still 40 even after choosing a range The proposal was created before range selection. Fixed the ordering
5 Signer reported "target messages not found" See below

Attempt 5 was written up as a property of MLS, unverifiable in this environment — which was wrong. Those messages were visibly present on the signer's screen, so it was not a decryption problem.

There were two actual causes, both fixed.

  • After the proposal was created the screen stayed on the preview step and never advanced to signing. The state mapping was fixed so PROPOSED routes to sign.
  • The E2E test was opening an old proposal card left in the conversation. The signer did not hold some of the older messages that proposal pointed at, so it was rejected. The defect was in the test, not the app.

The diagnosis was settled by actually printing the proposal's messageRefs alongside the signer's local message IDs and comparing them. Of 36 references, only the first 2 were missing and the other 34 matched exactly — meaning it was the old 36-message proposal, not the 2-message one just selected.

The lesson is worth keeping: before attributing an unreproducible failure to a property of the protocol, print the actual data.

5. Dead code removed

Target Reason
Control message kind recorded Never sent from anywhere
allowsEvidence, isVerifiedStatus, INBOX_CATEGORIES No use outside their definition files

What was kept is worth recording too.

  • The evidence rejection path is unfinished work, not dead code. Flow 5 in the screen flow includes [reject], and the backend, domain, and tests already exist. Only the UI wiring is missing.
  • PoC mode (VITE_CHAT_POC=1) is a development tool documented in the README. The real app covers the same ground, so dropping it would remove roughly 1,000 lines.

6. Remaining limits

Collected under "Known limits" in docs/architecture/user-flows.md. In short: the session is not restored on refresh, peer signatures are detected by 5 second polling, there is no evidence rejection button, and XMTP caps installations at 10 per inbox.

Faucet attestations expire 30 days after issuance. Wallet A expires around 2026-08-10 and wallet B around 2026-08-25; re-verifying the evidence success path after that requires reissuing them.

The Android app was not implemented. The reasoning and the migration boundary are in mobile/README.md.