Elements vs Prisma
Prisma is a venture-backed company that makes Prisma ORM, an open-source ORM (object-relational mapper) for TypeScript and Node.js, and sells hosted database and app services. In Prisma ORM 7 you describe your tables in Prisma's own schema language, and Prisma generates a typed client from that schema. Your code reads and writes data through the client, with calls such as prisma.user.findMany() in place of SQL, and Prisma Migrate turns schema changes into SQL migration files. The ORM is free. The company sells Prisma Postgres, a hosted Postgres billed by query operations and storage, from a free tier to several monthly plans, and Prisma Compute, app hosting billed by requests, memory, CPU and bandwidth (pricing).
Elements is not an ORM, and it is not a database company. It is an integrated app environment for building web apps, built for people and their agents. At its center is a project server that runs while you work, and around it the same system includes a package installer, a test runner, a bundled Postgres with a sql() function, transactions and SQL migrations, and a deploy command for any Ubuntu server you reach over SSH. It ships with a default framework that just works. Elements charges for that tooling, per machine, and meters nothing (pricing).
Prisma ORM stands between you and Postgres, and every layer of it is one more thing to learn. Your tables are described twice, once in Prisma's schema language and once in the database, with a generate step to run between them after every change. Your queries are written a third way, as nested objects passed to a generated client, and any Postgres feature the schema language has not wrapped, from PostGIS to check constraints to row-level security, sends you to raw SQL anyway. Elements is ready out of the box and stays close to the metal. Your schema is the SQL migration that creates it, a query is SQL in a sql() function, and a transaction is a tx() call. SQL is the language every agent already knows, and the one Postgres itself speaks. Every interpolated value becomes a query parameter, migrations apply the moment you save them, and your tests run your real queries against Postgres inside the build.
At a Glance
| Elements | Prisma | |
|---|---|---|
| What it is | A project server, a default framework, Postgres, tests and deploy, shipped as one coherent system | An ORM, plus hosted Postgres and app hosting |
| Business model | Sells the tooling you build with, per machine, with nothing metered (pricing) | A venture-backed company gives the ORM away and sells hosted Postgres billed per query, and app hosting (pricing) |
| Schema is written in | SQL migrations | Prisma's own schema language, then generated into a client |
| Queries are written in | SQL, in sql() |
Methods on a generated client, with raw SQL for what it cannot express |
| Generated code | None | A client, regenerated after every schema change |
| Postgres features | Any of them, because the schema is SQL | PostGIS, check constraints and row-level security are open requests, the oldest since 2019 |
| Column names | camelCase in code and SQL, snake_case in the database, converted for you | The field name, unless each field is renamed with @map |
| Migrations in development | Applied when you save the file; an edit replays it | prisma migrate dev, with a temporary shadow database |
| Migrations on deploy | Run once, in one transaction, and a failure stops the release | prisma migrate deploy, a step you add to your pipeline |
| Transactions | tx(): every query inside joins it |
$transaction(), with every query called on the transaction client |
| Tests | Part of the build, each in a rolled-back transaction | Not included; the docs cover mocking the client, or Docker and Jest |
| Realtime | LiveTable and channels, over Postgres | Not included |
| The database | Bundled Postgres, or any Postgres host | Bring your own, or Prisma Postgres, billed per query |
Agents Need Correction, Not an ORM
Coding agents such as Claude Code and Codex now write a growing share of app code, and for an agent the value is not in layer after layer of ORM abstraction. It is in correction: finding out what it got wrong the moment it gets it wrong. Prisma puts its engineering into the abstractions, and pitches its client as more productive than SQL, which its docs call "cumbersome" to send as plain strings (why Prisma). For an agent, each of those layers is one more thing to learn and one more place to be wrong, on top of SQL it already writes fluently:
- The schema language. A second description of tables Postgres already describes, which exists only inside Prisma (Prisma ORM).
- Generators.
prisma generateproduces whatever assets the schema's generator blocks name, and the docs list dozens of community generators for validation schemas, GraphQL types and diagrams (generators). Each is another package, and another output to keep in agreement with the schema. - Client extensions. Custom model methods, result fields and query hooks go through
$extends, a Prisma-only API with four component types (client extensions).
On a line from an agent writing machine code, which nobody does, to a package that does subtraction, the right amount of abstraction sits at the plumbing every app needs, and for a database that set is small: a safe way to pass values into a query, transactions, and migrations. The Elements app framework ships that plumbing, with sql() and the database driver beside routing, typed @rpc calls and sessions, and stops there. Past that plumbing comes a point of diminishing returns, where each further abstraction costs an agent more to learn and get right than it saves. A computed field is a column in a select, and a helper is a few lines of TypeScript around sql(), with no package to install and no Prisma API to look up.
Elements puts its engineering into correction instead. The framework and its build were made together, so one coherent system checks every page, function, sql() call, test and migration an agent writes, and elements build -json reports each mistake in microseconds from the build state the project server already holds. The terminal, the editor and every agent read that same build, and server code that calls sql() needs no await, so there is none for an agent to forget.
What the Schema Language Cannot Say
Postgres has decades of features. Prisma's schema language has the ones Prisma has written support for, and the rest wait in its issue tracker:
- "PostGIS/GIS support", open since June 2020, with 1,097 reactions.
- "Support SQL Check constraints", open since December 2019, with 361 reactions.
- "Support for row-level security (RLS)", open since April 2022, with 401 reactions.
- "Support for
pg_vector", open since March 2023, with 253 reactions. - "Define type of content of
Jsonfield", open since August 2020, with 1,094 reactions.
In Elements, the schema is the SQL that creates it, so a check constraint, a PostGIS column, a vector index or a row-level security policy is a line in a migration, and a query that uses it is a line in sql(). There is no schema language to wait on.
Queries Are SQL, Safe by Construction
In Prisma, you read and write rows through the generated client, and when you need SQL you drop down to $queryRaw. Prisma's docs warn that its sibling, $queryRawUnsafe, opens "the possibility for SQL injection attacks" when given user input (raw queries). TypedSQL, which types a raw query's result, needs prisma generate --sql and a live database connection to generate from (TypedSQL).
In Elements, SQL is the first way to query, not the escape hatch. You write it in the sql() function, and the build extracts every ${value} into a query parameter at compile time, so an interpolated value can never become query text. A dynamic order by or an optional where clause uses sql.raw, and only text written in that literal is ever spliced in. There is no unsafe variant to reach for by mistake. Calling sql() from code that runs in the browser, without going through a typed server function (an @rpc the page calls directly), is a compile error at the call site.
You write camelCase in your SQL, the same names your TypeScript uses. Elements converts them to snake_case at the database boundary and back on every result, in queries and in migrations. In Prisma, a camelCase field becomes a column with that exact name, unless you rename each field and model with @map and @@map (database mapping).
Queries Are Checked Against the Real Database
Prisma's generated types check that your code matches schema.prisma. They do not check that the query works against the database: change the schema, and until you run prisma generate the types describe the old one.
In Elements, a query is checked by running it: the tests that call it run against a real Postgres as part of the build. Misspell a column and the build fails with Postgres's own words: column users.nam does not exist. A query's row type is a TypeScript interface you write beside it, and it carries through to the browser, because a typed server function returns its rows to the page with the types intact (rpc).
Migrations Apply the Moment You Save
A migration changes the database's tables, and it has to land in development, in tests and in production. In Prisma, you edit the schema, then prisma migrate dev diffs it against the database and writes the SQL. To do that it creates and deletes a temporary shadow database, which needs a database user with permission to create databases (shadow database). In production, prisma migrate deploy is a step you add to your own pipeline.
In Elements, a migration is a SQL file in app/migrations/, and applying it is part of the build. Save the file and the project server applies it to the test database first and then to the app database, each in a single transaction that succeeds in full or rolls back in full. Edit a migration in development and Elements synchronizes the change to the database.
On deploy, migrations run once, on the first machine, after your tests pass on every machine. A failed migration stops the release, and the running app keeps serving the previous one (what deploy does).
Transactions Without a Client to Pass Around
A transaction makes several writes succeed or fail together. Prisma's interactive transactions give your callback a transaction client, and every query inside has to be called on it, so a helper function that writes to the database needs that client passed in (transactions).
In Elements, tx() holds one connection for the length of the callback, and every sql() call inside it, in any function it calls, runs on that connection with nothing passed (transactions). A tx() inside another joins the outer one, so helpers compose without change. A background job enqueued inside tx() commits with the rest of the writes, because the job queue is a table in the same Postgres (jobs).
app/shared/services/posts.tsElementstx(() => { sql(`update posts set published = true where authorId = ${authorId}`); logPublish(authorId); // its sql() calls join the same transaction });
Tests Run Your Real Queries
A test of database code is worth what it proves about the real database. Prisma ships no test runner. Its unit testing guide mocks Prisma Client with Jest, so the database is not touched, and its integration testing guide starts Postgres in Docker for Jest to run against.
In Elements, tests are part of the build and run against a test database Elements keeps next to your app database. Each test runs inside a Postgres transaction that rolls back when it ends, so there is no cleanup to write and no mock to maintain. A failing test is a failing build, reported by elements build -json alongside every other build error.
The Database Runs the Rest of the App
Prisma ORM covers the schema, queries and migrations. Everything else an app needs from its data, such as pushing changes to open browsers or queuing background work, is another library or service.
In Elements, Postgres runs those too. A LiveTable is a set of database rows that stays in sync in every browser showing it: the page arrives from the server with the rows in it, and inserts, updates and deletes stream to every watching browser. Channels carry any other message from the server to browsers over Postgres LISTEN and NOTIFY. Background jobs and cron run from a queue in the same database. None of it is a separate service to deploy.
Who Pays for Prisma
Prisma ORM is the on-ramp to Prisma Postgres, a database billed per query. Prisma is a venture-backed company, funded to build what it called an "Application Data Platform" (announcement). Its paid products are hosting: Prisma Postgres and Prisma Compute. Prisma Postgres "counts every query as one operation, and operations are what you pay for," with operations past each plan's allowance billed per million, and Prisma Compute bills the requests your app serves, its memory and CPU, and its outbound data (pricing).
That puts the ORM's direction in the hands of the hosting business, not yours. Accelerate, the connection pool and query cache Prisma built for the ORM, is being retired on December 1, 2026, and the first migration path its docs offer is moving your database to Prisma Postgres (Accelerate).
Elements bundles Postgres and sets it up for you, on your machine and on each server you deploy to (database). The bundled server suits a prototype or a single-server app. When several machines share one database, or you want managed backups and scaling, Elements recommends a hosted Postgres such as DigitalOcean Managed PostgreSQL or Amazon RDS, connected by setting DB_HOST (hosted Postgres).
Elements sells its tooling per machine, and nothing is metered: queries, requests and bandwidth never appear on an Elements bill (pricing). Elements charges for its tooling, not for the framework or the database layer: the app framework and runtime packages such as @elements/app, sql() and tx() included, are MIT licensed, and everything you build is yours (licenses).
One Feature, Both Ways
The same two tables, and a query for published posts with each author's name. In Prisma ORM 7, you write a User model and a Post model in schema.prisma, with attributes such as @id, @default and @relation marking keys, defaults and the link between them, then run prisma migrate dev and prisma generate; Prisma's quickstart walks through those steps with the same two models. The query is a prisma.post.findMany() call that describes the filter, the fields and the order as nested objects (select fields).
In Elements, the tables are a migration, applied when you save it:
app/migrations/20260930120000-create-posts.migration.sqlElementscreate table users ( id uuid primary key default uuidGenerateV7(), email text not null unique, name text, createdAt timestamptz not null default now() ); create table posts ( id uuid primary key default uuidGenerateV7(), authorId uuid not null references users (id), title text not null, published boolean not null default false, createdAt timestamptz not null default now() );
The query is SQL, and the test runs it against Postgres in the build:
app/shared/services/posts.tsElementsimport { sql } from "@elements/app"; interface PublishedPost { id: string; title: string; authorName: string; } export function publishedPosts(): PublishedPost[] { return sql<PublishedPost>(` select posts.id, posts.title, users.name as authorName from posts join users on users.id = posts.authorId where posts.published order by posts.createdAt desc `).all(); }
app/shared/services/posts.test.tsElementsimport { test, equal, sql } from "@elements/app"; import { publishedPosts } from "./posts"; test("publishedPosts", () => { let ada = sql<{ id: string }>(` insert into users (email, name) values ('ada@example.com', 'Ada') returning id `).firstOrThrow(); sql(` insert into posts (authorId, title, published) values (${ada.id}, 'Hello', true), (${ada.id}, 'Draft', false) `); equal(publishedPosts().map(p => p.authorName), ["Ada"]); });
There is no await: the build makes every function that calls sql() async for you (async). The Elements test runs in the build, and the rows it inserts are rolled back when it ends. The Prisma query has no test until you add Jest and a Postgres in Docker, as Prisma's integration testing guide describes.
Moving From Prisma
An agent can port a Prisma app easily. The models in your Prisma schema already exist as tables in Postgres, so they become SQL migrations, and the client calls become queries in sql(). You keep your data, and you gain one language for queries and schema: the SQL Postgres runs.
Questions
What is the difference between Elements and Prisma?
Prisma is a TypeScript ORM: you describe your tables in its own schema language and query them through a client it generates. Elements is an integrated app environment for building web apps, and it bundles Postgres. You query it in SQL, change its tables with SQL migrations that apply the moment you save them, and test your queries with a built-in test runner that rolls back each test's writes.
Is Elements a Prisma alternative?
Yes. Elements replaces the ORM, the migration tool and the database setup with SQL queries, SQL migrations and a bundled Postgres. It also includes what Prisma ORM leaves out: a test runner, realtime data in the browser, background jobs and a deploy command.
Who makes Prisma?
Prisma, a venture-backed company (announcement). Prisma ORM is free, and the company earns its revenue from Prisma Postgres, billed per query operation, and Prisma Compute app hosting (pricing). Elements is sold directly, per machine, so its revenue comes from the tooling itself (pricing).
Does Elements have an ORM?
No. Elements puts SQL first: you write queries in the sql() function, and the build turns every interpolated value into a query parameter at compile time. Results come back as plain objects with camelCase keys, typed by the interface you give the query.
How does Elements catch a wrong column without generated types?
By running the query. Your tests run your real queries against Postgres inside the build, so a misspelled column in a tested query fails the build with Postgres's own error. Each query's row type is a TypeScript interface you write, and the build checks your code against it.
How do Elements migrations compare to Prisma Migrate?
An Elements migration is a SQL file you write, applied to the test database and then the app database the moment you save it, with no schema diff and no shadow database. On deploy, pending migrations run once, in a single transaction, after your tests pass, and a failure stops the release.
Can Elements use a managed Postgres?
Elements works with any provider that exposes a standard Postgres connection, version 16 or newer, by setting DB_HOST. It also bundles Postgres on your machine and on each server you deploy to, so a single-server app needs no database host at all.