# Elements vs Convex Convex is a backend-as-a-service: a hosted backend built around a reactive database. You write your backend as TypeScript functions that run in Convex's cloud, and you query its document database with TypeScript code rather than SQL ([Convex overview](https://docs.convex.dev/understanding/)). Client libraries such as React's `useQuery` subscribe to a query, and Convex pushes the new result to the browser when the data it read changes. The web frontend is hosted separately, on a host such as Vercel or Netlify. Convex bills for the backend: the Professional plan charges per developer, and paid plans meter function calls, compute, storage and bandwidth past their allowances ([pricing](https://www.convex.dev/pricing)). Elements is not a backend-as-a-service, but it includes the backend Convex sells as a service, inside your own app. It is a coherent system for building and shipping a web app, centered on a project server that runs while you work, and the parts that answer Convex are its default framework and its database. Postgres comes bundled and is queried with SQL. The framework has live data built in: LiveTable for database rows that stay in sync in every open browser, and channels for any other realtime message. `@rpc` marks typed server functions the browser calls directly. Elements deploys the backend and the pages together to any Ubuntu server you reach over SSH, and charges for the tooling, per machine, metering nothing ([pricing](/pricing)). The difference is where the backend lives and what it is built on. On Convex, the backend is a service you rent: a database with its own query API, functions that run in Convex's cloud, a separate host for the pages, and a bill that counts function calls, compute, storage and bandwidth. In Elements, live data runs inside your own app, on standard Postgres you query with SQL, and the pages and the backend ship together to servers you choose. The same build checks the pages and the backend, so a broken query or a failing test shows up for you and your agent from one local command, with nothing pushed anywhere. ## At a Glance | | Elements | Convex | |---|---|---| | What it is | An integrated app environment: a project server, a default app framework, a bundled Postgres database and a deploy command | A hosted backend platform with a reactive database | | Database | [Postgres](/learn/man/database), bundled on your server or on any Postgres host | Convex's own document database | | Queries | [SQL](/learn/man/database/sql), with joins and aggregates run by Postgres | TypeScript code; joins and aggregates written by hand | | Live data | [LiveTable](/learn/man/livetable) and [channels](/learn/man/channel) | Reactive queries | | Server functions | [`@rpc`](/learn/man/rpc) functions in your app | Queries, mutations and actions in Convex's cloud | | Sign-in and sessions | [Built in](/learn/man/session) | Clerk, Auth0, WorkOS or Convex Auth | | Pages and frontend | [Part of the app](/learn/man/html), deployed with it | Hosted separately, on Vercel, Netlify or another host | | Tests | [Part of the build](/learn/man/tests), against real Postgres | A mock backend, or a local backend you run | | What you pay for | Tooling, per machine ([pricing](/pricing)) | Seats, plus metered calls, compute, storage and bandwidth | | Where it runs | Any Ubuntu server you reach over SSH | Convex's cloud, or its backend self-hosted | | Leaving | Standard Postgres tables that `pg_dump` and any host read | Rewrite every query, or self-host Convex's backend | ## Live Data Is a Feature of Your App, Not a Service You Rent Convex's case is reactivity: queries that stay live without subscription code, for an app that "keeps up with you and your agents" ([Convex](https://www.convex.dev/)). Live data does not require moving your data into a hosted backend. Elements builds it into its default framework, on the Postgres your app already uses. Think of a LiveTable as a query result that never goes stale: database rows that every browser displaying them keeps current. Unlike `useQuery`, it has no empty first state, because the server puts the rows into the HTML it sends. When a user adds a row, it appears on their screen at once while the server saves it, and every other watching browser updates the affected row in place ([LiveTable](/learn/man/livetable)). An `@rpc` can return a live view too, so a page can open live data after it loads ([rpc](/learn/man/rpc)). Channels send any other message from the server to the browsers listening, such as presence or a typing indicator ([channel](/learn/man/channel)). LiveTable and channels both run on Postgres `LISTEN`/`NOTIFY`, so several app servers stay in sync through the database with no extra service to deploy. A trigger in a migration makes writes from a job, a webhook or psql live too ([live from SQL](/learn/man/recipes/live-from-sql)). Here is a chat room. In Convex, you write a query that reads the messages and a mutation that checks the signed-in user and inserts one, both as functions that run in Convex's cloud, then a React component that subscribes with `useQuery`, sends with `useMutation`, and renders a loading state until the first result arrives. Convex's [tutorial](https://docs.convex.dev/tutorial/) builds this chat app step by step. In Elements, a LiveTable declares the messages and checks the session before each write: ```ts // app/shared/services/messages.ts (Elements) import { LiveTable, session } from "@elements/app"; export interface Message { id: string; body: string; author: string; createdAt: Date; } export let messages: LiveTable = new LiveTable({ insert: (item) => { session.isLoggedInOrThrow(); return messages.insert({ ...item, author: session.getOrThrow("userName") }); }, }); ``` The route passes a view of the table to the page, `new html({ messages: messages.view() })`, and the page is Elements HTML: standard HTML with reactive expressions. ```ehtml // app/pages/chat/template.ehtml (Elements) import { session, type LiveView } from "@elements/app"; import type { Message } from "#app/shared/services/messages"; function send(messages: LiveView, form: { body: string }) { messages.insert( { body: form.body, author: session.get("userName")!, createdAt: new Date() }, () => form.body = "", ); } , private form: { body: string } = { body: "" })>
  • +x.createdAt - +y.createdAt)}> {msg.author}: {msg.body}
