Architecture
System overview
Chat-in-Bio is a single-tenant application. One instance = one creator's bio page + chatbot. Multi-tenancy is handled externally (e.g., Kubernetes orchestration with a separate control plane).
┌─────────────────────────────────────────────────┐
│ Frontend │
│ Lit web components + A2UI renderer │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ bio-header│ │ link-tree│ │ chat-container │ │
│ └──────────┘ └──────────┘ │ ┌───────────┐ │ │
│ │ │ a2ui-root │ │ │
│ │ └───────────┘ │ │
│ └────────────────┘ │
└──────────────────┬──────────────────┬────────────┘
│ REST │ WebSocket
▼ ▼
┌─────────────────────────────────────────────────┐
│ Litestar Backend │
│ │
│ /api/bio ──────► BioController │
│ /api/admin/* ──► AdminControllers (CRUD) │
│ /ws/chat ──────► ChatWebSocketHandler │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PydanticAI │ │
│ │ Agent │ │
│ └──────┬───────┘ │
│ │ tool calls │
│ ┌─────────┼─────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ links │ │ events │ │ faq │ ... │
│ │ tool │ │ tool │ │ tool │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────┐ │
│ │ SQLite + SQLAlchemy │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ SurfaceManager (A2UI) │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Data flow: chat message
- Frontend sends
{"message": "text"}over WebSocket - ChatWebSocketHandler receives it, resolves the chat session
- The user message is persisted to SQLite
- PydanticAI agent runs with the user's message + conversation history
- The LLM decides which tools to call (intent routing)
- Each tool queries the database and builds A2UI components via
SurfaceManager - Tools return a text summary to the LLM so it can compose a natural-language response
- After the agent completes,
SurfaceManager.flush()returns A2UI protocol messages - The response (text + A2UI) is sent back over WebSocket as JSON
- The assistant message is persisted to SQLite
- Frontend renders the text as a chat bubble and A2UI components inline
Why single-tenant?
- Simpler data model — no tenant isolation concerns in every query
- Self-hostable — anyone can run their own instance
- Security — no risk of cross-tenant data leaks
- Performance — no noisy-neighbor problems
- Multi-tenancy is a deployment concern, handled at the infrastructure layer
Technology choices
| Choice | Rationale |
|---|---|
| Litestar | Modern Python ASGI with native WebSocket, dependency injection, and guards |
| PydanticAI | Framework-agnostic agent with typed tool dependencies and multi-provider support |
| A2UI | Declarative UI protocol — no code execution in chat, safe by design |
| Lit | Lightweight web components, Shadow DOM for style isolation, signal-based reactivity |
| SQLite | Zero-config, single-file database — perfect for single-tenant self-hosting |
| uv | Fast Python package manager with lockfile support |