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

488 lines
19 KiB
Markdown

---
title: Architecture
tags: [mortgagefi, architecture]
type: architecture
status: stable
updated: 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:**
```typescript
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:
```yaml
contracts:
cbbtc:
network: base
address: "0x..."
max_token_id: "5000"
```
---
## Deployment Modes
### Local Development
```bash
cd mortgagefi-frontend
npm install
npm run dev # localhost:3000
```
### Docker Compose (Full Stack)
```bash
# 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](#3-task-scheduler-nextjs-api-routes--redis)) |
| 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 |
---
## Related
[[Home]] · [[API Reference]] · [[Deployment]] · [[Development]] · [[Security Audit]]