The "why" behind every architectural choice. These aren't arbitrary – each one solved a real problem.
Design Decisions
The "why" behind every architectural choice. These aren't arbitrary – each one solved a real problem.
TypeScript for the Web App, Python for the Brain
The brain service orchestrates 7 agents with complex state management, async execution, and structured JSON output. Python's Anthropic SDK, asyncio, and the ML ecosystem (embeddings, NLP) are significantly more mature for this use case. The web app, API routes, and worker stay TypeScript for type-safe React and shared types with the database. They communicate via Supabase as shared state – no direct HTTP calls between services.
Supabase over Firebase / Prisma / Raw PostgreSQL
Supabase gives us PostgreSQL with RLS for multi-tenancy, pgvector for embeddings, Realtime for streaming agent progress, Auth for user management, and Storage for file uploads – all in one platform. Firebase lacks RLS and pgvector. Prisma adds an ORM layer we don't need when Supabase's client SDK is type-safe from generated types.
pgvector in Supabase over Pinecone / Weaviate
Our vector data (client intelligence embeddings) is tightly coupled to relational data (org_id, client_name, deal_history). A separate vector DB means syncing two databases. pgvector keeps vectors in the same PostgreSQL instance, queried with the same RLS policies, in the same transaction. At our scale (<100K vectors per org), HNSW index performance is more than sufficient.
Separate Brain Service over In-Worker Agents
The 7-agent pipeline needs 60-300 seconds and complex state management (resume-from-step, cached outputs, feedback loops). Running this in the TypeScript worker would require reimplementing Python's asyncio patterns and the Anthropic Python SDK's superior structured output support. The brain polls Supabase independently – if it goes down, the worker still processes simple jobs.
Poll-Based Job Queue over Message Broker
Both the worker and brain poll Supabase using FOR UPDATE SKIP LOCKED – an atomic claim pattern that prevents duplicate processing and supports horizontal scaling. No message broker to manage, no dead letter queues to monitor, no additional infrastructure. The jobs table IS the queue. Debuggable with a SQL query.
Return-Based Auth Errors over Throw-Based
requireSession() returns SessionContext | Response. API routes check if (ctx instanceof Response) return ctx. This eliminates try/catch boilerplate, makes the error path explicit in the type system, and prevents unhandled auth errors from leaking stack traces.
Language Boundary Map
SHARED STATE
Ready to create AI-powered proposals?
Start Free