Files
mortgagefi-helper/docs/Architecture/Architecture.md
Siavash Sameni 6ae581ab2e feat(ui): Ghibli/Miyazaki reskin + Obsidian docs vault + project audit
UI: warm daylight design system (Tailwind v4 @theme palette, gh-* component
classes, watercolor grain, Zen Maru Gothic + Klee One fonts), animated SSR-safe
GhibliBackground (drifting clouds, meadow hills, soot sprites), and a full reskin
of navbar, connect button, dapp page, loan cards, settings modal, and readme.
Fixes the bg-white-on-dark loan-card inconsistency. Web3/business logic untouched.

Docs: converted docs/ into an Obsidian vault (frontmatter, [[wikilinks]],
callouts, Home MOC, folders Architecture/Operations/Audits) and added a
full-project audit note (Project Audit 2026-06). Redacted a real leaked Schedy
key value from the security audit example (rotate it at Schedy).

Also commits the previously-untracked server layer: app/api (cron + tasks routes)
and lib (redis, ssrf-guard, task-store).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 08:13:53 +04:00

19 KiB

title, tags, type, status, updated
title tags type status updated
Architecture
mortgagefi
architecture
architecture stable 2026-06-14

MortgageFi Architecture

Overview

MortgageFi is a decentralized mortgage lending platform composed of a Next.js frontend DApp with embedded API routes, a Go-based NFT ownership cache service, an ntfy notification server, Redis for task persistence, and an nginx reverse proxy. All services are orchestrated via Docker Compose, and the scheduler runs as Vercel-native API routes with Upstash Redis.

The platform allows users to:

  • Deposit ERC-721 NFTs as collateral into debt vaults
  • Borrow stablecoins against their collateral
  • Pay down debt to extend liquidation timers
  • Receive scheduled alerts (email/push) before liquidation

System Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                              User Browser                                    │
│  ┌──────────────────────────────────────────────────────────────────────┐  │
│  │                    Next.js Frontend (Port 3000)                       │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌────────────────────────────┐ │  │
│  │  │  Wagmi/viem  │  │  localStorage │  │   Notification Settings    │ │  │
│  │  │  Web3 hooks  │  │   NFT Cache   │  │     (ntfy / gotify)        │ │  │
│  │  └──────┬───────┘  └──────┬───────┘  └─────────────┬──────────────┘ │  │
│  │         │                 │                        │                │  │
│  │         ▼                 ▼                        ▼                │  │
│  │   ┌──────────┐      ┌──────────┐           ┌──────────────┐         │  │
│  │   │  RPC     │      │ nftcache │           │  /api/tasks  │         │  │
│  │   │ Providers│      │   API    │           │  /api/cron   │         │  │
│  │   └────┬─────┘      └────┬─────┘           └──────┬───────┘         │  │
│  └────────┼─────────────────┼────────────────────────┼─────────────────┘  │
└───────────┼─────────────────┼────────────────────────┼────────────────────┘
            │                 │                        │
            ▼                 ▼                        ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Nginx Reverse Proxy (Port 80)                        │
│                                                                              │
│   /  ──► frontend:3000    /ntfy ──► ntfy:80                                │
│   /nftcache ──► nftcache:8090                                              │
└─────────────────────────────────────────────────────────────────────────────┘
            │                 │                        │
            ▼                 ▼                        ▼
    ┌─────────────┐   ┌─────────────┐        ┌─────────────────┐
    │  Blockchain │   │   ntfy:80   │        │  nftcache:8090  │
    │  (Base/Arb) │   │             │        │                 │
    └─────────────┘   └──────┬──────┘        └─────────────────┘
                             │                      │
                             ▼                      ▼
                    ┌─────────────────┐    ┌──────────────┐
                    │  SMTP Server    │    │  BadgerDB    │
                    │  (Email relay)  │    │  /data       │
                    └─────────────────┘    └──────────────┘

                             ▲
                             │
                    ┌────────┴────────┐
                    │  Redis /        │
                    │  Upstash        │
                    └─────────────────┘

Component Breakdown

1. Frontend (mortgagefi-frontend/)

Stack: Next.js 16, React 19, TypeScript, Tailwind CSS 4, wagmi 3, viem

