A production-shaped prototype for HCI research combining a multi-bot conversation engine built on LangChain primitives, React PWA frontend, passkey-first authentication (via h4ckath0n), SSE-based real-time chat, and vendor-neutral Web Push notifications.
| Pillar | Stack | Description |
|---|---|---|
| Backend | FastAPI + h4ckath0n + SQLAlchemy 2.x | Async API with passkey auth, multi-tenant project model, and SSE fan-out |
| Conversation Engine | LangChain agents + Router | Multi-bot architecture: Router (single writer) + Intake/Feedback/Coach specialists with proposal/commit flow |
| Frontend | React 19 + Vite + Tailwind CSS | PWA with service worker, SSE streaming chat, and Web Push notification support |
The engine uses a Router + specialist architecture:
- Router — Routes each turn to the correct specialist, validates all patch proposals, and owns the only commit path to UserProfile (Store A) and Memory (Store B).
- Intake Bot — Onboarding and profile setup. Can propose updates to onboarding fields.
- Feedback Bot — Habit tracking and barrier analysis. Can propose rolling coaching field updates and memory items.
- Coach Bot — Normal conversation and encouragement. Can propose candidates only (higher confidence threshold).
All bots use LangChain agents and tools. Proposals are validated against a permission matrix with confidence thresholds and evidence span requirements. See docs/current-architecture.md for full details.
The recommended way to run a production-like stack locally:
# Build the frontend artifact
bash scripts/ci/package_frontend.sh web
cp frontend.tar.xz web/frontend.tar.xz
# Build local images
docker build -t flow-web:local web/
docker build -t flow:local api/
# Start the stack
FLOW_WEB_IMAGE=flow-web:local FLOW_IMAGE=flow:local docker compose upThe stack runs at http://localhost:8080. Caddy serves the frontend and reverse proxies /api/* to the backend (stripping the /api prefix).
Cloudflare Tunnel (terminates TLS)
└─→ flow-web :8080 (Caddy, HTTP-only)
├─ / → static frontend files
└─ /api/* → flow :8000 (prefix stripped)
- flow-web is HTTP-only — TLS is terminated by Cloudflare Tunnel (or any upstream TLS terminator).
- The backend has no
/apiprefix on its routes. Caddy'shandle_pathstrips it before proxying.
FLOW_WEB_IMAGE=flow-web:local FLOW_IMAGE=flow:local \
H4CKATH0N_DATABASE_URL=postgresql+asyncpg://flow:flow@postgres:5432/flow \
docker compose --profile postgres upcd api
uv sync
# Copy .env.example to .env and configure
cp ../.env.example ../.env
uv run uvicorn app.main:app --reloadcd web
npm install
npm run devThe frontend dev server runs at http://localhost:5173 and proxies API requests to the backend.
To run real LLM responses instead of stub mode, set OPENAI_API_KEY (or H4CKATH0N_OPENAI_API_KEY) in your root .env before starting the backend. Keep this value secret and never commit it.
- Missing
OPENAI_API_KEY/H4CKATH0N_OPENAI_API_KEY→ chat runs in stub mode. - Missing
VAPID_PUBLIC_KEYorVAPID_PRIVATE_KEY→ push notifications are disabled.
Flow uses standard Web Push VAPID keys for push subscription and delivery.
Required environment variables:
VAPID_PUBLIC_KEYVAPID_PRIVATE_KEY
Generate keys (example using Python py_vapid):
python -m pip install py-vapid
python -m py_vapid --genCopy the generated public/private keys into .env.
Flow/
├── api/ # FastAPI backend
│ ├── app/
│ │ ├── main.py # App entry point (h4ckath0n create_app)
│ │ ├── routes.py # API route handlers
│ │ ├── models.py # SQLAlchemy 2.x models (incl. UserProfile, Memory, AuditLog)
│ │ ├── db.py # Database session management
│ │ ├── middleware.py # CSP and other middleware
│ │ ├── id_utils.py # Custom ID generation (p... / u...)
│ │ ├── agents/ # NEW: Multi-bot LangChain agents
│ │ │ ├── engine.py # Turn engine: Router + specialist pipeline
│ │ │ ├── router.py # Routing coordinator (structured output)
│ │ │ ├── intake.py # Intake specialist agent
│ │ │ ├── feedback.py # Feedback specialist agent
│ │ │ ├── coach.py # Coach specialist agent
│ │ │ └── orchestrator.py # Legacy orchestrator (backward compat)
│ │ ├── schemas/ # Pydantic models
│ │ │ ├── router.py # RouteDecision (INTAKE/FEEDBACK/COACH)
│ │ │ ├── patches.py # Proposals, evidence, permissions, profile/memory schemas
│ │ │ └── tool_schemas.py # Legacy tool argument schemas
│ │ ├── services/ # NEW: Business logic layer
│ │ │ └── profile_service.py # Profile/memory persistence, validation, audit
│ │ ├── tools/ # LangChain tools
│ │ │ ├── proposal_tools.py # NEW: propose_profile_patch, propose_memory_patch
│ │ │ └── langchain_tools.py # Legacy tool wrappers
│ │ └── engine/ # Legacy conversation engine (deprecated for new work)
│ │ ├── flow.py # Legacy orchestrator
│ │ ├── modules.py # IntakeModule, FeedbackModule, tool loop
│ │ ├── state.py # State enums, Pydantic models, DataKeys
│ │ ├── tools.py # Tool implementations
│ │ ├── scheduler.py # Daily prompts, reminders, auto-feedback
│ │ └── tone.py # Tone adaptation (EMA, hysteresis, whitelist)
│ ├── Dockerfile # Backend container image
│ ├── prompts/ # System prompt templates
│ ├── tests/ # Backend test suite
│ └── pyproject.toml
├── web/ # React PWA frontend
│ ├── Caddyfile # Caddy reverse proxy config (HTTP-only, :8080)
│ ├── Dockerfile # flow-web container image (Caddy + static assets)
│ ├── public/
│ │ ├── manifest.json # PWA web app manifest
│ │ └── sw.js # Service worker (push + notificationclick)
│ ├── src/
│ │ ├── App.tsx # Route definitions
│ │ ├── pages/ # Page components
│ │ │ ├── Dashboard.tsx # Project thread list
│ │ │ ├── Activation.tsx # Join project via invite link
│ │ │ ├── ChatThread.tsx # Real-time chat with SSE
│ │ │ ├── Notifications.tsx # Push notification management
│ │ │ ├── Landing.tsx # Public landing page
│ │ │ ├── Login.tsx # Passkey login
│ │ │ ├── Register.tsx # Passkey registration
│ │ │ └── Settings.tsx # User settings
│ │ ├── auth/ # Passkey auth (from h4ckath0n scaffold)
│ │ ├── api/ # API client and types
│ │ ├── components/ # Shared UI components
│ │ └── gen/ # Generated OpenAPI TypeScript client
│ └── package.json
├── docker-compose.yml # Production-like local stack (flow-web + flow + postgres)
├── docs/
│ ├── current-architecture.md # NEW: Current architecture (authoritative)
│ ├── legacy-conversation-flow-contract.md # DEPRECATED: Legacy behavior reference
│ └── parity-matrix.md # Legacy → new code mapping
├── .env.example
└── AGENTS.md # Agent behavior rules
All project-scoped endpoints require passkey authentication.
| Method | Path | Tag | Description |
|---|---|---|---|
GET |
/healthz |
infra | Readiness probe |
GET |
/me |
user | Current user profile (email, display_name, is_admin) |
PATCH |
/me |
user | Update email and/or display name |
GET |
/dashboard |
dashboard | List user's project memberships |
POST |
/p/{project_id}/activate/claim |
activation | Claim invite code, create membership + conversation |
GET |
/p/{project_id}/me |
activation | Get membership status, conversation ID |
POST |
/p/{project_id}/messages |
messaging | Send message, get assistant reply |
GET |
/p/{project_id}/events |
streaming | SSE event stream for real-time updates |
GET |
/p/{project_id}/push/vapid-public-key |
push | Get VAPID public key for push subscription |
POST |
/p/{project_id}/push/subscribe |
push | Store a push subscription |
POST |
/p/{project_id}/push/unsubscribe |
push | Revoke a push subscription |
PATCH |
/admin/projects/{project_id} |
admin | Update project name or status |
GET |
/admin/projects/{project_id}/push/channels |
admin | List push subscriptions for a project |
POST |
/admin/push/test |
admin | Send test push notification to selected subscriptions |
GET |
/demo/ping |
demo | Liveness ping |
POST |
/demo/echo |
demo | Echo with reverse |
GET |
/demo/sse |
demo | Authenticated SSE demo stream |
WS |
/demo/ws |
demo | Authenticated WebSocket demo |
| Table | ID Type | Description |
|---|---|---|
projects |
p... (custom, 32 chars) |
User-visible research projects |
project_invites |
auto-increment int | Hashed invite codes with expiry |
project_memberships |
auto-increment int | Links (project, user) with status; unique constraint |
participant_contacts |
auto-increment int | Legacy email contact metadata (per-membership) |
flow_user_profiles |
user_id (string PK) | User-level display_name (not per-project) |
conversations |
auto-increment int | 1:1 with membership |
messages |
auto-increment int | Chat history with server_msg_id (36-char string: m + 35 lowercase base32 chars; UUID-length for DB schema compatibility) |
conversation_runtime_state |
FK to conversation | JSON blob for engine state |
user_profiles |
auto-increment int | Store A — structured profile JSON (1:1 with membership) |
memory_items |
auto-increment int | Store B — semi-structured memory items per membership |
patch_audit_log |
auto-increment int | Audit trail: proposals, decisions, commits |
push_subscriptions |
auto-increment int | Web Push endpoints + crypto keys per device |
outbox_events |
auto-increment int | Durable scheduled events with dedupe keys |
The engine implements the architecture defined in docs/current-architecture.md.
- Persist user message
- Load UserProfile (Store A) + Memory (Store B) + recent chat history
- Router decides which specialist to run (INTAKE, FEEDBACK, or COACH)
- Invoke specialist agent (LangChain tool-calling agent)
- Collect patch proposals made during the agent run
- Router validates proposals (permissions, confidence, evidence) and commits approved ones
- Persist assistant message
- Emit SSE events for UI update
| Condition | Route |
|---|---|
| Required onboarding fields missing | INTAKE |
| Currently in feedback protocol | FEEDBACK |
| Profile complete, normal conversation | COACH |
Specialist bots propose changes via propose_profile_patch and propose_memory_patch tools. The Router validates each proposal against:
- Permission matrix: Intake → onboarding fields, Feedback → coaching fields, Coach → candidates only
- Confidence thresholds: INTAKE/FEEDBACK ≥ 0.5, COACH ≥ 0.8
- Evidence spans: Must reference recent message IDs
- Memory rules: Items ≤ 500 chars with source pointers
All proposals and decisions are logged in the patch_audit_log table.
The legacy conversation flow engine (api/app/engine/) is retained for backward compatibility. The legacy behavioral contract (docs/legacy-conversation-flow-contract.md) is deprecated.
| Route | Page | Description |
|---|---|---|
/ |
Landing | Public landing page |
/register |
Register | Passkey registration stepper: email → passkey → display name |
/login |
Login | Passkey login with return_to support |
/dashboard |
Dashboard | List project threads (active/ended) |
/p/:projectId/activate |
Activation | Join project via invite link; requests email only if missing |
/p/:projectId/chat |
ChatThread | Send messages via POST, receive via SSE |
/p/:projectId/notifications |
Notifications | PWA install guidance, enable push notifications |
/settings |
Settings | User profile (email, display name), theme, devices, passkeys |
/admin |
Admin | Project management, invite generation, push testing, debug tools |
- Web App Manifest —
web/public/manifest.jsonenables "Add to Home Screen" - Service Worker (
web/public/sw.js):- Handles
pushevents → shows system notifications - Handles
notificationclick→ deep-links to the relevant project chat (/p/{project_id}/chat)
- Handles
- Subscription flow: explicit user action → request permission → subscribe with VAPID public key from backend → store subscription server-side
- iOS: "Add to Home Screen" guidance is shown before enabling notifications
cd api && uv run python -m pytest tests/ -vTest modules:
test_api.py— API endpoint integration tests (messaging wired to new engine)test_new_architecture.py— NEW: Router permissions, confidence thresholds, evidence spans, profile/memory validation, proposal tools, deterministic routingtest_engine_integration.py— NEW: Full turn pipeline with async DB, profile persistence, memory persistence, audit logtest_langchain_router.py— Router structured output and deterministic routingtest_langchain_agents.py— Agent tool permissions and orchestrator pipelinetest_langchain_tools.py— LangChain tool schemas and invocationtest_flow.py— Legacy conversation engine routing and historytest_scheduler.py— Daily prompts, reminders, auto-feedback, intensitytest_tone.py— Tone adaptation, EMA, whitelist validationtest_tools.py— Tool execution and state management
The audit trail can be inspected by querying the patch_audit_log table after processing messages. The test_engine_integration.py::TestAuditLog tests verify that proposals and commit decisions are properly recorded.
cd web && npm test # Unit tests (Vitest)
cd web && npm run test:e2e # E2E tests (Playwright)This is a prototype. The following are mocked or incomplete:
- LLM calls — In stub mode, specialist agents return fixed responses. Connect a real LLM (OpenAI, Anthropic, etc.) by passing
llmandrouter_llmparameters to the engine. - Web Push delivery — Push subscriptions are stored in the database but no actual push messages are sent. Wire up
pywebpushwith VAPID keys to enable delivery. - Outbox event processing — Outbox events (reminders, auto-feedback timers) are created with dedupe keys but no background worker processes them. Add a polling worker or task queue to fire events at
available_at. - Message endpoint —
POST /p/{project_id}/messagesruns the Router + specialist pipeline in stub mode (deterministic routing, fixed responses). With a real LLM, it produces contextual responses.
Configure in .env at the repository root (see .env.example):
| Variable | Description |
|---|---|
H4CKATH0N_ENV |
Environment mode (development / production) |
H4CKATH0N_DATABASE_URL |
SQLAlchemy async database URL |
H4CKATH0N_RP_ID |
WebAuthn relying party ID (e.g., localhost) |
H4CKATH0N_ORIGIN |
Allowed origin for CORS and WebAuthn |
VITE_API_BASE_URL |
API base URL for the frontend (e.g., /api) |
OPENAI_API_KEY |
OpenAI API key for live LLM responses (backend only) |
H4CKATH0N_OPENAI_API_KEY |
Optional alternate env name for the OpenAI key |
VAPID_PUBLIC_KEY |
VAPID public key for Web Push |
VAPID_PRIVATE_KEY |
VAPID private key for Web Push (never log this) |
See LICENSE.