Otto

Chat is the front door.
Otto is the whole house.

A self-hosted AI platform — ingestion pipelines, a four-store knowledge model, governed memory, per-domain voices, and model routing — running as one Docker stack on hardware you control.

140K+ vectors indexed Every query owner-filtered Bootstrap → daily family use in 4 months Cloud migration = edit .env
The thesis

Assistant vs. platform

You've used Copilot, ChatGPT, Claude. World-class models — Otto uses those same models. What those products don't give you is everything underneath: where knowledge lives, how it gets there, what happens after the conversation ends, and who controls all of it.

Hosted assistantsOtto
Where your data livesTheir cloud, their termsYour hardware, four stores you can open
After the chat endsA transcript, maybe opaque memorySession becomes governed, searchable knowledge
Ingest your scanner, vault, podcasts, RSSNo — paste and prayTen intake channels, one enforced pipeline
Route tasks to different modelsOne vendor's picker~34 task routes across three providers + local models
Add a module (meals, chores, contacts…)Wait for the vendorSchema + panel on the same substrate

Those products end at the conversation. Otto begins there — self-contained and expandable because the storage and pipeline substrate exists.

Orientation

Architecture at a glance

Five layers, one Docker Compose stack. One ingress. Four stores at the center. Every external dependency is swappable configuration, not code.

INGRESS & USERS APPLICATION DATA — THE FOUR-STORE HERO PIPELINES & SCHEDULING AI / INFERENCE Household devices browser · mobile · shortcuts Reverse proxy (NPM) TLS · single entry point Authentik forward auth · SSO · groups nginx rate-limit sidecar per-IP limits ahead of the API Svelte 5 + Vite 8 UI streaming chat · household panels 4 themes · per-user navigation FastAPI Agent API ModelRouter hand-rolled tool loop tiered retrieval lens injection session memory cross-provider fallback slash commands · content builder Redis session cache quotas · streams Every document lives in all four stores MinIO immutable originals + text sidecars BLOB · S3-COMPATIBLE Qdrant 140K+ text + image vectors VECTOR · SEMANTIC Neo4j entities · relationships · co-citation GRAPH · STRUCTURAL MongoDB registry of cross-store pointers · state DOCUMENT · INDEX OF INDEXES owner: global · household · personal — enforced on every query, no unfiltered code path Intake channels upload · URL · iOS shortcut · vault sync podcast · SMB scanner · RSS business card · email signature · structured every channel converges on one pipeline Prefect — orchestrated flows 9-step enforced ingest · non-negotiable order · rollback on failure nightly persona evolution (Batch API) · nightly backups code-first · git-trackable · retries · observable runs heavy + scheduled work APScheduler (in-API) chore auto-generation session-memory embedder source routines · cost alerts hot, short jobs — no worker latency ModelRouter ~34 task routes · fallback chains usage ledger · pricing engine caching · effort · Batch API External providers (swappable) Anthropic Claude · OpenAI · Google Gemini classification runs on Gemini Flash (~10× cheaper) dashed = external, config-swappable Local models CLIP image search · Whisper STT ×2 Kokoro TTS zero marginal cost, GPU-scheduled Search & MCP weather · web-fetch · Exa self-hosted SearXNG quota-guarded augmentation solid — internal, synchronous dashed — external / swappable via config one Compose stack · two Docker networks · no service exposed except through the proxy
Current architecture, verified against the running stack. Frontend is Svelte 5; the agent loop is hand-rolled (no framework); every inference call passes through the ModelRouter and lands in the usage ledger.
Why multiple storage layers

One document, four stores

Different questions need different representations of the same source. These are not redundant copies — the design decision reads, verbatim: "distinct representations, not redundant copies."

Blob

MinIO

The immutable original + a plain-text sidecar. Re-index everything without re-uploading anything. S3-compatible — swap to R2 or Azure Blob by editing .env.

Meaning

Qdrant

Semantic vectors — 140K+ and counting. "Find things like this" across text and images. pgvector was considered for this role and rejected.

Structure

Neo4j

Entities and relationships extracted from the text. A theology paper yields chunks in Qdrant and graph edges between the people it discusses.

Registry

MongoDB

The index of indexes: cross-store pointers, pipeline state, application data. Chosen over Postgres for personal data whose shape keeps evolving.

