MaCH repo

This commit is contained in:
2025-07-24 12:46:01 -05:00
committed by Nick Ricketts
commit 9b7f2a7973
75 changed files with 6431 additions and 0 deletions

876
llms-full.txt Normal file
View File

@@ -0,0 +1,876 @@
# MACH (Modern Asynchronous C Hypermedia)
Declarative framework for asynchronous, reactive web apps in C23. Pipelines transform requests via composable steps. Data flows as arena-backed strings interpolated with `{{key}}`. Memory, concurrency, and async I/O are managed by the framework: no malloc/free, no threads/mutexes/locks in app code.
Bundled engines/integrations: SQLite, Postgres, MySQL, Redis/Valkey, DuckDB, HTMX, Datastar, Tailwind, session_auth.
================================================================
NOTATION
================================================================
C designated initializers at different brace depths:
{} single value or struct .get = { ... }
{{}} array of structs .databases = {{ ... }}
Multiple array elements: comma-separated inner braces: `.databases = {{...}, {...}}`.
Inside steps that accept multiple items (query, validate): `query({...}, {...})`.
Templates use Mustache SECTIONS, not dot paths. Open the section first, even for single-row queries:
✅ {{#blog}}{{title}}{{/blog}} (single-row or multi-row)
✅ {{#blog}}{{#comments}}{{body}}{{/comments}}{{/blog}} (joined data)
❌ {{title}} at root (no top-level key)
❌ {{blog.title}} (dot paths not supported)
Mustache tags must live inside C string literals. Section open/close tags on their own lines must be quoted:
"{{#blog}}"
"<h1>{{title}}</h1>"
"{{/blog}}"
================================================================
CONFIG STRUCT (top-level)
================================================================
Every app and module returns `config`. Root must define `mach()`. Modules define their own functions, registered in `.modules` by bare function reference.
```c
config mach(){ return (config){
.name = "...", // module identifier
.context = {{ "key", value }, ...}, // shared constants/assets
.publishes = {{ "event", .with={...} }},
.databases = {{ ... }},
.tasks = {{ ... }},
.events = {{ ... }},
.modules = { fn_ref, ... }, // composed modules
.resources = {{ ... }},
.errors = {{ http_code, { steps } }},
.repairs = {{ http_code, { steps } }},
};}
```
Module wiring: bring module into scope by `#include "module/module.c"` from main.c, then list it in `.modules`. (NOT `extern` — modules are composed by direct inclusion.)
```c
// main.c
#include <mach.h>
#include "todos/todos.c"
config mach(){ return (config){ .modules = {todos, sqlite} }; }
```
When root and module define the same name (context, database, error handler), ROOT WINS.
Modules don't call each other directly — they communicate via pub/sub events.
================================================================
RESOURCES
================================================================
`.resources` defines named URL endpoints with HTTP verb pipelines. Resources are identified by name. `{{url:name}}`, `redirect()`, `reroute()` all take `name[:arg1:arg2...]` identifier. Args fill `:params` of the URL pattern. Path specificity is automatic: exact (`/todos/active`) beats parameterized (`/todos/:id`) regardless of order.
Verb selection: by HTTP method, or by passing `http_method` as query/form parameter. Lets HTML forms (GET/POST only) reach any verb, gives SSE a connection path: `/todos?http_method=sse`.
Resource fields:
.name (pos) resource identifier
.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 for resource
.get .post .put .patch .delete verb pipelines (ordered step arrays)
.sse persistent SSE channel: { "channel/{{interp}}", steps... }
.errors resource-scoped error handlers
.repairs resource-scoped repair handlers
Example with all fields:
```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") }}}
}
```
URL helpers with args (positional, colon-separated, literal or context key):
{{url:todos}} → /todos
{{url:todo:5}} → /todos/5
{{url:todo:id}} → /todos/{{id}} (id from current scope)
{{url:org_todo:acme:5}} → /orgs/acme/todos/5
================================================================
PIPELINE STEPS
================================================================
Every step accepts `.if_context` and `.unless_context` for conditional execution.
----------------------------------------------------------------
validate
----------------------------------------------------------------
Checks request parameters (query, form body, URL params) against regex. On success, value promoted from `input:name` to app scope. On failure, errors land in `error:name`, raises `http_bad_request` triggering nearest error/repair handler. All validations in a call complete before error fires (multi-error forms).
.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 validator macros (defined in mach.h, define your own the same way):
validate_not_empty "^\S+.*$"
validate_alpha "^[a-zA-Z]+$"
validate_alphanumeric "^[a-zA-Z0-9]+$"
validate_slug "^[a-z0-9]+(-[a-z0-9]+)*$"
validate_no_html "^[^<>]*$"
validate_integer "^-?[0-9]+$"
validate_positive "^[1-9][0-9]*$"
validate_float "^-?[0-9]+(\.[0-9]+)?$"
validate_percentage "^(100|[1-9]?[0-9])$"
validate_email "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
validate_uuid "^[0-9a-fA-F]{8}-...{4}-...{4}-...{4}-...{12}$"
validate_username "^[a-zA-Z][a-zA-Z0-9_]{2,31}$"
validate_date "^YYYY-MM-DD$"
validate_time "^HH:MM$"
validate_datetime "^YYYY-MM-DDTHH:MM(:SS)?$"
validate_url "^https?://...$"
validate_ipv4 "^N.N.N.N$"
validate_hex_color "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
validate_zipcode_us "^[0-9]{5}(-[0-9]{4})?$"
validate_phone_e164 "^\+[1-9][0-9]{6,14}$"
validate_cron cron expression
validate_no_sqli "^[^;'\"\\]*$"
validate_token "^[a-zA-Z0-9_-]{16,128}$"
validate_base64 "^[A-Za-z0-9+/]+=*$"
validate_boolean "^(true|false|0|1)$"
validate_yes_no "^(yes|no)$"
validate_on_off "^(on|off)$"
----------------------------------------------------------------
find & query
----------------------------------------------------------------
Both run database queries. `.db` selects database. `.set_key` stores result in context as a TABLE (even single-row). Templates open the table as a section to read fields.
SQL: either inlined via `.query` OR loaded by name as positional from `.context`. NEVER both. Multiple items in single call run CONCURRENTLY. Two back-to-back `query({...})` steps run SERIALLY. For concurrency, pass all items to one call.
Difference: `find()` raises `http_not_found` (404) when zero rows; `query()` does not.
.template_key (pos) SQL asset name in .context (mutually exclusive with .query)
.query inline SQL string with {{interpolation}} (bound as prepared-statement parameters)
.set_key context key for result table
.db database name matching .databases entry
.if_context / .unless_context (per item) conditionally include/skip individual queries
```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';"}
)
```
For transactions: use BEGIN/COMMIT/ROLLBACK directly in queries.
----------------------------------------------------------------
join
----------------------------------------------------------------
Nests records from one context table into each matching record of another (in-memory JOIN). Each outer record gains a new field holding matched inner records.
CRITICAL: nesting only works if `render()` opens the OUTER table as a section. Iterating both as siblings at root makes join() a no-op.
.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"
)
// after: blog: [{id, title, content, comments: [{...}, ...]}]
// render: "{{#blog}}<h1>{{title}}</h1>{{#comments}}<li>{{body}}</li>{{/comments}}{{/blog}}"
```
----------------------------------------------------------------
fetch
----------------------------------------------------------------
HTTP request, response stored in context. JSON auto-parsed into tables/records (nested JSON → nested tables). Plain text stored as string.
.url (pos) request URL with {{interpolation}}
.set_key context key for 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"
)
```
Multiple items in one fetch() run concurrently, like query().
HTTP methods: http_get, http_post, http_put, http_patch, http_delete, http_sse_method
----------------------------------------------------------------
exec
----------------------------------------------------------------
Calls C function or block with access to context via Imperative API. Dispatched to shared thread pool, releases reactor; pipeline resumes on original reactor when call returns. Suitable for blocking I/O and CPU-heavy work. Trigger error/repair from inside via `error_set()`.
block (pos) inline ^(){ ... } block
.call named C function reference
```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)
```
----------------------------------------------------------------
emit
----------------------------------------------------------------
Triggers internal pub/sub event. Subscribers in other modules react in their `.events` pipelines.
event_name (pos) event to publish
```c
emit("todo_created")
```
----------------------------------------------------------------
task
----------------------------------------------------------------
Adds named job to task database, continues immediately (fire-and-forget). Task reactors pick up queued jobs.
task_name (pos) name of task in .tasks
```c
task("recount_todos")
```
----------------------------------------------------------------
sse
----------------------------------------------------------------
Pushes Server-Sent Event. With `.channel`: broadcast to all clients on that channel. Without: returned to requesting client.
.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 (provided by datastar module)
----------------------------------------------------------------
Datastar-formatted SSE for DOM updates and reactive client state. With channel: broadcast. Without: requesting client only.
.channel (pos) broadcast channel with {{interpolation}}
.target DOM element id for update
.mode fragment insertion mode
.elements render_config for 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 Mustache template using current context. Templates referenced by name from .context or inlined. Sections only, no dot paths.
.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 template engine: mustache (default) or mdm (Markdown-with-Mustache)
Accepts bare identifier or string: mustache, "mustache", mdm, "mdm"
.json_table_key context table to serialize as JSON response (auto-sets application/json)
```c
render("todos")
render(.template = "<h1>Hello {{name}}</h1>")
render("not_found", .status = http_not_found)
render(.engine = mdm, .template = "# Hello {{name}}\n\nYou have **{{count}}** todos.")
render(.json_table_key = "todos")
```
----------------------------------------------------------------
headers & cookies
----------------------------------------------------------------
Set HTTP response headers/cookies. 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 302 to client (browser navigates). `reroute()` re-enters router server-side, executes another resource's pipeline within same request. Both take resource identifier `name[:arg1:arg2...]`. Args literal or context key, with {{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}}
redirect("org_todo:acme:5") // 302 to /orgs/acme/todos/5
reroute("todo:{{id}}") // run that pipeline in-process
```
----------------------------------------------------------------
nest
----------------------------------------------------------------
Groups multiple steps into composite step. Apply one .if_context/.unless_context to a group.
.steps (pos) array of steps run as unit
.if_context / .unless_context
```c
nest({query({...}), emit("urgent_todo"), render("urgent")},
.if_context = "is_urgent")
```
================================================================
TEMPLATE HELPERS
================================================================
Helpers use `{{helper:args}}` syntax. Args positional, colon-separated, literal or context key.
{{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 after error).
{{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 associated with 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 <input> for forms.
CSRF: verification automatic. MACH checks submitted token (form/query) matches cookie value, rejects mismatches with 403. Nothing beyond emitting the helper required.
```c
render(.template =
"<link rel='stylesheet' href='{{asset:styles.css}}'>"
"<article>"
"{{#post}}<h2>{{title}}</h2>"
"<p>Rating: {{precision:score:1}}/5</p>"
"<div>{{raw:body_html}}</div>{{/post}}"
"<form method='post' action='{{url:comments}}'>"
"{{csrf:input}}"
"<input name='body' value='{{input:body}}'>"
"{{#error:body}}<span>{{error_message:body}}</span>{{/error:body}}"
"<button>Comment</button>"
"</form>"
"<a href='{{url:logout}}?csrf={{csrf:token}}'>Log out</a>"
"</article>"
)
```
================================================================
CONTEXT
================================================================
Pipelines read/write a shared context: scoped key-value store living for duration of request. Every step draws inputs from context, writes outputs back.
`.context` seeds at root with variables and assets available on every request. Templates and SQL stored here referenced by name in render(), query(), find().
Use `(asset){#embed "file"}` to bake files into binary at compile time. Docker secrets exposed to container available in context.
Three scopes:
input:xxx raw request parameters
error:xxx validation/error data
unprefixed app scope (query results, validated inputs, context variables)
`validate()` bridges input → 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 forward-only, index-based: array order, applied once each, append new at end. Seeds idempotent. Both tracked in `mach_meta` table.
Multi-tenant: `{{interpolation}}` in `.connect`. Connections pooled with LRU eviction.
.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
ONE DATABASE = ONE DOMAIN, MANY TABLES. A database maps to a domain (todos_db, blog_db), not an entity. Related tables go as additional migrations on the same database. Reach for second database only for genuinely separate domains (audit logs, analytics, third-party cache).
```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');"}
}}
```
================================================================
ERROR & REPAIR PIPELINES
================================================================
When a step fails, execution halts; MACH searches handler bottom-up: resource → module → root.
ERRORS are TERMINAL: matching pipeline sends response, ends request.
REPAIRS are RESUMABLE: fix context, then resume original pipeline at step AFTER failure.
If no matching repair: falls through to errors. Unhandled errors surface in TUI console + telemetry.
Error scope shared across validate() failures and error_set(): `{{error:name}}`, `{{error_code:name}}`, `{{error_message:name}}`. Raw input remains in `input:name` for form re-rendering.
```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 (any integer works; http_* are convenience names; define your own for domain errors, e.g. `#define err_quota_exceeded 723`):
http_ok = 200
http_created = 201
http_redirect = 302
http_bad_request = 400
http_not_authorized = 401
http_not_found = 404
http_error = 500
================================================================
EVENT PIPELINES
================================================================
Internal pub/sub for cross-module communication. Publisher doesn't know subscribers; subscribers don't know emitter. Adding a subscriber = new module with `.events` entry, no changes to publisher.
DURABLE BY DEFAULT: when `.publishes` is defined anywhere, MACH creates `mach_events` database to track delivery. Process crash → undelivered events replay on next boot.
.publishes outbound event contracts: {.event = name, .with = {context_keys_to_pass}}
.events subscriber pipelines keyed by event name
```c
// publisher (todos module)
.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 (activity module)
.events = {
{"todo_created", {
query({.db = "activity_db",
.query = "insert into activities(kind, user_id, ref) values('created', {{user_id}}, {{title}});"})
}}
}
```
================================================================
TASK PIPELINES
================================================================
Named pipelines run asynchronously on task reactors. Fire-and-forget: calling pipeline continues immediately. Defined at module or root level. Triggered on demand with `task("name")` or on schedule via `.cron`. Tasks can enqueue more tasks via task().
DURABLE BY DEFAULT: when `.tasks` defined, MACH creates `mach_tasks` database, checkpoints context after each step. Crash mid-task → resumes at exact step where left off (5 steps into 8-step pipeline restarts at step 6, not step 1).
.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 task
.cron standard cron schedule (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
================================================================
Module = self-contained system. Can declare its own resources, databases, migrations, context, error/repair handlers, tasks, event subscribers, nested modules.
Root main.c must define `mach()`. Modules define their own functions with any name, registered in `.modules` by bare function reference.
Bring module into scope by `#include`ing its `.c` file from main.c, then register it.
```c
// main.c
#include <mach.h>
#include "blogs/blogs.c"
config mach(){ return (config){ .modules = {blogs, sqlite} }; }
```
A module returns config with same shape as root (`.resources`, `.databases`, `.events`, etc.) plus `.name` for identity. Resource fields like `.url`, `.mime`, `.get` are NOT top-level config fields — they belong inside `.resources` entries.
When root and module both define same name (context variable, database, error handler): ROOT WINS.
Modules don't call each other directly; they communicate through pub/sub events.
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 initializer to .modules):
sqlite, postgres, mysql, redis, duckdb, htmx, datastar, tailwind, session_auth
MODULE-PROVIDED STEPS (session_auth):
session() attach current session to context (sets user_id, etc.); no-op when unauth
logged_in() guard, raises http_not_authorized when no active session
login() use inside POST pipeline to perform login
logout() use inside POST pipeline to perform logout
signup() use inside POST pipeline to perform signup
Common pattern: drop into resource's shared `.steps` slot as middleware:
```c
{"dashboard", "/dashboard", {session(), logged_in()},
.get = { render("dashboard") }
}
```
================================================================
STATIC FILES
================================================================
Files in `public/` at project root served directly. Use for images, fonts, pre-built CSS/JS, assets that don't need binary embedding.
Reference with `{{asset:filename}}` → URL with content-based checksum + immutable cache headers (browsers cache indefinitely, refresh when content changes).
```html
<link rel="icon" href="{{asset:favicon.png}}">
<link rel="stylesheet" href="{{asset:styles.css}}">
<script src="{{asset:app.js}}"></script>
```
================================================================
EXTERNAL DEPENDENCIES
================================================================
MACH expects containerized dev environment. Standard C23 against MACH APIs; no local toolchain required.
Two ways to bring in third-party C libraries:
`/vendor` directory: drop headers and libraries (.so, .a); auto-compiler discovers, includes, links them.
```
/vendor/
├── libsodium.h
└── libsodium.so
```
Custom Dockerfile: inherit from MACH base image, apt-get install system deps; reference from compose.yml.
```dockerfile
FROM mach:latest
RUN apt-get update && apt-get install -y libsodium-dev
```
Two helpers for bridging foreign memory back to the arena:
allocate(bytes) buffer from pipeline arena, reclaimed on request completion
defer_free(ptr) schedules cleanup for pointers from external libraries (e.g. malloc); runs when arena released
```c
char *buf = allocate(256);
char *out = third_party_alloc(256);
defer_free(out);
```
================================================================
ARCHITECTURE
================================================================
DATA-ORIENTED PIPELINES: mach() runs once at boot. Returned config processed into execution graph with precompiled pipelines, queries, templates. Each request executes matching pipeline as sequence of pre-warmed steps.
MULTI-REACTOR ARCHITECTURE:
Request Reactors handle HTTP traffic; each gets dedicated CPU core + event loop
Task Reactors handle background work; each gets dedicated core, monitors task DB, processes cron
Shared Thread Pool CPU-bound and blocking I/O work on remaining cores
When pipeline executes exec() step, work dispatched to shared thread pool, releases reactor; pipeline resumes on original reactor when call returns. task() step adds jobs to task DB picked up by task reactors. Tasks can call task() to enqueue more work.
Application code does NOT manage threads, mutexes, or locks. Multi-reactor architecture isolates request state to pipeline's context.
Request/task/cpu ratio settable in compose.yml.
MEMORY SAFETY: Each reactor maintains pool of arena allocators. Request → pipeline gets arena, all allocations from that arena. Pipeline complete → arena cleared, returned to pool. App code never calls malloc/free. No leaks, double-frees, use-after-free.
All framework data structures (tables, records, strings) enforce bounds checking. OOB reads + missing context values return nullptr instead of faulting. Pipelines exceeding memory limit (default 5MB, configurable) abort with 500, mitigates OOM DoS.
SQL INJECTION PREVENTION: Interpolations like `{{user_id}}` inside query()/find() bound as parameters in prepared statements.
XSS PREVENTION: render() auto-escapes context values in Mustache templates. Raw HTML requires explicit `{{raw:field}}` opt-in.
STRING INTERPOLATION: Any string (SQL, URLs, connection strings, templates) can reference context with `{{context_key}}`. Same scopes as Context section apply everywhere.
================================================================
IMPERATIVE API (available from exec blocks/functions)
================================================================
Context:
void* get(string name)
void set(string name, void const *value)
bool has(string name)
string format(string format_string)
Memory:
void* allocate(int bytes)
void defer_free(void const *ptr)
Errors:
void error_set(string name, error err)
error error_get(string name)
bool error_has(string name)
Tables:
table table_new()
int table_count(table)
record table_get(table, int index)
void table_add(table, record)
void table_remove(table, record)
void table_remove_at(table, int index)
Records:
record record_new()
void record_set(record, string name, string value)
string record_get(record, string name)
void record_remove(record, string name)
Types:
string = const char*
asset = const char[]
table = struct table_s const*
record = struct record_s const*
config = struct config_i const
================================================================
CONSTANTS REFERENCE
================================================================
MIME types (for .mime):
mime_html = "text/html"
mime_txt = "text/plain"
mime_sse = "text/event-stream"
mime_json = "application/json"
mime_js = "application/javascript"
HTTP methods (for fetch .method):
http_get, http_post, http_put, http_patch, http_delete, http_sse_method
HTTP statuses (for render .status, error .error_code):
http_ok = 200, http_created = 201, http_redirect = 302
http_bad_request = 400, http_not_authorized = 401, http_not_found = 404
http_error = 500
Database engines: sqlite_db, postgres_db, mysql_db, redis_db, duckdb_db
Render engines: mustache (default), mdm (Markdown-with-Mustache); accepts bare identifier or string
Datastar modes (ds_sse .mode):
mode_outer, mode_inner, mode_replace, mode_prepend, mode_append, mode_before, mode_after, mode_remove
================================================================
KEY PATTERNS
================================================================
PATTERN: POST → validate → insert → redirect (POST-redirect-GET)
```c
.post = {
validate({"title", .validation = validate_not_empty, .message = "title cannot be empty"}),
query({.db = "db", .query = "insert into todos(title) values({{title}});"}),
redirect("todos")
}
```
PATTERN: error handler that re-renders form with input/error preserved
Use reroute() to re-enter the GET pipeline. input: and error: scopes persist across reroute.
```c
.get = { query(...), render(.template = "...{{input:title}}...{{#error:title}}{{error_message:title}}{{/error:title}}...") },
.post = { validate(...), query(...), redirect("todos") },
.errors = { {http_bad_request, { reroute("todos") }} }
```
PATTERN: nested data via concurrent query + join + render
```c
.get = {
query(
{.set_key = "blog", .db = "db", .query = "select id, title from blogs where id = {{id}};"},
{.set_key = "comments", .db = "db", .query = "select id, blog_id, body from comments where blog_id = {{id}};"}
),
join(.target_table_key = "blog", .target_field_key = "id",
.nested_table_key = "comments", .nested_field_key = "blog_id",
.target_join_field_key = "comments"),
render(.template = "{{#blog}}<h1>{{title}}</h1><ul>{{#comments}}<li>{{body}}</li>{{/comments}}</ul>{{/blog}}")
}
```
PATTERN: task triggered both ways (cron + on-demand)
```c
.post = {
validate(...), query(...),
task("record_daily_stats"), // on-demand from request pipeline
redirect("todos")
},
.tasks = {
{"record_daily_stats", {
query({.db = "db", .query = "insert into daily_stats(todo_count) select count(*) from todos;"})
}, .cron = "0 0 * * *"} // also nightly
}
```
PATTERN: pub/sub between modules
Publisher declares `.publishes = {{"event_name", .with = {keys_to_pass}}}`, calls `emit("event_name")` in pipeline.
Subscriber (separate module) declares matching `.events = {{"event_name", {pipeline}}}`. Neither references the other.
PATTERN: external assets via #embed
```c
.context = {
{"todos_list", (asset){#embed "todos_list.mustache.html"}},
{"get_todos", (asset){#embed "get_todos.sql"}}
},
// then reference by name:
query({"get_todos", .set_key = "todos", .db = "todos_db"}),
render("todos_list")
// migrations accept assets directly:
.migrations = { (asset){#embed "create_todos_table.sql"} }
```
PATTERN: external HTTP data
```c
.get = {
fetch("https://api.example.com/random", .set_key = "data"),
render(.template = "{{#data}}<p>{{field}}</p>{{/data}}")
}
```
PATTERN: middleware via shared resource steps
The unnamed positional brace after the URL holds shared `.steps` that run before every verb pipeline.
```c
{"dashboard", "/dashboard", {session(), logged_in()},
.get = { ... },
.post = { ... }
}
```
================================================================
TOOLING (for context only — not API)
================================================================
Built-in TUI editor with HMR, LSP, integrated source control, topology-aware AI assistant. AI uses `app_info` command to inspect topology (routes, pipelines, schemas, event contracts, module boundaries).
CLI commands:
app_info [resources|pipelines|events|databases] inspect topology
unit_tests criterion-based unit tests
e2e_tests playwright browser tests
app_debug interactive debugger in TUI
app_build production Docker image
Deployment: standard Docker container. Does NOT terminate TLS — place behind reverse proxy/LB (Nginx, Caddy, AWS ALB).
Observability: every pipeline step emits OpenTelemetry spans. Logs/traces/errors/auto-profiling on telemetry server :4000. No manual instrumentation.
Built with: C23, Docker, libmicrohttpd, libuv, Mustach, Jansson, curl, Fossil, Fresh, clangd, LLDB, Criterion, Playwright, SigNoz + OpenTelemetry, Open Code.
License: LGPL.