Core concepts

The generator-identity invariant, the generation pipeline that turns one .forge schema into a tailored database, and the two kinds of runtime ForgeDB publishes.

ForgeDB is a code generator, not a database engine you point at a schema at runtime. You write a .forge schema; the forgedb CLI turns it into tailored Rust database code, a TypeScript SDK, a REST API, and an OpenAPI document. You ship those. Nothing ForgeDB publishes reads your schema while your app runs.

The identity invariant#

Your schema is read once, at build time, to generate code. Everything specific to your app — its types, tables, queries, filters, relations, API routes, validation — is generated for that schema and compiled in. There is no general-purpose engine that reconstructs any of it at runtime.

The generated code is not standalone. It links a fixed set of shared substrate crates (storage, the write-ahead log, the type system, and their peers), but those crates know nothing about your schema. You get type-safe code shaped for exactly your data; the cost is that changing the schema means regenerating and recompiling.

The test a feature must pass

(1) Is the app's tailored data logic still generated per-schema at compile time, and (2) does every published artifact stay schema-agnostic substrate or transport glue rather than a generic runtime that interprets schemas? If either fails, the feature is rejected or redesigned.

Rejected outright: a generic ORM or query builder that interprets an arbitrary schema at runtime, or moving your app's tailored logic into a shipped generic library.

The generation pipeline#

One .forge file drives every output. The CLI parses it to an AST, validates it (types, relations, directives), then runs a set of generators — one per artifact: the Rust database code, the TypeScript SDK client, the axum REST API, an OpenAPI 3.1 document, an optional browser read-replica, and an offline data-migration binary. It generates no UI or component code — a stubs folder just marks where your hand-written code goes.

The generated Rust is snapshot-tested, but a passing snapshot does not prove the code compiles — the snapshots only compare strings. So codegen changes are also verified by generating for a real multi-model schema and compiling the result. That check has caught real bugs.

The quickstart walks the CLI side of this (init → generate → build → serve).

The two kinds of published runtime#

ForgeDB does publish libraries with real APIs. They come in two kinds, both on the schema-agnostic side of the line.

Substrate is the crates the generated code links — storage, types, the WAL, the change feed, auth, query-string parsing, compaction. Real APIs, none of which read your schema. The query-string parser, for one, turns a URL into a generic filter/sort/pagination; every field-aware step is generated per model.

Transport glue is the layers that carry the already-generated API to another language or channel — language bindings, a browser host, a subscription socket. They expose the generated surface; they don't regenerate it.

Storage model#

Storage is append-only and columnar. Each model, and each many-to-many link table, is a directory of column files. A write only appends bytes; it never rewrites committed data. An update or delete appends a new version of the row (a delete appends a tombstoned version), and the generated code resolves the latest version per id.

Because nothing is mutated in place, a lot falls out cheaply: a snapshot is just a row-count mark, so point-in-time reads need no per-row version metadata, and readers never block the writer. Two costs come with it. Dead versions accumulate until an in-process compaction reclaims them, and that compaction renumbers the surviving rows — so a snapshot taken before it does not survive it. Only one process may write to a directory at a time; a directory lock refuses a second writer (the single-writer contract).

The request path#

The REST server is generated as well: api.rs builds its own axum router, and there is no generic HTTP server shipped. A request hits either the operational routes (health, readiness, metrics, snapshot) or, past an optional tenant guard, the data routes — CRUD, list with filter/sort/paginate, and WebSocket subscribe, live-query, and replicate. When auth is configured, it verifies the JWT and cross-checks the tenant. Every field-aware step past that point is generated per model; the substrate on the path reads no schema.

Design decisions and their trade-offs#

  • Compile-time generation over a runtime engine. Type-safe, per-schema code; the cost is that a schema change needs regeneration and recompilation.
  • Append-only over in-place mutation. Snapshots, backup, and the change feed stay simple and correct; the cost is storage grows with dead versions until compaction reclaims them.
  • Watermark snapshots over version chains. No per-row version metadata; the cost is that a compaction renumbers rows, so a pinned snapshot is scoped to the epoch it was taken in.
  • Substrate / compiler split. Generated code links only schema-agnostic crates, which is what makes the identity claim checkable rather than a promise.

Search documentation

Find pages across the ForgeDB docs