Elements vs Drizzle
Drizzle ORM is an open-source ORM (object-relational mapper) and SQL query builder for TypeScript, released under the Apache 2.0 license. It is made by Drizzle LLC, a company based in Ukraine whose core developers PlanetScale, a database hosting company, hired in March 2026; Drizzle LLC also sells Drizzle Studio Gateway and other paid tools, and does outsourcing and consulting work (sustainability). You declare your tables in TypeScript with functions such as pgTable, then query them with a builder that reads like SQL (db.select().from(users).where(...)) or with a relational API that loads nested data (overview). Its companion CLI, drizzle-kit, generates SQL migration files by comparing your TypeScript schema with the last snapshot, and applies them (drizzle-kit). Drizzle is free to use.
Elements is not an ORM or a query builder. 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 database 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.
Drizzle's own promise is "If you know SQL, you know Drizzle." That sentence admits the language you already know is SQL, and then asks you to learn a second one: SQL spelled as TypeScript method calls, plus a relational API beside it, plus a TypeScript copy of every table, plus a config file and a separate CLI with eight commands to keep that copy in step with the database. Every one of those is a seam, a place where the TypeScript and the database can disagree with no type error to show it, and a person or an agent finds out when a query runs. Elements is ready out of the box: you write SQL in the sql() function, every interpolated value becomes a query parameter, your schema is SQL migrations applied the moment you save them, and the build runs your tests against real Postgres. The vocabulary is sql(), tx() and a test file.
At a Glance
| Elements | Drizzle | |
|---|---|---|
| What it is | A project server, a default framework, Postgres, migrations, tests and deploy, shipped as one coherent system | A TypeScript ORM and query builder, with a separate migration CLI |
| Business model | Sells the system you build with, per machine, with nothing metered (pricing) | Free; its core team is paid by PlanetScale, which sells hosted Postgres and MySQL (sustainability, PlanetScale pricing) |
| Schema is written in | SQL migrations | TypeScript table declarations, mirrored in the database |
| Queries are written in | SQL, in sql() |
A SQL-like builder, a relational API, or a sql template |
| Relations | Foreign keys and joins, written in SQL | Declared a second time in TypeScript with defineRelations() for relational queries |
| Migrations in development | Applied when you save the file; an edit synchronizes the database | drizzle-kit generate, then drizzle-kit migrate, or push |
| Migrations on deploy | Run once, in one transaction, and a failure stops the release | A command or startup call you add to your pipeline |
| Transactions | tx(): every query inside joins it |
db.transaction(), with every query called on the transaction object |
| Tests | Part of the build, each in a rolled-back transaction | Not included |
| Realtime | LiveTable and channels, over Postgres | Not included |
| The database | Bundled Postgres, or any Postgres host | Bring your own |
A Second Language for SQL
A query library sits between your code and the database. Drizzle's sits there as a translation: select becomes .select(), join becomes .innerJoin(), and a condition becomes eq(users.id, posts.authorId). You and your agent think in SQL, translate it into the builder, then read the builder back to know what SQL it sends. Drizzle also gives you a second query API, the relational one, and its docs call having both "the best of both worlds" (overview). That is two APIs to learn on top of the SQL they stand in for, and the relational one needs relations declared separately from the tables before it can load anything (relations).
SQL has been the standard language of relational databases for decades, and agents write it fluently because they learned from decades of it. In Elements, you write it directly, and nothing stands between the query you read and the query Postgres runs.
Agents Need Correction, Not Endless Abstractions
For an agent, the value of a database layer 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. Drizzle stacks up the layers: a query builder, a TypeScript schema, drizzle-kit, drizzle-seed, and validator integrations for Zod, Valibot, TypeBox, ArkType and Effect Schema (v0 to v1 changes). A library pays off for an agent when it does something the agent cannot write quickly from what it already knows, and for database code that is a short list. A query builder asks an agent to recall a TypeScript API in place of the SQL it already writes, plus a second, relational API beside it. Where the abstraction cannot catch a mistake, Drizzle adds another tool: its ESLint plugin exists "for cases where it's impossible to perform type checks for specific scenarios, or where it's possible but error messages would be challenging to understand," and its two rules flag a .delete() or an .update() written without .where() (ESLint plugin).
Agents do not write machine code to build a web app, and some abstraction is basic plumbing every app needs. On a line from an agent writing machine code, which nobody does, to a package that does subtraction, the right amount of framework sits at that plumbing, and Elements ships it: routing, typed server calls with @rpc, sessions, sql() and the database driver. Past it there is a point of diminishing returns, where each further abstraction costs an agent more to learn and get right than it saves. So Elements stops at the plumbing and puts its engineering into correction. The app framework is the standards themselves: pages are HTML and CSS, a server call is a TypeScript function marked @rpc, a query is SQL in sql(), a table is a SQL migration, and a transaction is tx(). Because the framework and its build were made together, one coherent system checks every page, function, sql() call, test and migration: type checking, tests, migrations and program analysis, done in milliseconds. elements build -json returns every build error in microseconds, from the build state the project server already holds, and your terminal, your editor and every agent read the same build. Server code calls sql() with no await, so there is no await for an agent to leave out.
Queries Are SQL, Safe by Construction
Drizzle's sql template turns interpolated values into query parameters. For text that cannot be a parameter, such as a dynamic order by, it offers sql.raw(), which includes a string in the query "without any additional processing or escaping" (sql).
In Elements, sql is the first way to query, and its escape hatch is safe too. You write SQL in the sql() function, and the build extracts every ${value} into a query parameter at compile time. sql.raw splices only text written in its own literal, and any ${} inside it is still a parameter, so a string from a request cannot reach the query text through either one. 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.
Queries Are Checked Against the Real Database
A query is proven by running it. In Elements, the tests that call a query run it 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.
With Drizzle, the type checker checks your queries against your TypeScript declarations, not against the database. Whether the two agree, and whether a sql template is right, is something you learn by running a test suite you set up yourself, against a database you provide. In Elements, you and your agent learn it within moments of saving, from elements build -json, because the project server that runs while you work updates the build state almost instantly on every save (build).
Six Ways to Migrate, Chosen Before You Write a Table
Before a Drizzle project has a table, it needs a migration strategy. Drizzle's migrations guide lays out six options, from managing SQL yourself, to drizzle-kit push with no migration files, to generating files that drizzle-kit applies, that Drizzle applies at runtime, or that another tool applies (migrations). The drizzle-kit docs tell you to "pick SQL migration flow that suits your business needs best" before you start, then configure it in drizzle.config.ts and run it with one of eight commands: generate, migrate, push, pull, check, up, studio and export (drizzle-kit).
In Elements, there is one way, and it is ready out of the box. A migration is a SQL file in app/migrations/, and applying it is part of the build.
The Schema Lives in One Place
With Drizzle, your schema exists twice: as TypeScript table declarations, and as the tables in the database. drizzle-kit keeps them in step by diffing your declarations against its last snapshot, and a diff cannot tell a renamed column from a dropped one and a new one, so step 3 of drizzle-kit generate is to "prompt developer for renames if necessary" (migrations). That is an interactive question in the middle of generating a migration, asked of a person or an agent. Relational queries need a third declaration: the relations between tables, passed to Drizzle when you create the client (relational queries). And what the TypeScript schema cannot express, the database still needs. "[FEATURE]: CREATE TRIGGER functions" is the most-reacted open issue in Drizzle's repository, with 560 reactions, open since July 2023.
In Elements, the schema is the SQL that creates it, triggers included. Save a migration 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. You write camelCase names in migrations and queries alike, and Elements converts them to snake_case in the database. Edit a migration in development and Elements synchronizes the change to the database, so prototyping and versioned migrations are the same workflow. Relations are foreign keys and joins, written in SQL.
Migrations Run Once on Deploy
On deploy, Elements runs migrations 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).
With Drizzle, applying migrations in production is a drizzle-kit migrate command or a migrate() call at startup that you wire into your own deploy. Start that on two servers at once and nothing stops both from running: "[BUG]: migrate isn't protected against simultaneous execution" is labeled a priority bug and has been open since July 2023.
Transactions Without a Client to Pass Around
A transaction makes several writes succeed or fail together. In Drizzle, db.transaction() hands your callback a transaction object, and every query inside has to be called on it rather than on db (transactions), so a helper function that writes to the database needs that object passed in.
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. Drizzle is a query library, so testing is left to whichever runner and database setup you choose.
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 with every other build error.
The Database Runs the Rest of the App
A query library stops at queries. Everything else an app needs from its data, such as pushing changes to open browsers or queuing background work, is another package or another service.
In Elements, Postgres is the center of the app. 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.
Elements bundles Postgres and sets it up on your machine and on each server you deploy to, which suits a prototype or a single-server app. For several machines, or for managed backups and scaling, point DB_HOST at a hosted Postgres such as DigitalOcean Managed PostgreSQL or Amazon RDS (database).
Who Pays for Drizzle
Drizzle is now paid for by a company that sells hosted databases, and its docs show you that company's Postgres first. It was funded from 2021 by more than 100 sponsors and its makers' consulting work, and in March 2026 PlanetScale hired its core team and became, in Drizzle's words, "the biggest backer we have" (sustainability). PlanetScale sells hosted databases on monthly plans: Postgres, and Vitess, its MySQL-compatible database (PlanetScale pricing). Its announcement says Drizzle "will remain an independent open source project with its own roadmap and goals" (Drizzle joins PlanetScale). Its docs list PlanetScale Postgres first among the Postgres providers they show you how to connect. Drizzle LLC's own income comes from paid tools such as Drizzle Studio Gateway and OneDollarStats, a subscription analytics service, and from outsourcing and consulting for other companies (sustainability).
Elements sells its tooling directly, per machine, with nothing metered (pricing). Its database layer is not for sale: sql(), tx() and the rest of the app framework ship in runtime packages such as @elements/app under the MIT license, and everything you build is yours (licenses).
One Query, Both Ways
Published posts with each author's name. In Drizzle, you declare each table as a pgTable call in TypeScript, with a column builder per column (schema declaration), create a client from a connection string, and build the query from those declarations with .select(), .innerJoin(), eq() and .orderBy() (joins).
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() );
And the query is the SQL it means:
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(); }
There is no await: the build makes every function that calls sql() async for you (async). A test of publishedPosts inserts its own rows, runs the query against Postgres, and rolls back when it ends.
Moving From Drizzle
An agent can port a Drizzle app easily. The schema already lives in Postgres, so it becomes SQL migrations, and each query becomes the SQL that Drizzle builds, written in a sql() call. You keep your data, and you gain one language, SQL, for queries and schema.
Questions
What is the difference between Elements and Drizzle?
Drizzle is a TypeScript ORM and query builder: you declare tables in TypeScript and build queries from them, with drizzle-kit generating migrations. Elements is an integrated app environment that bundles Postgres. You query it in SQL itself, 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 Drizzle alternative?
Yes. Elements takes the place of the ORM, drizzle-kit, its config file, the database setup and the test runner a Drizzle app assembles, with SQL queries in sql(), SQL migrations applied when you save them, a bundled Postgres and tests that run in the build.
Who makes Drizzle?
Drizzle LLC, a company based in Ukraine. In March 2026 PlanetScale, which sells hosted Postgres and MySQL-compatible databases, hired Drizzle's core team and became its biggest backer (sustainability, PlanetScale pricing). Elements is sold directly, per machine, so its revenue comes from the tooling itself (pricing).
Does Elements have a query builder?
Elements uses SQL in place of a query builder: you write it in the sql() function, and the build turns every interpolated value into a query parameter at compile time. Dynamic ordering and optional conditions use sql.raw fragments, where only text written in the literal is spliced into the query.
Does Elements map camelCase to snake_case?
Yes, by default. You write camelCase in queries and migrations, and Elements converts to snake_case at the database boundary and back to camelCase on every result.
How do Elements migrations compare to drizzle-kit?
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 to generate. On deploy, pending migrations run once, in a single transaction, after your tests pass, and a failure stops the release.
Which databases does Elements support?
Postgres, version 16 or newer. Elements bundles it on your machine and on each server you deploy to, and connects to any managed Postgres, such as DigitalOcean Managed PostgreSQL or Amazon RDS, by setting DB_HOST.