A vector store alone can't answer "who influenced whom." A graph alone can't do fuzzy matching. Blobs alone can't search at all. The registry ties the other three together so a single document ID resolves to its original, its meaning, and its structure.

Redis sits beside them as the operational cache tier: session state, streaming coordination, API quotas.

Every record in every store carries an owner field — global, household, or personal. The ownership filter is applied at query construction in every retrieval path. There is no code path where a query runs unfiltered.
Getting knowledge in

Ten channels, one enforced pipeline

Anything the household touches can become knowledge. Every channel converges on the same nine-step sequence — the code comment reads "the 9-step order is non-negotiable."

manual uploadURL / web page iOS share sheetObsidian vault sync podcast / audio URLnetwork scanner (SMB) RSS feedsbusiness-card photo email signaturestructured data
1 verify blob 2 extract text 3 embed → Qdrant 4 find similar 5 extract entities 6 merge → Neo4j 7 registry → MongoDB 8 archive blob 9 mark complete

Fail at any step and the pipeline rolls back what it wrote — vectors deleted, graph nodes detached, the document marked failed, never half-ingested. The registry entry is written only after everything upstream has succeeded.

Why Prefect, not n8n

n8n was considered twice in the decision log and rejected twice — the recorded verdict: an external automation sidecar "adds operational complexity without unique capability." Orchestration here is code-first and git-trackable: flows are Python in the repo, reviewed like everything else, with retries and observable runs. A visual workflow tool would put pipeline logic outside version control in a second UI.

The honest nuance: Prefect is not used for everything. Hot, short jobs — chore auto-generation, the session-memory embedder, cost alerts — run on APScheduler inside the API process, because a worker round-trip adds ~30 seconds of latency that interactive-adjacent jobs shouldn't pay. Right tool per job class, not dogma.

Answering well

Confidence-gated retrieval

Hallucination control is an architecture property here, not a prompt suggestion. Every answer about your data passes through a three-tier gate.

Tier 1 — vectors + graph

Semantic search across the owner's collections plus graph traversal. Proceeds only when the top match clears a 0.75 confidence gate.

Tier 2 — scoped originals

Below the gate, Otto pulls the full text of the few most-plausible source documents from MinIO and answers from the originals — bounded, never a full-corpus dump.

Tier 3 — transparent uncertainty

Still nothing? Otto says it doesn't know, and shows near-miss candidates you can open or pin. No confident invention.

And when you want a document fully in context, /focus pins up to two documents as complete session context — no retrieval roulette for the conversation that matters.

What happens after the chat ends

Memory that behaves like memory

Hosted assistants give you memory as a black box. Otto's memory is governed: you can see it, decay it, correct it, and opt out of it.

One platform, many voices

Lenses: per-person, per-domain voice

A single system prompt gives everyone the same assistant. Otto maintains a lens per person per domain — how you think about study, home, research, and AI — and injects the right one into the answering call.

Why this matters to the whole: it's the difference between an assistant you configure and a platform that learns each person under governance. It is also the enterprise story in miniature — per-department voices with human-approved evolution is the same mechanism at a different scale.

Model routing & the agent runtime

Owning the AI loop

No agent in Otto calls a vendor SDK directly. Everything flows through one config-driven router — which is why models are swappable, costs are attributable, and outages degrade gracefully.

taske.g. "classify query" ModelRouter~34 task routes in one YAML primary modelcapability-checked fallback on errorcross-provider usage ledgerevery call priced

The LangGraph story

Otto adopted LangGraph early for agent orchestration — and then outgrew it. When the tool-calling runtime was built, the wrapper layer cost more code than it saved, and the one behavior Otto needed most — switching providers mid-loop and replaying the conversation — wasn't expressible. So in mid-2026 the dependency was deleted and replaced with a 158-line hand-rolled loop with a provider-neutral turn format. We adopt frameworks when they pay rent, and remove them when they stop.

The same reasoning applies to routing gateways like LiteLLM — considered, and rejected in the decision log for one recorded reason: less control. Routing is a product capability here — cost, quality, and resilience per task — not plumbing to outsource.

Why a custom UI

The interface is part of the platform

Generic chat frontends exist, and they're fine — for chat. Otto's interface is a household application in which chat is one panel among many.

Beyond the chat pane

