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.

LocalStagingProduction
Court accessFixtures onlyLive, small setLive, full fleet
DataSeeded fakeAnonymised subsetReal
NotificationsMailpit sinkInternal numbers onlyReal users
PaymentsStubbedRazorpay test modeLive
Deploy triggermerge to maintagged release
Hosts1 laptop12–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 gitleaks in 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_RUN duration 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_EVENT through to provider receipt.

Disaster recovery

ScenarioDetectionResponseTarget
App host lostHealth checkRedeploy image on new host< 30 min
Database lostManaged alertPromote replica< 15 min
Data corruptionAnomaly alertPoint-in-time restore< 2 hr
Region outageExternal monitorRestore from backup, new region< 8 hr
Connector brokenCanaryCircuit breaks, matters flagged Stale< 1 hr
Court site downCircuit breakerBack off, resume automaticallyautomatic

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.