Key Files:

  • app/dapp/page.tsx — Main DApp interface (1418 lines). Handles wallet connection, NFT scanning, debt position reading, payments, and notification scheduling.
  • app/dapp/position/[network]/[preset]/[tokenId]/page.tsx — Deep-link handler for specific positions.
  • providers/Web3Provider.tsx — Wagmi + TanStack Query provider setup.
  • config/web3.ts — Chain configuration (Base, Arbitrum, optional Mainnet) with RPC overrides.
  • utils/scheduler.ts — Client for the embedded /api/tasks scheduler API.
  • components/SettingsModal.tsx — Notification provider configuration UI.

Features:

  • Multi-chain support (Base, Arbitrum)
  • Preset vault pairs (cbBTC-USDC, WETH-USDC on Base; USDTO-WBTC on Arbitrum)
  • Manual wallet mode (pay debt for another address)
  • NFT discovery via on-chain ownerOf scanning or nftcache API
  • ERC20 approve/pay flow for stablecoin debt repayment
  • Liquidation alert scheduling with configurable lead time
  • Backup email support with delayed secondary alerts
  • Deep linking to individual positions

State Management:

  • Wagmi handles blockchain state (balances, allowances, contract reads)
  • localStorage persists NFT scan cache per contract+wallet
  • localStorage persists notification settings and per-position alert state

2. NFT Cache Service (nftcache/)

Stack: Go 1.22, BadgerDB v4

Purpose: Scans ERC-721 contracts via RPC to build a complete tokenId→owner mapping, then serves user-specific token lists via HTTP. This avoids making hundreds of RPC calls from the browser.

Key Files:

  • cmd/nftcache/main.go — HTTP server with CORS, API key auth, and three endpoints.
  • internal/fetcher/rpc.go — RPC client with rate limiting (5 TPS), exponential backoff, and retry logic.
  • internal/fetcher/alchemy.go — Alchemy NFT API client (optional fallback).
  • internal/store/store.go — BadgerDB persistence for contract ownership maps.
  • internal/config/config.go — YAML contract configuration loader.

Endpoints:

Method Path Description
GET /nfts?network=&nft_contract=&user_wallet= Returns token IDs owned by wallet
POST /nfts/refresh?network=&nft_contract= Force refresh of contract cache
POST /nfts/invalidate?network=&nft_contract= Delete contract cache entry

Caching Strategy:

  • Contract-level cache (all token owners) stored in BadgerDB
  • TTL-based stale-while-revalidate: returns cached data immediately, refreshes in background
  • Configurable via NFTCACHE_TTL (default 24h)
  • Rate limited to 5 TPS with exponential backoff on 429 errors

3. Task Scheduler (Next.js API Routes + Redis)

Stack: Next.js App Router API routes, Upstash Redis (or self-hosted Redis via REST proxy)

Purpose: Replaces the standalone Schedy Go service with Vercel-native serverless functions. Tasks are stored in Redis sorted sets and executed by a Vercel Cron job (or Docker cron loop) calling /api/cron.

Key Files:

  • app/api/tasks/route.ts — POST/GET tasks with SSRF protection and rate limiting
  • app/api/tasks/[id]/route.ts — DELETE task
  • app/api/cron/route.ts — Protected cron endpoint that finds due tasks and executes webhooks
  • lib/task-store.ts — Redis storage abstraction using sorted sets for time-ordered scheduling
  • lib/ssrf-guard.ts — URL validation to prevent Server-Side Request Forgery

Task Model:

interface Task {
  id: string;
  url: string;
  executeAt: number; // epoch seconds
  headers: Record<string, string>;
  payload: any;
  retries: number;
  retryInterval: number;
}

Storage: Redis sorted set mortgagefi:tasks:by:time with score = executeAt for O(log n) due task queries.


4. ntfy (ntfy service)

Image: binwiederhier/ntfy

Purpose: Self-hosted push notification server that also relays notifications via SMTP email.

Configuration:

  • SMTP settings passed via environment variables
  • Supports Gmail/App Password or custom SMTP relay
  • Served under /ntfy subpath via nginx proxy

Environment Variables:

  • NTFY_BASE_URL — Public URL including subpath
  • NTFY_SMTP_SENDER_ADDR — SMTP server:port
  • NTFY_SMTP_SENDER_USER / NTFY_SMTP_SENDER_PASS — Auth credentials
  • NTFY_SMTP_SENDER_FROM — From address

