Build apps with AI, on your machines.

Elements is the integrated app environment for building and deploying web apps. It's a project server, app framework and build tooling, engineered from the ground up to work together. The project server is always running, so your agent can check its work instead of guessing. Prototype quickly. Ship the same code to production. Scale to millions of users.

Try it: a working app in seconds.

Install elements: curl -fsSL 'https://install.elements.dev?os=darwin' | sh && export PATH=~/elements/bin:$PATH Create an elements app: cd ~/elements/projects elements create myapp cd myapp Start the app.

An always-on project server, to make your agent smarter.

One server per project. The first command starts it and everything after connects to the same one: elements build, elements test, elements install, and your editor over LSP. It holds the build state in memory, so checking costs microseconds instead of seconds, and your agent can check after every change rather than at the end.

The project server answers in microseconds.

Save a file and the rebuild and the full type check are already done, because the build state never left memory. Your agent asks the same question with -json and reads the answer without parsing a screen.

elements build
State: Ok
Elapsed: 0.000006s
j/k: down/up G/g: bottom/top y: copy q: quit
elements build -json
{
"path": "/Users/cmather/elements/projects/myapp",
"ok": true,
"errorCount": 0,
"warningCount": 0,
"message": "The build is ok.",
"buildGen": 15,
"buildRanAt": "2026-09-21T12:08:15.74752-07:00",
"started": "2026-09-21T12:09:41.121451-07:00",
"finished": "2026-09-21T12:09:41.121457-07:00",
"elapsedSeconds": 0.000006
}

A build is everything required to release the app.

Read the config, install packages, crawl, type check, run program analysis, emit, migrate the test database, run the tests, collect diagnostics, migrate the app database, release atomically, hot reload connected browsers.

Packages install in milliseconds.

Twenty nine of them. Install is the second phase of a build, so it has already happened by the time anything is type checked.

elements install
State: Ok
Elapsed: 0.000857s
PACKAGES:
@elements/app @0.7.0
@elements/style @0.7.0
@types/node @22.20.3
tslib @2.8.1
5 root, 29 total packages
j/k: down/up G/g: bottom/top y: copy q: quit

Your database stays in sync automatically.

Write a migration and it is applied on save, to the test database before the tests run and to the app database after. Nothing to remember, nothing to run.

elements db migrate
State: Ok
Elapsed: 0.035618s
MIGRATIONS:
node_modules/@elements/app/migrations/2026-07-11-000000-app-baseline.migration.sql [ok]
The application framework baseline migrations.
app/migrations/20260915155829-add-chat.migration.sql [ok]
add chat
j/k: down/up G/g: bottom/top y: copy q: quit

Tests are fast enough to be useful.

They run across child node processes with hot reload on, against the freshly migrated test database, as a phase of every build. Fast enough that they stop being the thing you write to be compliant and start being how you find out what you broke.

elements test
State: Ok
Elapsed: 0.000014s
TESTS:
app/pages/home/test.ts [pass]
└─ home [pass]
├─ a fresh install has a channel to open on [pass]
├─ an anonymous visitor cannot create a channel [pass]
├─ a guest creates a channel too [pass]
├─ a signed in user creates a channel under a slug [pass]
├─ a channel name that slugifies to nothing is refused [pass]
└─ two channels cannot share a name [pass]
j/k: down/up G/g: bottom/top y: copy q: quit

Program analysis stops unsafe code.

A database call reachable from the browser does not compile, so it never ships. The report carries the fix and the path that got there.

elements build
State: Error
Elapsed: 0.000010s
ERRORS:
You have 1 error:
1. app/pages/home/template.html:6:10
Security error: cannot call server code from the browser without going through an rpc
function.
Try:
Put the server code inside a function with an @rpc build tag. For example:
/**
* @rpc
*/
export function fn() {}
Then call fn().
5: function onSignIn(email: string) {
6: return sql(`select id from users where email = ${email}`).first();
7: }
Call stack:
2. app/pages/home/template.html:6:10 sql
1. app/pages/home/template.html:32:49 onSignIn
j/k: down/up G/g: bottom/top y: copy q: quit

