Cheatsheet
A one-page scannable reference to every .forge type, modifier, relation, and directive, plus a kitchen-sink schema.
A compact lookup for the whole .forge language. Follow the links for the full detail.
Scalar types#
| Type | Rust | TS | Indexable? |
|---|---|---|---|
u32 u64 i32 i64 | same | number | ✅ |
f64 | f64 | number | filter/sort (no ordered index) |
bool | bool | boolean | ✅ |
string | String | string | ✅ (exact-match) |
string(N) string(N!) | String (InlineStr<N> as a key) | string | ✅ — fixed N-character slot in the row; ! = exactly N, bare = at most. ASCII unless @utf8. Legal as an identity: Copy key, RFC 3986 pchar minus %, non-empty, no @utf8 |
bytes(N) | [u8; N] | any | ✅ (raw bytes, not text; struct-safe) |
uuid | Uuid | string | ✅ |
timestamp, timestamp(s|ms|us) | Timestamp | string (RFC 3339) | ✅ |
decimal | Decimal | string | ✅ (normalized key) |
json | serde_json::Value | unknown | ❌ |
No text type. See scalar types.
Identity#
Every model needs one: a field named id, else the first + field. id wins by name
even over a + field declared above it. Legal types, and nothing else:
uuid · u32 u64 i32 i64 · timestamp(s|ms|us) (+timestamp only when named id,
only at us) · string(N) string(N!) · *Model (resolves to the target's key)
Anything else — bare string, bytes(N), f64, bool, decimal, json, an enum, a
struct, an array, any nullable including ?Model — is a validation error naming the field.
See the identity field.
Modifiers#
| Sigil | Position | Meaning | Notes |
|---|---|---|---|
+ | prefix | auto-generate | u32/u64/uuid/timestamp only — all fill on create when omitted. Integer autos are monotonic, not gapless, and cannot take an explicit 0; any shape (identity, &, ^, or bare) is valid. A +timestamp is a stamp unless it is named id, and id: +timestamp must be declared us |
& | prefix | unique (enforced) | any type; also indexes |
^ | prefix | indexed | any indexable type |
? | postfix (or prefix) | nullable | any type |
Combine freely: id: +uuid, email: ^&string, bio: string?, external: +uuid?. See
modifiers.
Relations#
| Syntax | Kind |
|---|---|
[Model] | one-to-many (virtual) |
*Model | required FK |
?Model | optional FK |
[..] / [..] on both sides | many-to-many (junction) |
[type; N] | fixed-size array (fixed-size element) |
StructName / StructName? | inline struct (fixed-size fields only) |
A foreign key takes its target's identity type. See relations.
Enums#
enum Status { Active, Suspended, Closed }
Account { id: +uuid status: ^Status }PascalCase name + variants; 1-byte discriminant; serialized as the variant name; filterable / sortable (declaration order) / indexable. See enums.
Directives#
Enforced: @min @max @length @email @url @pattern/@regex (→ 422),
@utf8 (inline string(N) only — without it a non-ASCII value is 422),
@on_delete(restrict|cascade|set_null) (→ 409 / cascade / null).
On an inline string(N) the width is the length bound, so @max and the upper-bound
spellings of @length are schema errors there; @min/@length(min:) still apply.
Semantic-only markers: @default @computed @fulltext @materialized
@relations(*|fields) (component-only).
Model-level: @index(a, b, ...) (composite, ≥ 2 fields), @projection(name: cols),
@soft_delete.
Args accept numbers, bare idents, and quoted strings. See directives and indexes & projections.
Comments#
// line comments only. No block comments (/* */). Newlines are significant; no
semicolons. See comments & whitespace.
Not supported#
~ auto-update · text type · block comments /* */.
Kitchen-sink schema#
// Every construct in one file.
enum OrderStatus { Placed, Paid, Shipped, Delivered, Cancelled }
struct GeoPoint {
latitude: f64
longitude: f64
}
User {
id: +uuid
email: ^&string @email
name: string @length(1, 100)
bio: string?
home: GeoPoint?
balance: decimal
metadata: json?
created_at: +timestamp
orders: [Order]
tags: [Tag]
}
Order {
id: +uuid
status: ^OrderStatus
reference: &string(12!) // fixed-width inline string, exactly 12 chars
currency: ^string(3!)
total: decimal @min(0)
placed_at: +timestamp
customer: *User @on_delete(cascade)
courier: ?User @on_delete(set_null)
scores: [u32; 5]
@index(customer, placed_at)
@projection(summary: status, total, placed_at)
@soft_delete
}
Tag {
id: +uuid
label: ^&string @length(1, 50)
users: [User]
}