The Great Subtraction
Yesterday I opened x-reporter and found 123 uncommitted paths staring back at me: a whole architecture lying on the operating table, half-transplanted. This is the story of finishing the surgery and closing it with seven clean stitches.
The Diagnosis
x-reporter ingests content from X, Bluesky, and RSS feeds, scrapes the articles they reference, and produces executive digests with an LLM pipeline. The v1 codebase was a museum of 2024 enterprise defaults:
- NestJS β modules, injectors, decorators, dependency tokens for a service with exactly four endpoints
- Appwrite β an external BaaS for what is fundamentally one SQLite database
- BullMQ + Redis β a distributed job queue to processβ¦ digests for one user
- LangGraph β a graph runtime for a pipeline that is four functions called in order
None of these were wrong. They were all premature. The load-bearing wall was never load-bearing; we just liked the way it looked.
ADR-001: Rewrite, Donβt Refactor
The decision record says it plainly: rewrite in pure Bun. The new shape:
// The entire orchestration layer used to be a LangGraph StateGraph.
// Now it's this:
class DigestPipeline {
async run(input: RunDigestInput): Promise<RunDigestResult> {
let state = toInitialState(input);
state = await runClusterNode(state);
state = await runSummarizeNode(state);
state = await runRankNode(state);
return runComposeNode(state);
}
}
Four nodes, typed states, structured JSON prompts against OpenRouterβs OpenAI-compatible endpoint. No graph runtime. No framework. When your pipeline fits on one screen, a DAG library is just a tax.
The replacements, one for one:
| Died | Reborn as |
|---|---|
| Appwrite collections | PocketBase repos (UsersRepo, ItemsRepo, β¦) |
| BullMQ + Redis workers | One interval-based SchedulerService |
| NestJS controllers | Hono routes, composed like functions |
| LangGraph DigestGraph | Pure TS cluster β summarize β rank β compose |
| Session guards + Redis sessions | HMAC-signed cookies + AES-256-GCM token encryption |
Ingestion Became a Port
The old code had one source: Xβs API, polled by a dedicated processor. The new IngestionSource port has five adapters behind it β X API v2, Bluesky over ATProto, RSS/Atom, plain bookmarks, and an Apify Twitter scraper actor for when the API pricing gets hostile. The scheduler doesnβt know or care which one itβs holding. Thatβs the whole point of a port.
The Split Commit Problem
Hereβs where it got fun. The rewrite was done but uncommitted β 123 paths of deletions and additions tangled together. Committing that as one blob would have been a lie told to future git archaeologists. So we split by layer, additions before deletions:
feat(pocketbase)β persistence + collection bootstrap scriptfeat(ingestion)β the four source adaptersfeat(extraction)β local readability extractor, Firecrawl fallbackfeat(digest)β pure TS pipeline + OpenRouter providerfeat(app)β Hono app, routes, scheduler, authchore!β the mass deletion of the legacy stackdocsβ architecture, API, data-model rewritten for v2
Each commit reads like a sentence instead of a scream. And the final tree typechecks and passes every test.
The Numbers
The receipt everyone asks about:
- 36 tests, 0 failures, 230ms total. The old suite needed Redis running.
- Zero external queue dependencies. Zero LangChain dependencies. Zero decorator metadata reflectors.
- 81 files, ~4,600 lines β including tests, docs, and the collection bootstrap script.
The lesson isnβt βframeworks bad.β Itβs that every dependency you install is a bet that your problem will grow into its shape. Most problems donβt grow. Ours didnβt. Four async functions and an embedded SQLite file were enough all along β we just had to delete everything else to see it.
Delete first. Then look at whatβs left. Thatβs the project.