Project Layout
elements man start/layout Read as markdownWhat elements create writes, what each directory is for, and where your
code goes.
The first file you likely want to edit is app/pages/home/template.ehtml, the
page the user has open in their browser.
.elements/ # private build/server state; do not read, write, or search here
AGENTS.md # agent instructions (this getting-started guide)
index.ts # app entry: creates the App, registers routes, starts the server
worker.ts # services setup for job and test worker processes
config.jsoc # dependencies, the import map, and app config
package.lock # resolved dependency lockfile (generated; don't edit by hand)
config/
env/
development.env # committed; dev-safe values
production.env # gitignored; production secrets
app/
pages/
home/
index.ts # route handler
template.ehtml # page markup and inline TypeScript
style.css # page styles
test.ts # page tests
services.ts # per-page rpc + shared interfaces (add by hand as the page grows)
errors/
not-found/ # 404 page
unhandled/ # 500 page
shared/
templates/ # reusable templates (layout, etc.)
services/ # cross-page rpc, LiveTables, channels
styles/
page.css # site-wide page baseline (imports @elements/style)
email.css # site-wide email baseline
vars.css # design-token overrides
assets/ # favicons, images
jobs/ # background jobs
emails/ # email templates
migrations/ # generated migration files
types/
session.d.ts # SessionData augmentation (types session.get())
node_modules/
There is no root package.json or tsconfig.json. Dependencies, the import
map, and TypeScript options all live in config.jsoc, and the resolved
dependency set is written to package.lock (don't edit it by hand). Some
folders (app/jobs/, app/emails/, app/shared/services/,
app/migrations/) are created as you need them.
Hidden folders at the project root (anything starting with ., like
.elements/, .git/, .claude/) are not part of your source tree. Don't
grep, glob, or otherwise search inside them. Ripgrep and editor search already
skip hidden folders by default; if you shell out to grep -r or find, pass
--exclude-dir=.elements (grep) or -not -path '*/.elements/*' (find).
Quote a glob you pass to a flag, as in grep -rn load app --include='*.ts'.
zsh, the default shell on macOS, fails the whole command on an unquoted glob
that matches no file, and nothing after it on the line runs.
The Entry Point
index.ts at the project root constructs the App, registers routes, and
starts the server:
import { App } from "@elements/app";
import config from "#config";
import home from "#app/pages/home";
const app = new App();
app.route("/", home);
app.start(config);
worker.ts brings up the shared services (the database pool, email) that job
and test worker processes need, so sql(), tx(), and email() work inside
jobs and tests the same way they do in an @rpc handler. Delete it if your app
has no jobs and no database-touching tests.
Creating From a Scaffold
-scaffold creates the app from a published Elements app instead of the
built-in templates:
elements create myshop -scaffold=elementscode/scaffold-ecommerce
Name the scaffold as a GitHub repository in any of its spellings (shorthand,
the address bar, the clone url, the ssh remote) or as a link to a .tar.gz
archive. Add @ref for a branch, a tag, or a commit.
Elements fetches the scaffold as an archive, so nothing here needs git
installed and no history comes with it. The project is created first, exactly
as it is without -scaffold, and the scaffold is extracted over it.
AGENTS.md is rewritten with the current directives, and anything under
.elements in the archive is skipped so the project keeps its own id.
A scaffold writes an app's own config.jsoc, styles, and entry point, so it
applies to a new project and not over one that already has files in it. A
half-applied scaffold is not a smaller app, it is a broken one. Creating the
app and applying the scaffold is one step.
To publish a scaffold, push an Elements app to a public repository.