Deploying Freehold
A complete walkthrough for self-hosting Freehold on your own infrastructure: the Go API backend, the React web frontend, and the optional mobile PWA.
01Architecture at a glance
Freehold is a three-tier application that depends on three external services you provide: PostgreSQL, S3-compatible object storage, and an OIDC identity provider.
File bytes bypass the API. Uploads and downloads use pre-signed S3 URLs, so the browser talks directly to object storage. Your bucket must allow CORS from the frontend origin.
Auth is OIDC + PKCE with a confidential client. The backend brokers the OIDC exchange; the frontend never holds the secret.
The frontend is configured at runtime. A /runtime-config endpoint serves config on serverless platforms; plain static hosts fall back to build-time VITE_* variables.
02Prerequisites
Tooling (build machine)
External services to provision first
Uses uuid-ossp, pg_trgm, and a tsvector index. The role must allow CREATE EXTENSION.
Backblaze B2, Wasabi, MinIO, or AWS S3 — endpoint, region, bucket, and keys.
Authentik, Keycloak, or Auth0 as a confidential client.
03Provision the external services
3.1 — PostgreSQL
Create a database and a role. The role must be allowed to run CREATE EXTENSION — migrations install uuid-ossp and pg_trgm automatically.
3.2 — Object storage (S3-compatible)
Create a bucket (e.g. freehold-files) and an access-key / secret-key pair scoped to it. Note the endpoint and region. Keep path-style addressing on for B2, Wasabi, and MinIO.
3.3 — OIDC provider (confidential client)
Register a confidential OAuth2/OIDC application. Record the issuer URL, client ID, and client secret.
04Deploy the backend (Go API)
A single statically-linked binary plus a migrate helper and the migrations/ directory. Run it as a systemd service behind a TLS reverse proxy.
4.1 — Build
4.2 — Configure (.env)
The server fails fast if OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, STORAGE_ENDPOINT, or STORAGE_BUCKET are missing.
4.3 — Run migrations
Idempotent — applied versions are tracked in a schema_migrations table. Use ./migrate down to roll back one step.
Because the browser talks directly to storage, the bucket must allow GET / PUT / HEAD from your frontend origin. Or sign in as admin and use Admin → Storage → Apply CORS to push a working policy automatically.
4.5 — systemd + 4.6 reverse proxy
Keep the proxy body-size limit in sync with SERVER_BODY_LIMIT so large uploads aren't rejected. Health check: GET /api/v1/health → {"status":"ok"}
05Deploy the web frontend (React)
A static Vite build. The reference target is Cloudflare Pages (which also runs the bundled Functions); a netlify.toml is included, and any static host works.
Set project env vars on Pages / Netlify. The /runtime-config Function serves them at load — change config without rebuilding.
Put values in frontend/.env before npm run build. The app falls back to these when /runtime-config is unavailable.
06Deploy the mobile PWA (optional)
A standalone, touch-optimized PWA on the same API. Build it, serve dist/ as a static SPA, and replace the placeholder icons in mobile/public/ before production.
07Environment variable reference
S3_* and STORAGE_* names are interchangeable; if both are set, S3_* wins.
08One-command deploy (maintainer reference)
The repo ships scripts that automate the maintainer's own deployment. They are environment-specific — review and adapt hostnames, paths, and the SSH user before use.