Quickstart
From an empty directory to a running, type-safe database server with a typed TypeScript client — the whole init → generate → build → serve loop.
This is the whole init → generate → build → serve loop: from an empty directory to a
running, type-safe database server with a typed TypeScript client. Every command and output
below is from a real run against the published crates.
A generator, not a runtime ORM
You write a declarative .forge schema; ForgeDB transpiles it into tailored Rust database
code, a REST API, and a TypeScript SDK. Your schema is a compile-time input to
generation, never a runtime input to a generic engine. Read the honest scope in
what pre-1.0 is (and isn't).
1. Install#
Install the forgedb CLI for your ecosystem — or use the universal shell installer. Every
channel gives you the same binary; installation lists them all.
npm install -g @hoodiecollin/forgedb # or: bun add -g @hoodiecollin/forgedbuv tool install hoodiecollin-forgedb # or: pip install hoodiecollin-forgedbcargo install forgedb# no Go-native CLI channel — use the universal shell installer (macOS/Linux):
curl -fsSL https://get.forgedb.dev/install.sh | shVerify:
forgedb --version # forgedb 0.2.02. Scaffold a project#
forgedb init myblog --template blog --rust
cd myblog--template accepts blog, ecommerce, todo, or blank (the default). --rust
includes the Rust backend scaffold. init writes:
myblog/
schema.forge # your schema (the single source of truth)
forgedb.toml # project + database + api + codegen config
Cargo.toml # pins the schema-agnostic substrate crates
src/main.rs # env-driven axum server (tenancy, JWT, graceful shutdown)
Dockerfile # multi-stage build → slim runtime
.dockerignore
docker-compose.yml
deploy/ # systemd unit + env file
.gitignore
README.mdThe blog template's schema.forge:
User {
id: +uuid
username: ^&string
email: ^&string @email
password_hash: string
created_at: +timestamp
posts: [Post]
}
Post {
id: +uuid
title: string
slug: ^&string
content: string
published: bool
published_at: timestamp?
created_at: +timestamp
updated_at: +timestamp
author: *User
tags: [Tag]
}
Tag {
id: +uuid
name: ^&string
posts: [Post]
}The modifiers: + auto-generate (uuid/timestamp), & unique, ^ index, ? nullable,
*User a required foreign key, [Post] a one-to-many, [..]/[..] a many-to-many. See
the schema language for the full grammar.
3. Generate code#
The database core and REST API are the same whichever client language you use:
forgedb generate rust # → generated/database.rs
forgedb generate api # → generated/api.rs (+ package.json, tsconfig.json)Then generate the typed client SDK for your ecosystem:
forgedb generate node --sdk # → generated/types.ts (bun --sdk is equivalent)forgedb generate python --sdk # → generated/python-sdk/forgedb_client.pyforgedb generate rust --sdk # → generated/rust-sdk/ (a reqwest client crate)forgedb generate go --sdk # → generated/go-sdk/client.goOr forgedb generate all for everything at once (adds the OpenAPI spec). Output goes to
./generated/ by default (--output to change it). Every generator tailors its code to
your specific models; nothing reads the schema at run time.
The Rust generator emits one database.rs per schema: typed structs, columnar storage,
indexes, relation traversal, validation, and a crash-safe write path. All of it is produced
at compile time against your models, so there is no generic engine reflecting over the
schema while the app runs. That is the invariant the project is built on: the schema feeds
generation, not a runtime query engine.
4. Build#
cargo buildThe generated app links only the small, schema-agnostic substrate crates
(forgedb-storage, forgedb-wal, forgedb-types, …) that init pinned in Cargo.toml;
they resolve from crates.io. See the substrate version matrix in
installation.
5. Run the server#
The generated main.rs is an axum server configured entirely from the environment:
FORGEDB_PORT=3000 FORGEDB_DATA=./data ./target/debug/myblog
# INFO myblog: ForgeDB serving tenant=None data_root=./data addr=127.0.0.1:3000Key environment variables:
| Var | Default | Purpose |
|---|---|---|
FORGEDB_HOST | 127.0.0.1 | bind host (0.0.0.0 in containers) |
FORGEDB_PORT | 3000 | bind port |
FORGEDB_DATA | data | data directory (per-tenant root) |
FORGEDB_TENANT | (unset) | tenant this process serves |
FORGEDB_LOG_FORMAT | (text) | json for machine-parseable log lines |
The server also exposes operational routes that need no auth:
curl localhost:3000/health # {"status":"ok"} — liveness (never touches the DB)
curl localhost:3000/ready # {"status":"ready"} — acquires a read lock
curl localhost:3000/metrics # {"model_count":3,"rows_per_model":{"Post":0,"Tag":0,"User":0},"total_rows":0}The three routes map to standard load-balancer and Kubernetes probes. /health is
liveness: it returns ok without opening the database, so a live-but-busy process still
reports healthy. /ready is readiness: it acquires a read lock, so it reports ready only
once the data directory is actually openable. /metrics returns per-model row counts for
scraping. None require auth because none expose row data, only status and counts.
6. Use the REST API#
Each model gets a REST resource under /api/<model>:
# Create — the server fills +uuid/+timestamp fields; returns the new id (201)
curl -X POST localhost:3000/api/user -H 'content-type: application/json' -d '{
"username":"ada","email":"ada@example.com","password_hash":"x","posts":null
}'
# → {"id":"<server-generated uuid>"}
# List — paginated envelope
curl localhost:3000/api/user
# → {"data":[{...}],"limit":50,"offset":0,"total":1}Field validation is enforced at write and mapped to HTTP:
curl -X POST localhost:3000/api/user -H 'content-type: application/json' -d '{
"id":"22222222-2222-2222-2222-222222222222","username":"bob",
"email":"not-an-email","password_hash":"x","created_at":0,"posts":null
}'
# → 422 {"error":"field `email` violates `email`: must be a valid email address"}@email/@min/@max/@length/@url violations return 422; a &unique collision
or a dangling foreign key returns 409.
Create contract
Both the Rust db.create_<model> path and the REST POST /api/<model> that routes through
it auto-generate +uuid and +timestamp fields: omit id/created_at from the JSON
body (or send a nil/zero value) and the server fills them. You still send the concrete
scalar fields plus virtual relation fields as null (e.g. "posts":null). The generated TS
SDK's <Model>Create type omits id (server-assigned).
Two honest caveats: integer +u32/+u64 keys are not yet auto-incremented, so a create
must supply them; and beyond id, the SDK's typed Create body still lists the other +
fields for now (#187,
#188).
Full route set per model: GET /api/<model> (list, with ?limit&offset&sort&<field>=),
POST /api/<model> (create), GET|PUT|DELETE /api/<model>/{id}.
7. Use the typed client SDK#
The SDK you generated in step 3 is full CRUD, faithful to the REST contract — same methods and shapes in every language. Pick your ecosystem above:
forgedb generate node --sdk (or bun --sdk) emits generated/types.ts plus a
package.json/tsconfig.json (only if absent — regeneration never clobbers your edits), so
it's npm-publishable as-is:
import { ForgeDBClient } from './generated/types';
const db = new ForgeDBClient('http://localhost:3000');
// list → ListResult<T> = { data, total, limit, offset }
const { data, total } = await db.listUser({ limit: 20, sort: 'username' });
// get → the row, or null on 404
const user = await db.getUser('11111111-1111-1111-1111-111111111111');
// create → the new id; throws ForgeDBError on 409/422
const id = await db.createUser({
username: 'grace', email: 'grace@example.com',
password_hash: 'x', created_at: Date.now(), posts: null,
});
// update → false if the id doesn't exist; delete → true/false
await db.updateUser(id, { /* full record */ });
await db.deleteUser(id);No SDK? The REST API is plain HTTP — call it with fetch:
const res = await fetch('http://localhost:3000/api/user', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
username: 'grace', email: 'grace@example.com', password_hash: 'x', posts: null,
}),
});
const { id } = await res.json(); // → { id: "<server-generated uuid>" }forgedb generate python --sdk emits generated/python-sdk/forgedb_client.py, a
stdlib-urllib client with no runtime dependencies:
from forgedb_client import ForgeDbClient, UserCreate, ListOptions
db = ForgeDbClient("http://localhost:3000")
# list → ListResult { data, total, limit, offset }
page = db.list_user(ListOptions(sort="username", limit=20))
# get → the row, or None on 404
user = db.get_user("11111111-1111-1111-1111-111111111111")
# create → the new id; raises ForgeDbError on 409/422
user_id = db.create_user(UserCreate(
username="grace",
email="grace@example.com",
password_hash="x",
created_at=0,
))
# update → False if the id doesn't exist; delete → True/False
db.delete_user(user_id)forgedb generate rust --sdk emits generated/rust-sdk/, a reqwest-based client crate —
add it as a path/git dependency and use the typed ForgeDbClient:
use forgedb_client::{ForgeDbClient, UserCreate, ListOptions};
let db = ForgeDbClient::new("http://localhost:3000");
// list → ListResult<User> { data, total, limit, offset }
let page = db.list_user(&ListOptions {
sort: Some("username".into()),
limit: Some(20),
..Default::default()
}).await?;
// get → Option<User> (None on 404)
let user = db.get_user("11111111-1111-1111-1111-111111111111").await?;
// create → the new id; Err(ForgeDbError) on 409/422
let id = db.create_user(&UserCreate {
username: "grace".into(),
email: "grace@example.com".into(),
password_hash: "x".into(),
created_at: 0,
..Default::default()
}).await?;forgedb generate go --sdk emits generated/go-sdk/client.go, a net/http client package
(forgedbclient) plus a go.mod:
db := forgedbclient.NewClient("http://localhost:3000")
// ListUser → *ListResult[User] { Data, Total, Limit, Offset }
limit := 20
page, err := db.ListUser(&forgedbclient.ListOptions{Sort: "username", Limit: &limit})
// GetUser → (*User, error); nil on 404
user, err := db.GetUser("11111111-1111-1111-1111-111111111111")
// CreateUser → the new id; non-nil err on 409/422
id, err := db.CreateUser(&forgedbclient.UserCreate{
Username: "grace",
Email: "grace@example.com",
PasswordHash: "x",
})Each SDK surfaces write errors as a typed error carrying the HTTP status and parsed body
(TS ForgeDBError, Python/Rust ForgeDbError, Go's returned error) and maps get/delete
404s to a null / None / false result.
Next steps#
- Schema language — the complete
.forgereference. - Core concepts — the generation pipeline and identity invariant.
- What pre-1.0 is (and isn't) — the honest scope: guarantees and limits of v1.