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>
19 KiB
title, tags, type, status, updated
| title | tags | type | status | updated | ||
|---|---|---|---|---|---|---|
| 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/tasksscheduler 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
ownerOfscanning 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 limitingapp/api/tasks/[id]/route.ts— DELETE taskapp/api/cron/route.ts— Protected cron endpoint that finds due tasks and executes webhookslib/task-store.ts— Redis storage abstraction using sorted sets for time-ordered schedulinglib/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
/ntfysubpath via nginx proxy
Environment Variables:
NTFY_BASE_URL— Public URL including subpathNTFY_SMTP_SENDER_ADDR— SMTP server:portNTFY_SMTP_SENDER_USER/NTFY_SMTP_SENDER_PASS— Auth credentialsNTFY_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: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originX-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 amountcoinSize(tokenId)— Collateral amount lockedamountPaid(tokenId)— Total amount repaidstartDate(tokenId)/expiration(tokenId)— Loan timelinebaseSize(tokenId)— Base loan parameterspayDownContract(tokenId, amount)— Repay stablecoin debtcalculateAPR()— Current interest ratestablecoin()/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[],completeflag - Gap limit: 5 consecutive non-existent tokens marks scan complete
- Key:
- 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
- Key:
Security Considerations
Authentication
- nftcache API:
X-API-Keyheader required ifNFTCACHE_API_KEYis 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 aNEXT_PUBLIC_prefix — anythingNEXT_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 |
Related
Home · API Reference · Deployment · Development · Security Audit