Skip to content
Talk to our solutions team

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.

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.

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.yaml

A 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: main
datastores:
- name: main

Files 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.

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.

LayerSourceMay addMay override access
Service baseEntities embedded in the service binaryentities, fields, indexes, relationsno
ProductYAML published for the productentities, fields, indexes, relationsyes, per tier
TenantRows in the tenant’s own tenant_extensions tabletenant-scoped entities and fields onlyno

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.

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>.

  1. Tenancy — chassis middleware joins the four headers into customer:product:env:tenant. An unresolvable tenant is 400 invalid tenant before any handler runs.
  2. 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.
  3. 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.
  4. AST — the entity is resolved and the query or mutation AST built from the request.
  5. Hooks, pre-compileBeforeValidate then AfterValidate: 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 after AfterValidate.
  6. 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 WHERE predicate on reads, updates and deletes; an insert has no WHERE, so its create rule is evaluated in-process instead.
  7. ExecuteBeforeExec fills materialized computed fields, then the statement runs on the tenant’s pool.
  8. Hooks, post-executeAfterExec on mutations, AfterScan wherever rows were scanned; relation eager-loads and virtual computed fields resolve here. after_* triggers parse but are not registered in the deployable service.
  9. 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.

The engine ownsYou declare
DDL — every table, column, index and foreign key is derived from the definitions by the backend dialectentities, fields, indexes, references:
Migrations — discover the live database, diff, plan, apply. Never automatic: a definition change needs an explicit bootstrap or plan applynothing; the plan is generated
Validation — declarative rules run on every writevalidations: on fields, entityvalidations: and constraints: on entities
Access and row-level security — evaluated per request; an RLS rule the compiler cannot translate denies the rowaccess:, rowlevelsecurity:
Defaults and audit columns — defaultvalue: on create, createdby/createdon/updatedby/updatedon maintainedthe field declarations
History — validity columns, _version_num, and history/audit side tables per strategyhistory: (or cdc:, temporal:, scd:)
Soft delete — deletes rewritten to updates and reads filtered when the entity carries a delete-marker fieldthe trait or field that adds it
Tenancy — schema namespace, connection pool and engine per tenantthe tenant’s datastore and pool config
Retention — marker columns, archive companion tables, the erase passretention: / 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.

ConcernPage
One datastore and one entity, end to endYour first datastore
Datastores, pools and backendsDatastores and database pools
Entity keys, history, isolation planes, sealsEntities
Field keys, defaults, computed fieldsFields
Field types and custom typesTypes
References, cardinality, traversalRelations
Value listsEnums
Reusable field groupsTraits · Embeddables
Declarative rules and their codesValidations
State machines on a fieldStateflows
Operators, projection, paginationQuery DSL
Generated GraphQL surfaceGraphQL
Query-string flagsRequest flags
OpenAPI, JSON Schema, Zod, CUE, discoverySchema export and discovery
The four access tiersAccess rules
Rule-to-WHERE translationRow-level security
Masking and data classificationField protection
Endpoint-level HTTP referenceData API reference