Skip to content

System Design

Umul is a mobile-shaped web messenger that uses a wallet address as identity. Messaging runs on the XMTP dev network; verification lookups and conversation evidence run on GIWA Sepolia.

System architecture

The app encrypts and decrypts messages with a per-device XMTP installation key and a local MLS DB, while the server handles only login sessions and evidence state (Auth / Evidence). The editable source is architecture.drawio.

Components

Component Responsibility Access to message plaintext
frontend Wallet connection, XMTP E2EE messaging, Dojang lookups, evidence manifest creation and signing Yes
backend Login nonces and sessions, evidence request state and signature collection No
contracts EIP-712 multi-signature verification, Dojang re-check, evidence hash recording No

The frontend is the only component that touches message plaintext. The backend and the contract receive only hashes and signatures.

Layer boundaries

The frontend and the backend both use the same four layers.

  • domain: address normalization, inbox routing, evidence manifest rules, evidence state transitions
  • application: use cases and port interfaces
  • infrastructure: XMTP, wallet, RPC, PostgreSQL, and HTTP adapters
  • presentation / interfaces: screens and HTTP routes

presentation never calls an SDK directly; it uses only the ports from application. Implementations are injected at app startup in src/app/runtime/UmulRuntime.ts.

Runtime assembly

readConfig()  ->  createShell()  --wallet sign-in-->  createSession()
                     │                                    │
                     ├─ WagmiWalletAdapter                 ├─ XmtpMessagingAdapter
                     ├─ HttpSessionAdapter                 ├─ ViemEvidenceContractAdapter
                     ├─ ViemDojangAdapter                  └─ messaging / evidence use cases
                     └─ HttpEvidenceAdapter

createShell holds only the dependencies that can be built before sign-in. The XMTP client and the evidence contract adapter are built in createSession, once the wallet address is settled.

Without VITE_EVIDENCE_CONTRACT_ADDRESS, only the evidence feature is disabled; messaging still works.

Trust and routing

The result of DojangScroll.isVerified(address, attesterId) is cached per address for one hour. An RPC error is recorded as unknown rather than unverified, so that in an unconfirmable state messaging is still allowed and only evidence is blocked.

Inbox routing is decided in exactly one place, domain/inbox/classifyConversation.ts.

Condition Routing
User marked it spam (XMTP consent denied) Spam inbox
Every participant other than yourself is verified Main inbox
Anything else (including unverified or unknown) Request inbox

Evidence flow

[proposer] select messages -> build manifest -> re-query Dojang (bypass cache)
    -> POST /v1/evidence-requests only if everyone is verified
    -> encrypt plaintext and POST .../archive (the content key never reaches the server)
    -> send XMTP control message gasok:evidence:v1:<base64url-json> (key included)

[participant] receive control message -> recompute content hash from local messages
    -> sign under EIP-712 only on a match -> POST .../signatures
    -> POST .../archive/keys with the restore key wrapped by a wallet-signature-derived key

[everyone signed] state READY -> recordEvidence transaction -> POST .../submission

[restore] a wallet that signed calls GET .../archive -> decrypt with a wallet signature
    -> display only after re-verifying the content hash (see [Message History](message-history.md))

The contract re-queries Dojang at submission time. If verification is revoked after signing, the transaction is rejected even though the frontend's pre-check passed.