Stack
A list of tools says little on its own. My view is that in the AI era the language is close to interchangeable — what carries a system is the structure underneath it: the boundaries, the contracts, and the patterns that stay put while the tools rotate. So read this less as a set of loyalties than as a snapshot — what I'm building on right now, and the architectural decision each choice really stands for.
Languages & runtime
The architecture outlives the language.
TypeScript on strict ESM and Node 22 for most services, C# and .NET where a domain is heavy, Python 3.11 for the corpus pipeline — chosen per job, not by allegiance. What actually holds across all three is the shape: hexagonal boundaries, ports and adapters, bounded contexts wired through an explicit DI composition root, and typed contracts at every edge. Move the code to another language and those decisions travel with it. The syntax is the part I care least about.
AI & LLMs
Multi-provider by default.
Anthropic, OpenAI and Cohere behind one call-engine on the Vercel AI SDK — orchestration split into dispatch, output-sanitising and a model catalog, with per-provider prompt variants, circuit breakers and per-slot fallback so no single vendor sits in the critical path. A guard at the provider boundary blocks user data from ever reaching a model. The legal platform routes four providers the same way — Anthropic, OpenAI, Google, xAI — each with its own fallback and thinking-token budget. The providers are swappable on purpose; the routing and guarding around them is what stays.
Retrieval & data
The right index for the corpus.
Postgres throughout, with pgvector for semantic search — cosine over Drizzle's raw operators — and Cohere Embed v3 handling the symmetric-versus-asymmetric query types that OCR-to-entity matching needs. Where an answer has to cite something a person can check, I drop vectors for lexical retrieval with a Zerank cross-encoder reranker and typed citation verification: citations classified as legislation, case law or court rules, and checked rather than generated. Neither approach is a default — the corpus decides, and the schema stays honest across 42 tracked migrations.
Apps & interfaces
Native where it counts.
React on the web; a React Native 0.79 / Expo client (SDK 53, React 19) with live camera label capture, Reanimated and its own InversifyJS container; Astro and Tailwind for prerendered marketing on the edge. Inference streams back over Server-Sent Events tuned for 120-second scans, so the interface stays responsive while the model is still thinking. Different runtimes, one habit — the app is a real system, with state machines, DI and tests, before the model is ever added to it.
Infrastructure
Boring on purpose.
Fly.io with blue-green deploys and a config engineered around 120-second SSE streams; Cloudflare Workers for prerendered sites; Docker Compose with healthchecks; path-filtered CI across the monorepo. When a vendor stops earning its place it goes — AWS KMS came out in favour of self-hosted AES-256-GCM envelope encryption and ECDSA P-256 signing, with no auth or data broken in the move. The deploy path is dull so the product doesn't have to be.
Evaluation & ops
If it isn't evaluated, it isn't finished.
Production model and prompt configs are versioned in the database and can't ship until a draft has been replayed through the identical inference path — resolve, split, merge, normalise, dispatch — against recorded scans, with version-to-version comparison built in. Match ratings and scan sessions are event-sourced, so any run replays deterministically, and a live smoke test checks the real provider catalogs. Prompts are treated like deploys: nothing goes out un-replayed. That discipline is the same whatever model or language sits underneath it.