DiagnosticPro — system architecture

A customer describes an equipment fault, pays once, and receives a long-form diagnostic report as a PDF. The whole product is one Express container behind Caddy, writing to a single durable volume. Everything that could have been a managed cloud service is a local file — deliberately.

System topology

grounded in docker-compose.yml · backend package.json
index.js route table · Caddy site block

→ swipe the diagram sideways to pan

Browser (SPA)
React 18 · Vite build
Caddy
TLS · sole public face
  • diagnosticpro.io
  • proxy → :8089
  • file_server dist/
/srv/static
Vite dist · SPA assets
  • index.html + assets
  • no build on host
diagnosticpro-backend
Express · Node 20 · container · restart:always
HTTP surface
  • /saveSubmission
  • /createCheckoutSession
  • /analyzeDiagnostic
  • /reports/* · /view/:id
  • /evidence/:id
Analysis
  • 15-section framework
  • DTC extraction
  • retry + adaptation
  • no mock path, ever
  • rate-limited per route
Evidence intake
  • multer uploads
  • pdf-parse · mammoth
  • 48h orphan sweeper
Reports
  • pdfkit · IBM Plex
  • reportPdfProduction
  • served from /data
SQLite
better-sqlite3
uploads/
photos · documents
reports/
pdfkit output
LLM provider
OpenAI-compatible /v1
  • gpt-4o (default)
  • Groq · xAI · DeepSeek
  • Ollama = fully local
Stripe
checkout · webhooks
  • $4.99 per report
  • signed webhook
Whop
membership · free tier
  • OAuth PKCE
  • membership webhooks
SOPS + age
encrypted secrets committed to git, materialized to .env at deploy time · LLM key · Stripe keys · Whop keys · never plaintext in the image or the repo
DiagnosticPro request and data flow The browser reaches Caddy over HTTPS. Caddy serves the static SPA from disk and reverse-proxies API paths to the backend container on 127.0.0.1 port 8089. The backend writes to SQLite, an uploads directory and a reports directory, all on one /data volume. Outbound only, the backend calls an OpenAI-compatible LLM endpoint, Stripe for checkout, and Whop for membership; Stripe calls back with a signed webhook. Secrets are decrypted from SOPS at deploy time. https file_server proxy /v1 chat checkout webhook oauth env env

Request flow

Paid path

  • The form wizard posts to /saveSubmission; the row lands in SQLite as pending.
  • /createCheckoutSession hands off to Stripe for the $4.99 charge.
  • Stripe's signed webhook — not the browser — is what authorizes analysis. The client cannot self-promote a submission.
  • Analysis runs, the PDF is written to /data, and the row flips to ready.

Member path

  • Whop OAuth (PKCE) proves membership; /api/whop/analyze skips the charge.
  • Membership state is re-verified server-side rather than trusted from the client token.
  • membership.went_valid/invalid webhooks keep the local record honest.
  • Same analysis code path, same report — only the paywall differs.

Component details

Public edge

  • One Caddy site block terminates TLS and is the only thing listening publicly.
  • Static SPA assets are served straight off disk — no Node process in the page-load path.
  • API paths are reverse-proxied to 127.0.0.1:8089; everything is same-origin, so there is no CORS surface to get wrong.
  • Config changes go caddy validatereload, never restart.

Application plane

  • A single Express app — routes, analysis, evidence intake, and PDF generation in one deployable unit.
  • The port binding is 127.0.0.1:8089:8080: the container is unreachable from the internet even if Caddy is misconfigured.
  • Per-route rate limiters sit in front of submission, analysis, and evidence upload.
  • A container healthcheck polls /healthz; Docker restarts on failure.

Durable state

  • One named volume mounted at /data holds the SQLite database, uploaded evidence, and generated reports.
  • That is the entire backup surface — one mount, captured nightly.
  • Evidence uploads must live on the volume or they vanish on restart; a 48-hour sweeper deletes evidence for submissions that were never paid for.
  • better-sqlite3 is synchronous and in-process — no connection pool, no second container.

The LLM seam

  • Provider is entirely env-driven: LLM_BASE_URL, LLM_MODEL, LLM_API_KEY.
  • Default is gpt-4o. Groq, xAI, or DeepSeek are a config change; Ollama on the same host makes the deployment fully local.
  • There is deliberately no mock path in production code — a canned report must never be able to reach the database.
  • Malformed provider responses trigger bounded retry with parameter adaptation before surfacing an error.

Secrets

  • SOPS + age: encrypted secrets are committed to git and decrypted at deploy time.
  • The container image contains no credentials; .env is materialized on the host.
  • Rotating a provider key is a re-encrypt and a restart — no rebuild.

What is deliberately absent

  • No managed database, no object storage, no serverless functions, no cloud AI platform.
  • No message queue — analysis runs inline off the webhook, with an idempotent requeue endpoint for the failures.
  • No second service to keep in sync. The cost of this architecture is one VPS.
  • The tradeoff is honest: this scales vertically, not horizontally. At current volume that is the right trade.

Stay in the loop with Intent Solutions

AI systems that actually ship — new builds, deep-dives, and releases. No spam.