5. Nginx Proxy (nginx/)

Purpose: Single entry point that routes traffic to all backend services.

Routes:

Path Target Notes
/ frontend:3000 Next.js app with WebSocket HMR support
/ntfy/ ntfy:80 Push notification server
/nftcache/ nftcache:8090 NFT ownership cache API

Security Headers:

  • X-Frame-Options: DENY
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • X-XSS-Protection: 1; mode=block

Features:

  • WebSocket upgrade support for Next.js HMR
  • SSE/streaming support for ntfy (buffering disabled, long timeouts)
  • Subpath stripping for all proxied services
  • Only port 80 exposed; all backend services are internal-only

Note

Only port 80 is exposed externally. All backend services (frontend, ntfy, nftcache) are reachable only through the nginx proxy on the internal Docker network.


Data Flows

NFT Discovery Flow

User opens DApp
    │
    ▼
┌─────────────────┐
│ Check localStorage│
│  NFT scan cache   │
└────────┬────────┘
         │
    ┌────┴────┐
    ▼         ▼
 Has cache?  Empty?
    │         │
    ▼         ▼
 Return IDs  Check nftcache
             API enabled?
                 │
            ┌────┴────┐
            ▼         ▼
          Yes        No
            │         │
            ▼         ▼
      Call /nfts   On-chain scan
      (bulk cache)  ownerOf x12
            │         │
            └────┬────┘
                 ▼
          Store in localStorage
                 │
                 ▼
          Display positions

Debt Payment Flow

User enters amount → clicks Pay
         │
         ▼
┌─────────────────┐
│ Check stablecoin │
│ allowance to debt│
│    contract      │
└────────┬────────┘
         │
    ┌────┴────┐
    ▼         ▼
Insufficient  Sufficient
    │            │
    ▼            ▼
 Approve()   payDownContract()
    │            │
    └────┬───────┘
         ▼
   Refetch debt data
         ▼
   Update UI state

Liquidation Alert Flow

User enables alert for position
         │
         ▼
┌──────────────────────────┐
│ Read secondsTillLiq      │
│ from debt contract       │
└──────────┬───────────────┘
           │
           ▼
┌──────────────────────────┐
│ Compute runAt = now +    │
│ (secondsTillLiq - lead)  │
│ lead = daysBefore * 86400│
└──────────┬───────────────┘
           │
           ▼
   POST /api/tasks
   (URL = ntfy topic)
           │
           ▼
   Store in Redis sorted set
   (per-position notif state)
           │
           ▼
   ┌─────────────────┐
   │  At runAt time   │
   │  /api/cron runs  │
   └────────┬────────┘
            │
            ▼
   POST to ntfy topic
   (with X-Email header)
            │
            ▼
   ┌─────────────────┐
   │  ntfy delivers   │
   │  push + email    │
   └─────────────────┘

Auto-Reschedule Flow

On each debt data refresh
         │
         ▼
Compare current secondsTillLiq
with scheduled runAt
         │
    ┌────┴────┐
    ▼         ▼
  Drift >   Drift <=
  60s?      60s
    │         │
    ▼         ▼
 Cancel old  Keep existing
 job(s)      schedule
    │
    ▼
 Create new
 job(s)

Smart Contract Architecture

Collateral (ERC-721)

  • Represents deposited collateral positions
  • Each tokenId maps to a unique debt position
  • Examples: cbBTC collateral on Base, WBTC collateral on Arbitrum

Debt Contract

Key functions:

  • openDebt(tokenId) → returns (currentPaymentPending, debtAtThisSize, secondsTillLiq)
  • feeSize(tokenId) — Protocol fee amount
  • coinSize(tokenId) — Collateral amount locked
  • amountPaid(tokenId) — Total amount repaid
  • startDate(tokenId) / expiration(tokenId) — Loan timeline
  • baseSize(tokenId) — Base loan parameters
  • payDownContract(tokenId, amount) — Repay stablecoin debt
  • calculateAPR() — Current interest rate
  • stablecoin() / contractCoin() — Token addresses

Supported Networks

Network Chain ID Presets
Base 8453 cbBTC-USDC, WETH-USDC
Arbitrum One 42161 USDTO-WBTC
Ethereum Mainnet 1 USDC-WETH (disabled by default)

