ADR-002: Convex as Backend
Why Owlat chose Convex over PostgreSQL and Firebase for real-time reactivity, co-located TypeScript logic, and zero-config scaling.
- Status: Accepted
- Date: 2024-06-15
Context
Owlat needs a backend that supports real-time UI updates (email editor collaboration, live campaign stats), handles complex queries (segment evaluation, contact filtering), and scales without infrastructure management.
The main options considered:
- Traditional REST API + PostgreSQL — full control, but requires managing servers, writing real-time sync (WebSockets), building an ORM layer, and handling migrations.
- Firebase/Firestore — real-time out of the box, but limited query capabilities, no server-side logic co-location, and vendor lock-in with weaker TypeScript support.
- Convex — real-time serverless database with TypeScript functions co-located with the schema, automatic reactivity, and ACID transactions.
Decision
Use Convex as the backend platform. All server-side logic lives in apps/api/convex/ as TypeScript functions (queries, mutations, actions, and crons).
Consequences
Enables:
- Real-time reactivity with zero WebSocket boilerplate — UI components subscribe to queries and automatically update
- Full TypeScript from schema to API — the schema definition generates typed document interfaces
- Co-located server logic — queries, mutations, and crons live next to the schema they operate on
- Built-in scheduling (crons) for recurring tasks like campaign sending and cleanup
- Automatic scaling — no server provisioning or connection pool management
- ACID transactions across multiple tables without manual transaction management
Trade-offs:
- Vendor dependency on Convex — harder to migrate than a standard PostgreSQL setup
- Query language is Convex-specific (not SQL) — new contributors need to learn the Convex API
- No raw SQL access for complex analytical queries — segment evaluation requires custom logic in
segmentEvaluation.ts - Function execution limits (time, memory) constrain batch operations — large imports need chunking via
integrationImports.ts - Local development requires a Convex account and active deployment (no fully offline mode)