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.
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. An unknown key, trait name or type name fails the load with the name in the error, 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, with scripts in expr or cel and no remote entities | 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.
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.
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:env:product: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, andafter_*triggers run once the row is written. - Response:
{"data":[…],"meta":{"count":N,…}}. Rows underdata, always an array (a read-by-id is a one-element array); everything about them, count, total, affected, page, stats, undermeta. 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.
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, 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.
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 |