Skip to content

Infrastructure

VPS Services Overview

The VPS at 31.97.132.219 (Hostinger, Ubuntu 24.04.3, 193GB disk, 34% used as of April 15, 2026) is the workhorse for everything that needs to be online 24/7. Laptops come and go, the VPS never sleeps. Everything that answers a webhook, replies to a customer, or fires on a cron schedule runs here. See Machines for the machine registry and Network Diagram for how this plugs into the rest of the fleet.

Three jobs:

  1. Serve customer-facing apps that must stay online (CRMs, booking engines, upload pipelines).
  2. Run the automation layer (cron jobs that scan CRMs, send follow-ups, generate reports, email digests).
  3. Host the data plane (two self-hosted Supabase instances and an n8n workflow engine).

Every service below falls into one of those three buckets.

Customer-Facing Services (reverse-proxied through Caddy)

Section titled “Customer-Facing Services (reverse-proxied through Caddy)”

Caddy terminates TLS on the public IP and routes by hostname to a local process or Docker container. Auto-renews Let’s Encrypt certs, no config required.

DomainPortServiceWhat It Does
crm.gokartpark.com3200claude-code-crmGo Kart Park booking engine. Owns the full customer conversation — inbound SMS reply generation, welcome messages, follow-up escalation, staged message approval, Stripe payments, waivers, party countdown reminders, review monitoring. FastAPI.
crm.ddxweb.com3210ddxweb-crmMulti-tenant CRM. Receives submissions from marketing sites (Springville, cleaning leads) and routes them into the right GHL subaccount.
ath-upload.jameshurst.com9879ath-upload-receiverWayne Lewis’s job upload pipeline. Photos and notes from a handyman job flow in, get AI-processed, and get distributed to Google Business Profile, the All Things Handy website, and Facebook.
cc.jameshurst.com3847remote-claudeWeb interface to control Claude Code remotely from any browser. The way you talk to Claude when you are not at a keyboard.
upp.jameshurst.com9876Ultimate Prompt Pack APIPrompt library backend.
webhook.jameshurst.com9878webhook-receiverGitHub push webhooks land here and trigger auto-pull on every repo so the VPS stays in sync without you touching it.
n8n.srv1249251.hstgr.cloud5678n8nWorkflow automation. Auto-posts, integrations, glue between services.
db.jameshurst.com8000Supabase Kong (main)Main self-hosted Supabase project. Postgres, auth, storage, edge functions, studio.
gospel-library.jameshurst.com3100gospel-library appAI Gospel Library front-end.
gospel-db.jameshurst.com8001Supabase Kong (gospel)Second Supabase instance dedicated to the Gospel Library project. Kept separate from the main one so neither can starve the other.
meals-api.jameshurst.com8091meal-planning-apiRecipe scraper + meal planning API.
speech-helper.jameshurst.com8090speech-helperVoice helper endpoint.
quotes.jameshurst.comQuoteVaultQuote collection app.

Anything that needs to stay up between reboots runs as a systemd unit. systemctl status {name}.service shows health, journalctl -u {name}.service -f tails logs.

ServicePurpose
claude-code-crm.serviceGo Kart Park booking engine (the FastAPI app behind crm.gokartpark.com).
ddxweb-crm.serviceMulti-tenant CRM.
ai-assistant-webhook.serviceReceives GHL webhooks for the AI Assistant scanner (contact updates, conversation events).
ath-upload-receiver.serviceUpload endpoint for Wayne’s handyman jobs.
dmarc-receiver.serviceSMTP listener on port 25 that ingests DMARC aggregate reports. Part of the deliverability monitoring stack.
meal-planning-api.serviceRecipe scraper API.
remote-claude.serviceClaude Code web interface.
webhook-receiver.serviceGitHub auto-pull receiver.
caddy.serviceReverse proxy for everything above.

