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/forgedb

Verify:

forgedb --version      # forgedb 0.2.0

2. 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.md

The 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)

Or 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.

4. Build#

cargo build

The 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:3000

Key environment variables:

VarDefaultPurpose
FORGEDB_HOST127.0.0.1bind host (0.0.0.0 in containers)
FORGEDB_PORT3000bind port
FORGEDB_DATAdatadata 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}

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>" }

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#

Search documentation

Find pages across the ForgeDB docs