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

Options: All TypeScript vs Hybrid TS + Python

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

Options: Firebase vs Prisma + PG vs Supabase

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

Options: Separate vector DB vs pgvector extension

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

Options: Agents in TS worker vs Dedicated Python brain

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

Options: RabbitMQ / SQS vs PostgreSQL polling

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

Options: throw new AuthError() vs return Response(401)

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

Language Boundary Map
TypeScript
Next.js App (Vercel)
50+ API Routes
Edge Middleware
Worker (Fly.io)
React Components
SUPABASE
SHARED STATE
Python
FastAPI Brain (Fly.io)
7-Agent Orchestrator
Memory Manager
Embedding Generator
Prospect Scraper

Ready to create AI-powered proposals?

Start Free