Caching Strategy

Frontend Caching

  • NFT Scan Cache: Per-contract, per-wallet localStorage entries
    • Key: nftScan:v1:<chainId>:<nftAddressLower>
    • Stores: lastScannedIndex, tokenIds[], complete flag
    • Gap limit: 5 consecutive non-existent tokens marks scan complete
  • Notification Settings: Global localStorage key notif:settings
  • Position Notifications: Per-debt-contract key notif:positions:v1:<chainId>:<debtAddress>

Backend Caching (nftcache)

  • Contract Cache: Full tokenId→owner mapping in BadgerDB
    • Key: contract:<network>:<canonicalAddress>
    • TTL-based expiration with background refresh
    • Rate limit handling: invalidates cache on exhaustion

Security Considerations

Authentication

  • nftcache API: X-API-Key header required if NFTCACHE_API_KEY is set
  • Frontend: No server-side auth; all blockchain interactions signed by user's wallet
  • Cron endpoint: Protected by Authorization: Bearer {CRON_SECRET}

Input Validation

  • Ethereum addresses validated to be exactly 0x + 40 hex chars (rejected if malformed)
  • Token IDs parsed as BigInt with error handling
  • Amount inputs parsed with proper decimal scaling
  • RPC URLs validated against an allowlist of known providers

CORS

  • nftcache supports configurable CORS origins
  • Default allows specific origins only, not *
  • NEXT_PUBLIC_ secrets removed from client bundle

Warning

Never use * as the nftcache CORS origin in production, and ensure no secret values are placed behind a NEXT_PUBLIC_ prefix — anything NEXT_PUBLIC_ is inlined into the client bundle and publicly visible.


Configuration

Environment Variables

Variable Used By Description
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID Frontend WalletConnect project ID
NEXT_PUBLIC_RPC_BASE / ARB / MAINNET Frontend Custom RPC URLs
NEXT_PUBLIC_NTFY_URL Frontend ntfy base URL (usually /ntfy)
NEXT_PUBLIC_NFTCACHE_URL Frontend nftcache base URL (usually /nftcache)
UPSTASH_REDIS_REST_URL API routes Redis REST endpoint (Upstash or local proxy)
UPSTASH_REDIS_REST_TOKEN API routes Redis REST token
CRON_SECRET API routes Secret for /api/cron endpoint
NFTCACHE_API_KEY nftcache Server-side API key
NFTCACHE_TTL nftcache Cache TTL (default 24h)
ETH_RPC_URL / ARB_RPC_URL / BASE_RPC_URL nftcache RPC endpoints for scanning
NTFY_* ntfy SMTP and server configuration
CORS_ALLOW_ORIGIN nftcache Allowed CORS origin

Contract Configuration (config/contracts.yaml)

Maps short slugs to contract addresses and networks for nftcache:

contracts:
  cbbtc:
    network: base
    address: "0x..."
    max_token_id: "5000"

Deployment Modes

Local Development

cd mortgagefi-frontend
npm install
npm run dev  # localhost:3000

Docker Compose (Full Stack)

# From repo root
cp .env.example .env.local
# Edit .env.local with your values
docker compose up -d
# Access: http://localhost

Vercel (Frontend Only)

  • Set root directory to mortgagefi-frontend/
  • Build command: next build --turbopack
  • Configure environment variables in Vercel dashboard

Tip

See Deployment for the full production deployment runbook and Development for local environment setup details.


Technology Choices

Layer Technology Rationale
Frontend Framework Next.js 16 + App Router SSR, file-based routing, API routes
Web3 Library wagmi 3 + viem React hooks for Ethereum, type-safe contract interactions
Styling Tailwind CSS 4 Utility-first, minimal CSS overhead
Wallet Connection @web3modal/wagmi Multi-wallet support with WalletConnect
NFT Cache Go + BadgerDB Fast embedded DB, efficient Go concurrency for RPC scanning
Scheduler Next.js API Routes + Upstash Redis Vercel-native serverless jobs; replaces the standalone Schedy Go service (see §3)
Notifications ntfy Self-hosted, SMTP relay, no external SaaS dependency
Proxy nginx Mature, efficient subpath routing
Container Orchestration Docker Compose Simple local and small-scale deployment

Home · API Reference · Deployment · Development · Security Audit