Meals, chores, tasks, lists, contacts, notes, a lifelog, and a full settings surface — including a cost dashboard and per-user lens editing. A chat skin would have capped the product at chat.

Multi-user by design

An owner switcher on every scoped panel, per-user themes (four of them, token-driven), per-user navigation visibility, per-user model selection. The UI understands the household the same way the data layer does.

Deliberately boring tech

Svelte 5 + Vite 8, compiled to plain JS, built in Docker. The recorded decision chose Svelte over React and Vue for exactly this: reactive components with no runtime framework weight.

Streaming done once

Server-sent events are handled in one store, not scattered across components — the kind of discipline that makes a small codebase stay small.

Reaching out without losing the center

Outside tools augment. Collected knowledge stays primary.

Most assistants make web search the product. Otto inverts that: your accumulated knowledge is the primary context, and external tools serve it.

AI at the core

Modules on a platform: from chores to enterprise

Meal planning and chore scheduling aren't bolted-on features. They're proof that the substrate composes.

Each module reuses the same machinery: the four stores, the owner model, the ModelRouter, and the UI's registry system. Meals get per-person plans, pantry awareness, and AI-estimated nutrition; chores get recurrence rules and AI-generated weekly schedules under household time-zone logic. Neither required new infrastructure — a module is a data schema plus a panel, not a new stack.

That's the enterprise extrapolation: household → team, family member → employee, domain lens → department voice, ownership tiers → org permissions. The mechanisms chain because they were built once, at the core, instead of per-feature. (What that path still requires is stated honestly below — the tenancy model today is single-household.)

Run like a product

Operations & trust

Backup & recovery

Nightly backups of all five stateful systems, weekly encrypted offsite sync, a scripted restore with a written recovery runbook and stated recovery-point objectives. Every service image keeps a rollback tag — reverting a bad deploy takes about ten seconds.

Cost governance

A monthly spend ceiling ($50 default) that pauses AI calls when hit; per-job ($2) and per-user daily ($10) research caps; a pricing engine that prices every call, flags any unpriced model within a day, and reconciles measured spend against provider bills.

Change discipline

Separate dev and prod stacks, family-hours maintenance windows, and a required multi-agent review gate before anything touches core code. Main is always shippable.

The ledger that caught a regression

Every feature carries a public/private disposition in a reviewed ledger with a running audit log. That process once caught a stale-base commit that had silently broken an integration for five weeks — the audit found it, root-caused it, and turned it into a hard rule. Process that catches its own mistakes is the point.

We'd rather you hear these from us

Honest gaps — and what it would take

Otto is a production system for one household, built by one owner and AI agents. Taking it to enterprise means closing real gaps. Here they are, with the work each one requires.

Gap todayWhat it would take
Single host, no horizontal scaling The API is stateless-ready and storage is portable by design (.env-swappable, S3-compatible blobs) — scale-out means managed stores + a container platform, not a rewrite.
Dev and prod share the data layer Separate volumes and databases per environment; the compose split already exists, so this is provisioning work, not architecture work.
Secrets live in .env, not a vault Vault/KMS integration at the compose layer; a no-downtime rotation runbook already exists for every credential.
Auth trusts proxy headers Safe as deployed — the API is never exposed except through the authenticating proxy. The planned hardening adds a proxy-to-API shared secret so the guarantee no longer depends on network topology.
No CI/CD; images built on-host GitHub Actions + registry-published images are already designed for the public release track.
Tests thin but growing (256 passing) Coverage targets and CI enforcement; the suite grew 25% in the last release cycle alone.
Observability = health panel + chat-ops alerts Metrics and tracing (e.g. Prometheus/Grafana) alongside the existing per-service health checks and alert webhooks.
Single-household tenancy — the biggest true gap The ownership-tier model generalizes to orgs conceptually, but org-grade multi-tenancy (isolation, admin, billing) is real engineering and is stated as such, not hand-waved.
GPU shared with the family gaming rig Charming at household scale — a scheduler literally waits for the game to end before the nightly audio pipeline runs. Enterprise means a dedicated inference box. The scheduling discipline transfers.
What makes this unusual

Built differently, on purpose

Next steps

See it live

The best way to evaluate Otto is thirty minutes of the real system — the pipeline ingesting a document while you watch, retrieval citing its sources, the cost dashboard showing where every fraction of a cent went.

Trey Bayne · trey.bayne@gmail.com