Writing

Notes from the work: decisions, trade-offs and the occasional mistake, written up so I don't have to relearn them. Each post is grounded in a system I built or run.

RSS feed

2026

  • Idempotent batch APIs with an external_id

    Designing a certification platform's provider API so retried batches never duplicate certificates and partners can integrate without talking to us.

  • A channel taxonomy in one CSV

    The most useful thing to come out of a dbt spike was fifteen lines of CSV: one place where every vendor's channel name maps to ours.

  • A week with dbt Core and its sharp defaults

    A time-boxed spike to decide whether dbt fits our silver-to-gold layer: it did everything asked, and showed how easily it produces silently wrong data.

  • Test your row-level security as a non-superuser

    RLS tests that run as the postgres superuser prove nothing. How the Intelect test suite exercises the policies for real, and what that costs.

  • Two layers of tenant isolation: ORM hooks and Postgres row-level security

    How a multi-tenant CRM on one shared PostgreSQL database keeps tenants apart twice: once in SQLAlchemy, once in the database itself.

  • Migrating a WordPress site to Postgres without running MySQL

    Two education platforms moved from WordPress to FastAPI and Postgres 16. The migration pipeline is reproducible, keeps a safety net, and lets old passwords keep working.

  • Secrets hygiene for a small platform

    The written rules InkPigeon follows for passwords, API keys, DKIM keys, logging, configuration and migrations, and why they are rules rather than habits.

  • Where port 25 works: hosting an email platform in 2026

    Most cloud providers will not let you send mail on port 25. How that constraint shaped where and how InkPigeon runs.

  • Two privilege escalations I shipped, and the rules that came out of it

    InkPigeon's custom-roles feature passed its tests and still had two ways to escalate privileges. What a dedicated adversarial pass found, and the rules it produced.

  • The mail queue lives in PostgreSQL

    InkPigeon's delivery workers claim messages with FOR UPDATE SKIP LOCKED instead of a Redis or Celery queue. The reasoning and the trade-off.

  • A modular monolith on one VPS

    InkPigeon is one Python codebase deployed as about eight containers that scale independently. Why that beats microservices on a single server.

  • Build or adopt: why InkPigeon doesn't run on Mailu or Postal

    I evaluated five open-source mail products before building InkPigeon's own pipeline. Here is what each one is, and why only two components survived.

  • AI coding sessions need a standards file

    I use AI coding assistants heavily. The thing that keeps them from drifting is not a better prompt but a written standards file that outranks the model.

  • The cutover playbook: swap a base URL, add a pipe, prove it on auth first

    How we move frontend modules from legacy endpoints to a new backend one constructor at a time, and why the first cutover is the one that matters.

  • Deleting endpoints safely: a code audit is not enough

    Seven groups of old API endpoints have zero callers in our code. We still are not deleting them until the access logs agree.

  • Replacing a five-year rewrite with a bounded migration

    Our plan to migrate all 18 modules to a new architecture would have taken 3.5 to 6 years. Here is how we replaced it with a target the company could actually reach.

  • Who owns which layer: Python, dbt and Celery

    Before choosing a transform tool for our ETL v2 platform, we fixed which layer each component owns. The boundaries outlived the tool decision.

  • Idempotent ingestion: run IDs, file hashes and row hashes

    How the bronze layer of our ETL v2 design makes every load safe to re-run, and why a data contract has to exist before the first table does.

  • A security gate that doesn't block delivery

    How I built our GitHub Actions staging pipeline to fail on security findings by default while letting a reviewer approve a deployment with a record.

  • One hotel, five identifiers: reconciling identities across vendor APIs

    Why the hardest problem in a multi-source analytics platform is agreeing on which hotel a row belongs to, and how we made that explicit.

  • Grounding LLM reports in your own APIs

    How we added LLM-written narrative reports to an analytics platform without letting the model invent a single number.

  • What a coding agent costs per pull request

    Why I built a per-run cost ledger into my coding agent, and how knowing the price of a pull request changed which tickets I give it.

  • Four tools are enough: designing a bounded coding-agent loop

    How I built an agent that turns well-specified tickets into pull requests with four tools, hard limits and a provider abstraction.

  • Missing data should be a state, not a silence

    How the analytics platform I lead turned silent gaps in vendor data into tracked states with monitors, triggers and automatic resolution.

  • Running periodic vendor syncs with Celery beat and a database scheduler

    Why the analytics platform I lead keeps its sync schedules in PostgreSQL rather than in code, and what went wrong before it did.