Elements Builds Apps for Node.js
Node.js is an open-source, cross-platform JavaScript runtime: the program that runs JavaScript outside a web browser, most often on servers (Introduction to Node.js). It is a project of the OpenJS Foundation. Node is built on V8, the JavaScript engine inside Google Chrome. Around it, Node adds an event loop, which lets one process serve thousands of connections at once without stopping to wait on the network, the disk or a database (About Node.js). Its standard library includes modules for HTTP servers, the file system, cryptography, streams and child processes. Node also includes a built-in test runner, and it can run TypeScript files directly by stripping out the types. Node ships with npm, a package manager that downloads libraries from the npm registry, where most open-source JavaScript is published.
Elements is not a runtime. It is its own program, and the apps it builds run on Node.js. It is an integrated app environment for building web apps, built for people and their agents: one coherent system that includes a project server that runs while you work, a default app framework, a package installer, a type checker, a test runner, a job runner, a bundled Postgres database, a deploy command and a load balancer. The project server holds the build graph and build state in memory, so on every save the build state is updated almost instantly: it builds in milliseconds and answers agents and humans in microseconds. The framework, @elements/app, covers routing, sessions, typed server functions, realtime data, and pages written in Elements HTML: standard HTML with reactive expressions. An Elements app is a Node program, and npm packages install in it as is.
Elements does not replace Node; it replaces what developers build around it. On plain Node, the framework, bundler, type checker, test runner, database, job queue, process manager, proxy and deploy script are separate projects you choose and wire together yourself. Elements ships all of them together, built by one team, so an app starts with every one in place and working with the others. And because one build checks your code, your tests and your migrations together, your agent learns about every build error, a failing test, a broken migration or a mistake found by type checking or program analysis, within moments of saving, from one command.
Elements Targets Node on Purpose
Elements apps run on Node because of what Node is good at. Its event loop handles many slow network requests at once, which is most of what a web server does, and the npm registry grew up around it.
Node keeps adding what developers need to the runtime itself, and we admire that. Elements meets two of the same needs inside its build.
Node runs a TypeScript file by stripping out its types, and "no type checking is performed" (Node.js TypeScript). Elements adds the check inside the build, so a type error shows up moments after you save (typescript).
Node's test runner has been stable since v20 (node:test). Elements runs tests inside the build, reruns only the ones a change affects, and wraps each in a database transaction that rolls back (tests).
Elements builds on Node's own APIs instead of wrapping them:
- Server code is type checked against Node's own types (typescript).
- Route handlers receive Node's request and response objects, extended with the parsed URL, params and body (router).
- The compiler emits CommonJS, Node's original module format, because that is the format a running Node process can hot reload, swapping in new code without a restart (build).
- Every import is resolved at build time and rewritten to the exact file it will load, so a missing module is a build error, never a surprise in production (packages).
- The installer writes a real
node_modulesfolder, laid out the way Node looks up modules, so any tool that readsnode_modulesworks (packages).
Elements adds no second runtime and no compatibility layer, so your server code runs on Node itself. Elements vs Bun covers the other approach, a runtime that replaces Node.
What You Would Assemble on Node, and What Ships in Elements
Node runs your code. It has no page router, no database, no job queue and no deploy command, so a production app on plain Node is a set of separate projects. The table lists the usual pick for each job beside the part of Elements that does it.
| The job | Assembled on Node | In Elements |
|---|---|---|
| HTTP server and routing | Express or Fastify | The router, with routes written as URLPatterns (the web standard for URL patterns) for pages and APIs alike |
| Request bodies and file uploads | body-parser, plus multer for multipart | Built in; a form's File[] field reaches the server as files (recipes/file-upload) |
| Sessions and login | express-session plus a production session store | Sessions stored in Postgres, which work the same over HTTP and WebSockets |
| Browser to server calls | REST endpoints, a fetch client, and types shared by hand | @rpc functions: server functions the browser calls like local ones, with typed arguments and results |
| Realtime | ws or Socket.IO, plus a pub/sub layer | Channels, which broadcast messages to browsers, and LiveTable, database rows kept in sync in every open browser |
| Pages and UI | A frontend framework and a server rendering setup | Elements HTML, standard HTML with reactive expressions, rendered on the server and made interactive in the browser |
| Package installs | npm, plus npm link or monorepo tooling for packages you develop alongside the app |
The installer inside the project server, with a machine-wide cache so a package downloads once, and local packages that are watched for changes |
| Type checking | tsc, run separately |
The type checker inside the build, reporting errors moments after a save (typescript) |
| Bundling and dev server | Vite or esbuild | The project server; each browser file's name changes when its content does, so browsers cache it long term (build) |
| Tests | Vitest, Jest or node:test, plus database cleanup | Tests inside the build, each in a transaction that rolls back |
| Database | Postgres, installed and run by you | Bundled Postgres, installed and run by Elements on your machine and your servers, or a hosted Postgres you point it at |
| Queries and schema | An ORM such as Prisma or Drizzle, and its migration tool | sql() for plain SQL queries, and migrations that are part of the build, reapplied when you edit one in development |
| Background jobs and cron | BullMQ on Redis, plus node-cron | Jobs stored in Postgres with retries, and cron that fires once at each scheduled time, even when several machines run the app |
| Nodemailer and a template engine | email(), with templates in the same HTML language as pages |
|
| Config and secrets | dotenv | config.jsoc, a JSON config file with comments, where a missing or mistyped environment variable fails the build at the line that reads it |
| Process management | PM2 or systemd units you write | The Linux service definitions (systemd units) that keep the app running, installed on the first deploy (what deploy does) |
| Reverse proxy and TLS | nginx and certbot | A load balancer with Let's Encrypt certificates, on every machine |
| CI and deploy | A pipeline, a deploy script or container images | elements deploy over SSH, with tests run on the target machine before the release goes live |
Each row on the left is its own project, with its own configuration and its own release schedule. Some bring a second service with them: BullMQ is a queue "built on top of Redis" (BullMQ), so a queue means running Redis too. In an Elements app there is no Express, no Redis, no nginx, no certbot, no PM2 and no CI pipeline to choose, install or configure.
The Parts Were Designed Together
Choosing the parts is half the work. Making them agree with each other is the other half, and on a hand-built stack it falls to you. The queue has to respect the database's transactions. The session store has to work over WebSockets as well as HTTP. Something has to keep database calls out of browser code. The test runner has to reset the database between tests. The deploy script has to run migrations before the new code takes traffic.
Because Elements is one coherent system, each of those is already handled:
- A job queued inside a transaction is saved with your other writes or not at all, because the queue is a table in your own database.
tx(), the function that runs database writes as one transaction, covers the job too (jobs). - Signing in over an
@rpccall updates every template that reads the session, without a reload, because HTTP requests, WebSocket calls and templates share one session API (session). - Database and session calls cannot run in the browser by mistake. Calling
sql(),tx()orsession.login()from code that can reach the browser is a compile error at the call site, because the compiler knows which code can reach the browser (rpc). - Every test runs in a transaction that rolls back when it ends, so there is no setup or teardown to write. The test runner owns the database connection (tests).
- A failed deploy leaves the running app untouched. The first machine migrates a separate test database, every machine runs the tests, the first machine migrates the app database, and then each machine switches to the new release in one step. The previous release keeps serving traffic until the new one is fully ready (what deploy does).
One Project Server Runs While You Work
The parts of a hand-built Node stack do not just install separately; while you work, they run separately too. That means tsc --watch in one terminal, the dev server in another and the test runner in a third, each with its own view of your code and its own error output. A coding agent has to watch all of them to learn what it broke.
Elements replaces them with the project server, which runs while you work and holds the build graph and build state in memory. Because it already knows the build, the build state is updated almost instantly on every save. It redoes only what a change touched and hot reloads the running Node process. It builds in milliseconds and answers agents and humans in microseconds. Your terminal, your editor and every coding agent read the same build state through elements build -json (build), so an agent learns about a mistake from one report, right after the save. Several agents working in parallel share that one build state instead of each starting its own watchers.
Packages you develop alongside the app install from a directory with the version local, and the project server watches them, so there is no npm link step and no monorepo tooling to set up (packages).
The same project server runs on the machine you deploy to. When you deploy, the two servers compare their copies of your code over SSH, send only the files that changed, and build only what those files affect. The first deploy to a new machine takes a few seconds, and later deploys often finish in under a second (deploy).
Your Node Code and npm Packages Come With You
An Elements app is a Node app, and npm packages install as is (packages).
Node and the standards around it already give a coding agent most of what it needs. It knows TypeScript, HTML, CSS and SQL, Node's standard library covers HTTP, files and cryptography, and an npm package is a real shortcut where it does something the agent cannot write quickly itself. That surface is smaller than the registry suggests. A web app needs some plumbing, and Elements ships it on Node: routing, typed server functions with @rpc, sessions, sql() and the database driver. Past that plumbing, every extra library is one more API for the agent to look up and one more seam to get right, and the returns diminish quickly. What Elements adds to Node is correction: one coherent system checks every page, function, query, test and migration the agent writes, and elements build -json reports each mistake in microseconds.
The framework package, @elements/app, is MIT licensed, so the code you write on it is yours to keep and run anywhere Node runs. Elements charges for the tooling, not the framework: it is licensed per machine at a price known up front, and nothing is metered (licenses, pricing).
Questions
Does Elements run on Node.js?
Elements apps do. Elements itself is its own program, not a runtime: it builds apps that run on Node.js and does not replace Node. Server code is type checked against Node's types, route handlers receive Node's request and response objects, and the running Node process hot reloads new code without a restart.
Is Elements a Node.js framework?
It ships with a default framework. Elements is an integrated app environment that builds apps for Node: one coherent system with a project server that runs while you work, the @elements/app framework, a package installer, a test runner, a bundled Postgres database and a deploy command. Together they replace the stack you would otherwise assemble around Node.
Can I use npm packages with Elements?
Yes. The installer resolves packages from the npm registry and writes a real node_modules folder, laid out the way Node looks up modules. ESM-only and CommonJS-only packages both work, on the server and in the browser, because Elements compiles every module to one format.
Is Elements an alternative to Express?
Elements has its own router, with body parsing, multipart uploads, sessions and asset serving built in, so an Elements app does not need Express. Routes are declared in code with URLPattern and serve HTML pages and API endpoints alike.
Do I need nginx, PM2 or certbot to deploy an Elements app?
No. elements deploy installs systemd units for the app, the job worker and the load balancer, and the load balancer terminates TLS with certificates it issues and renews through Let's Encrypt. A single Ubuntu server reached over SSH runs the app, the load balancer and Postgres. For several servers, or managed backups and scaling, point DB_HOST at a hosted Postgres such as DigitalOcean Managed PostgreSQL or Amazon RDS.