Data Configuration
data.svc is configured by one YAML document, named with -f. Everything below is a key in
that document. Keys the service does not read are listed too, in
Keys with no effect: several of them appear in shipped example
profiles.
The boot document
Section titled “The boot document”data.svc -f ./data.yaml| Flag | Key it writes | Default |
|---|---|---|
-f, --config | . (the file path itself) | .data.yaml |
-p, --port | port | — |
--sid | sid | — |
--datacenter | datacenter | — |
--cluster | cluster | — |
--zerotrust | zerotrust | — |
--svccert | svccert | — |
--svckey | svckey | — |
--rootcacert | rootcacert | — |
--jwt_public_key | jwt_public_key | — |
--jwt_private_key | jwt_private_key | — |
--config_url | config.url | — |
--config_apikey | config.apikey | — |
--servicediscovery_url | servicediscovery_url | — |
--servicediscovery_apikey | servicediscovery_apikey | — |
A flag you actually pass overwrites the matching key in the loaded document; a flag left at its
default does not. -f is the one exception. It is a file path, never merged into the document,
because the config: block already owns that name.
Once the config client is up, the service configuration is the resolved cluster node with the
boot document merged on top. Both are readable by everything below, and the boot document
wins on a collision. A key you set at launch cannot be overridden by the config server. The
merge is by top-level key, not deep: a block present in both documents is taken whole from the
boot document, so declaring a partial timeouts: or ratelimit: at launch discards the config
server’s version of that block entirely rather than layering on it. Two
groups skip that merge and are read straight from the boot document: the config: block itself,
and the service-level meta.url / meta.apikey, a third, jwks.*, reads the boot document
first and falls back to the cluster node.
Every data <verb> runs the same boot sequence from the same -f file, minus the HTTP
listener, then does its work and exits, a one-shot verb therefore needs a valid port: as well,
and it waits up to 15 seconds for the configuration to load before resolving anything, see
Migrations.
Boot order
Section titled “Boot order”| # | Step | On failure |
|---|---|---|
| 1 | Load the YAML named by -f | Exit 1 |
| 2 | Resolve identity (datacenter, cluster, sid), zerotrust, TLS paths, port | Exit 1 on an empty port |
| 3 | Merge passed flags, default datacenter/cluster/sid, validate credential pairs | Exit 1 |
| 4 | Build the logger, set log.level, start the routine pool | Exit 1 on a pool error |
| 5 | Load TLS material and JWT keys | Exit 1 |
| 6 | Build the secret-store client when vault.url is set | Logged, boot continues |
| 7 | Register with service discovery when servicediscovery_url is set | Retried within the boot window, then exit 1 |
| 8 | Build the config client, then the secret-store and metadata clients | Exit 1 on the config client |
| 9 | Tenancy init check: tenancy is actually initialised at command construction, before step 1 | Exit 1, before the config file is even read |
| 10 | Boot config load: engine caches, timeouts, loadtenants, then the engine machinery: tenancy scopes, schemasource, rate limiter, response cache, API providers | Individually logged, boot continues |
| 11 | Product-subscription barrier | Exit 1 |
| 12 | Install SIGINT/SIGTERM handlers | — |
| 13 | Register /ready and /health, the tracer, the 404 handler, timeouts, middleware and service routes; flip healthy and ready true; bind host:port | Exit 1 after running shutdown |
The only tenant-specific work in that list is resolving tenant configuration (loadtenants and
the cluster node’s tenants:). No schema is fetched, no database is contacted and no engine is
built until the first request names a tenant, so boot does not depend on the
metadata service, on any database, or on any tenant being reachable, a misconfigured tenant
produces a 5xx on its own requests, never a failed process start. See
Tenancy and isolation.
A healthy start logs, in order:
boot: schemasource override active type localdir path /abs/path/databoot: rate-limit initialised profiles [endpoint_mutate_default endpoint_query_default v2_bulk_default v2_mutate_default v2_query_default]boot: response cache initialised max_entries 10000 default_ttl 30sboot: initialisedv2: surfaces mounted count 10 surfaces [schema rest graphql bulk queryhash actions script stateflows admin superadmin]data service started at 127.0.0.1:8080Listener and process identity
Section titled “Listener and process identity”| Key | Type | Default | Effect |
|---|---|---|---|
port | string | : required | HTTP listen port. Boot aborts with invalid/empty port provided when neither -p nor this key is set |
host | string | "" | Bind address, joined as <host>:<port>. Empty binds every interface |
sid | string | <hostname>-data-<port> | Instance id. Used as a log field, the service-discovery registration id and a container key suffix |
instance | string | falls back to sid | The deepest segment of this process’s cluster config path |
datacenter | string | default | Identity tag; first segment of the cluster config path |
cluster | string | default | Identity tag; second segment of the cluster config path |
labels | map of string | — | Cohort labels matched against a config entry’s match:. tags is read when labels is absent |
tags | map of string | — | Registered as service-discovery tags, plus a version tag |
boot.retry_window | duration or int seconds | 90s | How long to retry the service-discovery, config, secret-store and metadata clients, and the product-subscription barrier, before failing boot. Absent or non-positive applies 90s; there is no single-attempt setting |
log.level | string | error | trace, debug, info, warn, error, fatal, panic or disabled |
routinepool.size | int | 100 | Workers in the shared background goroutine pool |
routinepool.queue | int | 100 | Queue depth of that pool |
routinepool.spawn | int | 50 | Spawn increment of that pool |
realtimedebug | bool | false | Lets a request opt into debug logging with a query parameter or a debug header |
Timeouts
Section titled “Timeouts”| Key | Type | Default | Effect |
|---|---|---|---|
timeouts.read | int seconds or duration | 15s | HTTP server read timeout |
timeouts.write | int seconds or duration | 15s | HTTP server write timeout |
timeouts.readheader | int seconds or duration | 5s | HTTP server read-header timeout |
timeouts.defaultcontexttimeout | duration | 30s | The per-request context timeout. The chassis default is 1s; data.svc raises it at boot when the document names none, and logs which value it used. A cold-start request that builds a tenant’s engine, a bulk batch and a streamed export all take longer than a second |
timeouts.services[].name | string | — | Service this per-service block applies to. Must equal data |
timeouts.services[].defaultcontexttimeout | duration | — | Per-request timeout for this service, overriding the global one |
timeouts.services[].timeouts.enabled | bool | true | Turns the timeout checker off for this service |
timeouts.services[].pathconfig[].expr | regexp | — | Matches a request path |
timeouts.services[].pathconfig[].duration | duration | — | Timeout for paths matching expr |
timeouts.services[].pathconfig[].enabled | bool | true | false with an unparseable duration means no timeout for those paths |
timeouts: defaultcontexttimeout: 30s services: - name: data defaultcontexttimeout: 45s timeouts: enabled: true pathconfig: - expr: "^/bulk" duration: 120sA change stream is not a request/response, so it
does not run under defaultcontexttimeout: the connection is bounded by its own lifetime
parameter instead, 30 minutes by default. The request deadline still applies before the first byte
reaches the client, which is where a slow access check would show up.
The nesting of timeouts.enabled inside a timeouts.services[] entry is what the code reads,
not a transcription error. A malformed defaultcontexttimeout keeps the previous value; an
invalid expr drops that one path rule and logs it.
Script budget
Section titled “Script budget”Every hook, pointcut, trigger, rule and computed-field script runs under a deadline. Without one a script that does not return holds the request goroutine that ran it for the life of the process, and no request timeout can help: the interpreter has to be stopped from outside.
scripts: timeout: 5s # the default; `none` removes the bound| Key | Type | Default | Effect |
|---|---|---|---|
scripts.timeout | duration, or none | 5s | How long one script evaluation may run before the interpreter is stopped. none and 0 remove the bound and log a warning |
The budget never extends the caller’s own deadline. A request with 200 ms left gives its scripts
200 ms whatever is written here, so a client that disconnects stops the script it started. A
scripted endpoint’s own tx-timeout: replaces the budget for that invocation.
A value that is not a duration refuses boot rather than falling back to the default: a budget that quietly is not the one written is the failure this key exists to prevent.
A stopped script fails its operation and a REST caller gets script_timeout. The one exception is
a virtual computed field, which is an enrichment: the read still answers, with that field null
and a compute_failed warning naming the budget.
Script languages
Section titled “Script languages”A script reaches only what data.svc grants it: the data and schema namespaces and pure-compute
functions — nothing on the host. Some languages bring a standard library of their own that those
grants do not govern, so the deployment chooses which languages run:
scripts: runtimes: [expr, cel, js, starlark, wasm] # the default| Key | Type | Default | Effect |
|---|---|---|---|
scripts.runtimes | list, or comma-separated string | expr, cel, js, starlark, wasm | The script languages this deployment runs. expr is always included |
lua cannot be listed yet: it is not confined. go is not available on data.svc at all. Listing
either refuses boot, and so does an unknown name. A schema declaring a script in a language that is not offered is
refused when the tenant’s engine is built, naming each script and this key; a scripted endpoint in
one answers script language not offered by this deployment.
Remote entity egress
Section titled “Remote entity egress”Remote entities call public addresses only. A remote entity meant to reach an internal service needs that host named:
remote: internalhosts: [crm.internal, 10.0.4.12]| Key | Type | Default | Effect |
|---|---|---|---|
remote.internalhosts | list of host names or addresses | none | Hosts (exact, or a parent domain covering its subdomains) that may resolve to loopback, private or carrier-grade NAT addresses. Link-local addresses are refused whatever is listed. A value with a scheme, port or path refuses boot |
remote.allowprivate | bool | false | Lifts the address checks entirely. Test environments only — the boot log warns on every start |
The rest of the rules a remote call is held to are in Remote entities.
Event delivery targets
Section titled “Event delivery targets”An entity’s events: block is published to the
change stream a subscriptions: block serves, which
is in-process and live. Declaring a target here also queues each event for durable, signed delivery
to a URL, so a receiver that was down gets it when it comes back.
events: targets: billing: url: https://receiver.example/hooks/orders secret: a-shared-secret timeout: 10s topics: [entity.sales_order, entity.invoice]| Key | Type | Default | Effect |
|---|---|---|---|
events.targets.<name>.url | string | — | Where a delivery is posted. http or https |
events.targets.<name>.secret | string | — | Signs the body with HMAC-SHA256. Required |
events.targets.<name>.timeout | duration | 10s | Bounds one attempt |
events.targets.<name>.topics | list | — | Which topics this target wants. Required |
Every one of these refuses boot when it cannot work: no targets, an empty targets map, a misspelt key inside a target, a URL that is not http, no secret, no topics. A declared target that silently receives nothing is what the queue exists to prevent.
The queue row is written inside the write’s own transaction, so an event cannot outlive a rolled-back write, and a write cannot commit without its event recorded. Where the row cannot be written, the write fails.
scheduler: outbox: every: 10sscheduler.outbox.every runs a drain under a per-tenant lease. Each instance also drains on its own
tick and when a write records an event, so this key sets the fleet-wide floor rather than the only
cadence. It is not scheduled where no target is declared.
Transport security
Section titled “Transport security”| Key | Type | Default | Effect |
|---|---|---|---|
zerotrust | bool | true | On: the listener resolves TLS material and refuses to start without it. Off: plain HTTP end to end, no certificate material fetched |
svccert | path | "" | Service certificate. TLS is configured only when both svccert and svckey are set |
svckey | path | "" | Service private key |
rootcacert | path | "" | Root CA certificate |
rootcakey | path | "" | Root CA key |
insecureskipverify | bool | false | Skip peer certificate verification on outbound TLS |
auto_renew_certificates | bool | true | Renew the service certificate automatically |
jwt_public_key | path | "" | Public key for verifying inbound tokens |
jwt_private_key | path | "" | Private key |
Three pairings are checked at boot and fail it:
| Condition | Error |
|---|---|
servicediscovery_url set with neither servicediscovery_apikey nor svccert | error: either servicediscovery_apikey or svccert should be present |
servicediscovery_url set, svccert set, svckey absent | error: both svccert and svckey should be present |
config.url set with neither config.apikey nor svccert | error: either config.apikey or svccert should be present |
Token verification
Section titled “Token verification”| Key | Type | Default | Effect |
|---|---|---|---|
jwks.issuers.<issuer> | URL | — | Base URL for a trusted issuer’s key set, fetched from <base>/.well-known/jwks.json |
jwks.timeout | duration | 3s | Per-fetch timeout, bounding config read plus HTTP round trip. A non-positive or unreadable value falls back to the default, so a typo cannot disable it |
jwks.disabled | bool | false | Accept tokens without verifying the signature |
Only configured issuers are ever contacted, a token whose iss claim names an issuer that is
not declared is rejected outright rather than triggering a fetch, so an issuer you forget to
declare shows up as blanket 401s, not as a fetch error.
These are boot-plane keys: they are read from the boot document first, and only then from the cluster node. Placing them under a config-server hive alone may not take effect.
Where configuration comes from
Section titled “Where configuration comes from”The config: block selects one of three modes.
| Key | Type | Default | Effect |
|---|---|---|---|
config.url | URL | "" | Remote mode: cluster and tenant nodes stream from a config server |
config.path | path | "" | Local mode: a directory of hive YAML files |
config.apikey | string | "" | Bearer credential for the remote server |
config.cache | path | "" | On-disk warm-start cache holding the last known good configuration |
| Mode | Selected by | Source |
|---|---|---|
| Remote | config.url set | The config server |
| Local directory | config.path set | Hive YAML under that directory |
| Bootstrap-as-config | neither set | The boot document’s own hives: block |
Setting both logs both config.url and config.path set; using config.url, ignoring config.path
and proceeds with the URL.
Hives, paths and inheritance
Section titled “Hives, paths and inheritance”A source document has exactly one top-level key, hives:, holding a list of entries per hive.
Each entry carries a reserved path: (required) and an optional match:; everything else in
the entry is the configuration document at that node.
| Hive | Node address this process resolves |
|---|---|
cluster | <datacenter>:<cluster>:data[:<instance>] |
tenant | <customer>:<product>:<env>:<tenant>:data |
Resolution walks every prefix of that address from root to leaf and deep-merges each matching
entry, so deeper nodes win over shallower ones and an entry at path: "" applies to
everything. Within one node, entries are ordered by source priority, then by the number of
match: labels, so a more specific cohort wins, an explicit null in a deeper node deletes
the value an ancestor set.
hives: cluster: - path: "" api: key: cluster-apikey tenants: - acme:prod:shop:eu1 tenant: - path: "acme:prod:shop:eu1" name: eu1 active: true datastores: main: dbpool: main dbpools: main: type: postgres dbserver: db.internal:5432 dbuser: appuser database: appdb password: pool-password schema: eu1| Key | Type | Effect |
|---|---|---|
hives.<hive>[].path | string | Node address. "" is the root and matches every lookup |
hives.<hive>[].match | map of string | Applies the entry only to instances whose labels contain these pairs |
hives.cluster[].api.key | string | Cluster API key; also the last-resort fallback for the metadata credential |
hives.cluster[].api.tenantbound | bool | When true, the platform layer admits the cluster key only for a tenant whose own api.key equals it, before the request reaches the service. Off by default; the service’s own binding (api_key_not_bound) applies either way |
hives.cluster[].tenants | list of string | Tenant keys resolved eagerly at every configuration load. A tenant that fails to resolve keeps the loader retrying |
hives.tenant[].name | string | Tenant display name |
hives.tenant[].active | bool | Whether the tenant is enabled |
hives.tenant[].domain | list of string | Domains mapped to this tenant |
hives.tenant[].datastores.<name> | map | Logical datastore, bound to a pool by dbpool: |
hives.tenant[].dbpools.<name> | map | Physical connection endpoint |
The pool and datastore keys are enumerated in
Datastores and database pools. Prefix inheritance
is what makes declaring dbpools: once at a shared node dangerous: every tenant below then
inherits the same schema: and shares one set of tables.
| Key | Type | Default | Effect |
|---|---|---|---|
loadtenants | list of string | empty | Tenant keys resolved once during boot config load, so their configuration is resident before the first request. Each failure is logged and skipped, never fatal |
loadtenants is a boot-document key and runs once; hives.cluster[].tenants lives on the
resolved cluster node and re-runs on every configuration load. Neither builds an engine or a
pool. Both warm configuration only.
Service discovery
Section titled “Service discovery”| Key | Type | Default | Effect |
|---|---|---|---|
servicediscovery_url | URL | "" | Registration endpoint. Empty means no registration at all |
servicediscovery_apikey | string | "" | Credential. Required unless svccert is set |
Registration publishes the instance’s address (<ipv4>:<port>, or the IPv6 form when no IPv4 was
resolved), datacenter, cluster, service name and the tags map plus a version tag, on a
15-second lease. sid is not published: it stays a local identifier, and the registry’s lease id
is used instead when sid is empty.
Secret store
Section titled “Secret store”| Key | Type | Default | Effect |
|---|---|---|---|
vault.url | URL | "" | Endpoint. Empty means no client is built and every pool password: is used as a literal |
vault.apikey | string | "" | Credential |
vault.file | path | "" | File-backed secret source, used when vault.url is empty |
vault.client.transport | string | remote | remote, agent or both. An unknown value fails boot with vault-unknown-transport |
vault.client.transport_canary | int 0–100 | 0 | Share of reads served by the agent transport when the transport is both |
vault.cache.enabled | bool | true | Cache resolved secrets in process |
vault.cache.type | string | inmemory | Cache implementation |
vault.cache.jwtttl | duration | 58m | Token cache lifetime |
vault.cache.certttl | duration | 6h | Certificate cache lifetime |
vault.cache.secretttl | duration | 3h | Secret cache lifetime |
With a client registered, a pool’s password: is treated as a secret key and resolved through
Vault. With no client it is used verbatim, the local development path.
Field protection keys
Section titled “Field protection keys”The at-rest controls read these keys from the boot document’s properties: block and from the
vault. Nothing here has a permissive default: a missing key refuses the write it would have
protected and the boot log names the fields.
| Key | Type | Meaning |
|---|---|---|
properties.encryption.keys | list | Field-encryption keys by name: - {name: <key>, key: <source>}. A source beginning vault:// is read from the secret store under the name after the scheme; anything else is the key material itself. A vault path may carry {customer}, {env}, {product}, {tenant} or {cept}, which is what makes the key per tenant. Merged over the schema’s datastores[].encryption.keys by name — the deployment wins. See Providing encryption keys |
properties.fieldencryptionkey | string | Local field-encryption passphrase, used only when no vault is registered; warns at boot. Without a vault and without this key, writes to encrypted fields are refused with encryption_key_missing. See Providing encryption keys |
properties.fieldencryptionkeyversion.<name> | int | The current version of a named key: new ciphertext is written under <name>.v<n>, a vault secret (or properties.fieldencryptionkeys.<name>.v<n> locally); rows written under older ids keep decrypting through the id in their envelope |
properties.fieldencryptionkeys.<name> | string or block | Local passphrases by key name, for deployments without a vault. A block under the name carries its versions: fieldencryptionkeys: {default: {v2: <passphrase>}} is the id default.v2. See Providing encryption keys |
properties.fieldencryptiondevkey | bool | Development only: use the built-in constant key when nothing else is configured; the boot log repeats on every start that anyone with the binary can decrypt. Never in a deployment |
properties.fieldhashsalt | string | Salt for hash fields when the vault secret hash-salt is absent; unsalted hashes are named at boot |
properties.tokenizer | vault | memory | The tokenizer for tokenize fields; absent means every such write is refused with tokenizer_missing |
hives.tenant[].api.key | string | The API key this tenant accepts; it must equal the cluster api.key. A tenant that declares none accepts no API key (api_key_not_bound) |
properties.profile | dev | Marks a development boot. jwks.disabled: true is refused without it |
properties.statementtimeout | duration | Server-side statement_timeout on every pooled connection, the raw handle included. Default 30s; 0s disables it |
properties.schema | block | The schema-export policy: serve: (default true) and formats: — any of openapi, jsonschema, zod, cue, asyncapi, avro, odata, proto, graphql; absent serves every format. The same block a schema declares. Set here for every tenant, or in a tenant’s configuration for that tenant (the tenant’s wins). A product’s own schema: block narrows it further; a name outside the set refuses the boot, or that tenant’s engine. See Schema export |
properties.superadminrealm | string | The realm a superadmin token must be minted for; default superadmin. Empty honours the role alone |
Request ceilings
Section titled “Request ceilings”limits: maxbody: 8MiB # a JSON body on /rest, /actions, search, QUERY maxbulkbody: 64MiB # a bulk envelope or an import maxmultipart: 256MiB # a multipart upload maxexportrows: 100000 # the most rows one export may ask for graphqldepth: 12 # the deepest selection set a GraphQL document may carryA body over its ceiling is 413 body_too_large; an export over the row ceiling is 400 limit_too_large; a GraphQL document too deep answers 200 with a query_too_deep error before any
resolver runs. Every ceiling has the default shown; 0 disables it.
The scheduler
Section titled “The scheduler”scheduler: retention: { every: 1h, grace_hours: 24, dry_run: false } # entities: [..] narrows it materialize: { every: 30s } audit_verify: { every: 24h }The jobs the admin plane exposes as routes, on a timer, for every tenant this instance serves. A
job with no every does not run. Each tick takes a lease in the tenant’s data_locks table (<prefix>data_locks on a prefixed datastore) for the
length of the interval, so two instances sharing a database run a job once per interval between
them. Without this block a retention deadline depends on someone calling the route.
Boot validation
Section titled “Boot validation”The blocks this service owns, properties, sqllog, limits, scheduler, and each entry of
caches, are closed sets: a key the service does not read refuses the boot and names the known
keys, because every such key so far had degraded to the insecure default in silence. The legacy
cache: block and the globaltimeout: key, which look like they configure something and do
not, are refused outright. jwks.disabled: true is refused unless
properties.profile is dev. The tenants under loadtenants have their engines built before
/ready answers, and a tenant whose engine or init script fails marks the process not ready and
names itself.
Metadata service
Section titled “Metadata service”Definitions come from the metadata service unless a schemasource: override is set. The
endpoint is resolved per tenant, in order:
| Priority | URL key | Credential key |
|---|---|---|
| 1 | hives.tenant[].meta.url | hives.tenant[].meta.apikey |
| 2 | meta.url | meta.apikey |
| 3 | properties.metaurl | properties.metaapikey |
| 4 | — | hives.cluster[].api.key |
| Key | Type | Effect |
|---|---|---|
meta.url | URL | Service-level metadata endpoint |
meta.apikey | string | Service-level metadata credential |
meta.dir | path | Local metadata directory for the service’s own metadata client, read only when meta.url is empty |
hives.tenant[].meta.isexternal | bool | Marks the tenant’s metadata service as external when the client is built |
Resolving nothing yields the error code META_URL_UNSET, and that tenant’s schemas cannot be
fetched.
Definition source override
Section titled “Definition source override”| Key | Type | Default | Effect |
|---|---|---|---|
schemasource.type | string | absent | localdir is the only implemented value. Replaces the metadata-service fetch |
schemasource.path | path | — | Directory of definition YAML, walked recursively. Relative paths resolve against the working directory |
A malformed block never fails boot: an empty or unknown type, an empty path, a path that
does not exist and a path that is not a directory each log a warning and leave the
metadata-service source in place. Confirm the override took with the boot line
boot: schemasource override active. Full behaviour is on
Where definitions live.
Rate limiting
Section titled “Rate limiting”One process-wide limiter is built at boot and lives for the life of the process. Buckets are
keyed per tenant, user and profile, with blanks defaulting to unknown, anonymous and
default, so anonymous traffic is still limited per tenant.
| Key | Type | Default | Effect |
|---|---|---|---|
ratelimit.store.kind | string | memory | memory or distrib |
ratelimit.store.distrib.backend | string | "" | Backend name for the distributed store |
ratelimit.store.distrib.dsn | string | "" | Connection string for the distributed store |
ratelimit.profiles.<name>.algorithm | string | — | Bucket algorithm |
ratelimit.profiles.<name>.capacity | int | 0 | Burst capacity |
ratelimit.profiles.<name>.refill_rate | float | 0 | Sustained refill, requests per second |
ratelimit.profiles.<name>.leak_rate | float | 0 | Leak rate for leaky-bucket profiles |
ratelimit.profiles.<name>.limit | int | 0 | Fixed-window limit |
ratelimit.profiles.<name>.max | int | 0 | Maximum counter value |
ratelimit.profiles.<name>.window | duration | 0 | Window length. An unparseable value logs and becomes 0 |
ratelimit.profiles.<name>.ttl | duration | 0 | Bucket lifetime |
Five profiles ship. The block below is exactly the built-in configuration. Three more,
worksheet_create, worksheet_upload and worksheet_query, are added at boot for the
worksheet caps as one-minute fixed windows, unless the
block declares those names itself:
ratelimit: store: kind: memory profiles: query_default: { algorithm: token_bucket, capacity: 600, refill_rate: 10, ttl: 1h } mutate_default: { algorithm: token_bucket, capacity: 60, refill_rate: 2, ttl: 1h } bulk_default: { algorithm: token_bucket, capacity: 30, refill_rate: 0.5, ttl: 1h } endpoint_query_default: { algorithm: token_bucket, capacity: 300, refill_rate: 5, ttl: 1h } endpoint_mutate_default: { algorithm: token_bucket, capacity: 30, refill_rate: 1, ttl: 1h }| Profile | Applies to |
|---|---|
query_default | GET routes |
mutate_default | POST, PUT, PATCH, DELETE on per-entity routes |
bulk_default | The bulk envelope |
endpoint_query_default | Scripted endpoints that read and declare no limit |
endpoint_mutate_default | Scripted endpoints that write and declare no limit |
The pre-D77 spellings v2_query_default, v2_mutate_default and v2_bulk_default are still honoured under the current names, but boot logs an error for each one, rename them; a profiles: block replaces the built-in defaults, so a profile the gate cannot find would otherwise carry no limit at all.
read at boot. Changing them needs a restart. Scripted endpoints select a pre-declared profile by name; an inline limit specification on an endpoint is not honoured.
Response cache
Section titled “Response cache”| Key | Type | Default | Effect |
|---|---|---|---|
responsecache.max_entries | int | 10000 | Entry ceiling for the whole service. Values ≤ 0 keep the default |
responsecache.default_ttl | duration | 30s | Lifetime used when a scripted endpoint declares a cache block but no TTL |
One in-memory store is shared across every tenant; isolation is in the key prefix, not in the store. There is no per-tenant sizing. See Caching.
File fields
Section titled “File fields”A type: file field keeps a reference in its column and the bytes go here. Off unless said,
because most deployments have no file fields and an upload reaching unconfigured storage is worse
than a clear refusal:
files: enabled: true store: assets # a declared content store, by name root: /var/lib/data # or a directory, when there is no content-store catalogue| Key | Default | Effect |
|---|---|---|
files.enabled | false | Off: a multipart create answers 501 file_storage_unconfigured and ?download=true answers 501 file_downloader_unconfigured |
files.store | the tenant’s only one | Which content store the bytes go to. Required when the tenant declares more than one |
files.root | — | A directory instead of a content store, for a deployment with no catalogue. Each tenant gets its own folder under it |
Bytes are streamed through this service on the way out rather than handed over as a signed URL, so the access check on the request that asks for them is the one that decides. A deployment that wants presigned URLs registers its own resolver at boot instead.
Entity cache connections
Section titled “Entity cache connections”A schema’s caches: declares the entity cache backends by name and type; the boot document says
where the server behind a shared or tiered one is, so the schema carries no credentials:
caches: shared: url: "redis://dragonfly:6379/0" # required: a redis:// or rediss:// URL poolsize: 16 # optional: connections to the server| Key | Type | Effect |
|---|---|---|
caches.<name>.url | string | Required. The Redis-protocol server for the schema’s cache <name> |
caches.<name>.poolsize | int | Connections to that server; the declaration’s poolsize when absent |
Any other key under an entry refuses the boot. A shared or tiered cache with no entry and no
URL in its declaration fails the boot naming the cache, as does a server that does not answer. A
local cache needs no entry. See The entity cache.
Ports and endpoints
Section titled “Ports and endpoints”One listener serves everything on host:port. Surfaces mount at unprefixed paths; behind a
gateway the public path is /data/<service path>.
| Surface | Paths |
|---|---|
| Schema | /schema, /schema/entities, /schema/entities/:entity, /schema/graphql, /schema/openapi.json, /schema/jsonschema.json, /schema/jsonschema/:entity, /schema/zod.ts, /schema/zod/:entity, /schema/cue, /schema/cue/:entity, /cache |
| REST | /rest, /rest/:entity, /rest/:entity/id/:id, /rest/:entity/id/:id/restore, /rest/:entity/search, and the /anon/rest mirrors |
| GraphQL | /graphql, /anon/graphql |
| Bulk | /bulk |
| Query hashes | /rest/:entity/action/:action |
| Actions | /actions/:name |
| Scripted endpoints | /x/*path |
| Stateflows | /stateflows, /stateflow/:name |
| Change streams | /subscribe/:entity |
| Event delivery queue | /admin/data/events/pending, /stats, /drain, /requeue |
| Data admin | /admin/data/* |
| Superadmin | /superadmin/tenant/* |
| Worksheets | /worksheets/* |
| Content store | /contentstore/list |
| Probes | /ready, /health, /checktenant |
Only /ready and /health bypass the tenancy middleware. Every other route, /checktenant and
the /anon/* mirrors included, requires the four tenancy headers, the endpoint-level reference
, parameters, bodies, status codes, is at /blocks/baas/data/api/.
Health and readiness
Section titled “Health and readiness”| Probe | Tenancy headers | Success | Failure |
|---|---|---|---|
GET /ready | no | 200 with ready:true | 500 with ready:false |
GET /health | no | 200 with a JSON body | 500 with the same body, healthy:false |
GET /checktenant | yes | 200 {"success":true} | 400 |
/ready and /health are registered before service routes and are skipped by the tenancy
middleware, so neither needs headers. /health returns:
{ "healthy": true, "dependencies": {}, "memstats": { "Alloc": 12, "HeapAlloc": 12, "HeapSys": 27, "HeapIdle": 12, "HeapInUse": 14, "TotalAlloc": 41, "Sys": 44, "NumGC": 4 }, "version": "" }Memory figures are megabytes. dependencies and version are always empty here: this service
registers no health dependencies and sets no probe version, so the only signal in the body is
healthy. Do not build an alert on the other three fields.
Both flags flip true once service routes are registered, before TLS material is resolved and
before the listener binds. Readiness deferral exists in the shared scaffold but this service does
not use it, so a green /ready means the process is serving, not that any tenant is warm. Use
/checktenant with a tenant’s headers for that; it
confirms the tenant’s configuration resolves, and builds nothing. See
Tenancy and isolation.
Graceful shutdown
Section titled “Graceful shutdown”SIGINT or SIGTERM runs, in order:
- Stop the HTTP server. No new requests are accepted.
- Drain every per-tenant scope through tenancy: engines drain, materialized-refresh runners stop, script runtimes close, pools release.
- Stop every tenant metadata-client monitor and signal its done channel.
- Run the service shutdown hook: a second, idempotent tenancy drain, then the product and customer plane engines, then a backstop sweep that force-closes any shared endpoint pool whose reference count never reached zero, then the worksheet manager.
- Print
shutdown complete.
The ordering is load-bearing: closers may still consult the configuration and metadata clients, so those stop last. Per-scope closers run under a 30-second drain bound. That bound has no configuration key.
Keys with no effect
Section titled “Keys with no effect”These are read from configuration but change nothing in data.svc, or are not read at all.
Several appear in shipped example profiles.
| Key | Status |
|---|---|
name | Not read. The service identity is the compiled-in name data |
globaltimeout | Not a key, and refused at boot naming the real one: timeouts.defaultcontexttimeout |
enablecache | Not a key. The entity cache is declared in the schema’s caches: and connected through the boot document’s caches: block |
cache.active, cache.query.active, cache.data.active | Not read, and the whole cache: block is refused at boot |
telemetry.enabled | Not a key, and refused at boot. The block is read now: naming telemetry.collector builds the providers, and leaving it unset is what keeps the signals no-op |
log.loglinenumbers | Not read |
timeouts.enabled at the top level | Not read. The toggle lives inside a timeouts.services[] entry |
source.type | Not read |
clusterstore.* | Not read |
config_url, config_apikey (flat) | Not read for config-plane mode selection. Use the config: block |
Boot and first-request failures
Section titled “Boot and first-request failures”| Message or code | Cause |
|---|---|
invalid/empty port provided | No port: and no -p |
error: both svccert and svckey should be present | Half a certificate pair |
error: either servicediscovery_apikey or svccert should be present | servicediscovery_url with no credential |
error: either config.apikey or svccert should be present | config.url with no credential |
server start failed: failed to load TLSConfig | zerotrust on with no certificate material |
config not ready: <reason> | A one-shot data verb waited 15 seconds for the first configuration load |
tenancy product load failed | The product-subscription barrier rejected the configuration |
invalid tenant (400) | Missing or unparseable tenancy headers, or a tenant that resolves to nothing |
config client not initialised (400) | The config client was not registered during boot |
META_URL_UNSET | No metadata endpoint resolved for the tenant, and no schemasource: override |
engine_unavailable (500) | The engine for a (tenant, datastore) pair could not be built. The wrapped message names the failing step |
context-middleware-timed-out | The request exceeded timeouts.defaultcontexttimeout, which data.svc sets to 30s when the boot document names none. A deployment that names a short one sees this first on a cold start, where the request also builds the tenant’s engine and pool |
Failures specific to pools, namespaces and backends are tabulated on Datastores and database pools.