Deploy to your own servers, with one command.

Any Ubuntu box you can ssh into. Elements builds the release, ships it, runs the migrations and hot reloads the app. No server restart, no dropped connections.

elements deploy
State: Ok
Elapsed: 0.720184s

Peer to peer, and incremental.

A deploy is your project server talking directly to the one on each machine. Yours sends the source graph it already holds, theirs diffs it against the graph it already holds, and only what changed crosses the wire. Both ends are already hot. No queue behind a million other people's builds, no container image to assemble and push, and nothing rebuilt from scratch on the far side.

Start on one box. Grow from there.

The load balancer is part of Elements and starts alongside the app, on every machine. Each one knows about all the others, so point your DNS at one box or at all of them and whichever receives a request balances it across the cluster. Round robin, with failover to the machines still answering. Adding a second machine is adding a machine: it runs the same load balancer and takes its share. Nothing to install in front of the cluster, no config file to keep in step, no external service.

Automatic SSL on every machine.

Set a domain, point DNS at the machine, open 80 and 443. Elements issues the certificate through Let's Encrypt on the first request and renews it before it expires. Adding a domain or rotating a certificate takes effect without dropping connections.

Integrated Postgres.

On a single machine, use the Postgres cluster Elements already installed and manages there. For a more complicated application, or a cluster of several machines, point Elements at a hosted Postgres instead.

A completely integrated app framework that just works.

Everything a real application needs. No external packages required.

html
A reactive HTML language with typed template constructors. The first response is the rendered page. The browser attaches to it and patches only what changes.
rpc
Call a server function from the browser like any other function. The compiler turns the call into a request and keeps the body out of the browser bundle.
routes
Declared in TypeScript with a typed request and response. Return a page, or return data.
auth
Login, logout and authorization primitives are built in. Works in routes, rpc functions, and reactively in html templates.
sql
Write sql in your code. It is parameterized at build time, against the Postgres the install already gave you.
migrations
Applied by the build: the test database before the tests run, the app database after.
live tables
Real time publish and subscribe for your data, with optimistic mutations. Declare the set of rows a page needs and every browser subscribed to it stays current: insert, update or delete anywhere and the change arrives everywhere, painting locally before the server has answered.
channels
Broadcast from the server to every browser listening. Built on Postgres listen and notify.
jobs and cron
Queue work off the request path, or schedule it. A job server starts with the project, on your machine and on every machine you deploy to.
email
Write mail in the same html language as your pages. The compiler inlines the stylesheets so it renders in a mail client.
tests
Run across child node processes with hot reload on, against a freshly migrated test database, as a phase of every build.
app/pages/waitlist/template.html
import { sql, session } from "@elements/app";
/** @rpc */
function joinWaitlist(email: string) {
let user = sql<{ id: string }>(
`insert into users (email, name) values (${email}, 'waitlist') returning id`,
).firstOrThrow("insert failed");
session.login({ userId: user.id, userName: email });
}
<html (private email: string = "", private joined: boolean = false)>
<body>
<form onsubmit={() => { joinWaitlist(email); joined = true; }}>
<input type="email" value={email} required>
<button type="submit">Join</button>
</form>
<p e:if={joined}>Thanks, {session.get("userName")}.</p>
</body>
</html>

The difference between an app that works the first time and an afternoon of telling your agent to try again.

It matters more the larger the app gets. Save any file in the project and it is checked against every page, every route and every test, so your agent can change a schema in a codebase nobody remembers and find out immediately that it broke something three files away. Nothing regresses quietly, and an app you shipped six months ago is still one you can change with confidence.

I've been building Elements for more than a decade. When I started, the plan was to create better tooling for people, to make building web apps dramatically simpler. It turns out the same tools also make agents insanely more productive. It's finally here, and I can't wait to see what you build. I'd love to hear from you: hello@elements.dev.

Sincerely,

Chris Mather (creator of Elements)

The integrated app environment.

A project server, build tools, app framework and deployment, as one carefully engineered system. Built from the ground up to work together, on your own machines.

Install Elements