Section 14
Environments
Three environments plus a recovery path. Local must be runnable in under an hour by someone who has never seen the codebase.
Local development
flowchart TB
subgraph dev[Developer machine]
direction TB
CODE[Repo checkout]
COMPOSE[docker compose up]
subgraph svc[Containers]
API[API on 8000]
WORK[Worker]
PG[(Postgres)]
RD[(Redis)]
MINIO[(MinIO — S3 stand-in)]
MAIL[Mailpit — email sink]
end
FIX[Connector fixtures
saved HTML, no network]
end
CODE --> COMPOSE --> svc
FIX --> WORK
Fig 13.1 — Local environment
Onboarding target: git clone, cp .env.example .env, docker compose up, seeded database, app running. No live court access and no production credentials required — connector work happens entirely against saved fixtures. That last point is what makes the sub-one-hour goal realistic, and it is only possible because parse() is separated from search() in the connector interface.
Staging
One Hetzner machine, production images, anonymised data subset, real connectors against real court sites but with a small matter set. This is where connector changes are validated — production data is never copied down unanonymised.
| Local | Staging | Production | |
|---|---|---|---|
| Court access | Fixtures only | Live, small set | Live, full fleet |
| Data | Seeded fake | Anonymised subset | Real |
| Notifications | Mailpit sink | Internal numbers only | Real users |
| Payments | Stubbed | Razorpay test mode | Live |
| Deploy trigger | — | merge to main | tagged release |
| Hosts | 1 laptop | 1 | 2–3 |
Production
flowchart TB
NET[Cloudflare
DNS, WAF, DDoS, CDN] --> LB[Caddy
TLS, reverse proxy]
subgraph h1[Host 1 — application]
LB --> API1[API replicas]
API1 --> AI1[AI service]
end
subgraph h2[Host 2 — workers]
SCHED[Scheduler] --> WK[Worker pool
autoscaled by queue depth]
end
subgraph mgd[Managed services]
PG[(Postgres
primary + replica + PITR)]
RD[(Redis)]
R2[(Cloudflare R2)]
end
API1 --> PG
API1 --> RD
WK --> PG
WK --> RD
WK --> R2
RD --> WK
subgraph obs[Observability]
OTEL[OpenTelemetry collector] --> GRAF[Grafana Cloud
metrics, logs, traces]
end
h1 --> OTEL
h2 --> OTEL
Fig 13.2 — Production topology
Security posture
- Database and Redis on a private network only — no public listener, ever. TLS enforced.
- Per-service credentials with scoped roles. No shared admin user.
- Secrets in a manager (SOPS + age, or Infisical). Never in the repository — enforced by
gitleaksin CI. - Admin access via VPN, e.g. Tailscale. No bastion with a public SSH port.
- Cloudflare WAF and rate limiting at the edge.
- Under the DPDP Act, case data is personal data: encrypt at rest, log access, define retention.
The three failures the old system had, stated as rules
Every one of these was a live condition in the previous system. They are written as absolutes because each was reached by gradual, reasonable-seeming steps.
1. No credential is ever committed, in any form, to any repository. 2. No datastore ever listens on a public interface. 3. No dependency is ever resolved at deploy time — lockfiles are committed and installed, never re-locked on the server.
Observability
OpenTelemetry from the first commit — cheap now, painful to retrofit. Three things must be answerable at any time:
- What is the cost per poll, by court? Derived from
CONNECTOR_RUNduration and transport. This is the metric the business model turns on. - Which connectors are degraded? Success rate per court against baseline, surfaced on one dashboard.
- Did a given user's notification get delivered? Traced from
CHANGE_EVENTthrough to provider receipt.
Disaster recovery
| Scenario | Detection | Response | Target |
|---|---|---|---|
| App host lost | Health check | Redeploy image on new host | < 30 min |
| Database lost | Managed alert | Promote replica | < 15 min |
| Data corruption | Anomaly alert | Point-in-time restore | < 2 hr |
| Region outage | External monitor | Restore from backup, new region | < 8 hr |
| Connector broken | Canary | Circuit breaks, matters flagged Stale | < 1 hr |
| Court site down | Circuit breaker | Back off, resume automatically | automatic |
RPO 15 minutes, RTO 8 hours for total loss. Restore is tested weekly by an automated job that restores to a scratch instance and asserts row counts. A backup nobody has restored is a hypothesis.
Note that the bottom two rows are not really disasters — they are expected weekly events in this product. Treating a court site being down as a routine, automatically-handled condition rather than an incident is a defining characteristic of a system built for this domain.