← Learning

The backend

One async codebase, two databases

FastAPI, async all the way down, the same code from a SQLite file to Neon Postgres. The interesting part isn't the CRUD — it's the loop that learns from your edits.

How it works

CRUD, and a loop that learns

Async FastAPI + SQLAlchemy, UUID primary keys everywhere, JWT (access + refresh) auth behind a dependency. Memory comes in three layers: explicit (brand voice, facts, execs you set), implicit (learned from your edits), and semantic (similarity search — today keyword matching; pgvector is the honest stub).

The loop is the whole point. When you edit a draft (PUT /content), the backend diffs old against new, writes an EditSignal, and kicks a background job that asks Claude to categorize the change and update the brand-voice profile. That update lands in the cached org context every agent reads — so the next draft is closer to your voice, and you edit less.

The edit-signal learning loopEditing a draft writes an EditSignal; a background Claude job categorizes the diff and updates the brand-voice profile, which is injected into the next generation, closing the loop.You edit a draftPUT /contentEditSignalold → new diffClaude classifiescategory · voice (bg)Brand voice ↑the living contextInjected into the next generationthe cached org context every agent reads↺ the crew drifts toward your voice

Edit a draft and the backend diffs it, files an EditSignal, and asks Claude — in the background — to categorize the change and update the brand-voice profile. That update is in the context every agent reads next, so the crew drifts toward your voice and you edit less over time.

Output contracts depend on the consumer. The brand-voice score that writes to the database comes from a tool-enforced schema — the model must return an integer 0–100, not a sentence I have to parse. The chat a human reads streams plain prose. Where I crossed those wires — regex-parsing the chat’s prose to persist it — is the one fragile seam in the system, and I’d rather name it than hide it.

Decisions & trade-offs

What each choice cost

UUID keys, not auto-increment

mergeable · no enumeration

Every primary key is a UUID. Costs a few bytes and an index nuance; buys IDs I can generate anywhere, merge across environments, and expose in URLs without leaking a row count. For a multi-tenant app, sequential ids are a liability.

Background jobs, not request-time waits

instant save · eventual learning

The edit-learning (a Claude call) runs after the response returns. Saving a draft is instant; the voice update lands a beat later. The user never waits on the model to learn from them.

Tool-schema where it persists

right rigor per consumer

The score that updates a row is a tool call — the model must return a number in range, not prose I regex. The chat is prose because a human reads it. Match the format’s rigor to what a bad parse would cost.

Semantic search degrades, loudly

honest stub · same code both DBs

pgvector isn’t wired, so similarity search falls back to keyword matching — and the code says so in a comment, not a silent lie. One code path runs on SQLite and Postgres alike; the day embeddings land, it’s an upgrade, not a migration.

The learning

Match the rigor of your output format to what a bad parse costs — a number that hits the database gets a schema; prose a human reads doesn’t.

What I'd tell you

Three things I'd say out loud

The memory is the moat, and the loop is the memory. Anyone can inject a brand-voice doc. What a generic chatbot can’t copy is a profile that updates itself from your corrections. The loop — edit → signal → classify → update → inject — is the differentiator, and it’s maybe forty lines of glue. The leverage was in wiring it, not in any one piece.

Parse, don’t guess — but only where it counts. I didn’t make everything a tool call; that’s ceremony. I made the things that persist a tool call. The regex seam left in the chat path is a known, bounded risk — and naming it is more honest than pretending the whole system is rigorous. (It’s just structured outputs, applied selectively.)

Let the background do the slow, smart work. The synchronous path stays dumb and fast — save the row. The intelligence — classify the edit, embed the content — happens after the response. It’s the oldest trick (defer the expensive work), and it’s what keeps an AI-heavy backend feeling instant.

The backend is mostly ordinary async CRUD. The one thing worth copying is the edit-signal loop — the part that makes the product remember you — and it’s small enough that the honest version fits on this page.

A personal experiment — cost-capped and opinionated. The views here are my own, not my employer’s, and this is a learning writeup, not an enterprise blueprint.