Schema language
The .forge schema language — top-level declarations, a worked example, and a map of the reference.
A .forge file is the single source of truth for a ForgeDB application. You write
declarative models, structs, and enums; ForgeDB transpiles them at compile time into
tailored Rust database code, a TypeScript SDK, a REST API, and an OpenAPI spec. Your
schema is read once, when you generate — never by a running engine.
Top-level declarations#
A .forge file is a sequence of top-level declarations, which may appear in any order.
There are three kinds:
model— an entity that becomes a stored table, declared by name with no keyword:User { ... }. Models hold columns, relations, and optional model-level directives.struct— a reusable value type embedded into a model:struct Address { ... }. Its fields must all be fixed-size — nostring, no relations.enum— a closed set of named variants:enum OrderStatus { Pending, Paid }, referenced from a field by its bare name.
A struct is inlined into whatever model embeds it, not stored as its own table — the
embedding model lays out the struct's fields directly, which is why those fields must be
fixed-size. An enum is stored as a 1-byte discriminant, with variants mapped to 0..N in
declaration order, and serialized as the variant-name string, so REST, TypeScript, and
JSON all agree on "Paid". Order is free because the parser resolves every struct and
enum name across the whole file before wiring fields to them.
A worked schema#
A small but complete blog schema exercising every top-level declaration kind:
// A closed set of states — an enum, not a free string.
enum PostStatus { Draft, Published, Archived }
// A fixed-size embedded value type.
struct GeoPoint {
latitude: f64
longitude: f64
}
User {
id: +uuid
name: string @length(1, 100)
email: &string @email
home: GeoPoint?
created_at: +timestamp
posts: [Post]
}
Post {
id: +uuid
title: string @length(1, 200)
slug: ^&string @length(1, 200)
status: ^PostStatus
views: ^u64
published_at: timestamp?
author: *User
tags: [Tag]
created_at: +timestamp
@index(status, created_at)
}
Tag {
id: +uuid
name: ^&string @length(1, 50)
posts: [Post]
}Naming is parser-enforced
Model, struct, and enum names must be PascalCase; field names must be snake_case.
These are fatal parse errors, not warnings — userId and user { } both fail to parse.
What each part does#
id: +uuid— an auto-generated UUID primary key. The+modifier fills the value on insert.&string— a unique column;^string/^u64— an indexed column for fast lookups. See indexes & projections.status: ^PostStatus— an enum field, indexed.author: *User/tags: [Tag]/posts: [Post]— relations: a required foreign key, a many-to-many, and a one-to-many.@length(...),@email,@index(...)— directives.
Map of this section#
| Page | Covers |
|---|---|
| Models & fields | Model syntax, field form, modifier positions, declaration rules |
| Scalar types | Every scalar type with Rust/TS mappings and storage behavior |
| Modifiers | + & ^ ? in depth, with placement and type rules |
| Relations | One-to-many, required/optional FK, many-to-many, arrays, inline structs |
| Enums | Declaring and using enums; storage and filter/sort/index behavior |
| Directives | Every @ directive — enforced vs. semantic-only |
| Indexes & projections | ^, composite @index, @projection, &unique |
| Comments & whitespace | // comments (block comments are not supported) |
| Cheatsheet | One-page scannable reference |