> Nerak is a declarative C23 web framework for building asynchronous web applications. An app is a data transformation expressed as pipelines: ordered arrays of steps that turn a request into a response. This file is self-contained: it has the syntax, signatures, constants, and conventions needed to write working Nerak code without following links.
## Core Model
- `config(name){ ... }` is a constructor that runs ONCE at boot and declares a module (registers resources, databases, tasks, subscribers, middleware). `app.c` declares `config(app)`.
- A **module** is any `.c` file declaring `config(name){ ... }` (e.g. `todos/todos.c` → `config(todos)`). A module's assets live in its folder; assets at the project root are shared across modules. Nerak scans the project for `config(...)` declarations and loads every module it finds.
- A **pipeline** is an ordered array of steps. For each request, Nerak runs: the resource's `.all` steps → the owning module's `middleware()` → the matched verb pipeline.
- **Context** is a scoped key/value store living for one request. Every step reads inputs from context and writes outputs back. Three scopes: `input:xxx` (raw request params), `error:xxx` (validation/error data), and unprefixed names (app scope: validated inputs, query results, computed values). `input()` promotes values from `input:` to app scope.
- **Everything is a string.** Context values are strings (scalars) or tables (query/fetch results). Interpolate strings into SQL, templates, URLs, headers, etc. with `{{context_key}}`. No dot notation in templates: use `{{#a}}{{b}}{{/a}}`, not `{{a.b}}`.
Minimal app:
```c
#include <nerak.h>
config(app){
context("hello", "<h1>Hello, world!</h1>"); // register a named template inline
resource("home", "/", .get = {respond("hello")});
}
```
## Assets (file → context naming)
Every non-`.c` file is an asset, loaded into context under its **basename** (the part before the first dot), as if `context(name, contents)` had been called in the module.
- Steps read assets by key: `mustache("todos","todos_s")`, `sqlite_query({"db","get_todos","todos_data"})`, `sqlite_config("db","file:todo.db?mode=rwc",{"create_todos_table"})`.
- `context(name, value)` seeds the same way from a string literal (for small content): `context("ping","select 1");`.
- A module is seeded with every asset from its own folder up to the project root. Assets at the root are shared by all modules; assets inside a module's folder are seen only by that module (the scan runs up the tree, never into a sibling module's folder).
- In dev, this scan is live: editing an asset reloads that file into every module holding it, and saving a `.c` file recompiles and reloads that module alone. A production build runs the scan once and compiles each module's assets into its binary.
- Comments: `{{! ignored }}`. Set delimiters: `{{=<% %>=}}`.
- Partials: `{{> name }}` inlines asset `name` against current scope.
- Layout inheritance: `{{< parent}}{{$block}}override{{/block}}{{/parent}}`. Any asset declaring `{{$block}}default{{/block}}` blocks can be a parent (this is how shared layouts work — no special layout type).
Built-in helpers (`{{helper:args}}`, colon-separated, literal or context key):
- `{{precision:field:N}}` — format number with N decimals.
- `{{input:field}}` — raw unvalidated request param (repopulate forms after a validation error).
- `{{error:field}}` — section, truthy when field has an error: `{{#error:title}}...{{/error:title}}`.
- `{{error_message:field}}` — human message for a field error.
- `{{error_code:field}}` — HTTP status code for a field error.
- `{{url:name}}` — resolve a resource name to its URL; `:params` are read from current scope by name. Works per-row inside a section.
- `{{asset:filename}}` — cache-busted URL for a `public/` file.
- `{{csrf:input}}` — hidden `<input>` with a CSRF token (for forms). `{{csrf:param}}` — `csrf=<token>` (for query strings). Both set an httponly/secure/samesite cookie; state-changing requests are verified against it.
- `{{http_verb:input}}` / `{{http_verb:param}}` — `http_method` override so forms/links reach non-GET verbs (see Resources). One pair per verb: `http_get`, `http_post`, `http_put`, `http_patch`, `http_delete`, `http_sse`. `:input` → `<input type='hidden' name='http_method' value='<verb>'>` (forms are GET/POST only); `:param` → `http_method=<verb>`, e.g. `{{url:todo}}?{{http_delete:param}}`.
## Databases
Each engine is a module: `#include <engine.h>` then register with `<engine>_config(...)`. Engines: `sqlite`, `postgres`, `mysql`, `redis`, `duckdb`. They share config; only `.conn` differs.
Migrations/seeds are forward-only, index-based (run once each in array order, append new ones to the end), tracked in `nerak_meta`. Connections are pooled with LRU eviction.
Other engines: `postgres_config("...", "postgres://...", ...)`, `mysql_config("...", "mysql://...", ...)`, `redis_config("...", "redis://...", ...)`, `duckdb_config("...", "duckdb:analytics.db", ...)`. Each has a matching `<engine>_query(...)`.
## Resources
Resource-based, not route-based. `resource("name", "/url/pattern", ...fields)`. The name is used by `{{url:name}}`, `redirect()`, `reroute()`; `:params` in patterns are filled from current scope by matching key names. Exact paths beat parameterized ones automatically (definition order irrelevant). Clients pick a verb by HTTP method or by passing `http_method` as a query/form param (lets forms reach PUT/PATCH/DELETE and gives SSE a path: `/todos?http_method=sse`). Templates emit it via `{{http_verb:input}}` / `{{http_verb:param}}` (see Templates).
Fields:
- `.all = {steps}` — run before every verb pipeline on the resource.
Every step accepts `.if_ctx` / `.not_ctx` (conditionals) and `.map` / `.map_key` (iteration). Step argument convention: leading positional values are noted *(by order)*; the rest are named (`.field = ...`).
### input — validate request params
On success promotes `input:name` → app scope. On failure writes `error:name` and raises `n_bad_request` (400) to the nearest error/repair pipeline. All validations run before the error fires, so all field errors are available together.
By order: `{ctx_key, regex, err_msg}`. Named: `.opt` (skip if absent), `.def` (default when absent). `regex` is a regex string or a built-in macro. Define custom macros: `#define n_zipcode "^\\d{5}$"`.
For non-regex checks (uniqueness, cross-field), pair with a query + `run()` calling `err_set()`.
### query — `<engine>_query(...)`
`sqlite_query`, `postgres_query`, `mysql_query`, `redis_query`, `duckdb_query`. Multiple items in one call run CONCURRENTLY. Prepared statements: interpolated `{{values}}` are bound, not spliced (SQL-injection-safe). Transactions: put `BEGIN`/`COMMIT`/`ROLLBACK` in SQL. Results are always tables (even single rows).
By order per item: `{db, q, ctx_key}`. `ctx_key` optional (omit for inserts without `RETURNING`). Named: `.err_on_empty = true` (404 if zero rows), `.if_ctx`/`.not_ctx` (per item).
By order: `join(parent_ctx_key, parent_field_key, child_ctx_key, child_field_key)`. Optional `.parent_join_key` (new field name on parent records; defaults to the child table name). After it, each parent record gains a field holding its matched child records.
`run(^(){ ... })` (inline block) or `run(.call = fn)` (named C function) for short, non-blocking logic between steps (enrich/aggregate/transform results, set flags for conditionals, call `err_set()`). `run_worker(...)` takes the same forms but offloads blocking/CPU-bound work (external libs, heavy compute, blocking I/O) to the shared thread pool, freeing the reactor; the pipeline resumes after it returns. Inside blocks/`.call` use the Imperative API.
```c
run(^(){
auto t = get("challengers");
auto p0 = tbl_get(t, 0);
auto p1 = tbl_get(t, 1);
rec_set(p0, "opponent_id", rec_get(p1, "id"));
rec_set(p1, "opponent_id", rec_get(p0, "id"));
})
run(.call = assign_opponents)
```
### emit — fire an internal pub/sub event
`emit("event_name")`. Subscribers in other modules react; no direct dependency. See Events.
### run_task — run a task inline
`run_task("task_name")`. Runs a named task inline as a step in the calling pipeline; control returns to the next step when it finishes. For reusable pipelines composed into workflows. Task must be defined with `task(...)`.
### dispatch — enqueue a durable background job
`dispatch("task_name")`. Enqueues a named task as a durable background job and returns immediately; task reactors pick it up. Checkpointed after each step, so a crash mid-task resumes where it stopped. Requires `#include <dispatch.h>`. Task must be defined with `task(...)`.
With `.chan` (by order, first value, supports interpolation) broadcasts to all clients on the channel; without it, returns to the requester. Named: `.event` (event line), `.data` (array of strings, one per data line), `.comment` (comment/keep-alive line).
`redirect("name")` returns a 302 (browser navigates). `reroute("name")` re-enters the router server-side, running another resource's pipeline within the same request. Both take only the resource name; `:params` read from context by matching key names.
### nest — group steps as one composite step
`nest({step, step, ...}, .if_ctx = "flag")` — apply one condition to several steps without repeating it.
## Imperative API (inside run/run_worker/.call)
Context: `get(name)` → stored value (string for scalars, table for results) or `nullptr`; `set(name, value)`; `has(name)` → bool; `fmt(fmtstr)` → string with `{{interpolation}}` resolved against context.
Memory: `alloc(sz)` → arena buffer (auto-reclaimed on request end); `defer_free(ptr)` → schedule `free()` for a foreign/library pointer. Do not use `malloc`/`free`.
if (title && strlen(title) > 40) rec_set(t, "is_long", "1");
}
})
```
## Conditionals
`.if_ctx = "key"` runs the step only when the value is present; `.not_ctx = "key"` only when absent. Works on any context value (validated inputs, query results, framework flags like `is_htmx`, or flags set in `run()`). For multi-state branching, set flags in `run()` and key downstream steps off them.
`.map = "table"` runs a step once per row, ALL ROWS CONCURRENTLY; the row's fields land in scope as bare `{{interpolations}}`. `.map_key = "key"` (pairs with `.map`) exposes the current row as a single-row table under `key` (useful for `mustache`). With a `ctx_key`, results collect into a table aligned with the input (one entry per row).
mustache("todo","todo_s", .map = "todos", .map_key = "todo_d") // render per row, collect into todo_s
```
## Error and Repair Pipelines
On failure, Nerak finds a handler by error code: resource `.errors`/`.repairs` first, then the module's `error()`/`repair()`; first match wins (resource overrides module for the same code). **Errors** are terminal (send a response, end request). **Repairs** are resumable (fix context, then resume at the step AFTER the failure). Repairs resolve first; unmatched repairs fall through to errors; unhandled errors fall through to Nerak's internal handler. The `error:` scope is shared by `input()` failures and `err_set()`; raw values stay in `input:name`.
Built-in codes: `n_bad_request` 400, `n_not_authorized` 401, `n_not_found` 404, `n_error` 500. Any integer works; define your own: `#define err_quota_exceeded 723`.
- `subscribe("event", { steps }, .errors=..., .repairs=...)` — subscriber pipeline (own handlers, then its module's).
- `emit("event")` — step that fires it (carries the published keys).
Add a subscriber = add a new module with `subscribe(...)`; the publisher does not change.
## Task Pipelines
A task is a named, reusable pipeline defined inside a module with `task("name", { pipeline }, ...)`. Registration and invocation are separate — three ways to invoke:
- `run_task("name")` — run it INLINE as a step in the calling pipeline; control returns to the next step when it finishes. For reusable pipelines composed into workflows.
- `dispatch("name")` — run it as a durable background job, returning immediately; task reactors pick it up. Requires `#include <dispatch.h>`, which provides the persistent task tables and checkpoints context after each step, so a crash mid-task resumes where it stopped.
- `.cron = "0 8 * * *"` — run it in the background on a schedule, no caller.
`task(...)` fields: `.accepts = {"keys"}` (pull caller context keys into the task), `.cron` (schedule), `.errors` / `.repairs` (own handlers, then its module's). Any pipeline or task can call `run()`, `run_worker()`, `run_task()`, and `dispatch()` (the last requires `dispatch.h`).
```c
// reusable, run inline via run_task("recount_todos"):
`config(name)` declares a module, usually in a `name/name.c` file whose folder holds its assets; it owns its resources, databases, migrations, tasks, event contracts, middleware, and folder assets. Nerak scans the project for `config(...)` declarations and loads every module it finds.
- `middleware(steps)` (inside `config`) — shared steps run on every request to a resource in that module (session loading, tenant resolution).
Serves the runtime as the `{{> htmx }}` partial; sets `is_htmx` on requests with the `HX-Request` header. Return a fragment to htmx and full page to direct visits via `.if_ctx`/`.not_ctx`. Use `hx-boost='true'` to upgrade links/forms. Put `{{> htmx }}` once in `<head>`.
### datastar — `#include <datastar.h>`
Serves `{{> datastar }}` partial; `datastar()` pushes reactive patches over an SSE channel (page opens a resource `.sse` channel; pipelines push patches). First value (by order) = channel (interpolation ok). Named:
- `.target` — target element to patch, an element id or CSS selector (interpolation ok).
Compiles the Tailwind classes used in templates and serves the stylesheet as `{{> tailwind }}` (put once in `<head>`). Use classes directly; no build/config.
### daisyui — `#include <daisyui.h>`
Compiles the DaisyUI classes used in templates and serves the stylesheet as `{{> daisyui }}` (put once in `<head>`). Use classes directly; no build/config.
### session_auth — `#include <session_auth.h>`
Cookie-based auth as steps. `session()` loads the current `user` record into context (run as `middleware()` in modules whose pipelines need it). `logged_in()` guards a resource (in `.all`), redirecting anonymous visitors to login. `login()` / `logout()` / `signup()` are verb-pipeline actions. The login page asset is named `login`. Templates read `{{#user}}{{short_name}}{{/user}}`.
`sqlite postgres mysql redis duckdb`. Each: `#include <engine.h>`, `<engine>_config(...)`, `<engine>_query({...})`. Shared config; only `.conn` is engine-specific.
## Static Files & External Dependencies
- `public/` files are served directly; reference with `{{asset:filename}}` (content-checksummed, cache-busted, immutable cache headers). Distinct from SQL/HTML assets (which are embedded and read by context key).
- Third-party C source: drop into `vendor/`; Nerak compiles and links it. Call from `run()`/`run_worker()`. Register library-owned pointers with `defer_free()`. For non-source deps (system packages/build tooling), provide a custom `Dockerfile`.
## Safety Guarantees (handled by the framework — do not reimplement)
- Memory: per-request arena allocators; no `malloc`/`free` in app code (use `alloc()`/`defer_free()`). All framework structures bounds-checked; OOB reads / missing keys return `nullptr` rather than faulting. Pipeline memory cap (default 5MB) aborts with 500.
- SQL injection: `{{interpolation}}` in query SQL is bound as prepared-statement parameters.
- XSS: `mustache()`/`mdm()` auto-escape; raw HTML requires explicit `{{{field}}}`/`{{&field}}`.
- CSRF: state-changing requests verified against a per-session token; emit via `{{csrf:input}}` / `{{csrf:param}}`.
## Project Layout & Run
```
.
├── app.c # config(app){ ... }
├── home.html # → every module (often the shared layout)
├── public/ # static files served directly
│ └── favicon.png
└── todos/ # a module (folder + matching .c)
├── todos.c # config(todos){ ... }
├── todos.html
├── create_todos_table.sql
└── get_todos.sql
```
Everything runs in Docker (dev server :3000, telemetry :4000) with file watching, auto-compilation, hot code reloading, and HMR: