Building in the open

OpenBuilds

What I'm building outside Krypton, and the architecture behind it. Design decisions and what they cost, rather than a feature list.

Drizzle

In progress

A voice-first career mentor that actually remembers you — entered through the acute pain of a job search.

Every career tool is a forms problem pretending to be a thinking problem. You paste a résumé, it reformats it, and nothing in the system knows why you took the job in 2019 or what you'd never do again. The interesting question isn't how to render a CV — it's how to hold an evolving model of a person that gets more accurate over time and can be argued with.

So Drizzle is built around a spine rather than a feature set, and you fill it by talking — actual voice, not a chat box. The genuinely hard part, and the reason it's open, is persistent context modelling: structured facts versus transcripts versus embeddings, what to retrieve and when, and how a profile updates without quietly contradicting itself.

TypeScriptNext.js 15Postgres · SupabaseDrizzle ORMVapiAnthropic APIOllamaZod
Piece by piece
  1. 01

    Drop a résumé

    One dropzone. No account, no form.

    The file is parsed to text locally, then stored verbatim as an immutable source row before anything else touches it. That ordering matters: the raw input exists independently of whatever the extractor makes of it, so every fact downstream can be traced back and re-derived when the prompts improve.

    Drizzle upload screen — drop your résumé
  2. 02

    Extract into a relational shape

    A clean, editable résumé, in seconds.

    A Zod schema is compiled to JSON Schema and handed to the model as a structured-output contract, so the extractor returns typed rows rather than prose to parse. Each row lands with the sourceId it came from and a confidence. Editing a field writes a new user_edit source — the edit is evidence too.

  3. 03

    Talk to it

    A call, not a chat box.

    Vapi handles speech; the reasoning stays here. It POSTs OpenAI-format turns to an endpoint in the app, which loads a narrow slice of the map, builds a steering prompt and streams SSE chunks back to be spoken as they arrive — capped at 400 tokens, because nobody wants a monologue read aloud.

    Drizzle voice mentor screen
  4. 04

    Let it change its mind

    Nothing visible. That's the point.

    After the call, a second agent reads the transcript and writes insights — aspirations, energisers, blockers — each with a confidence. Nothing is ever deleted: a changed view supersedes the old one and keeps the chain. Contradiction stops being a data bug and becomes the thing the mentor can point at.

Drizzle architecture — résumé and voice ingest paths, a fast streaming voice turn, a post-call extraction agent, an agent runner writing observability rows, and a four-layer data spine
Two ingest paths, one spine. The fast path streams; the slow path thinks.
The decisions, and what they cost
01

Separate the immutable from the derivable

The data spine

Layer 1 is an append-only source log that is never mutated. Every derived row — an experience, a skill, an insight — carries the sourceId it came from plus a confidence. That costs a join everywhere and some write amplification, and it buys two things I wasn't willing to give up: the mentor can always say "you told me this, in this call", and the whole derived state can be rebuilt from the log when the extraction prompts improve.

02

Supersede, never delete

Insights as an evolving map

Layer 3 insights carry a status of active, superseded or contradicted, plus a supersedesId chain. Nothing is deleted when the model of you changes. That turns contradiction from a data-integrity bug into the actual product mechanic — the system can notice that what you said in March doesn't match what you're saying now, and say so.

03

Split the fast path from the slow path

Voice architecture

A spoken turn has a latency budget of a few hundred milliseconds, so the live path does the least work possible: load a narrow slice of the map, build a steering prompt, stream tokens back, capped at 400 tokens because nobody wants a monologue. All the expensive reasoning — extracting what the conversation revealed — runs after the call as a separate agent. The voice turn is deliberately not modelled as a discrete agent, because it's a stream, not a unit of work.

04

Own the LLM endpoint, not just the prompt

The Vapi integration

Rather than configure a model inside the voice provider, the app exposes an OpenAI-compatible chat/completions endpoint and Vapi calls it every turn. The userId rides in the URL path because Vapi appends the route itself, and any system message it sends is ignored — the endpoint is the authoritative brain. It means the voice vendor is swappable and the interesting logic stays in the codebase.

05

One choke point for every agent call

Observability

Agents are a plain interface with a pure run function; a single runner wraps every invocation and writes an agent_runs row with model, token counts, duration and error. That one table is the cost surface, the eval surface and the debugging surface. Logging is best-effort by design — an observability failure must never take down the path it's observing.

06

Two providers behind one interface

Cost and iteration speed

Anthropic for anything a user sees, Ollama locally for everything else, chosen by an env var behind a single provider interface. Local models make the high-volume extraction loop free to iterate on, and the seam is already in place to route by task rather than globally — cheap model for bulk work, Claude where quality is visible.

Where it is
  • The spine — four-layer schema, sources as evidence
  • Résumé ingest — parse, structured extraction, editable view
  • Voice mentor — custom LLM endpoint, streaming turns
  • Insight extraction — transcript to evolving map
  • Agent runner and cost observability
  • pgvector embeddings + retrieval over the spinenext
  • Opportunity matching against aspiration insightsnext
  • Résumé variants generated per aspirationnext