Deployment Guide
RichView has three deployment targets that work together:
| Component | Target | URL |
|---|---|---|
| Web SPA | Cloudflare Pages | https://richview.uk |
| API Server | Fly.io | https://richview-api.fly.dev |
| Self-hosted | Docker Compose | Your infrastructure |
Self-Hosting with Docker Compose
The simplest way to run your own RichView instance.
Prerequisites
- Docker Engine 24+ and Docker Compose v2
- A machine with at least 512MB RAM and 1GB disk
Quick Start
# Clone the repo
git clone https://github.com/richview-universe/richview-v2.git
cd richview-v2
# Create your environment file
cp .env.example .env
# Generate an encryption key
echo "RV_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
# Build the web SPA (needed for the web container volume)
# Option A: build locally and copy into the volume
pnpm install && pnpm build
docker compose up -d richview-server
docker compose cp packages/web/dist/. richview-web:/srv/
docker compose up -d richview-web
# Option B: just run the server (API only, no SPA)
docker compose up -d richview-serverWhat You Get
- Server at
http://localhost:4400-- API, WebSocket collaboration, published reports - Web at
http://localhost:3000-- the editor SPA served via Caddy, proxying API calls to the server - SQLite database persisted in the
richview-dataDocker volume at/data/richview.db
Populating the Web Volume
The richview-web service serves static files from /srv. You need to build the SPA and get the files into that volume. Options:
- Build locally and use
docker compose cp(shown above) - Add a build stage to your own Dockerfile that copies
packages/web/distinto the Caddy container - Mount a host directory instead of the volume: replace
web-dist:/srvwith./packages/web/dist:/srv:roindocker-compose.yml
Health Check
curl http://localhost:4400/health
# {"status":"ok","version":"0.0.1"}Backups
The SQLite database lives in a Docker volume. To back it up:
# While the server is running (SQLite WAL mode allows safe reads)
docker compose exec richview-server cp /data/richview.db /data/richview-backup.db
docker compose cp richview-server:/data/richview-backup.db ./richview-backup.dbCloudflare Pages (Web SPA)
The web SPA is deployed as a static site on Cloudflare Pages, served at richview.uk.
Initial Setup
- Create a Cloudflare Pages project named
richview - Connect your GitHub repo or use direct upload
- Set the build output directory to
packages/web/dist
If using the Cloudflare dashboard:
- Build command:
pnpm install && pnpm build - Build output directory:
packages/web/dist - Root directory:
/(repo root)
SPA Routing
The packages/web/public/_redirects file ensures all routes serve index.html:
/* /index.html 200This is automatically included in the build output.
Custom Domain
In the Cloudflare Pages dashboard:
- Go to your project settings
- Add custom domain:
richview.uk - Cloudflare handles SSL automatically
CI/CD
The GitHub Actions workflow (.github/workflows/ci.yml) deploys on every push to main after tests pass:
npx wrangler pages deploy packages/web/dist --project-name=richviewRequired GitHub secrets:
CLOUDFLARE_API_TOKEN-- API token with Pages edit permissionsCLOUDFLARE_ACCOUNT_ID-- your Cloudflare account ID
Fly.io (API Server)
The API server runs on Fly.io with a persistent SQLite volume.
Initial Setup
# Install flyctl
curl -L https://fly.io/install.sh | sh
# Authenticate
flyctl auth login
# Create the app (already configured in fly.toml)
flyctl apps create richview-api
# Create a persistent volume for SQLite (London region)
flyctl volumes create richview_data --region lhr --size 1Set Secrets
flyctl secrets set \
RV_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
BASE_URL="https://richview.uk" \
GITHUB_CLIENT_ID="your-id" \
GITHUB_CLIENT_SECRET="your-secret" \
GITHUB_REDIRECT_URI="https://richview.uk/api/oauth/github/callback" \
GOOGLE_CLIENT_ID="your-id" \
GOOGLE_CLIENT_SECRET="your-secret" \
GOOGLE_REDIRECT_URI="https://richview.uk/api/oauth/google/callback"Deploy
# Deploy from the repo root
flyctl deploy --remote-onlyConfiguration Details
From fly.toml:
- Region:
lhr(London) -- single region because SQLite is single-writer - Machine:
shared-cpu-1xwith 512MB RAM - Volume: 1GB persistent volume mounted at
/data - Auto-stop: disabled (WebSocket connections need a long-lived process)
- Min machines: 1 (always running)
- Health check:
GET /healthevery 15 seconds
CI/CD
The GitHub Actions workflow deploys on every push to main:
Required GitHub secret:
FLY_API_TOKEN-- generate atflyctl tokens create deploy -x 999999h
Monitoring
# View logs
flyctl logs
# SSH into the machine
flyctl ssh console
# Check the database
flyctl ssh console -C "ls -la /data/"Scaling Constraints
SQLite is a single-writer database. This means:
- One machine only -- do not scale horizontally
- One region only -- no multi-region replication
- If you need multi-region or horizontal scaling, migrate to PostgreSQL
For RichView's expected load (hundreds of concurrent users), a single Fly.io machine with SQLite in WAL mode handles this well. SQLite WAL supports unlimited concurrent readers with one writer.
Environment Variables Reference
| Variable | Required | Default | Description |
|---|---|---|---|
PORT | No | 4400 | Server HTTP port |
DB_PATH | No | richview.db | SQLite database file path |
RV_ENCRYPTION_KEY | Yes | -- | 64-char hex key for encrypting credentials. Generate: openssl rand -hex 32 |
BASE_URL | No | -- | Public URL of the app (used for OAuth redirects, email links) |
GITHUB_CLIENT_ID | No | -- | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET | No | -- | GitHub OAuth app client secret |
GITHUB_REDIRECT_URI | No | -- | GitHub OAuth callback URL |
GOOGLE_CLIENT_ID | No | -- | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | No | -- | Google OAuth client secret |
GOOGLE_REDIRECT_URI | No | -- | Google OAuth callback URL |
NODE_ENV | No | -- | Set to production in deployed environments |
Deployment-only variables (GitHub Actions secrets)
| Variable | Used By | Description |
|---|---|---|
CLOUDFLARE_API_TOKEN | CI | Cloudflare API token with Pages edit permission |
CLOUDFLARE_ACCOUNT_ID | CI | Cloudflare account identifier |
FLY_API_TOKEN | CI | Fly.io deploy token |
Cost Estimates
Fly.io (API Server)
| Resource | Tier | Cost |
|---|---|---|
| shared-cpu-1x, 512MB | Free tier (3 machines) | $0/month |
| 1GB persistent volume | Free tier (3GB included) | $0/month |
| Bandwidth (100GB) | Free tier included | $0/month |
| Total | $0/month |
If you exceed free tier:
- Machine: ~$3.50/month for shared-cpu-1x with 512MB
- Volume: $0.15/GB/month
- Bandwidth: $0.02/GB after free tier
Cloudflare Pages (Web SPA)
| Resource | Tier | Cost |
|---|---|---|
| Static site hosting | Free plan | $0/month |
| Custom domain + SSL | Free plan | $0/month |
| Bandwidth (unlimited) | Free plan | $0/month |
| Total | $0/month |
Total Production Cost
$0/month on free tiers. Under heavy load: ~$5-10/month.
Budget cap of 100 EUR/month gives significant headroom for growth.
Setting Up Billing Alerts
Fly.io: Go to https://fly.io/dashboard/personal/billing and set a spending limit. Fly.io emails you when you approach the limit.
Cloudflare: Cloudflare Pages free tier has no bandwidth limits. No billing alerts needed unless you use Workers or other paid features.
Architecture Diagram
richview.uk (Cloudflare DNS)
|
┌────────────┴────────────┐
| |
richview.uk/* richview.uk/api/*
(Cloudflare Pages) (proxied to Fly.io)
| |
Static SPA (React) richview-api.fly.dev
packages/web/dist |
┌───────┴───────┐
| Hono Server |
| Port 4400 |
| + WebSocket |
| + Scheduler |
└───────┬───────┘
|
/data/richview.db
(persistent volume)For self-hosted deployments, the Caddy reverse proxy replaces Cloudflare's routing, proxying /api/* to the server container.