database.json¶
GET /rel/database.json serves the same introspected schema database.ts renders
as TypeScript, as plain JSON instead. Reach for it when you're not in a TypeScript project at
all, when you want to feed the schema into your own tooling (a generator, a schema browser, a
non-JS client), or when you're building something like an automatic admin UI that needs the raw
introspected facts — constraint names, index coverage, nullability, every declared foreign key
including one the query engine can't efficiently join — rather than database.ts's
already-collapsed TypeScript types.
It's wired up exactly like database.ts: gated behind http.typescript.enable, GET only, and
filtered by the same schemas query param (intersected with the configured
http.typescript.schemas whitelist — a schemas value outside the whitelist is a 400, not a
silent drop) and http.typescript.blacklist. See
Configuration reference for both settings. The response is
Content-Type: application/json; charset=utf-8, and every object key is snake_case.
The schemas whitelist and the blacklist both decide which relations/functions are exported as
first-class, independently-reachable entries (their own keys in relations/functions) —
neither one removes a relation the types registry's own transitive closure still needs to
resolve a "composite" entry's relation key. A composite type's backing relation is always
present in relations under its own key once anything exported references it, regardless of its
schema or the blacklist — the same way database.ts unconditionally inlines a referenced
composite/domain/enum's body.
Top-level shape¶
interface DatabaseJson {
version: 1
search_path: string[]
types: Record<string, TypeInfo>
relations: Record<string, RelationInfo>
relationships: Record<string, RelationshipVariant[]>
functions: Record<string, FunctionInfo[]>
functions_by_name: Record<string, string>
computed_properties: Record<string, Record<string, ComputedFieldInfo>>
wellknowns: Record<string, WellknownInfo>
}
Every Record above is keyed "schema.name", except types (keyed the same way, but never
schema-filtered — see types below) and wellknowns (keyed by the well-known query's
own declared name). Every entry that lives under a "schema.name" key also repeats its own
schema/name fields, so you never have to split the map key back apart to know what you're
looking at.
version is a schema-format version for this JSON payload itself, bumped whenever a breaking
shape change is made to any section below — unrelated to the database's own schema version.
search_path is the database's search path, in lookup order. It's what functions_by_name's
winners were resolved against — you need it too if you're re-deriving unqualified-name
resolution yourself.
types¶
type TypeInfo = { schema: string; name: string } & (
| { kind: "scalar"; comment?: string }
| { kind: "enum"; comment?: string; labels: string[] }
| { kind: "domain"; comment?: string; base: string; not_null: boolean }
| { kind: "array"; element: string }
| { kind: "composite"; comment?: string; relation: string }
)
Keyed "schema.name" (a scalar built-in like pg_catalog.int4 included). One entry per type
referenced, directly or transitively, by a column or function argument/return type across
every relation/function this response exports, regardless of which schema that type itself lives
in — a column in an exported schema can be typed by a composite/domain/enum declared in one that
isn't otherwise exported.
"scalar" covers every built-in Postgres type with no further structure — kind alone tells you
there's nothing more to resolve. "domain" names its base type by key (recurse through base
for a domain-of-domain) and carries not_null separately from any given column's own
nullability — a domain's not-null constraint applies regardless of what a column built on it
reports. "composite" names its backing relation by key into relations instead of repeating
its column list — a composite type's shape is always exactly its relation's own columns.
Why: a registry keyed by type identity, referenced by string rather than inlined, means a type used by a hundred columns is described once. It also means you can resolve a type reference without caring which section it came from — column, function argument, or return type all point into the same
typesmap.
relations¶
interface RelationInfo {
schema: string
name: string
kind: "table" | "view" | "materialized_view" | "type"
comment?: string
columns: ColumnInfo[]
primary_key: { name: string; columns: string[] } | null
unique_constraints: { name: string; columns: string[] }[]
indexes: IndexInfo[]
}
interface ColumnInfo {
name: string
type: string
comment?: string
is_nullable: boolean
is_primary_key: boolean
is_identity: boolean
is_generated: boolean
is_updatable: boolean
has_default: boolean
}
interface IndexInfo {
name: string
unique: boolean
columns: string[]
}
Keyed "schema.name". Every relation resolves here except a function's own synthetic row-type
relation, which is inlined on that function instead of getting its own relations entry — see
functions below. kind is "type" for a standalone composite
type's own pseudo-relation — exactly the relation a types entry's "composite" relation key
points to, so it's reachable both ways: through the type registry, or directly here if you only
care about shape.
The relation's own comment and each column's own comment are their respective COMMENT ON
TABLE/COMMENT ON COLUMN text — the same source database.ts surfaces as a /** ... */ doc
comment — omitted entirely when unset, never an empty string.
columns is declared order. type is a key into types. is_nullable is the raw introspected
value (information_schema.columns.is_nullable); a domain's own not_null
(types[column.type].not_null when type's kind is "domain") is a separate fact you have
to combine yourself — database.json never pre-collapses the two the way database.ts's column
typing does. has_default is whether the column has a default expression; the expression text
itself isn't exposed (arbitrary SQL, not meant for a JS consumer to parse or re-run).
primary_key names the relation's own PRIMARY KEY constraint and its ordered column list, null
when it has none. unique_constraints lists every UNIQUE constraint the same way (the primary
key is never repeated there). indexes lists every index found on the relation; columns is the
true declared key-column order with INCLUDE-only columns already excluded — join eligibility
elsewhere in this document (relationships) is decided by whether a column set is covered by
some leading prefix of one of these, so you can re-derive that coverage yourself instead of it
being restated per relationship.
relationships¶
interface RelationshipVariant {
target: string
direction: "outgoing" | "incoming"
on: Record<string, string>
unique: boolean
eligible: boolean
shortcut: string
constraint_name: string
}
Keyed "schema.name" of the owning relation, one array entry per declared foreign key, in both
directions — every foreign key touching this relation gets a variant here, whether or not it's
actually usable in a join(). target is the key of the other side's entry in relations.
direction is "outgoing" when this relation itself holds the FK columns (on's keys, pointing
at target's own PRIMARY KEY/UNIQUE columns), "incoming" when target holds them instead
(pointing back at this relation). on maps this relation's own column names to the corresponding
column name on target, in the direction implied above — the same shape the query language's own
join "on" field already takes (e.g. {"director_id": "id"}), so you can build a join clause
directly from it without parsing anything. unique is whether following on from this relation
to target yields at most one row — always true for "outgoing" (an FK's target columns are
always PRIMARY KEY/UNIQUE-backed), and for "incoming" only when this relation's own FK columns
are themselves unique (a 1:1 reverse relationship, rare). constraint_name is the backing
foreign key's own name — the same name on both directions' variants, since they're the two sides
of one constraint.
eligible is false when the FK-holding side's own on columns aren't indexed (database.ts's
own relationships silently drop these entirely); shortcut is still the correct join-syntax
string in that case, it just isn't safely usable in a live join() — the query engine rejects it
at request time. A relation with no foreign key in either direction is simply absent from this
map, never present with an empty array.
Why: an automatic admin needs every declared relationship to render a linked-record picker or a related-records tab, not only the ones the query engine can efficiently join — an unindexed FK is still a real, navigable relationship, just one the admin has to resolve with its own lookup instead of a
join(). Filtering it out entirely, the waydatabase.tsdoes, would silently hide part of the schema from that use case.
functions / functions_by_name¶
interface FunctionInfo {
schema: string
name: string
comment?: string
volatility: "immutable" | "stable" | "volatile"
is_strict: boolean
args: FunctionArgInfo[]
returns_set: boolean
returns: string | null
relation?: string
}
interface FunctionArgInfo {
name: string
type: string
mode: "in" | "out" | "inout" | "variadic" | "table"
has_default: boolean
}
functions is keyed "schema.name", one array entry per overload (Postgres allows several
functions to share a name, distinguished by argument list) — plain functions only, same
blacklist as database.ts. volatility/is_strict are collapsed from Postgres' own
immutable/stable/volatile flags (exactly one is ever true) and its strictness flag — useful for
deciding whether a call is safe to cache or repeat. args lists every argument in declaration
order, mode carrying Postgres' own IN/OUT/INOUT/VARIADIC/TABLE distinction verbatim — unlike
database.ts, which only ever renders the callable IN/INOUT/VARIADIC subset, database.json
keeps OUT and TABLE-mode arguments too, since a consumer working with HTTP routes needs the full
argument shape, not just the callable one. has_default is only ever true for a trailing
IN/INOUT argument; always false for "out"/"table"/"variadic" arguments.
returns is a key into types for a scalar/enum/domain/composite/array return type. It's null
exactly when the function has no real backing return type (a RETURNS TABLE/OUT-parameter
function sharing Postgres' generic record pseudo-type) — in that case the row shape is exactly
this same function's own args entries whose mode is "out" or "table", in declaration
order, never duplicated into a separate shape. relation is present only when returns_set is
true and the function's return composite type resolves to a real, exported relation (a key into
relations).
functions_by_name maps a bare (unqualified) function name to the one "schema.name" key in
functions an unqualified call resolves to via search_path. A name whose only candidates all
live outside search_path is omitted, matching Postgres' own resolution.
computed_properties¶
type ComputedProperties = Record<string, Record<string, ComputedFieldInfo>>
interface ComputedFieldInfo {
returns: string
returns_set: boolean
}
Outer key is the owning relation's "schema.name", inner key the computed field's own name —
filtered to non-blacklisted functions, the same set database.ts documents for select
authoring. See Computed fields. returns is a key into
types (never null — a computed field's function is always callable with exactly one argument
and typed on the relation's own composite row, so it never falls into the record-shape case
functions.returns can). A relation with no computed fields is absent from the outer map.
wellknowns¶
interface WellknownInfo {
name: string
query: unknown
params: WellknownParamInfo[]
}
interface WellknownParamInfo {
name: string
pg_type: string
kind: "number" | "string" | "boolean" | "unknown"
has_default: boolean
}
Keyed by the well-known query's own declared name
(also repeated as name). query is the compiled query JSON verbatim — the same value
database.ts embeds as a const ... as const literal — so a consumer not resolving types
through types/relations can still read the raw query structure directly. params lists the
query's declared parameters; pg_type is the declared param's own type string verbatim (an
arbitrary author-written Postgres type name, e.g. "int", never resolved against the
introspected types registry), and kind is the same coarse bucket used for the wire-level type
check — not a types key, since a well-known param's declared type was never introspected from a
real column or argument in the first place.