Skip to content

Deployment Guide

RichView has three deployment targets that work together:

ComponentTargetURL
Web SPACloudflare Pageshttps://richview.uk
API ServerFly.iohttps://richview-api.fly.dev
Self-hostedDocker ComposeYour 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

bash
# 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-server

What 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-data Docker 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:

  1. Build locally and use docker compose cp (shown above)
  2. Add a build stage to your own Dockerfile that copies packages/web/dist into the Caddy container
  3. Mount a host directory instead of the volume: replace web-dist:/srv with ./packages/web/dist:/srv:ro in docker-compose.yml

Health Check

bash
curl http://localhost:4400/health
# {"status":"ok","version":"0.0.1"}

Backups

The SQLite database lives in a Docker volume. To back it up:

bash
# 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.db

Cloudflare Pages (Web SPA)

The web SPA is deployed as a static site on Cloudflare Pages, served at richview.uk.

Initial Setup

  1. Create a Cloudflare Pages project named richview
  2. Connect your GitHub repo or use direct upload
  3. 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  200

This is automatically included in the build output.

Custom Domain

In the Cloudflare Pages dashboard:

  1. Go to your project settings
  2. Add custom domain: richview.uk
  3. Cloudflare handles SSL automatically

CI/CD

The GitHub Actions workflow (.github/workflows/ci.yml) deploys on every push to main after tests pass:

bash
npx wrangler pages deploy packages/web/dist --project-name=richview

Required GitHub secrets:

  • CLOUDFLARE_API_TOKEN -- API token with Pages edit permissions
  • CLOUDFLARE_ACCOUNT_ID -- your Cloudflare account ID

Fly.io (API Server)

The API server runs on Fly.io with a persistent SQLite volume.

Initial Setup

bash
# 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 1

Set Secrets

bash
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

bash
# Deploy from the repo root
flyctl deploy --remote-only

Configuration Details

From fly.toml:

  • Region: lhr (London) -- single region because SQLite is single-writer
  • Machine: shared-cpu-1x with 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 /health every 15 seconds

CI/CD

The GitHub Actions workflow deploys on every push to main:

Required GitHub secret:

  • FLY_API_TOKEN -- generate at flyctl tokens create deploy -x 999999h

Monitoring

bash
# 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

VariableRequiredDefaultDescription
PORTNo4400Server HTTP port
DB_PATHNorichview.dbSQLite database file path
RV_ENCRYPTION_KEYYes--64-char hex key for encrypting credentials. Generate: openssl rand -hex 32
BASE_URLNo--Public URL of the app (used for OAuth redirects, email links)
GITHUB_CLIENT_IDNo--GitHub OAuth app client ID
GITHUB_CLIENT_SECRETNo--GitHub OAuth app client secret
GITHUB_REDIRECT_URINo--GitHub OAuth callback URL
GOOGLE_CLIENT_IDNo--Google OAuth client ID
GOOGLE_CLIENT_SECRETNo--Google OAuth client secret
GOOGLE_REDIRECT_URINo--Google OAuth callback URL
NODE_ENVNo--Set to production in deployed environments

Deployment-only variables (GitHub Actions secrets)

VariableUsed ByDescription
CLOUDFLARE_API_TOKENCICloudflare API token with Pages edit permission
CLOUDFLARE_ACCOUNT_IDCICloudflare account identifier
FLY_API_TOKENCIFly.io deploy token

Cost Estimates

Fly.io (API Server)

ResourceTierCost
shared-cpu-1x, 512MBFree tier (3 machines)$0/month
1GB persistent volumeFree 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)

ResourceTierCost
Static site hostingFree plan$0/month
Custom domain + SSLFree 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.

Released under the Elastic License 2.0 (ELv2).