Elements vs TanStack
TanStack is a family of open-source libraries for building web apps in JavaScript and TypeScript, created by Tanner Linsley and released under the MIT license. TanStack Query (first released as React Query) fetches, caches and updates server data in the browser. TanStack Router is a type-safe router, and TanStack Table and TanStack Form handle the logic of data tables and forms and leave the markup to you. TanStack DB is a reactive client store for an app's API data. Most of the libraries work with several UI frameworks, including React, Vue, Solid, Svelte and Angular. TanStack Start is a full-stack web framework for React and Solid, built on TanStack Router, that adds server rendering, server functions, and deployment to hosts such as Cloudflare, Netlify, Railway and Vercel.
Elements 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, and a deploy command for any Ubuntu server you reach over SSH. It ships with a default framework too, covering routing, server rendering, typed server functions, sessions, realtime data, background jobs and email, with pages written in Elements HTML: standard HTML with reactive expressions. Elements apps are written in TypeScript and run on Node.js. 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.
A TanStack app is assembled from parts. Router, Query, Form, Table and DB are separate libraries, each with its own API and its own docs. A Start app adds Vite or Rsbuild, a server runtime and a host, then a database, an auth provider, a migration tool, a test runner, a job queue and an email service that you pick and wire in yourself. Every seam between those parts is a place the app can break: a library that expects a runtime the host does not provide, two configs that have to agree, an error that surfaces in one tool and was caused in another. That costs a person time and sends an agent in circles. Elements is ready the moment you run elements create: one well engineered system with nothing to assemble, so you and your agent build productively from the first minute. It ships the plumbing every app needs and stops there, with no endless framework abstractions on top. It stays close to the metal: HTML, CSS, TypeScript, SQL and function calls. The framework vocabulary is the bare minimum, app.route(), session.login(), sql(), @rpc and tests, and the rest is web standards you and your agent already know.
| Elements | TanStack Start | |
|---|---|---|
| What it is | An integrated app environment: a project server, a default app framework, a Postgres database and a deploy command | A full-stack framework for React or Solid, built on TanStack Router |
| Parts to assemble | None: elements create makes a project that runs against a real database, with tests and deploy ready |
Router, Query, Form and Table as separate libraries, plus a build tool, database, auth, tests and host of your own |
| UI language | Elements HTML: standard HTML with reactive expressions | React or Solid components |
| Routing | Declared in code; every route handler runs on the server with direct database access | TanStack Router; a route's data loader runs on the server and in the browser |
| Build errors | One build reports type errors, failing tests, migration errors and server code reachable from the browser, built in milliseconds | Vite leaves type checking to tsc; tests and migrations are separate tools |
| Database | Postgres 18, bundled, or a hosted Postgres | Bring your own; docs point to Neon, Convex, or Prisma Postgres |
| Migrations | Plain SQL files, applied instantly in development and on every deploy | Not part of Start |
| Sessions and auth | Built in: signed-cookie sessions, the same over HTTP and WebSockets | A useSession cookie helper you configure with a secret and cookie flags, then login and middleware you write; or a managed provider |
| Server calls | @rpc: a marked server function the browser calls like a local one |
createServerFn with a validator and a handler |
| Server-only code | Server-only APIs such as sql and session.login are marked; calling one from browser code is a compile error |
File-level import protection, by file name or marker import |
| Realtime | LiveTable (rows kept in sync with every browser) and channels | Not part of Start |
| Jobs and cron | Built in, with the queue in Postgres | Not part of Start |
| Built in, with templates in Elements HTML | Not part of Start | |
| Tests | Part of the build and every deploy, each test in a rolled-back transaction | Not part of Start |
| Deploy | elements deploy to any Ubuntu server over SSH, load balancer and TLS included |
A guide per host, including Cloudflare, Netlify, Railway, Vercel, Nitro, Node and Docker |
TanStack Is Many Libraries, and Every Seam Is Yours
TanStack's libraries page lists its projects in five groups: framework, data and state, UI, UX and tooling. A Start app can draw on several of them: Start and Router for routes and server functions, Query for server data in the browser, Form for forms, and often Table. Each is a separate package with its own API and its own docs, and the app is where they meet. Router loads data in route loaders, Query caches it in the browser, and Form holds input state, so an app that uses all three has three APIs to learn and wire together, either through Router's Query integration or by hand.
A Start app needs many more decisions before it can store data or sign anyone in. It builds with Vite or Rsbuild and runs on whichever server runtime your host provides. Its docs say it is "designed to work with any database provider" and name three partners: Neon, Convex and Prisma Postgres. The authentication overview lists WorkOS and Clerk as partners, Better Auth and Auth.js as libraries, and Supabase Auth, Auth0 and Firebase Auth as hosted services, and the server guide advises, "If you can use a managed solution like Clerk or WorkOS, prefer that". Migrations, tests, jobs and email are left to you. Each pick is another API to learn, another config to keep in agreement with the others, and another seam to verify, and swapping one later means verifying its seams again.
The seams show up in Start's issue tracker. The most-reacted open issue in the repository that holds Start and Router, with 65 reactions, open since April 2025, is "Performance is horrible when using recommended Authentication patterns". Another, filed in August 2026, reads: "Start + Nitro on Vite 8.2: SSR service chunk re-exports an undeclared namespace (ssr_exports) → every request 500s while vite build exits 0". That is three projects, Start, Nitro and Vite, and a build that passes while every request fails.
In Elements there is nothing to assemble. A project from elements create builds and runs against a real Postgres database immediately (getting started), and routes, server functions, sessions, migrations, tests, jobs, email, realtime and deploy already work together, because they were engineered together. What you learn is short and close to the web platform: app.route() declares a route, sql() queries Postgres, session.login() signs a user in, @rpc marks a server function, and a test is a TypeScript file beside the page. The rest is HTML, CSS, TypeScript and SQL.
Start's Production Checklist Is Built Into Elements
Before shipping, the seams have to be checked. Start's production checklist lists what that leaves to you: build with a unique test secret and search the emitted client files for it, rehearse rollback, "account for any database migration separately," and turn repeatable checks into CI tests.
In Elements, the build and the deploy handle each item on that list:
- The secret search is the compiler's job. Every config key stays out of the browser bundle unless it is tagged
@browser, and calling server-only code from code the browser can reach fails the build at the call site, as the next section shows. - Rollback is a deploy of the previous commit, and incremental deploys often land in under a second. A deploy that fails any phase, tests included, never goes live: the previous release keeps serving until the new one is swapped in atomically.
- Migrations are plain SQL files. Edit one in development and the database updates immediately. A deploy applies pending migrations in one transaction before the release goes live.
- CI tests become tests in the build. Each test runs in a Postgres transaction that rolls back when it ends. Results land in your editor, and a failing test blocks the release.
Postgres is bundled and set up on your laptop and on every deploy machine, with a test database beside the app database. On a server it suits a prototype or a single-server app. You still choose the server, the CDN and, for several machines or managed backups and scaling, a hosted Postgres such as DigitalOcean Managed PostgreSQL or Amazon RDS (set DB_HOST).
Server Code Is Marked at the Function, Not the File
A full-stack framework keeps server and browser code in one codebase, so it needs a way to keep database queries and secrets out of the browser. TanStack Start's import protection works on files. It blocks a file named *.server.*, or a file that imports a server-only marker, from the client build. A production build fails on a violation; during development the default is to warn and substitute a mock. For a single function, Start offers createServerOnlyFn, which throws if it is called in the browser.
Elements marks the server-only APIs themselves. Calling sql(), tx(), session.login() or email from code the browser can reach is a compile error at the call site, whatever the file is named and whether or not you marked the file. An @rpc function is where server code begins: the browser calls it over the network, and its body, with everything it depends on, stays out of the browser bundle.
Route Handlers Run on the Server and Query the Database Directly
A router maps each URL to the code that answers it and loads the data the page needs. TanStack Router, the base of TanStack Start, types every path, param and search param, and the price is machinery of its own. A bundler plugin generates a routeTree.gen.ts file from your route files, and its Vite setup guide asks you to "make sure that '@tanstack/router-plugin' is passed before '@vitejs/plugin-react'" and warns, "If you are using VSCode, you may experience the route tree file unexpectedly open (with errors) after renaming a route." In Start, a route's loader fetches the page's data, and the execution model guide explains that loaders are isomorphic: they run on the server for the first page load and in the browser during navigation. Reading the database or a secret from a loader goes through a server function, so every route with a loader carries a seam of its own: the same loader code has to be correct in two environments.
An Elements route handler runs on the server. It is declared in code with app.route() in the project's index.ts, matches URLs with URLPattern (the web standard for URL matching), calls sql and session directly, and returns a typed template. Elements renders the template on the server and hydrates it: the browser picks up the server-rendered page and makes it interactive. One build checks the route, the template's attributes, and every @rpc function's parameters and return value.
Agents Need Fast Feedback, Not a Library for Every Job
Much of the case for TanStack, and for React around it, is the ecosystem: a library for every job and a large community using them. More app code is now written by AI coding agents such as Claude Code and Codex, and for an agent a library is a shortcut only when it does something the agent cannot write quickly from standards. That surface is small. An agent would rather write HTML and CSS directly than learn one of fifty UI libraries, it does not need a package to do what a few lines of TypeScript do, and every library past that surface is another API to look up and another seam to get wrong. What it needs is standards it already knows (HTML, CSS, SQL, TypeScript, SSH, Ubuntu) and tooling that reports each mistake right away.
Frameworks have a place. Between an agent writing machine code and a package that subtracts two numbers, the right amount of framework is the plumbing every app needs, so Elements ships it: routing with app.route(), typed server calls with @rpc, sessions with session.login(), and sql() with the database driver. Past that plumbing there is a point of diminishing returns, where each further framework abstraction costs an agent more to learn and get right than it saves. Beyond it, the value for an agent is in correction: finding out what it got wrong the moment it gets it wrong. So Elements keeps everything past the plumbing close to the standards, pages in HTML and CSS and server code as TypeScript function calls, and puts its engineering into correction. One coherent system checks every page, function, query, test and migration the agent writes, so every build error, from type checking, tests, migrations and program analysis, is found in milliseconds, and elements build -json answers in microseconds because the project server already holds the build state. Your terminal, your editor and every agent see the same result, and server code needs no async or await, so there is no await for an agent to forget.
A Start app spreads that correction across separate tools. Vite, one of Start's two build tools, leaves type checking, tests and migrations to others: its docs say it "only performs transpilation on .ts files and does NOT perform type checking" and recommend running tsc --noEmit alongside it.
Server Data Stays Current Without a Cache to Wire
Most apps load data from a server and keep it current in the browser. TanStack Query handles that job for React apps. Its docs explain that "most core web frameworks do not come with an opinionated way of fetching or updating data", so apps end up caching, deduping and refetching server state by hand. Query takes on that job as one more library: a QueryClient provider at the root, a key for each query, and an invalidation after each mutation so the cache does not go stale.
Elements handles data in the framework. A route's data is server-rendered with the page in the first response. An @rpc function returns typed results to the template that called it. A LiveTable, a live set of database rows held in the browser, applies inserts, updates and deletes optimistically, and patches every watching page when another user changes the same rows. A LiveTable's rows stay in sync with the server, so there is no client cache to invalidate.
Sessions, Jobs, Email and Realtime Come Ready
Most apps also need sessions, background jobs, email and realtime updates. TanStack Start's guides show how to set session cookie flags by hand and write middleware to look the session up. They do not cover jobs, email or realtime, so a Start team picks a separate library or service for each and wires it in. In Elements all four are ready in a new project:
- Sessions work the same over HTTP and WebSockets, and a template that reads the session re-renders the moment a user logs in.
- Jobs and cron keep their queue in your Postgres database, so a job scheduled inside a transaction commits with your writes or not at all.
- Email templates are written in Elements HTML, like pages, with styles inlined for mail clients.
- Channels broadcast from the server to every listening browser, over Postgres LISTEN/NOTIFY.
Server Calls Without Builders or await
Both let browser code call a function that runs on the server, and both validate the input there. In TanStack Start that is a server function: you call createServerFn with the HTTP method, chain a validator such as a Zod schema and an async handler that writes through whichever database client the app uses, and then await it from a component, passing the input inside a data object (server functions guide).
In Elements, it is an ordinary function marked /** @rpc */, here in the same .ehtml file as the form that calls it. It validates by throwing ValidationError and writes with the sql() function that ships with the framework. The email check is kept short; Zod or any other npm validator works inside an @rpc too.
app/pages/signup/template.ehtmlElementsimport { sql, ValidationError } from "@elements/app"; interface UserForm { name: string; email: string; } interface User { id: string; name: string; email: string; } /** @rpc */ function createUser(form: UserForm): User { if (!form.email.includes("@")) { throw new ValidationError("invalid email"); } return sql<User>(` insert into users (name, email) values (${form.name}, ${form.email}) returning * `).firstOrThrow("insert returned no row"); } <html (private form: UserForm = { name: "", email: "" })> <form onsubmit={() => createUser(form)}> <input value={form.name}> <input value={form.email}> <button>save</button> </form> </html>
Three things differ. There is no builder chain. There is no async or await to write, because the compiler adds them to calls to sql and to other rpc functions (sync-style async). And the compiler enforces the server boundary at the function, as described above.
Deploys Often Land in Under a Second, on a Machine You Control
Deploying puts the app on a server where users can reach it. TanStack Start's hosting guide has a different setup, and a different set of seams, for each target: a Vite plugin for Cloudflare Workers, another for Netlify, Nitro for Railway, Vercel and other hosts, or a Node server you assemble yourself.
Elements deploys the same way everywhere: to any Ubuntu server you reach over SSH, from any provider. elements deploy provisions the machine, brings up Postgres, starts the load balancer with TLS from Let's Encrypt, and runs the tests on every machine before applying migrations once, on the first. A failing test aborts the deploy, and the previous release keeps serving until the new one goes live with an atomic swap. The project server on your machine talks to the one on the deploy machine, so only the files that changed transfer. A first deploy to a new machine takes a few seconds, and incremental deploys often complete in under a second.
Who Pays for TanStack
TanStack is free. Its overview says TanStack LLC is "100% bootstrapped and self-funded" and partners with companies "who offer both financial support and resources." Its partners page lists those companies, and the docs recommend them by name: the hosting guide points to "Official Hosting Partners: Cloudflare, Netlify, or Railway", and the authentication overview lists Clerk and WorkOS under "Partner Solutions".
A project funded by partners depends on that funding continuing, which is a fragile foundation to bank a business on. The default path through its docs already leads to the partners' services, each one another account to open and another seam in your app. Elements is a company that sells its tooling directly, per machine, with nothing metered (pricing), so what it earns from is the tooling itself. It charges for the tooling, not the framework: the code your app runs on, the app framework and runtime packages such as @elements/app, is MIT licensed, and everything you build is yours (licenses).
Elements HTML Is Literally HTML
TanStack Start builds pages from React or Solid components. Elements pages are written in Elements HTML: standard HTML tags and attributes, with TypeScript expressions in {} and a few e: attributes. A template declares typed attributes, like a function's parameters. Inside it, {expressions} update when data changes, value= binds an input two ways, and control-flow attributes such as e:if, e:for and e:switch handle conditions and lists. It reads as HTML, so anyone who knows HTML can follow it.
Elements is not React compatible. Its templates are HTML that the compiler reads for both the server and the browser, so the markup is checked once, the page renders on the server, and the browser hydrates it. From then on, the runtime patches only the text and rows that changed. Generated migrations give every table an id column, which the runtime keys rows on, so lists update row by row.
On React, one of Start's two targets, components re-render when state changes, and a discipline has grown up around keeping that fast, with tools like memo for skipping renders. Elements has no memo equivalent and no render tuning to learn.
Questions
Is Elements an alternative to TanStack Start?
Yes. TanStack Start is a React or Solid framework you pair with a database, an auth provider, a test runner and a host. Elements is an integrated app environment. It includes a project server that runs while you work, a default app framework with sessions, jobs and email built in, a test runner, a bundled Postgres database with migrations, and a command that deploys to your servers.
Can I use TanStack Query with Elements?
Its framework-agnostic core, @tanstack/query-core, installs like any npm package. You will rarely need it: routes server-render a page with its data, rpc functions return typed results, and LiveTable keeps rows in the browser in sync with Postgres.
Does Elements use React?
No. Elements pages are written in Elements HTML: standard HTML with typed template attributes and reactive expressions, rendered on the server and hydrated in the browser.
What does TanStack Start not include?
Its docs leave the database, authentication and hosting to you or to its partners, and its guides do not cover migrations, background jobs, email, realtime updates pushed to every browser, or a test runner. Elements includes each of these, and one build checks the code that uses them along with the rest of the app.
Where can I deploy an Elements app?
Any Ubuntu server you can reach over SSH, from any provider. Elements provisions it and runs Postgres and a load balancer with TLS on it. A first deploy takes a few seconds, and incremental deploys often complete in under a second.