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:

  1. Traditional REST API + PostgreSQL — full control, but requires managing servers, writing real-time sync (WebSockets), building an ORM layer, and handling migrations.
  2. 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.
  3. 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)