Project structure
Project structure
This page describes how Mesh will work, not how it works today. It is a live spec of the developer experience, written before the code. Mesh is not released: nothing here can be installed or run yet, and any detail may change.
Mesh does not own your project. You choose where resource files live and where generated files go; Mesh reads the first and writes the second. This page proposes the starter template’s layout, using the todo-app project from Example: a todo list.
The layout
Beyond generated/model.json and the rule that domain sets the output directory, the architecture does not specify a directory layout. The folders, per-resource filenames and index.ts below are this live spec’s proposal; the milestone column describes when their underlying features are planned, not an accepted filename contract.
todo-app/
mesh.config.ts
package.json "mx": { "contracts": "generated/mx-contracts.js" }
resources/ .mx files (one resource per file)
list.mx
todo.mx
generated/ written by `mesh build`, committed, guarded
model.json
index.ts connect(), disconnect(), re-exports every action function and type
schema.ts Drizzle table definitions (adapter's build half)
mx-contracts.js composed contracts module for MX tooling (M6)
todos/ one folder per `domain`; no domain: generated/ itself
list.types.ts list.actions.ts list.validators.ts
todo.types.ts todo.actions.ts todo.validators.ts
migrations/ SQL from `mesh migrate generate`, committed
extensions/ project-local extensions, if any (build entry + run-time entry)
src/
actor.ts registers the actor type
main.ts the program
Resource files
One resource per .mx file, under the folder resources names in the configuration; nested folders are included. A configured glob or file list can select a subset instead. A file holds exactly one resource; two resources in one file is a build error.
Tag and attribute names follow Ash’s DSL, in kebab-case with the trailing ? dropped (ADR-0034). Every example is written in MX concise syntax, the indentation-based form (ADR-0041). The full list of tags is in the Resource file reference.
In Ash a belongs_to creates its foreign-key attribute as <name>_id (list_id). The roadmap uses listId, following the fixture. Which name Mesh generates is settled when relationships are built. The vocabulary mapping, row D12, records both.
table sits on the resource tag in Mesh. In Ash it lives in the data layer’s own section, because a data layer is an extension there. Mesh’s recommendation is to keep it on resource, since both v1 SQL adapters use the same table name. The vocabulary mapping, exception X1, holds the options.
The generated tree
mesh build writes generated/ and you commit it. The reason is review: a resource file is a small declarative change, and the committed TypeScript beside it is what the program actually runs. A reviewer reads the diff of both together.
| Proposed path | Written by | Feature milestone |
|---|---|---|
generated/model.json |
the compiler | M1 |
generated/todos/todo.types.ts |
the compiler | M1 |
generated/todos/todo.actions.ts |
the compiler | M2 |
generated/todos/todo.validators.ts |
the compiler | M2 |
generated/schema.ts |
the data adapter | M2 |
generated/index.ts |
the compiler (this spec’s proposal) | M2, with generated handlers |
generated/mx-contracts.js |
the compiler, composed from core plus enabled extensions | M6 |
In this proposal, your program imports generated functions and types through generated/index.ts. It exports connect and disconnect and re-exports the resource modules.
The flat export surface needs a collision rule. The roadmap rejects duplicate resource names, but does not settle collisions after action and resource names are combined into exported identifiers; adding a domain prefix or requiring unique export names are still options. Do not assume a new resource can always be added to the same import without a naming check.
The subfolder todos/ is named after the domain attribute on the resource. A resource with no domain has its files directly in generated/. Grouping by domain keeps one feature’s generated code in one folder.
Column names equal attribute names. Mesh does not transform names from camelCase to snake_case, so an attribute dueOn becomes a column dueOn. Drizzle quotes identifiers, so this works on both databases without a naming convention.
Whether Mesh should transform attribute names to a column convention is not settled. The mapping records that a name an author chooses is a value and stays as written, following the fixture; a snake_case option would be a separate decision. The vocabulary mapping, row D33.
The guard. mesh build --check regenerates the tree in memory, writes nothing, and fails if the result differs from what is committed. It catches hand edits and stale trees. See Generated code and the guard.
Whether the committed SQL in migrations/ is covered by the guard is not stated. Until it is, a hand-edited migration and a stale generated/schema.ts can disagree.
Configuration
One file, mesh.config.ts, at the project root. It names the resource folder, the output folder, the data adapter and the enabled extensions. See Configuration.
package.json carries one extra key, "mx": { "contracts": "generated/mx-contracts.js" }. That points MX tooling at the composed contracts module, which mesh build writes: one self-contained module holding the tag contracts of core plus every enabled extension (ADR-0021). It arrives in M6; before that the contracts are composed in memory for the build only.
Migrations
migrations/ holds plain SQL files that mesh migrate generate writes and you commit. They are the record of how the database got to its current shape, and they are how another machine or another environment reaches it. Nothing is applied automatically; mesh migrate apply is explicit.
For a file-backed SQLite database in development, mesh db push is quicker. It is the same schema without a history, and it is a development tool.
Extensions
extensions/ holds project-local extensions. An extension has two entries: a build-time one, which contributes tags, transforms, verifiers, emitters and mesh subcommands through a manifest, and a run-time one, which supplies behaviour. The run-time entry follows the same import rule as @mesh/runtime: no compiler, no model, no MX, so a deployed program never carries the build pipeline.
First-party extensions are installed from npm instead, and are enabled by listing them in mesh.config.ts. @mesh/ext-policies is the one that exists in v1. See Extension host.
Your own code
src/ is yours. Mesh never reads it. In the example:
src/actor.tsregisters the actor type by module augmentation, soscope.actoris typed as whatever your application says a caller is.src/main.tsis the program: connect, call actions, disconnect.
Nothing in src/ must be named a particular way, and nothing in it is generated.
What to commit
| Path | Commit? |
|---|---|
resources/*.mx |
yes, always |
generated/ |
yes, always |
migrations/*.sql |
yes, always |
mesh.config.ts, package.json, bun.lock |
yes |
extensions/ |
yes |
src/ |
yes, it is your code |
node_modules/ |
no |
*.db, *.db-journal (SQLite database files) |
no |
.env |
no |
Next
- Example: a todo list — the resources behind this layout.
- Configuration.
- Usage — the loop, including what the guard checks.