forgedb 0.3.1

ForgeDB — an application database generator. Compiles a declarative .forge schema into tailored Rust database code, a TypeScript SDK, and a REST API.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
---
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.