Projects
Backend systems, AWS architectures, voice platforms, and integration frameworks I've designed, shipped, and operated. Each case study covers context, approach, and outcome.
Turns a meal-planner shopping list into a reviewable cart proposal — and stops there, deliberately
## Problem My self-hosted meal planner knows what I need to buy. My grocer's site knows what things cost, which promotions apply, and what is on clearance. Reconciling those by hand every week is exactly the kind of small recurring chore that automation should absorb. ## My role Designed and built it, including the boundaries — which are most of the design. ## Constraints - Spanish-language grocery data, where the same product has several names and matching has to be deterministic enough to review. - Promotion logic is genuinely complex: multi-buy offers, percentage and fixed discounts, clearance pricing, loyalty rewards. - It touches money. That constraint drove everything else. ## Architecture Shopping-list needs are normalized, matched deterministically against the catalogue, and evaluated against active promotions to produce a **cart proposal a human reviews**. Local preference recording means a correction I make once is remembered. It exposes its capabilities over Model Context Protocol so an assistant can call it. Dry-run is the default. Cart mutation exists but is disabled behind an explicit apply flag. The out-of-scope list is written into the project and is not a TODO: no checkout, no payment, no delivery scheduling, no order submission, no loyalty-balance spending, no autonomous purchasing, and no attempt to work around bot protection. ## Outcome Working end to end at v0.1.0, in weekly use, with a fixtures-first test suite so promotion logic can be verified without hitting a live site. ## What I'd do differently Nothing structural — this is the project where I got the boundary right on the first try, and the reason is that I wrote the out-of-scope list before the feature list. **Automation that touches money should propose, not act.** The review step costs me thirty seconds a week and removes an entire category of failure I would otherwise have to engineer against.
GPU-local script-to-render automation, deployed twice: CUDA at home, CPU-only for public demo
## Problem Producing short-form video consistently is a scheduling problem disguised as a creative one. I wanted the mechanical half — script, matched footage, captions, music bed, render — automated and running on hardware I already own, rather than paying per minute for hosted generation. ## My role This is a **fork of the open-source MoneyPrinterTurbo project by harry0703**, which provides the core generator. Upstream authors hold the majority of the commit history and deserve the credit for the foundation. My contribution is the homelab integration and a set of extensions: a licensed music pool so audio is cleared for use, "content packs" that separate an account's identity from the generation machinery, insight measurement that goes beyond hashtag counting, and a motion-clip pool bounded by wall-clock time. ## Constraints - One GPU, shared with a local LLM stack and a second brain that runs a morning briefing. Video generation must not starve the things that run on a schedule. - The public demo has no GPU at all, so the same codebase has to degrade to CPU-only. ## Architecture Topic to script to matched footage to subtitles to music to rendered short. Hardware-accelerated encode and local speech-to-text on the workstation; the same stack deployed CPU-only behind the homelab's reverse proxy as a public demo. ## Outcome Two live deployments from one codebase. Roughly 57 commits of my own on top of upstream, concentrated in the scheduling, licensing and resource-contention work rather than the generator itself. ## What I'd do differently The bug I remember is a resource-contention one, and it taught me something general: a job queued by a scheduler inherits a fresh duration budget, not the remaining wall-clock window. Something meant to run overnight ran four hours into the working day, holding the GPU that the morning briefing needed. **A duration limit can never bound a wall clock** — if a job must be finished by a time, that time has to be the constraint you actually encode.
A compiled memory layer that answers infrastructure questions in 2-5K tokens, with citations
## Problem I wanted a local model, on my own GPU, to answer questions about my own infrastructure — what depends on what, why a thing is configured the way it is, what changed recently. Plain retrieval over my documentation cost 30,000 to 50,000 tokens per question and had no notion of time: a decision written in June and an observation from this morning came back with equal authority. ## My role Designed and built the whole thing solo, over roughly eight weeks, as 220 merged pull requests. ## Constraints - Must run entirely local. A 24GB consumer GPU that is frequently busy with other work. - Must never become another source of truth. Git and live runtime observations stay authoritative; anything derived has to be deletable and rebuildable. - Must be honest. An operations assistant that confabulates is worse than no assistant, because you cannot tell a thin answer from a wrong one. ## Architecture A **compiled** layer over authoritative sources: a temporal fact store that closes facts rather than overwriting them, a relationship graph (940 nodes, 1,449 edges), generation-stamped summary capsules, and a vector index — assembled into a token-budgeted context packet. Retrieval is staged and stops early: exact, then lexical BM25, then capsule, then graph, then semantic. Conflicts resolve by fixed precedence, runtime over fact over capsule, and a derived artifact whose generation lags its source is marked stale so the query path falls back to raw evidence. On top sit the analysis layers: decay (which documented decisions are no longer true), correctness validation against live sources with a committed baseline and a CI regression gate, drift reconciliation between declared and observed state, blindspot detection, causal and multi-hop reasoning, and a gated detect-propose-approve-act loop whose executor stops at a draft pull request and never merges. Throughout, one rule: **grounded or blocked.** Selection and ranking are deterministic and reproducible offline. A language model may phrase an already-selected result and may never add, drop, or reclassify one. ## Outcome Around a 99% reduction in tokens per query, validated in shadow mode against the previous approach before promotion. 31 command-line tools, 93 read-only tools exposed over Model Context Protocol, and 293 tests over the memory subsystem alone within a 116,000-line Python operations layer carrying 1,865 tests. ## What I'd do differently I built the fact store before I built the thing that audits whether the facts are still true. Those belong in the same release — a memory without self-audit is a confident cache. And I would have separated "what did I observe" from "what do I conclude" in the detector contract from the start; conflating them produced a batch of well-cited alerts that were all the same bug.
14 hosts, ~110 containers, and the documentation discipline to keep them legible
## Problem A homelab stops being a hobby the moment your household depends on it. Mine crossed that line — DNS, home automation, media, document storage and password-adjacent SSO all run on it. At that point the failure mode is no longer "a service is down." It is "I no longer remember why this is configured this way, and the person who knew is me six weeks ago." ## My role Sole architect and operator. Every host, every compose file, every decision record. ## Constraints - Single operator, evenings and weekends. Anything that requires my attention on a schedule will eventually not get it. - Real dependents. DNS and home automation failures are household-visible within minutes. - No cloud escape hatch for the things that matter — the point is that it runs here. ## Architecture A Proxmox node hosts purpose-separated Docker VMs: shared infrastructure (reverse proxy, SSO, metrics, logs, uptime, push notifications), an operational backend (self-hosted Git, CI runners, an append-only event ledger), human-facing knowledge and document apps, and a sandbox for experiments and public demos. A NAS holds persistence and backups and doubles as the second DNS node. A separate GPU workstation runs the local model stack. Ingress is a single reverse proxy with forward-auth in front of everything internal. DNS runs highly available across two nodes with one-way policy sync from a designated source of truth, so the replica can never drift into being a second opinion. Everything is Git-backed and Git-first: inventory, compose files, runbooks and decision records live in one repo, and the running fleet is compared against that repo by read-only collectors that change nothing and record their own errors rather than crashing. ## Outcome 14 hosts and around 110 containers, documented in 368 markdown pages: 21 architecture decision records, 131 runbooks — including a restore procedure for every stateful service — and postmortems kept honest enough to include the fixes that didn't work. ## What I'd do differently Write the decision records from day one rather than reconstructing them. The expensive part was never the configuration; it was recovering the reasoning behind configuration I had already forgotten. And buy the UPS before the unclean power loss teaches you to.
Next.js 14 app with a RAG assistant, LLM i18n pipeline, and multilingual PDF resume
## Problem A resume asserts seniority; it can't demonstrate it. I wanted a portfolio that is itself a production system — every feature a verifiable work sample a recruiter can click, read, and interrogate. ## My role Everything: product, design, backend, frontend, and operations — designed, built, and run solo. ## Constraints - Serverless deployment must not exhaust Postgres connections, and public pages must stay ISR-cacheable behind strict security headers. - Content ships in English, Spanish, and German without triple-authoring every post. - The AI assistant answers from my real content only, within a daily cost budget, and has to resist prompt injection. ## Architecture Next.js 14 App Router with TypeScript and Prisma on Postgres. The assistant is retrieval-augmented: site content is chunked and embedded into pgvector, retrieved per question, and answered with OpenAI models — with prompt-injection defenses, per-IP rate limits, a daily token budget, and cron-driven re-indexing. An LLM translation pipeline generates the Spanish and German locales with hash-based staleness detection. The admin surface sits behind NextAuth credentials plus WebAuthn passkeys under a nonce-based CSP. The resume PDF is rendered server-side with PDFKit in all three languages. Blog search uses Postgres full-text search; every route emits canonical/hreflang metadata, JSON-LD, per-route Open Graph images, a sitemap, RSS, and llms.txt. ## Outcome The site you are reading: multilingual blog and case studies, an AI assistant grounded in my own writing, and a recruiter-ready PDF resume — all generated from one codebase and operated in production. ## What I'd do differently Design the i18n routing from the start — retrofitting it meant every public page now exists in two route trees. And put project metrics in the schema instead of prose so they can render as highlight chips.
One Factory Method backbone for Slack, Teams, Google Chat, and Webex at Resolve Systems
## Problem Each enterprise chat channel — Slack, Microsoft Teams, Google Chat, Webex — started life as a bespoke integration. When fulfillment code branches on channel, you are maintaining N products that drift apart under deadline pressure. ## My role Designed and implemented the Factory Method–based integration framework and the shared testing utilities that every channel adapter builds on. This is the backbone every channel on the platform is written against, voice included. The design goal was an interface that says nothing about *when* a reply arrives: the chat channels are asynchronous, a phone call is synchronous and blocking, and both are adapters behind the same factory. An abstraction that only works for channels sharing a timing model is not an integration framework, it is a chat framework. ## Constraints - Channels differ in authentication models, payload shapes, and rate limits — the differences had to stay inside the adapters. - Existing live integrations had to keep working while the framework replaced them underneath. - Secure-by-default patterns (authentication, MFA, secrets handling) were table stakes for enterprise customers. ## Architecture Channel adapters normalize every inbound message into a common envelope; a Factory Method instantiates the right adapter per channel; fulfillment code operates on intents and entities and never branches on channel. Shared testing utilities give each new adapter the same conformance suite from day one. ## Outcome Slack, Microsoft Teams, Google Chat, and Webex all run on one backbone, and each new channel costs less than the one before it. Zero critical vulnerabilities maintained across these enterprise integrations for two years. ## What I'd do differently Define the shared intent schema on day one and version it like an API — merging divergent intent definitions later is the expensive path. Build the adapter last: get the shared envelope and the testing story right first.
Bridging async chatbot conversations into Amazon Connect IVR flows at Resolve Systems
## Problem Resolve Systems' virtual support agent was built for asynchronous chat, where a two-second reply feels snappy. A phone call is a blocking, real-time channel — callers hear every hop in the pipeline as silence. The platform needed a voice channel without rewriting the existing chatbot backend. ## My role Designed the architecture end to end and implemented it: intent routing, speech-to-text and text-to-speech orchestration across Amazon Connect, Amazon Lex, AWS Lambda and Polly, and the handoff into existing fulfillment. The part that made it possible was not voice-specific. I designed and built the Factory Method integration layer the chatbot platform runs on: every channel normalizes into one envelope, a factory instantiates the right adapter, and fulfillment operates on intents and entities without ever branching on channel. The property that mattered is that **the abstraction is neutral about time**. Slack, Teams, Google Chat and Webex are asynchronous; a phone call is synchronous and blocking. Both are adapters. The interface makes no claim about whether a reply arrives in two seconds or two hundred milliseconds, so voice was added against an interface that already existed rather than forking production infrastructure to accommodate it. Getting that boundary right first is the reason the hardest channel was addable at all. ## Constraints - Voice latency budgets are unforgiving — callers notice sub-second gaps that chat users never see. - The asynchronous fulfillment backend was production infrastructure shared with every chat channel; it could not be forked for voice. - Enterprise security requirements — authentication, secrets handling — applied to every new hop. ## Architecture Amazon Connect captures audio and streams to Lex for intent resolution; a Lambda bridge translates between the synchronous IVR flow and the asynchronous chatbot backend, then Polly synthesizes responses back into the call. Downstream lookups start speculatively on utterance-start instead of after intent resolution, and a caller's journey is correlated across Connect, Lex, Lambda, and downstream APIs so traces survive the channel boundary. ## Outcome Shipped as a production channel of the virtual support agent platform that helped reduce help-desk call volume by 40–60%. ## Where it went next The channel has since moved off the AWS stack to Vapi, which does more of the real-time orchestration as a platform than the assembled-primitives approach did. That is worth stating plainly: after the effort of building the bridge, a purpose-built voice platform turned out to be the better agent. Evaluation of further voice platforms is ongoing and runs in parallel with that work, because the deciding constraints on this kind of system are rarely only technical. What survived the migration is the part I would keep building the same way — the integration layer underneath. Because fulfillment never branched on channel, replacing the voice front end did not touch it. ## What I'd do differently Design cross-service correlation in from day one rather than adding it during hardening — traces that die at a channel boundary are not traces. And budget for barge-in cancellation semantics early: every in-flight synthesis becomes cancellable, which changes idempotency requirements downstream.