Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

0.1.0

Not yet published. There is no tag and no published image. This page describes what 0.1.0 will contain; the date and the pull command go in when it ships.

The first release. One vertical slice, thin in every layer: a model is parsed, validated, provisioned into PostgreSQL, and served as a REST API whose every query — read or write — is filtered by the caller’s role.

What you can build

An application is a directory of YAML. From roughly forty lines you get:

  • A database schema, provisioned from your model and migrated additively.
  • A REST API with cursor pagination, conditional writes and RFC 9457 error documents.
  • An OpenAPI document generated per caller — entities and fields a token cannot reach are absent from it.
  • Access control at three levels — entity, field and row — compiled into every query rather than applied afterwards.
  • Lua flows, sandboxed, for the logic a declarative model cannot express.
  • Business metrics you declare and never instrument.
  • OpenTelemetry for traces, metrics and logs, over OTLP only.

See Your first application for the whole loop.

Worth knowing before you start

These are the decisions most likely to surprise you. Each is deliberate, and each has bitten somebody on a product that decided otherwise.

Money is never a number. decimal crosses every boundary as a string — in JSON and in Lua. A binary float cannot represent 0.10. Sending 12.10 as a JSON number is an error, not a rounding.

Writes require If-Match. PATCH and DELETE without one are refused with 428. Two people editing the same record is not a race condition, it is a Tuesday, and last-write-wins loses one of the changes with no error at all.

There is no PUT. A role that cannot read every field cannot send a complete representation, so the only honest full-replacement semantics would be “and blank everything I cannot see”.

A hidden row and a missing row are both 404. Answering differently would let anyone confirm an identifier by asking for it. The same reasoning makes a field you cannot read report as “not a field of this entity” — identical to a typo.

A flow runs as whoever called it. There is no runAs: and no way to elevate. A flow that reports a total across rows the caller cannot itemise is not expressible.

Metric labels are enum and boolean only. A label with a thousand values is a thousand time series. The model does not load if you try.

Running it

Requires PostgreSQL 18 or later, and an OCI engine to run the image. The runtime is a single stateless binary; run it with a read-only root filesystem, because it writes nothing. The full list of what you have to provide is in Deployment, which is where it stays current — this page describes one version.

Two configuration values are worth setting deliberately before anything else — RUNTIME_DATABASE_MAX_CONNECTIONS, whose real value is this times your replica count, and terminationGracePeriodSeconds above 35, without which pods are killed mid-drain. Deployment covers both.

Known limitations

Worth checking before you build something that needs one:

LimitationDetail
No web application layerAn application is an API only; no pages, forms or layout yet
No sorting, and no total countRows come back by creation time; keyset pagination has no count
No CORSA browser client must be same-origin, behind a proxy
No ?filter=?expand= exists; there is no way to ask for a subset of rows
?expand= is forwards-onlyreference associations only, and no reverse direction
One row per writeNo bulk create or update
referenceSet cannot be writtenJunction tables are created; nothing fills them
Metrics are counters only“Orders placed” works; “revenue booked” does not
A flow operates on one rowNo collection, scheduled or write-triggered flows
Lua onlyThe WebAssembly engine — and so Python — is designed for, not built
Single tenantOne model, one database, per deployment
No rate limitingA body limit and a request timeout exist; nothing bounds volume
No hard CPU bound on a scriptInterpreted loops and memory are bounded; a long C call is not
coroutine is absent from flowsA Lua hook is per-thread, so it would escape the instruction budget
The model is mounted, not pulledThe design is an OCI artifact fetched at start-up; today it is a directory

Upgrading

Nothing to upgrade from. When there is, upgrade notes go here, and anything breaking is called out at the top of the page rather than in a table halfway down.