This is where most of the day-to-day magic happens. Roughly 25 cron jobs live on the VPS. Grouped by purpose:

  • Follow-up engine — every minute. Scans for contacts with a pending follow-up step and fires the next SMS.
  • Party countdown — hourly. Sends role-aware reminders as a party approaches (waiver not signed, payment pending, driver list incomplete).
  • Birthday re-engagement — daily at 6 AM UTC. Moves contacts into the birthday re-engagement stage and kicks off the message sequence.
  • Message audit — 3x daily (8 AM, noon, 6 PM MST). Pipeline health check on ongoing conversations.
  • DB backup — every 6 hours.
  • ATH and GKP scanners — every 3 hours during business hours. Score contacts, surface what needs attention, alert you via SMS.
  • Callback checker — every 5 minutes during business hours. Catches missed callbacks before the customer goes cold.
  • CRM scanner (narrative) — daily at 8 AM MST. Contact-by-contact audit with “what should James do next” recommendations, delivered by email.
  • Auditor — daily at 6:15 AM. Proactive anomaly hunt across the CRM data.
  • GSC daily health report — 8 AM MT. Multi-site Search Console sweep, auto-saves a benchmark snapshot.
  • Cloudflare audit — 2 AM. Zone config drift check.
  • Content-hash (YouTube) — 7:13 AM MT. Detects duplicate content before upload.
  • Auto-journal email — 10:30 PM MST. Stitches calendar, git commits, and the day’s Claude conversation into a narrative email.
  • Daily health check — 6 AM. Infrastructure pulse across the fleet.
  • Usage tracker — 10 PM MST. Claude Code token consumption summary.
  • Token keepalive — 6 AM. Prevents OAuth tokens from aging out.
  • Upwork scanner — 4x daily (6 AM, 10 AM, 2 PM, 6 PM). Scans for relevant jobs, scores them, emails the hot ones.
  • Cron inventory — weekly Monday 7 AM. Emails the full cron list so nothing gets forgotten.
  • Fleet health check — daily 7:15 AM. Cross-machine status sweep.
  • Update manager — daily 2 AM. Checks for package and dependency updates.
  • UPP content library refresh — weekly Sunday 3:17 AM.

Two Supabase instances and n8n run as Docker Compose stacks. Containers are visible via docker ps.

Standard self-hosted Supabase: supabase-db (Postgres 15.8), supabase-kong (API gateway), supabase-auth (gotrue), supabase-rest (postgrest), supabase-storage, supabase-realtime, supabase-studio, supabase-edge-functions, supabase-analytics (logflare), supabase-pooler (supavisor), supabase-imgproxy, supabase-vector, supabase-meta.

Gospel Library Supabase (gospel-db.jameshurst.com)

Section titled “Gospel Library Supabase (gospel-db.jameshurst.com)”

Identical stack, separate instance. Prefixed gospel-*. Isolates the Gospel Library project so it cannot affect the main Supabase tenants.

n8n-n8n-1 on port 5678, fronted by n8n-traefik-1.

A customer text lands at crm.gokartpark.com → Caddy proxies to claude-code-crm on port 3200 → FastAPI routes the incoming message through claude_responder.py → AI generates a reply → either queues for approval or sends directly via GHL → logs the outcome → a cron job an hour later checks if the party countdown needs to fire → that same cron writes a row to Postgres → which gets backed up every 6 hours → and the daily CRM scanner email tells James at 8 AM what happened overnight.

Each layer is boring in isolation. The pattern is: Caddy handles ingress, systemd keeps long-running apps alive, cron drives the scheduled work, and Docker holds the databases. Everything else is glue code.

  • VPS repos live at /root/apps/, not /Users/ojhurst/apps/. Symlinked in spirit only. Commits authored on the VPS carry the James Hurst (VPS) git identity tag so you always know where something was written.
  • VPS dirty check before closeoutbash ~/apps/cc/vps-dirty-check.sh catches uncommitted work on the VPS. VPS sessions often leave changes that would otherwise sit forever.
  • No direct analytics on the VPS. Web analytics for the sites that front this server (gospel-library, meals-api, etc.) lives at the Cloudflare edge. See the How We Build Websites SOP for the Cloudflare plus D1 pattern.
  • DNS is Cloudflare-managed. Records for jameshurst.com point to the VPS IP. *.gokartpark.com, *.ddxweb.com, and a handful of other domains follow the same pattern.
  • Tailscale is active (tailscaled on 100.87.130.105). Enables the vps-claude SSH alias and secure cross-machine access.
  • No uptime monitoring dashboard. UptimeRobot or BetterStack on the public endpoints would catch outages before a customer reports one. The daily health check email is reactive, not proactive.
  • No service dependency map. If claude-code-crm goes down, what else breaks? Nothing tracks this explicitly.
  • Docker stacks are not documented as code here. Supabase and n8n compose files live on the VPS only. Worth pulling them into tms-internal for reference.
  • Service-level logging is inconsistent. Some services log to /root/apps/{name}/logs/, some to journalctl, some to /tmp/. A single convention would make triage faster.