Column types
A column's type is not an abstraction over the database. It is the column type, written straight into the DDL. That is worth knowing before you write your first entity, because it explains every "why is there no string" question at once.
type is raw PostgreSQL
Only these are supported:
text integer bigint smallint uuid boolean
jsonb json timestamptz timestamp date time
numeric numeric(p,s) serial bigserial bytea
inet cidr macaddr point real double precision
text[] integer[] jsonb[]
So there is no string, no decimal, no float, and timestamp is the timezone-less variant — you almost always want timestamptz.
The mapping from the types you may be used to:
| You might write | Use instead |
|---|---|
string | text |
decimal with precision/scale | numeric(12,2) |
float | double precision (but use numeric for money) |
timestamp for "date and time with timezone" | timestamptz |
string with an enum | text with a check |
default and check are SQL, not values
Both are raw SQL expression strings. That means a literal string default needs SQL quoting inside the JSON string:
"status": { "type": "text", "default": "'in_stock'" },
"attributes": { "type": "jsonb", "default": "'{}'::jsonb" },
"created_at": { "type": "timestamptz", "default": "now()" },
"is_active": { "type": "boolean", "default": "true" }
"default": "in_stock" without the inner quotes is a SQL identifier, not a string, and will fail.
Constrained values are a check, which is also how you replace the enum you may be looking for:
"status": {
"type": "text",
"notNull": true,
"default": "'in_stock'",
"check": "status in ('in_stock','sold','in_service')"
}
Constraints spanning more than one column go in the entity's checks array instead.
Choosing a type
Money is numeric(p,s). double precision is binary floating point: it will quietly turn 1.23 + 4.56 into something you do not want on an invoice. numeric(12,2) for a price, numeric(14,3) for a quantity that can be fractional.
Time is timestamptz. A timezone-less timestamp is a bug waiting for a tenant in another region, and a bug that only shows up twice a year.
Identifiers are uuid with gen_random_uuid(). Safe to generate anywhere, no collisions, no sequence to coordinate, and several schema features require a uuid id column outright.
Semi-structured data is jsonb, not json. jsonb is the indexable one. If you find yourself filtering on a key inside the document, either index the expression or promote the key to its own column.
Naming matters more than you would like. Column names travel into generated clients for every SDK target the platform emits, so a column called class, function, new, default or return breaks code generation for at least one of them. Rename before you deploy — a column rename after the fact takes three releases.
generated
A stored computed column. PostgreSQL evaluates the expression on every write.
"completeness_pct": {
"type": "numeric",
"generated": { "expression": "((completeness->>'ratio')::numeric * 100)" }
}
The expression may reference any non-generated sibling column of the same entity. It may not reference another generated column, itself, or tenant_id. A generated column must not also declare pk, notNull, default or references — the expression is the value.
The expression is fixed at creation. Changing it in a later version is rejected, because PostgreSQL cannot alter a generation expression. If it has to change, retire the column and add a differently named one.
generator
A bounded, declarative hint for generating sample data during development. No arbitrary SQL.
"serial": { "type": "text", "generator": { "kind": "sequence", "prefix": "SN-" } },
"status": { "type": "text", "generator": { "kind": "pick", "values": ["in_stock", "sold"] } },
"weight": { "type": "numeric", "generator": { "kind": "decimal", "min": 0.1, "max": 40, "scale": 2 } }
kind is one of sequence, pick, range, decimal, uuid, bool, timestamp, date, lorem, with prefix, values, min, max and scale as applicable.
generator affects development data only. It has no effect on a production table.
Next steps
- Schema reference — the rest of the file.
- Relationships —
referencesbetween your entities. - Migrations — what happens when you change a type or a name.