---
title: "Quickstart"
description: "From an empty directory to a running, type-safe database server with a typed TypeScript client — the whole init → generate → build → serve loop."
purpose: "orientation"
---
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.
<Callout type="note" title="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)](/docs/what-pre-1-0-is/).
</Callout>
## 1. Install
Install the `forgedb` CLI for your ecosystem — or use the universal shell installer. Every
channel gives you the same binary; [installation](/docs/installation/) lists them all.
<Eco lang="node">
```bash
npm install -g @hoodiecollin/forgedb # or: bun add -g @hoodiecollin/forgedb
```
</Eco>
<Eco lang="python">
```bash
uv tool install hoodiecollin-forgedb # or: pip install hoodiecollin-forgedb
```
</Eco>
<Eco lang="rust">
```bash
cargo install forgedb
```
</Eco>
<Eco lang="go">
```bash
# no Go-native CLI channel — use the universal shell installer (macOS/Linux):
curl -fsSL https://get.forgedb.dev/install.sh | sh
```
</Eco>
Verify:
```bash
forgedb --version # forgedb 0.2.0
```
## 2. Scaffold a project
```bash
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`:
```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](/docs/schema/overview/) for the full grammar.
## 3. Generate code
The database core and REST API are the same whichever client language you use:
```bash
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:
<Eco lang="node">
```bash
forgedb generate node --sdk # → generated/types.ts (bun --sdk is equivalent)
```
</Eco>
<Eco lang="python">
```bash
forgedb generate python --sdk # → generated/python-sdk/forgedb_client.py
```
</Eco>
<Eco lang="rust">
```bash
forgedb generate rust --sdk # → generated/rust-sdk/ (a reqwest client crate)
```
</Eco>
<Eco lang="go">
```bash
forgedb generate go --sdk # → generated/go-sdk/client.go
```
</Eco>
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.
<DiveDeeper summary="what's inside the generated database.rs">
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.
</DiveDeeper>
## 4. Build
```bash
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](/docs/installation/).
## 5. Run the server
The generated `main.rs` is an axum server configured entirely from the environment:
```bash
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:
| 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:
```bash
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}
```
<DiveDeeper summary="liveness vs readiness, and what each probe checks">
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.
</DiveDeeper>
## 6. Use the REST API
Each model gets a REST resource under `/api/<model>`:
```bash
# 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:
```bash
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**.
<Callout type="note" title="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](https://github.com/hoodiecollin/forgedb/issues/187),
[#188](https://github.com/hoodiecollin/forgedb/issues/188)).
</Callout>
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:
<Eco lang="node">
`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:
```typescript
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`:
```typescript
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>" }
```
</Eco>
<Eco lang="python">
`forgedb generate python --sdk` emits `generated/python-sdk/forgedb_client.py`, a
stdlib-`urllib` client with no runtime dependencies:
```python
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)
```
</Eco>
<Eco lang="rust">
`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`:
```rust
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?;
```
</Eco>
<Eco lang="go">
`forgedb generate go --sdk` emits `generated/go-sdk/client.go`, a `net/http` client package
(`forgedbclient`) plus a `go.mod`:
```go
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",
})
```
</Eco>
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](/docs/schema/overview/)** — the complete `.forge` reference.
- **[Core concepts](/docs/concepts/)** — the generation pipeline and identity invariant.
- **[What pre-1.0 is (and isn't)](/docs/what-pre-1-0-is/)** — the honest scope: guarantees and
limits of v1.