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.jsonindex.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 repoRequest flow
Paid path
- The form wizard posts to
/saveSubmission; the row lands in SQLite aspending. /createCheckoutSessionhands 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 toready.
Member path
- Whop OAuth (PKCE) proves membership;
/api/whop/analyzeskips the charge. - Membership state is re-verified server-side rather than trusted from the client token.
membership.went_valid/invalidwebhooks 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 validate→reload, 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
/dataholds 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-sqlite3is 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;
.envis 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.