send(messages, form)}>
``` Both versions update every open browser. The differences are in the first load and in the send. `useQuery` "returns `undefined` while the data is first loading" ([Convex React](https://docs.convex.dev/client/react)), so the component renders a loading state first; the Elements page renders on the server with the messages already in it. And the Elements message appears on the sender's screen the moment they press Send, before the server confirms it, with no extra code. In Convex, that takes an optimistic update function, registered on each mutation with `.withOptimisticUpdate` ([optimistic updates](https://docs.convex.dev/client/react/optimistic-updates)). ## SQL Is the Standard Your Agent Already Knows Convex's database has no SQL: "In Convex, your database queries are just TypeScript code written in your server functions. There is no SQL to write" ([Convex overview](https://docs.convex.dev/understanding/)). Joins, aggregations and grouping are loops you write yourself: "there is no specific query language for complex logic like a join, an aggregation, or a group by. Instead, you can write the complex logic in JavaScript" ([reading data](https://docs.convex.dev/database/reading-data/)). A query uses an index only through an explicit `withIndex()` call, and `.filter()` does not change which documents are scanned ([indexes](https://docs.convex.dev/database/reading-data/indexes/)). Those loops run inside limits. In a Convex query or mutation, your code may run for one second, and the function may scan up to 32,000 documents and read up to 16 MiB ([limits](https://docs.convex.dev/production/state/limits)). An aggregate over more documents than that has to be computed some other way, such as by keeping running totals as the data is written. SQL is the standard language for this work, and coding agents already write it. Elements puts it inside TypeScript with the [`sql()` function](/learn/man/database/sql): Postgres plans the joins, picks the indexes and runs the aggregates, and the build turns every `${value}` into a query parameter at compile time, so an interpolated value cannot inject SQL. In Elements, a report over orders and customers is one query: ```ts // app/pages/reports/services.ts (Elements) import { session, sql } from "@elements/app"; /** @rpc */ export function topCustomers(): { name: string; total: number }[] { session.isLoggedInOrThrow(); return sql<{ name: string; total: number }>(` select c.name, sum(o.amount) as total from orders o join customers c on c.id = o.customerId where o.createdAt > now() - interval '1 year' group by c.name order by total desc limit 10 `).all(); } ``` ## Your Backend Is Checked in Milliseconds, on Your Machine Convex pitches itself to agents, but what an agent needs most is correction: knowing what it got wrong as soon as it gets it wrong, not a new API to learn. On Convex, the agent writes against Convex's query API on the server and a client library such as `useQuery` in the browser, two interfaces it looks up instead of knowing, joined across a network boundary. A client library is a shortcut only past what an agent writes quickly from standards, and a query over an app's own data is SQL it already writes. Developing on Convex means pushing code to a deployment. `npx convex dev` "watches the local filesystem. When you change a function or the schema, the new versions are pushed to your dev deployment," a Convex cloud deployment unless you set up a local one ([CLI](https://docs.convex.dev/cli)). For tests, Convex's `convex-test` library is "a mock implementation of the Convex backend," and Convex notes that "it doesn't have many of the behaviors of the real Convex backend": it does not enforce limits, and it does not run cron jobs ([convex-test](https://docs.convex.dev/testing/convex-test)). Elements checks your backend where you write it, with no dev deployment in the loop. It ships the plumbing, `sql()` for queries and `@rpc` for the functions a page calls, with no layers past it, and because Elements is one coherent system, every page, function, query, test and migration goes through the same build. While you work, the project server holds the build graph and build state in memory; on every save the build state is updated almost instantly, and it builds in milliseconds and answers agents and humans in microseconds ([build](/learn/man/build)). `elements build -json` returns every build error in one answer: failing tests, migration errors, and what type checking and program analysis find in code and templates. Your terminal, your editor and every agent share that build. The build also guards the boundary between browser and server: calling `sql()`, `tx()` or `session.login()` from code that runs in the browser is a compile error at the call site ([rpc](/learn/man/rpc)). Tests run against the real Postgres, not a mock. Each test runs inside a transaction that rolls back when it ends, so there is no cleanup to write, and only the tests a change affects run ([tests](/learn/man/tests)). A LiveTable write in a test runs the same handler and the same authorization check as a write from the browser ([mutations](/learn/man/livetable/mutations)). Schema changes follow the same loop. Convex validates a schema file against your existing documents when you push it ([schemas](https://docs.convex.dev/database/schemas)). Elements has no schema file to validate after the fact. A schema change is a SQL [migration](/learn/man/migrations), applied to your development database the moment you save it, and at deploy time every pending migration runs once inside one transaction, where any failure halts the release. ## One App, One Deploy A Convex app is several services. The backend deploys to Convex. The frontend deploys to another host: Convex's docs say "the easiest way to publish your full-stack web app is to use a hosting provider like Vercel or Netlify" ([hosting](https://docs.convex.dev/production/hosting/)). Sign-in comes from Clerk, Auth0 or WorkOS, or from Convex Auth ([auth](https://docs.convex.dev/auth)). An Elements app is one project. Pages, `@rpc` functions, [sessions](/learn/man/session), [background jobs and cron](/learn/man/jobs), and the code that sends [email](/learn/man/email) through any SMTP provider all live in the same project and ship with `elements deploy` to [any Ubuntu server you reach over SSH](/learn/man/deploy). Sessions and sign-in are part of the framework: `session.login()` signs a user in, the session is checked the same way in routes and `@rpc` functions, and a template that reads it updates the moment a user signs in ([session](/learn/man/session)). Where Convex needs a backend push and a frontend deploy, Elements sends pages and backend out as one release. Your local project server hands the server's project server just the changed files, so after a first deploy of a few seconds on a new machine, deploys often take under a second. Each machine runs the tests and the first one runs the migrations, and a failure at any step leaves the current release serving ([what a deploy does](/learn/man/deploy/what-happens)). One server runs the app, the load balancer, which issues and renews TLS certificates, and Postgres, which suits a prototype or a single-server app. To grow, or when you want backups and scaling run for you, add machines and point them at a hosted Postgres such as DigitalOcean Managed PostgreSQL or Amazon RDS by setting `DB_HOST`; the built-in load balancer spreads traffic across them ([load balancer](/learn/man/deploy/load-balancer)). ## Nothing in Elements Is Metered Convex's Professional plan charges per developer, and past each plan's allowance Convex meters function calls, action compute, database storage and I/O, file storage and data egress ([Convex pricing](https://www.convex.dev/pricing)). Elements is licensed per machine, on a subscription ([licenses](/learn/man/licenses)). Nothing is metered: function calls, live updates, requests, bandwidth and storage never appear on an Elements bill. Your server and your database cost what your provider publishes. ## Your Data Stays in Standard Postgres Where your data lives decides what leaving costs. Convex Cloud stores your data "on top of PlanetScale using MySQL as its persistence layer," and your code reaches it only through Convex's API ([Convex overview](https://docs.convex.dev/understanding/)). Moving off Convex Cloud means rewriting every query, or running Convex's backend yourself, which Convex's docs say "is not for everyone" ([self-hosting](https://docs.convex.dev/self-hosting)). Elements keeps your data where any tool can reach it: ordinary Postgres tables that `pg_dump`, psql and every Postgres host understand, written to by TypeScript and SQL that run on Node.js. Leaving a host is a `DB_HOST` change, or a new server and another `elements deploy` ([deploy setup](/learn/man/deploy/setup)). There is no query layer to rewrite, and `@elements/app` and the other runtime packages are MIT licensed ([licenses](/learn/man/licenses)). ## Questions ### What is Convex? Convex is a backend-as-a-service built around a reactive database. You write queries, mutations and actions as TypeScript functions that run in Convex's cloud, and client libraries subscribe to queries so the page updates when the data changes. The frontend is hosted separately, and the Professional plan charges per developer, plus metered usage. ### Is Elements a Convex alternative? Yes. Elements delivers live data with LiveTable, typed server functions with @rpc, and realtime messages with channels, all inside your own app on standard Postgres. It also ships sessions, background jobs, email, a test runner and a deploy command, and the backend and the pages deploy together to your own server. ### Does Elements have reactive queries like Convex? Elements has LiveTable: rows from your database, kept current in each open browser, and already present in the server-rendered HTML on first load. A user's change shows at once, with no optimistic update code, and every open browser updates when a row is inserted, updated or deleted. Channels push any other message, and a Postgres trigger makes writes from jobs, webhooks or psql live too. It all runs on Postgres LISTEN/NOTIFY, with no separate service. ### Can I use SQL with Convex? No. Convex's docs say "there is no SQL to write"; queries are TypeScript code, and joins, aggregations and grouping are written in JavaScript. Elements queries standard Postgres with SQL through its sql() function, and the build turns every interpolated value into a query parameter. ### How much does Elements cost compared to Convex? Convex's Professional plan charges per developer, plus metered function calls, compute, database storage and I/O, file storage and data egress past each allowance ([Convex pricing](https://www.convex.dev/pricing)). Elements is licensed per machine, with nothing metered ([Elements pricing](/pricing)). ### Where does an Elements app run? On any Ubuntu server you reach over SSH, from any provider, with the pages and the backend deployed together. One server runs the app, the load balancer, which issues and renews TLS certificates, and Postgres. To grow, add machines: the load balancer spreads traffic across them, and they share one hosted Postgres.