# MACH (Modern Asynchronous C Hypermedia) Declarative framework for asynchronous, reactive web apps in C23. Below is a tutorial guide that builds a working app step by step, then a complete reference. Every code block is real, compilable code (not pseudo-code). ## Critical rules - Templates use Mustache **sections**, not dot paths. Open the section first, even for single-row queries: `{{#blog}}{{title}}{{/blog}}`, never `{{blog.title}}` or bare `{{title}}` at root. - Inline templates are C string literals. Every Mustache tag, including section open/close tags on their own lines, must be inside quotes. - Modules are composed by `#include "module/module.c"` from `main.c` and listing them in `.modules`. **Never** use `extern` declarations. - Modules communicate only through pub/sub events (`emit()` + `.publishes` + `.events`). They never call each other directly. - Resource fields like `.get`, `.url`, `.mime` are not top-level config fields — they belong inside entries of `.resources`. - Multiple items in a single `query()` or `fetch()` call run **concurrently**. Two back-to-back calls run serially. - SQL `{{interpolation}}` is bound as prepared-statement parameters. Never spliced. - `render()` auto-escapes context values. Use `{{raw:field}}` only when the value is trusted HTML. - Application code never calls `malloc`/`free` and never manages threads. Use `allocate(bytes)` and `defer_free(ptr)` for arena-managed memory inside `exec()`. --- ## Guide A walkthrough that builds a working todo app in nine steps, introducing one concept at a time. ### 1. A Page Two resources, each with a GET pipeline. The home page links to the todos page with `{{url:todos}}`, which resolves to the target resource's URL at render time. ```c #include config mach(){ return (config) { .resources = { {"home", "/", .get = { render(.template = "" "

Welcome

" "My Todos" "" ) } }, {"todos", "/todos", .get = { render(.template = "" "

My Todos

" "

Nothing yet.

" "" ) } } } }; } ``` Each resource names itself (`"home"`, `"todos"`) so other pages reference it by name, not hardcoded path. ### 2. Show Data Add a SQLite database with one migration and one seed, then read from it in the GET pipeline. ```c #include #include config mach(){ return (config) { .resources = { // ... home resource unchanged from step 1 {"todos", "/todos", .get = { query({.set_key = "todos", .db = "todos_db", .query = "select id, title from todos;"}), render(.template = "" "

My Todos

