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-requestsreturned 401 right after sign-in — and was silently swallowed by acatchstartEvidence()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
previewstep and never advanced to signing. The state mapping was fixed soPROPOSEDroutes tosign. - 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.