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:
v2boot: schemasource override active type localdir path /abs/path/datav2boot: rate-limit initialised profiles [endpoint_mutate_default endpoint_query_default v2_bulk_default v2_mutate_default v2_query_default]v2boot: response cache initialised max_entries 10000 default_ttl 30sv2boot: 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 | 1s | The per-request context timeout |
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: 120sThe 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.
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:shop:prod:eu1 tenant: - path: "acme:shop:prod: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[].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.
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
v2boot: 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:
ratelimit: store: kind: memory profiles: v2_query_default: { algorithm: token_bucket, capacity: 600, refill_rate: 10, ttl: 1h } v2_mutate_default: { algorithm: token_bucket, capacity: 60, refill_rate: 2, ttl: 1h } v2_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 |
|---|---|
v2_query_default | GET routes |
v2_mutate_default | POST, PUT, PATCH, DELETE on per-entity routes |
v2_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 |
Reloading the limiter on a configuration change is not wired: the profiles in force are the ones 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.
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 |
| 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 /api/data/.
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 shared and superadmin scope 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. The per-request timeout is timeouts.defaultcontexttimeout |
enablecache | Not a key. The cache switches are cache.active and its two children |
cache.active, cache.query.active, cache.data.active | Parsed, and each sets an internal flag that nothing reads. No cache is built |
telemetry.enabled | Not read. Telemetry initialisation is never invoked from the shared scaffold, so the engine’s metric instruments record into no-op providers |
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 |
v2.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 — most often a cold start against the 1-second default |
Failures specific to pools, namespaces and backends are tabulated on Datastores and database pools.