" "
    {{#todos}}
  • {{title}}
  • {{/todos}}
" "" ) } } }, .databases = {{ .engine = sqlite_db, .name = "todos_db", .connect = "file:todos.db?mode=rwc", .migrations = { "CREATE TABLE todos (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "title TEXT NOT NULL" ");" }, .seeds = {"INSERT INTO todos(title) VALUES('Learn MACH');"} }}, .modules = {sqlite} }; } ``` `query` runs the SELECT and stores the rows under `todos` in pipeline context. `render` walks the section with `{{#todos}}...{{/todos}}`. Migrations run on the first connection. > **Concurrent queries.** Multiple items in a single `query()` run concurrently. Two separate `query({...})` steps run serially. > ```c > query( > {.set_key = "todos", .db = "todos_db", .query = "select id, title from todos;"}, > {.set_key = "count", .db = "todos_db", .query = "select count(*) as n from todos;"} > ) > ``` > **Sections, not dot paths.** Write `{{#todos}}{{title}}{{/todos}}`, not `{{todos.title}}`. Same syntax for single-row records: `{{#count}}{{n}}{{/count}}`. Dot paths render empty. ### 3. Accept Input Add a POST verb that validates a `title` parameter, inserts it, and redirects back to GET (POST-redirect-GET). ```c config mach(){ return (config) { .resources = { // ... home resource unchanged {"todos", "/todos", .get = { // ... unchanged from step 2; template gains a form (see below) }, .post = { validate({"title", .validation = validate_not_empty, .message = "title cannot be empty"}), query({.db = "todos_db", .query = "insert into todos(title) values({{title}});"}), redirect("todos") } } }, // ... .databases and .modules unchanged from step 2 }; } ``` The GET template gains a form pointing at `{{url:todos}}`: ```c "
" "" "" "
" ``` The POST pipeline validates first; on success, the title is promoted from `input:title` to app scope. Interpolated `{{title}}` is bound as a prepared-statement parameter, not spliced. `redirect("todos")` returns 302 to `/todos`. ### 4. Handle Errors Validation failure raises `http_bad_request`. Add a resource-scoped error handler that re-enters the GET pipeline with `reroute("todos")`, and add error markup to the form template. ```c {"todos", "/todos", .get = { query({.set_key = "todos", .db = "todos_db", .query = "select id, title from todos;"}), render(.template = "" "

My Todos

" "
    {{#todos}}
  • {{title}}
  • {{/todos}}
" "
" "" "{{#error:title}}{{error_message:title}}{{/error:title}}" "" "
" "" ) }, .post = { // ... unchanged from step 3 }, .errors = { {http_bad_request, { reroute("todos") }} } } ``` `reroute("todos")` re-enters the GET pipeline in-process. The `input:` and `error:` scopes persist across the reroute, so `{{input:title}}` repopulates the field and `{{#error:title}}` renders the message. ### 5. Nested Data Add a `/todos/:id` page that fetches a todo and its comments concurrently, nests comments inside the todo record, and renders them together. Comments belong to the same domain as todos, so the new `comments` table is added as a migration on the existing `todos_db`. The todos list also gains links: ``. ```c {"todo", "/todos/:id", .get = { validate({"id", .validation = validate_integer, .message = "must be an integer"}), query( {.set_key = "todo", .db = "todos_db", .query = "select id, title from todos where id = {{id}};"}, {.set_key = "comments", .db = "todos_db", .query = "select id, todo_id, body from comments where todo_id = {{id}};"} ), join( .target_table_key = "todo", .target_field_key = "id", .nested_table_key = "comments", .nested_field_key = "todo_id", .target_join_field_key = "comments" ), render(.template = "" "{{#todo}}" "

{{title}}

" "

Comments

" "
    {{#comments}}
  • {{body}}
  • {{/comments}}
" "{{/todo}}" "" ) } } ``` New migration appended to `todos_db.migrations`: ```c "CREATE TABLE comments (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "todo_id INTEGER NOT NULL REFERENCES todos(id)," "body TEXT NOT NULL" ");" ``` Two queries run in parallel under one `query()` call. `join()` lifts `comments` inside each todo record. The template enters `{{#todo}}` first and reaches `{{#comments}}` from within. Iterating `{{#comments}}` at root would skip the nesting; dot paths like `{{todo.title}}` do not resolve. ### 6. Tasks Tasks are named pipelines that run asynchronously on task reactors. Triggered on a cron schedule or enqueued from another pipeline with `task("name")`. Add a nightly task that records the current todo count, and re-run it from POST so stats stay fresh. ```c config mach(){ return (config) { .resources = { // ... home, todo/:id unchanged {"todos", "/todos", .get = { /* unchanged from step 4 */ }, .post = { validate({"title", .validation = validate_not_empty, .message = "title cannot be empty"}), query({.db = "todos_db", .query = "insert into todos(title) values({{title}});"}), task("record_daily_stats"), redirect("todos") }, .errors = { {http_bad_request, { reroute("todos") }} } } }, .tasks = { {"record_daily_stats", { query({.db = "todos_db", .query = "insert into daily_stats(todo_count) " "select count(*) from todos;"}) }, .cron = "0 0 * * *"} }, .databases = {{ .engine = sqlite_db, .name = "todos_db", .connect = "file:todos.db?mode=rwc", .migrations = { // ... existing todos and comments tables, plus: "CREATE TABLE daily_stats (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," "todo_count INTEGER NOT NULL" ");" }, .seeds = {"INSERT INTO todos(title) VALUES('Learn MACH');"} }}, .modules = {sqlite} }; } ``` The same task definition is reused two ways: `.cron` runs it nightly, `task("record_daily_stats")` enqueues it on demand from the POST. Both invocations land on a task reactor, on separate cores from request reactors, so POST returns immediately. If a task needs values from the calling context, list them in `.accepts` and reference inside the task with `{{user_id}}` interpolation. Tasks are durable: if the process crashes mid-task, MACH checkpoints context after each step and resumes at the step where it left off. ### 7. Modules & Events Features split into self-contained modules. A module declares its own resources, databases, migrations, context, error/repair handlers, tasks, and event subscribers. Modules communicate through pub/sub events. Modules are plain C files. Each defines a function returning `config`. `main.c` pulls them in with `#include` and registers in `.modules`. ``` . ├── activity/ │ └── activity.c ├── todos/ │ └── todos.c └── main.c ``` **`main.c`** — thin root, composes modules and handles cross-cutting concerns. ```c #include #include #include "todos/todos.c" #include "activity/activity.c" config mach(){ return (config){ .resources = { {"home", "/", .get = { render(.template = "" "

Welcome

" "My Todos · " "Activity" "" ) } } }, .modules = {todos, activity, sqlite} }; } ``` **`todos/todos.c`** — the existing todos config from step 6, wrapped as `config todos()` with `.name` and `.publishes`. POST gains `emit("todo_created")`. ```c #include #include config todos(){ return (config){ .name = "todos", .publishes = { {"todo_created", .with = {"title"}} }, .resources = { // ... todos and todo/:id resources from step 6 // POST gains: emit("todo_created") between query() and redirect() }, .tasks = { // ... record_daily_stats from step 6 }, .databases = {{ // ... todos_db with todos, comments, daily_stats from step 6 }}, .modules = {sqlite} }; } ``` **`activity/activity.c`** — new subscriber module with its own database, its own resource, and an event handler. ```c #include #include config activity(){ return (config){ .name = "activity", .resources = { {"activity", "/activity", .get = { query({.set_key = "activities", .db = "activity_db", .query = "select kind, ref, created_at from activities " "order by created_at desc;"}), render(.template = "" "

Activity

" "
    {{#activities}}" "
  • {{created_at}}: {{kind}} — {{ref}}
  • " "{{/activities}}
" "" ) } } }, .events = { {"todo_created", { query({.db = "activity_db", .query = "insert into activities(kind, ref) " "values('created', {{title}});"}) }} }, .databases = {{ .engine = sqlite_db, .name = "activity_db", .connect = "file:activity.db?mode=rwc", .migrations = { "CREATE TABLE activities (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "kind TEXT NOT NULL," "ref TEXT NOT NULL," "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ");" } }}, .modules = {sqlite} }; } ``` When POST in `todos` calls `emit("todo_created")`, MACH propagates `title` from current context to every subscriber's pipeline. Neither module references the other; they only agree on the event name and payload. Events are durable. With `.publishes` defined, MACH tracks delivery in a `mach_events` database — undelivered events replay on next boot. Adding a third subscriber is a new file with `.events`; the publisher doesn't change. ### 8. External Assets Once templates and SQL grow past a few lines, extract them into their own files and load with `(asset){#embed "..."}` in `.context`, then reference by name. ``` . ├── activity/ │ ├── activity.c │ ├── activity.mustache.html │ ├── create_activities_table.sql │ ├── get_activities.sql │ └── insert_activity.sql ├── static/ │ └── home.mustache.html ├── todos/ │ ├── create_todos_table.sql │ ├── create_comments_table.sql │ ├── create_daily_stats_table.sql │ ├── get_todos.sql │ ├── create_todo.sql │ ├── get_todo.sql │ ├── get_comments.sql │ ├── record_daily_stats.sql │ ├── todos.c │ ├── todos_list.mustache.html │ └── todo_detail.mustache.html └── main.c ``` `static/` is not a module (no `.c` file) — it's a plain directory for root-level templates. **`main.c`** with assets: ```c #include #include #include "todos/todos.c" #include "activity/activity.c" config mach(){ return (config){ .resources = { {"home", "/", .get = { render("home") } } }, .context = { {"home", (asset){#embed "static/home.mustache.html"}} }, .modules = {todos, activity, sqlite} }; } ``` **`todos/todos.c`** with assets — pipelines reference assets by name; `.context` lists every asset; `.migrations` accept assets directly. ```c config todos(){ return (config){ .name = "todos", .publishes = {{"todo_created", .with = {"title"}}}, .resources = { {"todos", "/todos", .get = { query({"get_todos", .set_key = "todos", .db = "todos_db"}), render("todos_list") }, .post = { validate({"title", .validation = validate_not_empty, .message = "title cannot be empty"}), query({"create_todo", .db = "todos_db"}), task("record_daily_stats"), emit("todo_created"), redirect("todos") }, .errors = {{http_bad_request, { reroute("todos") }}} }, {"todo", "/todos/:id", .get = { validate({"id", .validation = validate_integer, .message = "must be an integer"}), query( {"get_todo", .set_key = "todo", .db = "todos_db"}, {"get_comments", .set_key = "comments", .db = "todos_db"} ), join( .target_table_key = "todo", .target_field_key = "id", .nested_table_key = "comments", .nested_field_key = "todo_id", .target_join_field_key = "comments" ), render("todo_detail") } } }, .tasks = { {"record_daily_stats", { query({"record_daily_stats", .db = "todos_db"}) }, .cron = "0 0 * * *"} }, .context = { {"todos_list", (asset){#embed "todos_list.mustache.html"}}, {"todo_detail", (asset){#embed "todo_detail.mustache.html"}}, {"get_todos", (asset){#embed "get_todos.sql"}}, {"create_todo", (asset){#embed "create_todo.sql"}}, {"get_todo", (asset){#embed "get_todo.sql"}}, {"get_comments", (asset){#embed "get_comments.sql"}}, {"record_daily_stats", (asset){#embed "record_daily_stats.sql"}} }, .databases = {{ .engine = sqlite_db, .name = "todos_db", .connect = "file:todos.db?mode=rwc", .migrations = { (asset){#embed "create_todos_table.sql"}, (asset){#embed "create_comments_table.sql"}, (asset){#embed "create_daily_stats_table.sql"} }, .seeds = {"INSERT INTO todos(title) VALUES('Learn MACH');"} }}, .modules = {sqlite} }; } ``` `activity/activity.c` follows the same pattern: extract its template and SQL into `activity/`, reference from `.context`. SQL interpolation (`{{title}}`) still works the same as inline — bound as prepared-statement parameters. Templates are still Mustache (sections, not dot paths). The pipeline shape is unchanged across files; only where the strings live has moved. ### 9. External Data Pipelines reach external HTTP services with `fetch()`. JSON is parsed into tables and records (nested JSON → nested context tables); plain text stored as a string. Add a quote of the day to the home page. **`static/home.mustache.html`** ```html

Welcome

{{#quote}}
{{content}} — {{author}}
{{/quote}} My Todos · Activity ``` **`main.c`** ```c config mach(){ return (config){ .resources = { {"home", "/", .get = { fetch("https://api.quotable.io/random", .set_key = "quote"), render("home") } } }, .context = { {"home", (asset){#embed "static/home.mustache.html"}} }, .modules = {todos, activity, sqlite} }; } ``` The Quotable API returns `{"author": "...", "content": "..."}`. MACH parses it into a single-row table under `quote`. Template opens `{{#quote}}...{{/quote}}` to read fields, same as query results. Like `query()`, multiple items in a single `fetch()` run concurrently. `fetch()` also supports POST/PUT/PATCH/DELETE, custom headers, JSON or text request bodies, and URLs with `{{interpolation}}`. --- ## Reference ### Notation C designated initializers at different brace depths: | Braces | Meaning | Example | |--------|---------|---------| | `{}` | Single value or struct | `.get = { ... }` | | `{{}}` | Array of structs | `.databases = {{ ... }}` | Multiple array elements: comma-separate inner braces: `.databases = {{...}, {...}}`. Inside steps, each `{}` initializes one item. Steps that accept multiple items (such as `query` and `validate`) use comma-separated items: `query({...}, {...})`. > **Fields are only reachable from inside their section.** Templates use Mustache sections, not dot paths. Open the section first, even for single-row queries. > - ✅ `{{#blog}}{{title}}{{/blog}}` works for single-row or multi-row queries > - ❌ `{{title}}` at root: no top-level `title` exists > - ❌ `{{blog.title}}`: dot paths are not supported ### Context Pipelines read from and write to a shared context: a scoped key-value store living for the duration of a request. `.context` seeds at the root with variables and assets available on every request. Templates and SQL stored here are referenced by name in `render()`, `query()`, and `find()`. Use `(asset){#embed "file"}` to bake files into the binary at compile time. Docker secrets exposed to the container are available in context. Three scopes: - `input:xxx` for raw request parameters - `error:xxx` for validation/error data - unprefixed names for app scope (query results, validated inputs, context variables) `validate()` bridges input to app scope. ```c .context = { {"site_name", "MACH App"}, {"version", "1.2.0"}, {"layout", (asset){#embed "static/layout.mustache.html"}}, {"home", (asset){#embed "static/home.mustache.html"}}, {"get_todos", (asset){#embed "todos/get_todos.sql"}}, {"create_todo", (asset){#embed "todos/create_todo.sql"}} } ``` ### Databases Each `.databases` entry defines a data store. Migrations are forward-only and index-based: they run in array order, each applied once, with new migrations appended to the end. Seeds are idempotent and safe to re-run. Both are tracked in a `mach_meta` table. Multi-tenant databases use `{{interpolation}}` in `.connect`. Connections are pooled with LRU eviction. | Field | Description | |-------|-------------| | `.engine` | Database engine constant from a module: `sqlite_db`, `postgres_db`, `mysql_db`, `redis_db`, `duckdb_db` | | `.name` | Identifier referenced by `.db` in `query()`/`find()` | | `.connect` | Engine-specific connection string with `{{interpolation}}` | | `.migrations` | Array of SQL strings or assets, applied once each in order | | `.seeds` | Array of idempotent statements, safe to re-run on every boot | ```c .databases = {{ .engine = sqlite_db, .name = "blog_db", .connect = "file:{{user_id}}_blog.db?mode=rwc", .migrations = { "CREATE TABLE blogs (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "title TEXT NOT NULL," "content TEXT NOT NULL" ");", "CREATE TABLE comments (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "blog_id INTEGER NOT NULL REFERENCES blogs(id)," "body TEXT NOT NULL" ");" }, .seeds = { "INSERT OR IGNORE INTO blogs(id, title, content) VALUES(1, 'Hello', 'First post');" } }} ``` A single database can contain multiple tables; declare each as a separate migration in array order. > **One database = one domain, many tables.** A database maps to a domain (`todos_db`, `blog_db`), not to a single entity. Related tables go as additional migrations on the same database. Reach for a second database when the domain is genuinely separate: audit logs, analytics, third-party cache. ### Resource Pipelines Each entry in `.resources` defines a named URL endpoint with HTTP verb pipelines. Resources are identified by name: `{{url:name}}`, `redirect()`, and `reroute()` all take a `name[:arg1:arg2...]` identifier with colon-separated positional args. Args fill the `:params` of the URL pattern in order. Path specificity is automatic: exact matches (`/todos/active`) beat parameterized matches (`/todos/:id`) regardless of definition order. > **`{{url:name}}` with URL params.** Arguments after the name are positional, colon-separated, and can be literals or context keys: > - ✅ `{{url:todo:5}}` resolves to `/todos/5` > - ✅ `{{url:todo:id}}` reads `id` from current scope > - ✅ `{{url:org_todo:acme:5}}` fills multiple `:params` in URL-pattern order Clients select a verb via the request method, or by passing `http_method` as a query/form parameter. This lets HTML forms (limited to GET/POST) reach any verb, and gives SSE a connection path: `/todos?http_method=sse`. | Field | Description | |-------|-------------| | `.name` *(pos)* | Resource identifier used by `{{url:name}}`, `redirect()`, `reroute()` | | `.url` *(pos)* | URL pattern with optional `:params` | | `.steps` *(pos)* | Shared steps run before every verb pipeline (unnamed positional brace block after URL) | | `.mime` | Default response content type | | `.get` `.post` `.put` `.patch` `.delete` | Verb pipelines (ordered step arrays) | | `.sse` | Persistent SSE channel: `{"channel/{{interp}}", steps...}` | | `.errors` | Resource-scoped terminal error handlers | | `.repairs` | Resource-scoped resumable repair handlers | Combined: ```c {"todo", "/todos/:id", { validate({"id", .validation = "^\\d+$", .message = "must be a number"}) }, .mime = mime_html, .get = { find({"get_todo", .set_key = "todo", .db = "todos_db"}), render("todo") }, .patch = { validate({"title", .validation = validate_not_empty, .message = "required"}), query({.db = "todos_db", .query = "update todos set title = {{title}} where id = {{id}};"}), redirect("todo:{{id}}") }, .delete = { query({.db = "todos_db", .query = "delete from todos where id = {{id}};"}), redirect("todos") }, .sse = {"todo/{{id}}", sse(.event = "ready") }, .errors = {{http_not_found, { render("404") }}} } ``` **MIME types (for `.mime`):** `mime_html`, `mime_txt`, `mime_sse`, `mime_json`, `mime_js` ### Template Helpers Helpers use `{{helper:args}}` syntax. Args are positional, colon-separated; each can be a literal or a context key. | Helper | Description | |--------|-------------| | `{{raw:field}}` | Emit context value WITHOUT HTML-escaping (`render()` escapes by default) | | `{{precision:field:N}}` | Format numeric value with N decimal places | | `{{input:field}}` | Raw, unvalidated request parameter (form repopulation) | | `{{error:field}}` | Truthy when field has error (use as Mustache section) | | `{{error_message:field}}` | Human-readable message for field error | | `{{error_code:field}}` | HTTP status code for field error | | `{{url:name[:arg1:...]}}` | Resolve resource identifier to URL | | `{{asset:filename}}` | Resolve file in `public/` to cache-busted URL | | `{{csrf:token}}` | Random hash, sets httponly/secure/samesite cookie, outputs same value inline | | `{{csrf:input}}` | Same as `csrf:token` but as hidden `` for forms | > **CSRF verification is automatic.** MACH checks that the incoming token (from form field or query parameter) matches the value stored in the CSRF cookie and rejects mismatches with a 403. Nothing beyond emitting `{{csrf:token}}` or `{{csrf:input}}` in the rendered response is required. ```c render(.template = "" "
" "{{#post}}" "

{{title}}

" "

Rating: {{precision:score:1}}/5

" "
{{raw:body_html}}
" "{{/post}}" "
" "{{csrf:input}}" "" "{{#error:body}}{{error_message:body}}{{/error:body}}" "" "
" "Log out" "
" ) ``` ### Pipeline Steps Every step accepts `.if_context` and `.unless_context` for conditional execution. #### validate Checks request parameters (query string, form body, URL params) against regex patterns. On success, each value is promoted from `input:name` to app scope. On failure, errors land in `error:name` and a `400 Bad Request` triggers the nearest error/repair pipeline. All validations in one call complete before the error fires. | Field | Description | |-------|-------------| | `.param_key` *(pos)* | Parameter name | | `.validation` | Regex pattern or built-in validator macro | | `.message` | Human-readable error | | `.optional` | Skip if parameter absent | | `.fallback` | Default value if absent | ```c validate( {"email", .validation = validate_email, .message = "must be a valid email"}, {"title", .validation = validate_not_empty, .message = "cannot be empty"}, {"page", .fallback = "1", .validation = "^\\d+$", .message = "must be a number"}, {"filter", .optional = true, .validation = "^(active|done)$", .message = "must be 'active' or 'done'"} ) ``` **Built-in validators** (define your own with `#define validate_zipcode "^\\d{5}$"`): - Strings: `validate_not_empty`, `validate_alpha`, `validate_alphanumeric`, `validate_slug`, `validate_no_html` - Numbers: `validate_integer`, `validate_positive`, `validate_float`, `validate_percentage` - Identity: `validate_email`, `validate_uuid`, `validate_username` - Dates & times: `validate_date`, `validate_time`, `validate_datetime` - Web: `validate_url`, `validate_ipv4`, `validate_hex_color` - Codes: `validate_zipcode_us`, `validate_phone_e164`, `validate_cron` - Security: `validate_no_sqli`, `validate_token`, `validate_base64` - Boolean: `validate_boolean`, `validate_yes_no`, `validate_on_off` #### find & query Both run database queries. `.db` selects the database, `.set_key` stores the result in context as a table (even single-row). Templates open the table as a section to reach fields. SQL is either inlined with `.query` or referenced by name as the positional, in which case it is loaded from `.context`. Multiple items in a single step run **concurrently**. Queries use prepared statements; interpolated `{{values}}` are bound, not spliced. For transactions, use `BEGIN`/`COMMIT`/`ROLLBACK` directly. The only difference: `find()` raises `404 Not Found` when zero rows; `query()` does not. | Field | Description | |-------|-------------| | `.template_key` *(pos)* | SQL asset name in `.context` (mutually exclusive with `.query`) | | `.query` | Inline SQL with `{{interpolation}}` (mutually exclusive with positional asset name) | | `.set_key` | Context key for result table | | `.db` | Database name matching a `.databases` entry | | `.if_context` / `.unless_context` *(per item)* | Conditionally include/skip individual queries | > **Positional asset name OR `.query`, not both.** > - ✅ `query({"get_todos", .set_key = "todos", .db = "todos_db"})` > - ✅ `query({.set_key = "todos", .db = "todos_db", .query = "select id, title from todos;"})` > - ❌ Combining the two is rejected > **Concurrency = multiple items in one step, not multiple steps.** `query({...}, {...})` runs both in parallel. Two back-to-back `query({...})` steps run serially. ```c query( {"get_todos", .set_key = "todos", .db = "todos_db"}, {.set_key = "count", .db = "todos_db", .query = "select count(*) as n from todos where user_id = {{user_id}};"}, {.if_context = "show_urgent", .set_key = "urgent", .db = "todos_db", .query = "select id, title from todos where user_id = {{user_id}} and priority = 'high';"} ) ``` #### join Nests records from one context table into each matching record of another (in-memory JOIN). After the step, each outer record gains a new field holding its matched inner records. This nesting only works if `render()` opens the outer table as a section. A template that iterates both tables as siblings at root treats `join()` as a no-op. | Field | Description | |-------|-------------| | `.target_table_key` | Outer table receiving nested children | | `.target_field_key` | Field on outer to match against | | `.nested_table_key` | Inner table to nest | | `.nested_field_key` | Field on inner pointing at outer | | `.target_join_field_key` | New field on outer holding matched inner records | ```c join( .target_table_key = "blog", .target_field_key = "id", .nested_table_key = "comments", .nested_field_key = "blog_id", .target_join_field_key = "comments" ) ``` Context shape: ``` after query(): { blog: [{id, title, content}], comments: [{id, blog_id, body}, ...] } after join(): { blog: [{id, title, content, comments: [{id, blog_id, body}, ...]}] } ``` Common failure modes (both produce empty-looking output): 1. Assuming the blog's fields are flat at root because `blog` has one row. 2. Iterating `{{#comments}}` at root instead of from within `{{#blog}}`. ```c // ❌ Wrong — fields assumed flat at root, comments iterated at root: render(.template = "
" "

{{title}}

" // empty "
{{content}}
" // empty "
    {{#comments}}
  • {{body}}
  • {{/comments}}
" // empty or unnested "
" ) // ✅ Right — enter {{#blog}} first; reach comments from within: render(.template = "
" "{{#blog}}" "

{{title}}

" "
{{content}}
" "
    {{#comments}}
  • {{body}}
  • {{/comments}}
" "{{/blog}}" "
" ) ``` #### fetch Makes an HTTP request and stores the response in context. JSON is parsed into tables and records (with nested tables for nested JSON); plain-text responses are stored as a string. | Field | Description | |-------|-------------| | `.url` *(pos)* | Request URL with `{{interpolation}}` | | `.set_key` | Context key for the response | | `.method` | HTTP method (default `http_get`) | | `.headers` | Array of `{name, value}` pairs | | `.json` | Context key serialized as JSON request body | | `.text` | Context key sent as plain-text request body | ```c fetch("https://api.payments.dev/charge", .set_key = "receipt", .method = http_post, .headers = { {"Authorization", "Bearer {{api_key}}"}, {"Idempotency-Key", "{{order_id}}"} }, .json = "order" ) ``` **HTTP methods (for `.method`):** `http_get`, `http_post`, `http_put`, `http_patch`, `http_delete`, `http_sse_method` #### exec Calls a C function or block with access to the context via the Imperative API. Execution is dispatched to the shared thread pool, which releases the reactor; the pipeline resumes on the original reactor when the call returns. Suitable for blocking I/O and CPU-heavy work. To trigger an error/repair pipeline from inside, call `error_set()`. | Field | Description | |-------|-------------| | Block *(pos)* | Inline `^(){ ... }` block | | `.call` | Reference to a named C function | ```c exec(^(){ auto t = get("challengers"); record_set(table_get(t, 0), "opponent_id", record_get(table_get(t, 1), "id")); }) exec(.call = assign_opponents) ``` **Imperative API** (available from `exec` blocks and functions): - Context: `get(name)`, `set(name, value)`, `has(name)`, `format(fmt)` - Memory: `allocate(bytes)`, `defer_free(ptr)` - Errors: `error_set(name, err)`, `error_get(name)`, `error_has(name)` - Tables: `table_new()`, `table_count(t)`, `table_get(t, i)`, `table_add(t, r)`, `table_remove(t, r)`, `table_remove_at(t, i)` - Records: `record_new()`, `record_set(r, name, value)`, `record_get(r, name)`, `record_remove(r, name)` #### emit Triggers an internal pub/sub event. Subscribers in other modules react in their `.events` pipelines. ```c emit("todo_created") ``` #### task Adds a named job to the task database and continues immediately. Fire-and-forget. Task reactors pick up queued jobs. ```c task("recount_todos") ``` #### sse Pushes a Server-Sent Event. With `.channel`: broadcast to all clients on that channel. Without: returned to the requesting client. | Field | Description | |-------|-------------| | `.channel` *(pos)* | Broadcast channel with `{{interpolation}}` | | `.event` | SSE `event:` line value | | `.data` | Array of strings, one per `data:` line | | `.comment` | SSE `:` comment line | ```c sse( .channel = "todos/{{user_id}}", .event = "todo_updated", .data = {"id: {{todo_id}}", "title: {{title}}"}, .comment = "broadcast at {{timestamp}}" ) ``` #### ds_sse Datastar-formatted SSE for DOM updates and reactive client state. Provided by the `datastar` module. With channel: broadcast. Without: requesting client only. | Field | Description | |-------|-------------| | `.channel` *(pos)* | Broadcast channel with `{{interpolation}}` | | `.target` | DOM element id for the update | | `.mode` | Fragment insertion mode | | `.elements` | `render_config` for the DOM fragment (positional asset name; supports `.template`, `.engine`) | | `.signals` | JSON string updating Datastar reactive client state without touching DOM | | `.js` | JS snippet evaluated on client | ```c ds_sse("todos/{{user_id}}", .target = "todo-list", .mode = mode_prepend, .elements = {"todo_row"}, .signals = "{\"count\": {{count}}}", .js = "window.scrollTo(0, 0)" ) ``` **Modes:** `mode_outer`, `mode_inner`, `mode_replace`, `mode_prepend`, `mode_append`, `mode_before`, `mode_after`, `mode_remove` #### render Outputs a Mustache template using the current context. Templates are referenced by name from `.context` or inlined. Sections only, no dot paths. | Field | Description | |-------|-------------| | `.template_key` *(pos)* | Asset name in `.context` | | `.template` | Inline Mustache template string | | `.status` | HTTP response status (default `http_ok`) | | `.mime` | Override response content type | | `.engine` | `mustache` (default) or `mdm` (Markdown-with-Mustache); accepts bare identifier or string | | `.json_table_key` | Context table to serialize as JSON response (auto-sets `application/json`) | ```c render("todos") render(.template = "

Hello {{name}}

") render("not_found", .status = http_not_found) render(.engine = mdm, .template = "# Hello {{name}}\n\nYou have **{{count}}** todos.") render(.json_table_key = "todos") ``` > **Mustache tags live inside C string literals.** Inline templates are concatenated by adjacent-string-literal rules. Every Mustache tag, including section open/close tags on their own lines, must be inside quotes. > - ✅ Multi-line with section tags on their own quoted lines (the canonical format): > ```c > "
" > "{{#blog}}" > "

{{title}}

" > "
    {{#comments}}
  • {{body}}
  • {{/comments}}
" > "{{/blog}}" > "
" > ``` > - ❌ Section tags on their own lines without quotes — compile error: > ```c > "
" > {{#blog}} // NOT in quotes > "

{{title}}

" > {{/blog}} > "
" > ``` **HTTP statuses (for `.status`):** `http_ok` (200), `http_created` (201), `http_redirect` (302), `http_bad_request` (400), `http_not_authorized` (401), `http_not_found` (404), `http_error` (500) **MIME types (for `.mime`):** `mime_html`, `mime_txt`, `mime_sse`, `mime_json`, `mime_js` #### headers & cookies Set HTTP response headers and cookies declaratively. Both accept an array of name/value pairs; values support `{{interpolation}}`. ```c headers({{"X-Request-Id", "{{request_id}}"}, {"Cache-Control", "no-store"}}) cookies({{"session", "{{session_id}}"}}) ``` #### redirect & reroute `redirect()` returns a 302 to the client (browser navigates). `reroute()` re-enters the router server-side, executing another resource's pipeline within the same request. Both take a resource identifier in the same `name[:arg1:arg2...]` format as `{{url:name}}`. Args can be literals or context keys, and support `{{interpolation}}`. **Context persists across reroute** (input/error scopes preserved). ```c redirect("todos") // 302 to /todos redirect("todo:5") // 302 to /todos/5 redirect("todo:{{id}}") // 302 to /todos/{{id}} from context redirect("org_todo:acme:5") // 302 to /orgs/acme/todos/5 reroute("todo:{{id}}") // run that pipeline in-process ``` #### nest Groups multiple steps into a single composite step. Apply one `.if_context`/`.unless_context` to a group instead of repeating per step. ```c nest({query({...}), emit("urgent_todo"), render("urgent")}, .if_context = "is_urgent") ``` ### Conditionals Every step accepts `.if_context` and `.unless_context`, which name a context variable. Works for any context value: validated inputs, query results, framework flags such as `is_htmx`, or flags set from `exec()`. ```c render("fragment", .if_context = "is_htmx") render("full_page", .unless_context = "is_htmx") ``` For multi-state branching, set context flags from `exec()`, then key downstream steps off them: ```c exec(.call = classify_todo), render("urgent_confirmation", .if_context = "is_urgent"), render("standard_confirmation", .unless_context = "is_urgent") ``` ### Error and Repair Pipelines When a pipeline step fails, execution halts and MACH searches for a handler bottom-up: resource → module → root. Errors are **terminal**: the matching pipeline sends a response and ends the request. Repairs are **resumable**: they fix the context and resume the original pipeline at the step after the failure. If no matching repair is found, resolution falls through to errors. The `error` scope is shared across `validate()` failures and `error_set()` calls: `{{error:name}}`, `{{error_code:name}}`, `{{error_message:name}}`. Raw input remains in `input:name` for re-rendering forms. ```c .errors = { {http_not_found, { render("404") }}, {http_bad_request, { render("form") }}, {http_error, { render("500") }} }, .repairs = { {http_not_authorized, { exec(.call = refresh_session_token) }} } ``` **Built-in error codes (for the `.error_code` positional):** `http_ok` (200), `http_created` (201), `http_redirect` (302), `http_bad_request` (400), `http_not_authorized` (401), `http_not_found` (404), `http_error` (500). Any integer works; the `http_*` constants are convenience names. Define your own for domain errors, e.g. `#define err_quota_exceeded 723`. ### Event Pipelines Internal pub/sub for cross-module communication. The publisher doesn't know who listens; the subscriber doesn't know who emits. Adding a subscriber is a new module with an `.events` entry, no changes to the publisher. Events are durable by default. When `.publishes` is defined anywhere, MACH creates a `mach_events` database to track delivery. Process crash → undelivered events replay on next boot. ```c // publisher .publishes = { {"todo_created", .with = {"user_id", "title"}} }, .resources = { {"todos", "/todos", .post = { validate({"title", .validation = validate_not_empty}), query({"insert_todo", .db = "todos_db"}), emit("todo_created"), redirect("todos") } } } // subscriber (separate module) .events = { {"todo_created", { query({.db = "activity_db", .query = "insert into activities(kind, user_id, ref) " "values('created', {{user_id}}, {{title}});"}) }} } ``` ### Task Pipelines Tasks are named pipelines that run asynchronously on task reactors. Fire-and-forget. Defined at module or root level. Triggered on demand with `task("name")` or on a schedule via `.cron`. Tasks can enqueue more tasks via `task()`. Tasks are durable by default. When `.tasks` is defined, MACH creates a `mach_tasks` database and checkpoints context after each step. Crash mid-task → resumes at the exact step where it left off. | Field | Description | |-------|-------------| | `.name` *(pos)* | Task identifier called via `task("name")` | | Steps *(pos)* | Pipeline body (second positional brace block, before designated fields) | | `.accepts` | Context keys to pull from caller into the task | | `.cron` | Standard cron schedule for recurring tasks (no caller required) | ```c .tasks = { // on-demand: enqueued via task("recount_todos") {"recount_todos", { query({.db = "todos_db", .query = "update users set todo_count = " "(select count(*) from todos where user_id = users.id) " "where id = {{user_id}};"}) }, .accepts = {"user_id"}}, // recurring: runs on schedule, no caller {"daily_digest", { query({.db = "todos_db", .query = "insert into digest_reports(generated_at) values(now());"}), emit("digest_ready") }, .cron = "0 8 * * *"} } ``` ### Modules & Composition Every MACH app and module returns a `config` struct. The root `main.c` must define a function named `mach()`; modules define their own functions with any name and register them in `.modules` by bare function reference. A module owns its own resources, databases, migrations, templates, and event contracts. When the root and a module both define something with the same name (a context variable, a database, an error handler), the **root wins**. Modules don't call each other directly; they communicate through pub/sub events. | Field | Description | |-------|-------------| | `.name` | Module identifier | | `.modules` | Other modules to compose into this one (root or nested) | Bring the module into scope by `#include`ing its `.c` file from `main.c`, then register it with `.modules`. ```c // main.c #include #include "blogs/blogs.c" config mach(){ return (config){ .modules = {blogs, sqlite} }; } ``` A module returns a `config` with the same shape as the root app (`.resources`, `.databases`, `.events`, etc.), plus a `.name` for identity. Resource fields like `.url`, `.mime`, `.get` are not top-level config fields; they belong inside entries of `.resources`. ```c #include #include config blogs(){ return (config){ .name = "blogs", .resources = { {"blog", "/blogs/:id", .get = { /* validate → query → join → render */ } } }, .databases = {{ .engine = sqlite_db, .name = "blog_db", .connect = "file:blogs.db?mode=rwc", .migrations = { "CREATE TABLE blogs (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "title TEXT NOT NULL," "content TEXT NOT NULL" ");", "CREATE TABLE comments (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "blog_id INTEGER NOT NULL REFERENCES blogs(id)," "body TEXT NOT NULL" ");" } }} }; } ``` Project layout: ``` todos/ # todos module ├── todos.c # config todos() { ... } ├── todos.mustache.html ├── create_todos_table.sql └── get_todos.sql activity/ # activity module └── activity.c static/ # root-level templates (NOT a module) ├── layout.mustache.html └── home.mustache.html public/ # static files served directly └── favicon.png main.c # registers modules ``` **Bundled modules (add the initializer to `.modules` to use):** `sqlite`, `postgres`, `mysql`, `redis`, `duckdb`, `htmx`, `datastar`, `tailwind`, `session_auth` **Module-provided steps.** Modules can ship step functions that plug into pipelines alongside built-in steps. The `session_auth` module provides: - `session()` — attaches the current session to context (sets `user_id`, etc.); no-op when unauthenticated - `logged_in()` — guard that raises `http_not_authorized` when there's no active session - `login()`, `logout()`, `signup()` — use inside POST pipelines to perform the auth action Common pattern: drop into a resource's shared `.steps` slot as middleware: ```c {"dashboard", "/dashboard", {session(), logged_in()}, .get = { render("dashboard") } } ``` ### Static Files Files placed in `public/` at the project root are served directly. Use it for images, fonts, pre-built CSS/JS. Reference them with `{{asset:filename}}`, which resolves to a URL with a content-based checksum and immutable cache headers. ```html ``` ### External Dependencies MACH expects a containerized dev environment. Standard C23 against MACH APIs; no local toolchain required. **`/vendor` directory**: drop headers and libraries (`.so`, `.a`); the auto-compiler discovers, includes, and links them. ``` /vendor/ ├── libsodium.h └── libsodium.so ``` **Custom `Dockerfile`**: inherit from the MACH base image and `apt-get install` system deps; reference from `compose.yml`. ```dockerfile FROM mach:latest RUN apt-get update && apt-get install -y libsodium-dev ``` **`allocate(bytes)`**: buffer from the pipeline arena, reclaimed on request completion. ```c char *buf = allocate(256); ``` **`defer_free(ptr)`**: schedules cleanup for pointers from external libraries (e.g. via `malloc`); runs when the arena is released. ```c char *out = third_party_alloc(256); defer_free(out); ```