Data API
The Data API block is data.svc: one process that reads a product’s entity definitions and serves them
as an API, for any number of tenants across any number of products.
The model
Section titled “The model”You declare a datastore of entities in YAML. At runtime the engine parses those definitions, folds the layers that apply to the requesting tenant, compiles one schema, and answers requests against it. Nothing is generated: there is no build step, no emitted client, no code to check in. Changing a definition takes effect on the first request after a tenant configuration change drops the cached schema.
A definition change is never applied to the database implicitly. See migrations.
Definitions on disk
Section titled “Definitions on disk”A product’s definitions live under data/. The manifest names the datastores; each datastore gets a
folder, walked recursively for *.yaml and *.yml.
data/ access.yaml # product-level authorization datastores/ datastores.yaml # manifest: defaultdatastore + datastores[] forge/ # one folder per datastore (matches datastores[].path) types.yaml entities/ product/stateflows.yaml admin/build/releasepolicy.yaml charts/ charts.yaml code/js/ # scripted-endpoint sources plugins/<name>/plugin.yamlA datastore declaration is a logical name. It pins no backend — the tenant’s connection pool supplies
the database and the backend type is inferred from it. A schema file declares the list the engine
loads and names the default; the key there is default-datastore, not the manifest’s
defaultdatastore.
default-datastore: maindatastores: - name: mainFiles merge by name into one schema. Entities and traits keep the first definition and log the duplicate; every other collection is last-write-wins. Unknown keys are dropped silently, and key matching is case-sensitive.
Layers
Section titled “Layers”Three layers fold into the schema a tenant sees. Structure is additive; a name collision keeps the lower layer and emits a diagnostic. No fold is ever fatal — a bad layer is dropped and logged, never returned as an error.
| Layer | Source | May add | May override access |
|---|---|---|---|
| Service base | Entities embedded in the service binary | entities, fields, indexes, relations | no |
| Product | YAML published for the product | entities, fields, indexes, relations | yes, per tier |
| Tenant | Rows in the tenant’s own tenant_extensions table | tenant-scoped entities and fields only | no |
data.svc embeds no entities of its own, so the fold it runs is product then tenant. The service-base
row applies to blocks that ship their own entity definitions inside the binary.
Access overrides are per tier (services, actions, rls, fields) and replace a tier wholesale. A
service entity can freeze named tiers with access-lock: or seal itself entirely with final: true;
a sealed entity rejects even pure field additions.
Entities carry an isolation plane through scope: — tenant (the default), shared, or superadmin.
Scoped entities are split into their own engine, so a tenant query cannot reference them at all.
The request path
Section titled “The request path”Public paths are /data/<service path>; the gateway strips the prefix. Every tenanted route requires
X-Customer, X-Product, X-Env, X-Tenant and Authorization: Bearer <token>.
- Tenancy — chassis middleware joins the four headers into
customer:product:env:tenant. An unresolvable tenant is400 invalid tenantbefore any handler runs. - Auth — the token is verified and the principal (user id, roles, impersonation actor) attached,
then the rate-limit gate applies. The
/anon/*mirrors drop the token, not the tenant headers. - Engine resolution —
?datastore=or the schema default selects the target. The first request for a(tenant, datastore)pair builds everything: fetch definitions, fold layers, compile, open the pool, run datastore init scripts once. Every later request reads the cache. - AST — the entity is resolved and the query or mutation AST built from the request.
- Hooks, pre-compile —
BeforeValidatethenAfterValidate: access rules, field defaults, declarative field, entity and cross-entity validations, stateflow guards,before_*triggers and pointcuts. Hooks run by phase then priority, ties in registration order. Payload edits stop taking effect afterAfterValidate. - Compile — the AST becomes SQL in the backend’s dialect, qualified with the tenant’s physical
PostgreSQL namespace. Row-level security is injected here, as a
WHEREpredicate on reads, updates and deletes; an insert has noWHERE, so itscreaterule is evaluated in-process instead. - Execute —
BeforeExecfills materialized computed fields, then the statement runs on the tenant’s pool. - Hooks, post-execute —
AfterExecon mutations,AfterScanwherever rows were scanned; relation eager-loads and virtual computed fields resolve here.after_*triggers parse but are not registered in the deployable service. - Response —
{"data":[…],"count":N}. A read-by-id still returns a one-element array. Errors are{"error","code","details"}.
PostgreSQL is the backend the deployed service serves; a pool with no type resolves to it, and it is
the only type with a registered pool factory.
What the engine owns
Section titled “What the engine owns”| The engine owns | You declare |
|---|---|
| DDL — every table, column, index and foreign key is derived from the definitions by the backend dialect | entities, fields, indexes, references: |
| Migrations — discover the live database, diff, plan, apply. Never automatic: a definition change needs an explicit bootstrap or plan apply | nothing; the plan is generated |
| Validation — declarative rules run on every write | validations: on fields, entityvalidations: and constraints: on entities |
| Access and row-level security — evaluated per request; an RLS rule the compiler cannot translate denies the row | access:, rowlevelsecurity: |
Defaults and audit columns — defaultvalue: on create, createdby/createdon/updatedby/updatedon maintained | the field declarations |
History — validity columns, _version_num, and history/audit side tables per strategy | history: (or cdc:, temporal:, scd:) |
| Soft delete — deletes rewritten to updates and reads filtered when the entity carries a delete-marker field | the trait or field that adds it |
| Tenancy — schema namespace, connection pool and engine per tenant | the tenant’s datastore and pool config |
| Retention — marker columns, archive companion tables, the erase pass | retention: / compliance: |
Extension points take scripts in Expr, CEL, JavaScript, Lua, Starlark, Go or WASM — used for access rules, computed fields, pointcuts, triggers and scripted endpoints. Scripts reach data through the engine’s own namespaces, never through a database connection.
Continue with
Section titled “Continue with”| Concern | Page |
|---|---|
| One datastore and one entity, end to end | Your first datastore |
| Datastores, pools and backends | Datastores and database pools |
| Entity keys, history, isolation planes, seals | Entities |
| Field keys, defaults, computed fields | Fields |
| Field types and custom types | Types |
| References, cardinality, traversal | Relations |
| Value lists | Enums |
| Reusable field groups | Traits · Embeddables |
| Declarative rules and their codes | Validations |
| State machines on a field | Stateflows |
| Operators, projection, pagination | Query DSL |
| Generated GraphQL surface | GraphQL |
| Query-string flags | Request flags |
| OpenAPI, JSON Schema, Zod, CUE, discovery | Schema export and discovery |
| The four access tiers | Access rules |
Rule-to-WHERE translation | Row-level security |
| Masking and data classification | Field protection |
| Endpoint-level HTTP reference | Data API reference |