Schema and discovery
These are the routes a client calls to learn what entities exist and what shape they
have. Paths are shown as an external client sees them, with the /data/ prefix the
gateway strips before forwarding; the service itself serves
the discovery and export routes at /schema/... and the two catalogue routes at
/admin/data/....
What each export format contains, which variants a field appears in, and the field-type mappings are on Schema export and discovery. This page is the wire contract: paths, parameters, bodies, and status codes.
Routes
Section titled “Routes”| Method | Path | Returns | Content type |
|---|---|---|---|
GET | /data/schema | Schema summary — name lists only | application/json |
GET | /data/schema/entities | Entity inventory with counts | application/json |
GET | /data/schema/entities/{entity} | One entity’s fields and relations | application/json |
GET | /data/schema/graphql | Generated GraphQL SDL | text/plain; charset=utf-8 |
GET | /data/schema/openapi.json | OpenAPI 3.1 document | application/json; charset=utf-8 |
GET | /data/schema/jsonschema.json | JSON Schema 2020-12 bundle | application/schema+json; charset=utf-8 |
GET | /data/schema/jsonschema/{entity} | JSON Schema for one entity | application/schema+json; charset=utf-8 |
GET | /data/schema/zod.ts | Zod TypeScript bundle | application/typescript; charset=utf-8 |
GET | /data/schema/zod/{entity} | Zod for one entity | application/typescript; charset=utf-8 |
GET | /data/schema/cue | CUE bundle | text/x-cue; charset=utf-8 |
GET | /data/schema/cue/{entity} | CUE for one entity | text/x-cue; charset=utf-8 |
GET | /data/admin/data/references/{entity} | Foreign keys pointing at an entity | application/json |
GET | /data/admin/data/hashes | Compiled-query catalogue | application/json |
Request
Section titled “Request”Every route on this page is GET and takes the four tenant headers plus a bearer token:
GET /data/schema/entities/order HTTP/1.1Host: api.example.comX-Customer: acmeX-Product: erpX-Env: prodX-Tenant: mainAuthorization: Bearer <token>| Parameter | In | Required | Meaning |
|---|---|---|---|
datastore | query | no | Which datastore to describe. Omit for the schema’s default. |
entity | path | on per-entity routes | Exact entity name. Each export route trims its own extension, so order and order.json are the same request on the JSON Schema route. |
There is no anonymous mirror for any of these routes. /data/anon/rest/* and
/data/anon/graphql serve unauthenticated callers, but schema discovery and the exports
always require a token — a published anonymous integration has to be handed its schema
out of band.
Schema summary
Section titled “Schema summary”GET /data/schema returns names only, never definitions. Every list is a sorted array of
strings.
{ "name": "erp", "version": "1.4.0", "default_datastore": "main", "datastores": ["main", "reporting"], "entities": ["order", "order_item"], "relations": [], "types": ["credit_score"], "enums": ["OrderStatus"], "traits": ["auditable"], "embeddables": ["address"], "stateflows": ["order_lifecycle"], "views": [], "functions": []}Entity inventory
Section titled “Entity inventory”GET /data/schema/entities lists every entity with counts, sorted by name. fields and
relations are integers, not arrays.
{ "entities": [ { "name": "order", "fields": 12, "relations": 2 }, { "name": "order_item", "fields": 7, "relations": 1 } ], "count": 2}Entity detail
Section titled “Entity detail”GET /data/schema/entities/{entity} is the shape call. Both fields and relations come
back in declaration order.
{ "name": "order", "table_name": "orders", "primary_key": "id", "materialization": 0, "fields": [ { "name": "id", "type": "", "required": true, "nullable": false, "unique": true, "primary_key": true, "computed": false, "default": "ulid()" }, { "name": "risk_band", "type": "credit_score", "required": false, "nullable": true, "unique": false, "primary_key": false, "computed": false, "default": null } ], "relations": [ { "name": "items", "type": 2, "to_entity": "order_item", "from_field": "id", "to_field": "order_id" } ]}| Top-level key | Type | Meaning |
|---|---|---|
name | string | Entity name as declared |
table_name | string | Physical table, which may differ from name |
primary_key | string | Single-column primary key |
materialization | int | Storage ordinal — 0 table, 1 view, 2 remote, 3 contract, 4 virtual |
fields | array | Columns, in declaration order |
relations | array | Relations, in declaration order |
| Field key | Type | Meaning |
|---|---|---|
name | string | Column name as declared |
type | string | Declared type spelling, or empty — see the caution below |
required | bool | Declared required |
nullable | bool | Declared nullable |
unique | bool | Declared unique |
primary_key | bool | True when the name matches the entity’s primary_key |
computed | bool | True for computed fields |
default | any | Declared default; null when unset |
| Relation key | Type | Meaning |
|---|---|---|
name | string | Relation name, and the include key used to eager-load it |
type | int | Cardinality ordinal — 0 unspecified, 1 one-to-one, 2 one-to-many, 3 many-to-one, 4 many-to-many |
to_entity | string | Target entity |
from_field | string | Column on this entity |
to_field | string | Column on the target entity |
Fields carrying a pii, phi, pci, gdpr or noexport
compliance marker are still listed
here. That gate applies to the export formats only, not to this route or the SDL.
GraphQL SDL
Section titled “GraphQL SDL”GET /data/schema/graphql returns the generated SDL as plain text, not JSON.
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data/schema/graphqlEntity references
Section titled “Entity references”GET /data/admin/data/references/{entity} lists every foreign key pointing at the
named entity — the “what breaks if I drop this” view. Rows are sorted by from_entity,
then from_field.
{ "tenant": "acme:erp:prod:main", "datastore": "main", "entity": "order", "count": 1, "references": [ { "from_entity": "order_item", "from_field": "order_id", "to_field": "id", "on_delete": "cascade", "on_update": "noaction" } ]}on_delete and on_update render as cascade, setnull, restrict or noaction. Both
keys are always present: noaction is the zero value, so a foreign key that declares
neither behaviour reports noaction rather than omitting the key. datastore echoes the
?datastore= parameter verbatim and is "" when you omit it. Outgoing references are not
returned; read them from the target entity’s own detail response.
Query-hash catalogue
Section titled “Query-hash catalogue”GET /data/admin/data/hashes is the replacement for the retired /data/hashes/list.
{ "tenant": "acme:erp:prod:main", "datastore": "main", "count": 0, "hashes": []}This route is a stub. It answers 200 so callers can probe that the cache is reachable,
but hashes is always empty and count is always 0 — the compiled-query cache exposes
no enumeration API yet. Do not build on its contents.
Admin plane access and error shape
Section titled “Admin plane access and error shape”The two /data/admin/data/* routes above sit behind the admin policy’s Read gate, which
admits the admin and superadmin roles (role matching is case-insensitive). Both have
superadmin mirrors that target a sibling tenant by name —
/data/superadmin/tenant/{tenant}/references/{entity} and
/data/superadmin/tenant/{tenant}/hashes — where {tenant} is the bare tenant segment,
not a full four-part key.
The admin plane mixes two error envelopes. The role gate runs before the handler and emits
the standard coded envelope — 403 v2.requires_admin on the admin routes,
403 v2.requires_superadmin on the superadmin mirrors. Everything the handlers themselves
reject emits a bare {"error": "<message>"} with no code field. A client that
branches on code therefore gets a code for an authorisation failure and nothing to
branch on for any other failure.
Status codes
Section titled “Status codes”For /data/schema/*:
| Status | Code | Cause |
|---|---|---|
| 200 | — | Success |
| 400 | v2.missing_entity | The {entity} path segment was empty |
| 403 | v2.no_tenant | No tenant key on the request — tenant headers missing, or the service was called directly |
| 404 | v2.entity_not_found | The entity is not in the tenant’s resolved schema; check the spelling and ?datastore= |
| 429 | v2.rate_limited | The caller’s rate-limit bucket is empty |
| 500 | v2.engine_unavailable | The tenant’s engine could not be resolved or built |
| 500 | v2.export_render_failed | A renderer failed while producing the document |
Every route on this page is wrapped by the authenticated gate, which spends one rate-limit
token per request. A 429 carries Retry-After alongside the X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset headers the successful responses also set.
For the two admin routes, a missing role returns 403 with the coded envelope; the other
conditions return 400, 404 and 500 with the bare {"error": "..."} body described above.
Making a definition change visible
Section titled “Making a definition change visible”These routes render from the tenant’s cached schema. Loading new definitions does not
change what they return until the cached engine is dropped with DELETE /data/cache,
documented on Sync, transfer, and schema.
Routes removed since v1
Section titled “Routes removed since v1”| Retired route | Replacement |
|---|---|
GET /data/entities/list | GET /data/schema/entities |
GET /data/entities/list/{entity} | GET /data/schema/entities/{entity} |
GET /data/entities/references/{entity} | GET /data/admin/data/references/{entity} |
GET /data/hashes/list | GET /data/admin/data/hashes |
POST /data/entities/index/{entity} | none |
POST /data/entities/validate | No direct replacement. A dry-run plan generation covers the same check — see Sync, transfer, and schema. |
The tag and store filters the retired list routes accepted do not exist on any
replacement. ?datastore= is the only selector.