Skip to content
Talk to our solutions team

Data

The Data 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. An unknown key, trait name or type name fails the load with the name in the error, 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 only, with scripts in expr or cel and no remote entitiesno

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.

Writing tenant_extensions is changing a tenant’s schema, so data.svc requires the data-service role for every create, update and delete on it, whatever the product’s own access rules allow — those still apply on top and can only narrow it. A caller without the role gets 403 access_denied. Reads follow the product’s rules.

Access overrides are per tier (services, actions, rls, fields), and inside the actions tier they are per action. A service entity decides what the layers above it may do with layering: — per element, one of final, additive (the layer may narrow, never widen) or replace — or with its two coarse forms, access-lock: for whole tiers and final: true for the entity; 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:env:product: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-compile: BeforeValidate 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. Execute: BeforeExec fills materialized computed fields, then the statement runs on the tenant’s pool.
  8. Hooks, post-execute: AfterExec on mutations, AfterScan wherever rows were scanned; relation eager-loads and virtual computed fields resolve here, and after_* triggers run once the row is written.
  9. Response: {"data":[…],"meta":{"count":N,…}}. Rows under data, always an array (a read-by-id is a one-element array); everything about them, count, total, affected, page, stats, under meta. Errors are {"errors":[{"code","message","details?"}]}, always an array.

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, Starlark or WASM — the languages data.svc offers by default (scripts.runtimes; Lua cannot be offered yet and Go is not available) — used for access rules, computed fields, pointcuts, triggers and scripted endpoints. Scripts reach data through the engine’s own data and schema namespaces, never through a database connection, and reach nothing on the host: the engine grants those two namespaces on top of pure-compute functions and nothing else. A tenant overlay may carry scripts in expr and cel only; a tenant contribution with any other script, or a remote entity, is dropped with a diagnostic.

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