Section 13
Technology Decisions
Every choice, with the alternative rejected and why. Several of these deliberately depart from the stack in the original brief.
Decisions at a glance
| Layer | Choice | Rejected | Reason |
|---|---|---|---|
| Backend | Python 3.12 + FastAPI | NestJS, Fastify | Connectors and AI are irreducibly Python; a second ecosystem doubles the hiring pool needed |
| Database | PostgreSQL 16 | MongoDB | Natural keys need enforcement; cause-list matching is a join |
| Queue | Redis + ARQ | Kafka, RabbitMQ | Thousands of jobs/day, not millions/hour |
| Object store | Cloudflare R2 | S3 | Zero egress fees — the product serves PDFs constantly |
| Web | Next.js + Tailwind + shadcn/ui | Vue 2 (EOL) | App and admin in one codebase |
| Mobile | Flutter | React Native | Already built and working — no reason to churn |
| Auth | OIDC + phone OTP | Email-first | Indian advocates are phone-first |
| Payments | Razorpay | Stripe | Stripe's India support is materially weaker for UPI |
| Observability | OpenTelemetry + Grafana Cloud | Self-hosted LGTM | Free tier covers this scale; no ops burden |
| Hosting | Hetzner + Docker Compose | AWS EKS | 3–5× cheaper; K8s needs an owner you don't have |
| CI/CD | GitHub Actions | Jenkins, CircleCI | Free at this scale, no infrastructure |
| Search | Postgres FTS, then Meilisearch | Elasticsearch | Not needed until case-law search ships |
The three departures worth arguing about
1. Python everywhere, not NestJS departs from brief
NestJS is genuinely excellent and I would recommend it for a different product. Here it means two backend languages, because the ~70 connectors and the AI service cannot reasonably be anything but Python — the parsing and OCR ecosystem lives there, and the existing connector knowledge is written in it.
Two ecosystems means two dependency stories, two CI paths, two sets of idioms, and a hiring requirement for people fluent in both. At your team size that tax buys nothing. FastAPI gives you the typed, decorator-driven, OpenAPI-generating developer experience that makes NestJS attractive in the first place.
Revisit if: the API surface grows far beyond the connector fleet in complexity, or you hire a strong Node team.
2. No Kubernetes departs from brief
The brief asked for Kubernetes. I would not adopt it now. At ₹800/user/year, infrastructure that requires a dedicated operator is a direct tax on survival. Docker Compose across two or three Hetzner machines runs this workload for roughly €50–150/month, versus $300+/month for equivalent AWS capacity before egress.
The workload also does not have the property that makes K8s pay: bursty, unpredictable, multi-team scaling. As noted in System Architecture, poll load is known in advance — a function of tracked matters, on a schedule you control. That is the easiest capacity-planning problem there is.
Revisit when: you have someone whose actual job is running it, or you genuinely need multi-region. The design keeps this cheap — everything is a container behind a load balancer, so the migration is real but bounded.
3. Postgres over MongoDB departs from existing system
Covered in Domain Model. The short version: the old system ships a remove-duplicate-matters command, which is what a missing unique constraint looks like in production.
Deliberately deferred
Good technologies that would be premature. Named here so that deferring them is a decision rather than an oversight.
| Technology | Adopt when |
|---|---|
| Kafka / NATS | Redis queue depth becomes a real bottleneck |
| Elasticsearch / OpenSearch | Case-law search ships as a product |
| ClickHouse | Poll analytics outgrow Postgres — likely year two |
| gRPC | Internal service count justifies it — currently one API |
| Multi-region | Users outside India, or a regulatory requirement |
| Vector DB | Semantic search over orders becomes a feature |
Automation from day one
Each of these is cheap to add at the start and expensive to retrofit.
CI on every PR
Lint, type-check, test, connector golden files. GitHub Actions.
Secret scanning
gitleaks pre-commit and in CI. The old system's defining failure — make it structurally impossible.
Automated deploys
Merge to main deploys staging; tag deploys production. OIDC, no long-lived tokens.
Connector canaries
Scheduled live checks per court, alerting to a watched channel.
Dependency updates
Renovate, grouped weekly, auto-merged when CI is green.
Backup verification
Nightly dump plus a weekly restore test. An untested backup is not a backup.
Generated API clients
OpenAPI to TypeScript and Dart on every merge. No hand-written client drift.
Migrations in CI
Alembic, forward-only, applied automatically on deploy.
Developer experience targets
| Target | Measure | Enabled by |
|---|---|---|
| Onboarding | < 1 hour | Compose stack, seeded DB, fixtures instead of live courts |
| CI feedback | < 5 min | Pure parse() tests need no network |
| Add a court | < 1 day | Connector interface + config registry |
| Deploy to staging | on merge | GitHub Actions + OIDC |
| Client drift | zero | Generated from OpenAPI |
Conventions: trunk-based development with short-lived branches, Conventional Commits, squash merges, and release notes generated from commit history. Nothing exotic — the goal is that none of it requires explanation.