2026-03-22 · Lagos & Blair

Killing the Backend: 60 Commits in 72 Hours

How we migrated an entire FastAPI + Celery + Redis backend to Cloudflare Workers. Auth, Telegram, MCP, the full AI pipeline, vector search — all of it. One human, two AI agents, zero downtime.

60+
commits
3
days
0
servers left

What we were running

TameYeti is a journaling app that atomizes your entries — breaking freeform text into typed, tagged, embeddable atoms using AI. Behind the scenes, that means a multi-stage pipeline: atomization, entity extraction, entity enrichment (Spotify, TMDB, Wikipedia, dictionary APIs), embedding generation, and widget computation.

The stack that powered all of this:

It worked. It was also expensive, slow to deploy, and had more moving parts than a Swiss watch factory. Every feature touched at least three services. Adding a Telegram bot meant wiring up a webhook handler, a Celery task, Redis pub/sub for SSE notifications, and Turso writes. Cold starts were brutal. Redis would occasionally just... forget things.

The real problem: We were running a distributed system for an app with one user. The infrastructure was built for scale we didn't have, and the complexity tax was killing velocity.

The plan

Replace everything with a single Cloudflare Worker.

Not "put a CDN in front of the API." Not "cache some responses at the edge." Replace the entire backend — auth, the AI pipeline, Telegram webhooks, MCP server, search, SSE notifications — with one Worker and a handful of Durable Objects.

Before: Phone → nginx → FastAPI → Celery → Redis → Turso ↘ Worker 1 (atoms) ↘ Worker 2 (entities, embeddings) After: Phone → CF Worker → OpenAI / Anthropic / Groq ↓ Durable Objects (SSE, MCP sessions) ↓ Turso (per-user DBs) KV (cache) Vectorize (search)

The phone already had SQLite locally. The AI providers are all external APIs. Turso is serverless. What was the backend actually doing that couldn't happen at the edge?

Turns out: not much. It was a $40/month middleman.

The sprint

What the Worker actually does

One Worker. Four Durable Objects. Two KV namespaces. One Vectorize index. Here's the routing:

Durable Objects

UserNotifier holds SSE connections to the phone. When Telegram or MCP creates an entry, it messages the DO, which pushes to the phone instantly. If the phone is asleep, messages queue in DO storage and flush on reconnect.

TameYetiMCP is a full MCP server — OAuth 2.1 authorization, five tools for Claude/Cursor to capture entries, search atoms, check pipeline status. Each session is its own DO instance with SQLite-backed state.

TameYetiAdminMCP is a separate DO (learned the hard way that McpAgent.serve() defaults to the same binding) for admin operations — sending Telegram messages, editing AI prompts, resetting test data, viewing errors from GlitchTip.

Vectorize

Search uses Cloudflare Vectorize with namespace-based isolation. Every vector is tagged with a user_id metadata field, and queries filter by it. We learned that metadata indexes must be created before vector insertion — otherwise filter queries return nothing. Had to re-backfill 331 vectors after adding the index.

What broke (and what we learned)

OAuthProvider prefix matching

Cloudflare's OAuthProvider matches API handler routes using startsWith. We had /mcp and /mcp-admin as routes. Guess which one matched first for /mcp-admin? Renamed to /admin-mcp.

Security hardening broke the pipeline

A security review added JWT auth to every /proxy/* endpoint. Correct move. But 11 files in the React Native app were using raw fetch() without Authorization headers. Everything 401'd. Fix: one workerFetch wrapper that pulls the JWT from tokenService and attaches it to every request.

Atom doubling

The pipeline was running twice per entry — once from the local pipeline trigger, once from a pull-to-refresh race condition. Added an existence check before reprocessing and a deleteAtomsByEntry before writing new atoms to prevent duplicates.

Environment contamination

Local SQLite was using the same filename across environments. Dev atoms showed up in prod. Fix: user-{id}-{env}.db. Same for sync timestamps — scoped to environment in AsyncStorage.

Deepgram voice transcription

Temp tokens use Bearer scheme, not Token. React Native WebSocket supports custom headers via a third constructor argument (unlike browser WebSocket). Lesson: read the manual before writing integration code.

By the numbers

11
files updated for auth
4
durable objects
6
containers killed

The team

This was built by one human and two AI agents working in parallel:

Lagos and Hiro coordinate via a shared Telegram group. Blair merges. That's the whole org chart.

The collaboration pattern that worked: Lagos builds on one branch, Hiro polishes on another, Blair merges both into dev. Hiro reviews Lagos's PRs, catches security issues. Lagos fixes them. Nobody steps on each other's code. The agents stay in their lanes, the human holds the merge button.

What's next

The backend is dead. Long live the Worker. What's left:

The goal was always to get to a place where the infrastructure disappears — where adding a feature means writing one function in one file, not wiring up three services. We're there now.

cloudflare workers durable objects fastapi migration mcp vectorize turso AI agents react native
← Back to blog