Priva Architecture
High-Level Overview
┌─────────────────────────────┐
│ privanet.dev │
│ (Caddy reverse proxy) │
│ HTTPS (Let's Encrypt auto) │
└───────┬──────┬───────┬────────┘
│ │ │
┌───────┘ ┌───┘ ┌──┘
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Web App │ │ API │ │ Docs (static)│
│ Next.js │ │ Node.js │ │ /docs/* │
│ :3001 │ │ :3000 │ │ /srv/docs │
└──────────┘ └────┬─────┘ └──────────────┘
│
┌────────────┴───────────┐
▼ ▼
┌────────────┐ ┌───────────────┐
│ PostgreSQL │ │ Redis │
│ :5432 │ │ :6379 │
│ (pgdata) │ │ (sessions, │
└────────────┘ │ cache) │
└───────────────┘
┌──────────────────────────────────────────┐
│ P2P Network Layer │
│ │
│ ┌────────────┐ ┌────────────────┐ │
│ │ node-client│ │ node-client │ │
│ │ (your VPS)│◄───►│ (other nodes) │ │
│ │ :8336 │ │ :8336 │ │
│ └─────┬──────┘ └────────────────┘ │
│ │ libp2p QUIC/TCP │
└────────┼──────────────── ─────────────────┘
│
▼ reports uptime + work proofs
┌────────────────┐
│ Priva API │
│ (reward calc) │
└────────────────┘
Components
Caddy (Reverse Proxy)
Caddy is the public-facing entry point, running on ports 80 and 443. It handles:
- Automatic HTTPS via Let's Encrypt (zero configuration)
- Routing —
/api/*→ API,/docs/*→ static files, everything else → Next.js web app - Security headers — HSTS, X-Frame-Options, Content-Type-Options
- www → apex redirect —
www.privanet.devredirects toprivanet.dev
API (Node.js / Express)
The backend API provides:
- Authentication — JWT-based (access + refresh tokens)
- Node registry — stores registered node metadata, verifies P2P reachability
- Reward calculation — tracks uptime proofs and calculates PRIVA earnings
- Points system — daily farming, streak tracking, referral bonuses
- Database migrations — managed by Drizzle ORM with automatic migration on startup
Tech stack: Node.js 20, TypeScript, Express, Drizzle ORM, PostgreSQL, Redis
Web App (Next.js)
The user-facing dashboard built with Next.js 14:
- App Router architecture
- Server-side rendering for authenticated pages
- Web3 wallet integration via WalletConnect / wagmi
- Real-time updates via WebSocket/polling
Deployed with: output: 'standalone' for minimal Docker image size
node-client (Go)
The Priva node, written in Go:
- libp2p for P2P networking (QUIC transport over UDP, TCP fallback)
- LevelDB for local peer state persistence
- Prometheus metrics on
/metricsendpoint - Keystore — encrypted wallet identity (unlocked via passphrase)
- API heartbeat — reports uptime proofs to the Priva API every minute
Built with: Go 1.25, libp2p, LevelDB
PostgreSQL
Relational database storing:
- User accounts and authentication
- Node registrations and metadata
- Reward history and ledger
- Governance proposals and votes
- Referral relationships
Migrations run automatically at API startup via Drizzle.
Redis
In-memory store for:
- JWT refresh token blacklist
- Rate limiting counters
- Session cache
- API response caching
Docker Network
All services communicate over an internal Docker bridge network named privanet. Only Caddy and node-client expose ports publicly:
| Service | Public Ports | Internal Port |
|---|---|---|
| caddy | 80, 443 | — |
| node-client | 8336 (TCP+UDP) | 9090 (metrics) |
| api | — | 3000 |
| web | — | 3001 |
| postgres | — | 5432 |
| redis | — | 6379 |
Data Flow: Daily Farming
User → Browser → privanet.dev (Caddy)
→ /api/v1/farming/claim (API)
→ Validate JWT
→ Check last claim time (Redis)
→ Calculate points (streak × multiplier)
→ Update PostgreSQL
→ Return new balance
Data Flow: Node Reward
node-client → API /api/v1/nodes/heartbeat (every 60s)
→ Verify node signature
→ Calculate uptime %
→ Update node metrics in PostgreSQL
→ (Reward distribution runs via scheduled job)
Security Model
- TLS everywhere — all public traffic over HTTPS (Let's Encrypt)
- No public database — PostgreSQL and Redis are internal-only
- JWT with short expiry — access tokens expire in 15 minutes, refresh tokens in 7 days
- Argon2 password hashing — used for user passwords
- Node identity via cryptographic keypair — nodes can't impersonate each other
- Admin API protected — separate
ADMIN_SECRETheader, not user-accessible