The suite
Three hosts, one box. Each app is independent — its own repo, its own deploy, its own data — but they all sit behind a single reverse proxy.
The stack
Deliberately small and boring — the fewest moving parts that do the job.
☁️ One cloud VM
A single Google Cloud virtual machine (Ubuntu, Python). Sole host — no Kubernetes, no fleet. Easy to reason about.
🧭 nginx
One reverse proxy fronts every host: serves static apps from disk and proxies dynamic apps to their local app server.
🔒 Auto-renewing TLS
Let's Encrypt certificates via certbot — one certificate covers every host and renews itself, no manual steps.
🔑 Secrets vault
Every credential lives in Google Secret Manager and is resolved at runtime. Nothing sensitive is ever committed to git or kept on disk in plaintext.
✉️ Transactional email
Outbound mail goes through Resend on the verified domain — used today for automated reports, available to any app in the suite.
🗂️ Per-app repos
Each app is its own private Git repository; a master repo holds the shared architecture, the root site, and the links between them.
How we build & run it
A handful of conventions every app follows, so the next one is easy to add.
- Static vs. dynamic, one pattern each. Static apps are deployed to a served directory with a one-command script; dynamic apps run as a managed background service that nginx proxies to.
- Source in the repo, served copy beside it. Code lives in version control under the home directory; a deploy step copies the built site to where the web server reads it — keeping private files private.
- Secrets never touch code. They're stored in the vault and referenced by name; local development re-uses the same vault rather than copying values around.
- Scheduled work is defined on the server. The VM's own scheduler is the single source of truth for recurring jobs — no second place to drift out of sync.
- Ship from
main. The production branch is the deployed branch; changes land there and go live, with the architecture docs updated in the same change.