Mesh
Describe your domain once. Mesh builds the rest.
Your domain is what your program keeps, what can be done to it, and who may do it. In Mesh you write it down as one small .mesh.mx file per entity.
From those files Mesh builds your core domain and business logic as a module of typed TypeScript functions, with the input validation, the authorization, the database tables and the migrations. Connect that module to anything: an HTTP API, a command line, a worker, a user interface, an agent. Change a file and the module is rebuilt from it, so nothing drifts.
Read the introductionQuick start
Mesh is not released yet. These pages describe Mesh 1.0.
import { List } from "./list.mesh.mx"entity :Todo table="todos" attributes uuid :id primary-key string :title min=1 boolean :done default=false timestamp :insertedAt on=:create relationships belongs-to :list entity=List actions auto=[:read, :destroy] create :create input &title &list update :rename input &title read :pending filter=() => &done === false sort asc &insertedAt policies policy :owner types=[:create, :read, :update, :destroy] authorize-if=({ actor }) => &list.ownerId === actor.idList is another entity, imported at the top like any TypeScript module.Lives in list.mesh.mx, next to this file.Plain text, in quotes.Every line is kind :name options. The first word says what the line declares.Declares the entity this file is about: one file, one entity.:name is a name. The colon says so: not a string, not a variable, the name itself.The entity's name. It shows up in everything Mesh builds: the Todo type, the createTodo function.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.The database table. Mesh writes its schema and its migrations.A section header. Everything indented under it belongs to it.What a Todo stores: one line per column.Every line is kind :name options. The first word says what the line declares.The attribute's type: uuid, which becomes string in TypeScript.:name is a name. The colon says so: not a string, not a variable, the name itself.The record's id.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.This field identifies the record.Every line is kind :name options. The first word says what the line declares.The attribute's type: string, which becomes string in TypeScript.:name is a name. The colon says so: not a string, not a variable, the name itself.The todo's text.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.At least one character. An empty title is rejected before your code runs.A number or a boolean, exactly as in TypeScript.Every line is kind :name options. The first word says what the line declares.The attribute's type: boolean, which becomes boolean in TypeScript.:name is a name. The colon says so: not a string, not a variable, the name itself.Whether the todo is complete.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.A new todo starts not done.Every line is kind :name options. The first word says what the line declares.The attribute's type: timestamp, which becomes Date in TypeScript.:name is a name. The colon says so: not a string, not a variable, the name itself.When the record was created.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.Filled in when the record is created, never by the caller.:value picks one option from a fixed list that Mesh defines.On the create action.A section header. Everything indented under it belongs to it.How a Todo connects to other entities.Every line is kind :name options. The first word says what the line declares.A Todo belongs to one List. Mesh adds a listId column to todos for you.:name is a name. The colon says so: not a string, not a variable, the name itself.The relationship's name. Load it as todo.list; refer to it here as &list.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.The entity on the other side.List is another entity, imported at the top like any TypeScript module.Imported at the top of the file.A section header. Everything indented under it belongs to it.Everything you can do with a Todo. Each action becomes one function.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.Two actions Mesh writes for you: readTodo and destroyTodo.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.:value picks one option from a fixed list that Mesh defines.The generated readTodo.:value picks one option from a fixed list that Mesh defines.The generated destroyTodo.Every line is kind :name options. The first word says what the line declares.An action that creates one record.:name is a name. The colon says so: not a string, not a variable, the name itself.The action's name. Call it as createTodo(input, context).A section header. Everything indented under it belongs to it.What the caller must send, one line per field. Nothing else gets in.&name points at something declared in this file: an attribute, a relationship, an action.Takes title exactly as declared above: a string, at least one character.&name points at something declared in this file: an attribute, a relationship, an action.The relationship declared by belongs-to above. The caller sends the id of a List; Mesh stores it in listId.Every line is kind :name options. The first word says what the line declares.An action that changes one existing record.:name is a name. The colon says so: not a string, not a variable, the name itself.The action's name. Call it as renameTodo({ id, title }, context).Every line is kind :name options. The first word says what the line declares.A query with a name.:name is a name. The colon says so: not a string, not a variable, the name itself.The action's name. Call it as pendingTodo(input, context); it returns the matching records.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.Which records come back. Mesh turns this into the SQL WHERE.Plain TypeScript. Mesh turns it into SQL when it can and runs it otherwise.&name points at something declared in this file: an attribute, a relationship, an action.Reads done on each record.A section header. Everything indented under it belongs to it.The order of the results.Sort order: asc or desc, one field per line.&name points at something declared in this file: an attribute, a relationship, an action.Oldest first.A section header. Everything indented under it belongs to it.Who may do what. An action no policy covers is forbidden: Mesh fails closed.Every line is kind :name options. The first word says what the line declares.One rule.:name is a name. The colon says so: not a string, not a variable, the name itself.The rule's name. It shows in the breakdown when a call is refused.An option on its line. Rules about a field live on that field's line, so one line tells the whole story.The action types this rule applies to.:value picks one option from a fixed list that Mesh defines.Actions with type create: here, createTodo.:value picks one option from a fixed list that Mesh defines.Actions with type read: here, readTodo and pendingTodo.:value picks one option from a fixed list that Mesh defines.Actions with type update: here, renameTodo.:value picks one option from a fixed list that Mesh defines.Actions with type destroy: here, destroyTodo.Plain TypeScript. Mesh turns it into SQL when it can and runs it otherwise.Whoever is calling. Your app passes it on every call; Mesh never guesses.&name points at something declared in this file: an attribute, a relationship, an action.Follows the list relationship to its ownerId. Mesh writes the join.Your app
Types todo.types.ts
from attributes, relationships
interface Todo {
id: string; title: string;
done: boolean; listId: string;
insertedAt: Date }
interface CreateTodoInput {
title: string; list: List["id"] }Functions todo.actions.ts
one per action, from actions, imported from #mesh
createTodo(input, context)
pendingTodo(input, context)
renameTodo, readTodo, destroyTodoInput validation todo.validators.ts
Every input is checked before your code runs: no empty title, and no field the action does not accept.
from attributes, actions
Authorization in every function
A write is checked before it runs; a read gets the policy in its query. canCreateTodo asks first.
from policies
The database
The table schema.ts
The todos table, its columns and the foreign key to List, written by the data adapter.
from the entity line, attributes, relationships
Migrations migrations/*.sql
Plain SQL from mesh migrate generate when the table changes; applied by mesh migrate apply.
from the table
For your tools
Rules for agents rules.md
from the whole file
entity :Todo table="todos"
actions auto=[:read, :destroy]
create :create
input
&title
&listThe model model.json
Every declaration with its source position: what mesh inspect prints.
from the whole file
Connect it to anything
Mesh is not a server and does not assume one. The module is your domain, and whatever you build around it calls the same functions, with the same checks.
- An HTTP API. A route handler turns the request into an input and an actor, and calls the function. The HTTP layer stays yours.
- A command line or a worker. A task calls the same function a request handler would, with the same validation and the same policies.
- A user interface. The server side of your UI calls the functions directly; there is no client to generate and keep in step.
- An agent. Give an agent's tool a function to call. The policies still decide what the agent's actor may do.
Ready for your agent
The less your agent writes, the less it can get wrong. With Mesh it writes one small declarative file per entity; everything else is built from that file by Mesh's own generators.
- Fewer places to make a mistake. The types, functions, validators and checks are generated, the same for every project and the same on every build, instead of written fresh by the agent each time.
- A smaller diff to review. You read the change to the entity file, the part that carries the intent. The generated code is committed beside it, but it follows from the file.
- Less to write, less to read. One file is the whole truth about an entity, so an agent changing a rule reads and writes that file, not a model, a schema and a service layer.
- Mistakes stop at build time, with a fix. A misspelled member in
inputis an error at that name, with the line, the column and a suggestion, which an agent can act on in one step. - Tools that answer instead of guessing.
mesh buildwrites a rules file for the agent to read;mesh inspectprints the model with the source position of every declaration;mesh explainprints the plan a call will follow.
What the agent writes, and what it does not
It writes the 28 lines of todo.mesh.mx above. It does not write the types, the action functions, the input validators, the authorization checks, the table schema, the migration SQL, the rules file or the model: the diagram above shows where each comes from.
src/domain/todo/todo.mesh.mx:16:9 error &titel is not a member of :Todo.
Did you mean &title?
More in Working with AI agents: the rules file, mesh inspect, mesh explain, and what Mesh does not solve for an agent.
Mesh is open source under the MIT licence. It is modelled on Ash, the declarative resource framework for Elixir, and runs on Bun.