--- title: "The Build" author: "@chris" author_url: https://elements.dev/u/chris published: 2026-10-02T15:11:56.268Z url: https://elements.dev/feed/01a0fd2b-fb76-7bc5-b106-83c0247c291e kind: lesson format: article --- # The Build by [@chris](https://elements.dev/u/chris) ยท 2026-10-02 ## Description One build for the server and the browser. Type errors, failing tests and broken migrations show up the instant you save. In Elements, a build is everything it takes to get your app ready to release. It installs packages, compiles and type checks your code for the server and the browser, applies migrations to the database, runs the tests and writes the release. You never run those steps yourself and you never wire them together. The project server runs all of them as you save, and a single-file save is usually built and hot reloaded in a few milliseconds. ## See the state of the build Open a terminal in your project and run: ```bash Terminal elements build ``` **What you see when the build is ok:** ![The elements build view with State: Ok and Elapsed: 0.000014s, and its key help at the bottom: j/k down and up, G/g bottom and top, y copy, q quit](/learn/01a0fd2b-f0a8-7f1b-8e8e-2ce8ecf38900/01a0fd2b-fb76-7bc5-b106-83c0247c291e/images/01a0fd2b-fbbb-7c69-a42a-d6a3ee82be63) Green means the build is ok. Red means the build has errors. **What you see when the build has errors:** ![The elements build view with State: Error and one error in app/shared/templates/sidebar/index.ehtml at line 62: Property 'nmae' does not exist on type 'SidebarRow', with lines 61 to 63 of the template and the misspelled property in red](/learn/01a0fd2b-f0a8-7f1b-8e8e-2ce8ecf38900/01a0fd2b-fb76-7bc5-b106-83c0247c291e/images/01a0fd2b-fc09-70df-9642-cf22d84b923c) Each error names its file, line and column, and shows the code around it. With several errors, step through them with `j` and `k`. The view stays open and updates as files change, so leave it running in a second terminal while you or your agent work. ## What a build does Every save runs one build on the project server, in this order: 1. **Config.** Reads `config.jsoc` and your environment files, such as `config/env/development.env`, checks every value, and marks every part of your app that uses a value that changed, so those parts are rebuilt too. 2. **Install.** Installs the packages listed in `config.jsoc` and `package.lock`, when that list has changed. 3. **Crawl.** Follows every import from your app's entry points, and reads, parses and evaluates each file that changed. 4. **Bind.** Connects every name in your code to the thing it refers to, across files and packages. 5. **Type check and program analysis.** Checks your code for the server and the browser at the same time. Program analysis catches what types alone cannot, such as browser code that reaches server code without going through an `@rpc` function, and HTML that breaks its own content rules. 6. **Emit.** Writes the code that runs, for each target. Unused code is shaken out, `@rpc` calls in the browser become network requests, server-only code is left out of the browser, `await`s are added for the Elements functions that support them, such as `sql()`, and every browser file gets a hash in its name. 7. **Migrate the test database.** Applies new or edited migrations to your app's test database. 8. **Test.** Runs the tests your change touched, against that test database. 9. **Migrate the app database.** Applies the same migrations to your app's database, only when every step before it passed. 10. **Release.** Makes the new build the release your app runs. Every step reports into one list of errors, so a failing test, a broken migration and a type error all show up in one place, the moment you save. Every step is incremental, so it only redoes the work your change affects. And the release only ever holds a build where every step passed. ## One build, two targets Your app runs in two places, on the server and in the browser, and Elements builds both from the same code in the same build. Every file is checked for each target it reaches, against the types that target actually has: server code sees Node's types, and browser code sees the DOM. - **Server-only code stays on the server.** The browser build leaves out server code such as queries and secrets on its own, with nothing for you to mark. Calling `sql()` from code that reaches the browser is a build error at that line. - **Server functions you call from the browser.** An `@rpc` function runs on the server. In the browser build, each call to it becomes a network request to the server, and its arguments and return value are type checked at build time. - **The browser gets only what it uses.** Unused code is dropped from the browser build, and every browser file has a hash in its name, so browsers and any CDN in between cache it until it changes. ## TypeScript, compiled into Elements Elements is the first build tool to build the TypeScript compiler into itself. The compiler's source code, the Go implementation, is part of the Elements source code and compiles into the same `elements` binary. It is not a separate program Elements runs, and not a library it links to. Elements adapted that code to run on its own source graph, so type checking shares the build's versions and cache, and to check one program for two targets at once. Then it built its HTML templates and its config language on the compiler's own binder and checker. So type checking is a step of the build, in the same process and against the same in-memory source as every other step, not a tool you run beside it. There is no `tsconfig.json` to keep in sync. It builds two resolve graphs, one for the server and one for the browser, and type checks each at the symbol level. Some symbols exist only on the server, such as a LiveTable declaration or `sql()`. For each target, the checker follows only the symbols that target reaches and checks them against that target's types. Server code is checked as server code, browser code as browser code, and a server-only symbol reached from the browser is an error at that line. Elements' HTML templates and config files are built on that same checker. A template attribute is a TypeScript declaration, and the expressions inside `{}` are checked like any `.ts` file, with the same error messages. A misspelled field in a template is a red build with a line number, not a blank spot on the page that a user finds later. ## Config is hot Your app's configuration is source code too. `config.jsoc` and your environment files, such as `config/env/development.env`, are part of the build like every other file. Change a setting or an environment variable, save, and the build rebuilds every part of your app that uses it, and only those parts. Your running app has the new value, with no restart. And it is checked like code. Environment variables are read at build time, so a missing or misspelled one is a build error with a line number, not a crash after you deploy. ## Only what changed, in milliseconds Every file has a version made from its content and the versions of everything it imports. When you save, Elements redoes only the work your change affects and nothing else, at every step: checking, compiling, testing and reloading. Ask for the state of an unchanged project and the answer comes back in microseconds, because the project server already did the work. Hot reload patches your running app and the open browser at the smallest level the change needs, so each save shows up without a restart. ## Tests run in every build You never run your tests to find out if they pass. The build runs them on every save, as one of its steps, and a failing test turns the build red like any other error: - **Only the tests your change touched run.** Every other test keeps its last result, and a test whose code has an error waits until the error is fixed. - **In parallel.** Test files run at the same time, so a large suite still answers quickly. - **Against their own database.** Every app has a separate test database, and each test runs inside a transaction that rolls back when it ends. Tests never touch the data you see in the browser and never leave anything behind. - **Failing tests block the release.** The release only ever holds a build where every test passed. ### See the build through its tests `elements test` does not run a separate test pass. It shows you the same build, organized as a tree of your test files and the tests inside them: ```bash Terminal elements test ``` **Every test in the app, from the build:** ![The elements test view with State: Ok in 0.000010 seconds, then a tree of test files: app/pages/admin/test.ts and app/pages/channel/test.ts, each with its tests marked pass, such as only an admin can create a channel, and a message is unread for everyone but its author](/learn/01a0fd2b-f0a8-7f1b-8e8e-2ce8ecf38900/01a0fd2b-fb76-7bc5-b106-83c0247c291e/images/01a0fd2b-fc73-7f8f-a16d-86c39a4251bb) When you are working on one part of your app, name the file to see only its tests: ```bash Terminal elements test app/pages/channel/test.ts ``` **Just the tests for the channel page:** ![The elements test view for app/pages/channel/test.ts alone, with State: Ok in 0.000011 seconds and each channel test marked pass, such as the author comes from the session, not the payload](/learn/01a0fd2b-f0a8-7f1b-8e8e-2ce8ecf38900/01a0fd2b-fb76-7bc5-b106-83c0247c291e/images/01a0fd2b-fcc3-785f-8039-9befda651e79) The answer comes back in microseconds, because the build already ran the tests. An agent gets the same tree as data with `elements test -json`. ## Migrations are tested before they touch your data Because the test database is migrated and tested first, a migration that breaks a test never reaches the data you are working with. ## Packages ### Add and remove packages with `elements install` ```bash Terminal elements install dayjs ``` `elements install` runs as its own task on the project server's build loop, in line with builds and test runs and never at the same time as one. That task resolves the package, writes it to `node_modules`, and records it in `config.jsoc` and in `package.lock`, which pins the exact version. When the task finishes, the project server queues a build, and that build brings the new package into your app and rebuilds against it. **What you see after adding a package:** ![The elements install view after elements install dayjs: State: Ok, then the app's packages with their versions, dayjs at 1.11.23 marked installed, and 6 root, 22 total packages](/learn/01a0fd2b-f0a8-7f1b-8e8e-2ce8ecf38900/01a0fd2b-fb76-7bc5-b106-83c0247c291e/images/01a0fd2b-fd04-7b13-a3b4-ec0223208c98) `elements uninstall dayjs` removes a package the same way. ### The build keeps your packages in step The build has its own install step. When the list of packages in `config.jsoc` and `package.lock` has changed, it installs those packages before anything is compiled. That covers editing the dependency list in `config.jsoc` by hand and saving, a new `package.lock` from git, and a fresh clone with no `node_modules` yet. So you can open a project, save a file, and have its packages installed at the versions `package.lock` pins. Every npm package installs as is. Installs write only the packages that changed, and a package you have installed once on your machine lands in the next project without a download. ### Build a package and the app that uses it A package you are writing yourself installs from its folder instead of a registry: ```bash Terminal elements install my-package@local ``` The build watches it like your own code. Save a file in the package and your app rebuilds against the change, with one editor and no linking step. When you deploy, the package goes with your app, so an app built on a package you have not published deploys like any other. ## Everything connects to the same build The project server runs one build, and everything that touches your project connects to it: the terminal, your editor and your agent. It runs their commands one after another, so a build, an install and a test run never step on each other, and all three see the same errors at the same moment. The project server also speaks the language server protocol, so your editor shows those errors as you type. **The same error in an editor:** ![An editor showing app/shared/templates/sidebar/index.ehtml with a red underline under nmae on line 62, and a popup with the error Property 'nmae' does not exist on type 'SidebarRow'](/learn/01a0fd2b-f0a8-7f1b-8e8e-2ce8ecf38900/01a0fd2b-fb76-7bc5-b106-83c0247c291e/images/01a0fd2b-fd82-7e95-943c-1edf676f463c) An agent asks the same server with `elements build -json` and gets the errors as structured data, with each file, line and message, in microseconds. So it checks its work after every edit instead of guessing, and fixes what broke before it tells you it is done. ## There is no CI to set up A deploy runs the same build on your server. It compiles, runs the migrations against the server's test database, runs the tests, and swaps in the new release only when every step passes; until then the previous release keeps serving. So your tests run in three places from one definition: in your editor as you type, on your machine as you save, and on your server before it switches over. There is no pipeline to write and nothing to keep in sync between them. ## Try the build Break something on purpose. Open any template in your app, misspell a field, and save. Watch the build go red and name the line. Fix it, save, and watch it go green. The manual: [build](/learn/man/build), [cli](/learn/man/cli), [typescript](/learn/man/typescript), [html](/learn/man/html), [jsoc](/learn/man/jsoc), [config](/learn/man/config), [tests](/learn/man/tests), [migrations](/learn/man/migrations), [packages](/learn/man/packages).