Skip to content
Talk to our solutions team

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.

Terminal window
data.svc -f ./data.yaml
FlagKey it writesDefault
-f, --config— (the file path itself).data.yaml
-p, --portport
--sidsid
--datacenterdatacenter
--clustercluster
--zerotrustzerotrust
--svccertsvccert
--svckeysvckey
--rootcacertrootcacert
--jwt_public_keyjwt_public_key
--jwt_private_keyjwt_private_key
--config_urlconfig.url
--config_apikeyconfig.apikey
--servicediscovery_urlservicediscovery_url
--servicediscovery_apikeyservicediscovery_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.

#StepOn failure
1Load the YAML named by -fExit 1
2Resolve identity (datacenter, cluster, sid), zerotrust, TLS paths, portExit 1 on an empty port
3Merge passed flags, default datacenter/cluster/sid, validate credential pairsExit 1
4Build the logger, set log.level, start the routine poolExit 1 on a pool error
5Load TLS material and JWT keysExit 1
6Build the secret-store client when vault.url is setLogged, boot continues
7Register with service discovery when servicediscovery_url is setRetried within the boot window, then exit 1
8Build the config client, then the secret-store and metadata clientsExit 1 on the config client
9Tenancy init check — tenancy is actually initialised at command construction, before step 1Exit 1, before the config file is even read
10Boot config load — engine caches, timeouts, loadtenants, then the engine machinery: tenancy scopes, schemasource, rate limiter, response cache, API providersIndividually logged, boot continues
11Product-subscription barrierExit 1
12Install SIGINT/SIGTERM handlers
13Register /ready and /health, the tracer, the 404 handler, timeouts, middleware and service routes; flip healthy and ready true; bind host:portExit 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/data
v2boot: 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 30s
v2boot: initialised
v2: surfaces mounted count 10 surfaces [schema rest graphql bulk queryhash actions script stateflows admin superadmin]
data service started at 127.0.0.1:8080
KeyTypeDefaultEffect
portstringrequiredHTTP listen port. Boot aborts with invalid/empty port provided when neither -p nor this key is set
hoststring""Bind address, joined as <host>:<port>. Empty binds every interface
sidstring<hostname>-data-<port>Instance id. Used as a log field, the service-discovery registration id and a container key suffix
instancestringfalls back to sidThe deepest segment of this process’s cluster config path
datacenterstringdefaultIdentity tag; first segment of the cluster config path
clusterstringdefaultIdentity tag; second segment of the cluster config path
labelsmap of stringCohort labels matched against a config entry’s match:. tags is read when labels is absent
tagsmap of stringRegistered as service-discovery tags, plus a version tag
boot.retry_windowduration or int seconds90sHow 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.levelstringerrortrace, debug, info, warn, error, fatal, panic or disabled
routinepool.sizeint100Workers in the shared background goroutine pool
routinepool.queueint100Queue depth of that pool
routinepool.spawnint50Spawn increment of that pool
realtimedebugboolfalseLets a request opt into debug logging with a query parameter or a debug header
KeyTypeDefaultEffect
timeouts.readint seconds or duration15sHTTP server read timeout
timeouts.writeint seconds or duration15sHTTP server write timeout
timeouts.readheaderint seconds or duration5sHTTP server read-header timeout
timeouts.defaultcontexttimeoutduration1sThe per-request context timeout
timeouts.services[].namestringService this per-service block applies to. Must equal data
timeouts.services[].defaultcontexttimeoutdurationPer-request timeout for this service, overriding the global one
timeouts.services[].timeouts.enabledbooltrueTurns the timeout checker off for this service
timeouts.services[].pathconfig[].exprregexpMatches a request path
timeouts.services[].pathconfig[].durationdurationTimeout for paths matching expr
timeouts.services[].pathconfig[].enabledbooltruefalse 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: 120s

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.

KeyTypeDefaultEffect
zerotrustbooltrueOn: the listener resolves TLS material and refuses to start without it. Off: plain HTTP end to end, no certificate material fetched
svccertpath""Service certificate. TLS is configured only when both svccert and svckey are set
svckeypath""Service private key
rootcacertpath""Root CA certificate
rootcakeypath""Root CA key
insecureskipverifyboolfalseSkip peer certificate verification on outbound TLS
auto_renew_certificatesbooltrueRenew the service certificate automatically
jwt_public_keypath""Public key for verifying inbound tokens
jwt_private_keypath""Private key

Three pairings are checked at boot and fail it:

ConditionError
servicediscovery_url set with neither servicediscovery_apikey nor svccerterror: either servicediscovery_apikey or svccert should be present
servicediscovery_url set, svccert set, svckey absenterror: both svccert and svckey should be present
config.url set with neither config.apikey nor svccerterror: either config.apikey or svccert should be present
KeyTypeDefaultEffect
jwks.issuers.<issuer>URLBase URL for a trusted issuer’s key set, fetched from <base>/.well-known/jwks.json
jwks.timeoutduration3sPer-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.disabledboolfalseAccept 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.

The config: block selects one of three modes.

KeyTypeDefaultEffect
config.urlURL""Remote mode: cluster and tenant nodes stream from a config server
config.pathpath""Local mode: a directory of hive YAML files
config.apikeystring""Bearer credential for the remote server
config.cachepath""On-disk warm-start cache holding the last known good configuration
ModeSelected bySource
Remoteconfig.url setThe config server
Local directoryconfig.path setHive YAML under that directory
Bootstrap-as-configneither setThe 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.

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.

HiveNode 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
KeyTypeEffect
hives.<hive>[].pathstringNode address. "" is the root and matches every lookup
hives.<hive>[].matchmap of stringApplies the entry only to instances whose labels contain these pairs
hives.cluster[].api.keystringCluster API key; also the last-resort fallback for the metadata credential
hives.cluster[].tenantslist of stringTenant keys resolved eagerly at every configuration load. A tenant that fails to resolve keeps the loader retrying
hives.tenant[].namestringTenant display name
hives.tenant[].activeboolWhether the tenant is enabled
hives.tenant[].domainlist of stringDomains mapped to this tenant
hives.tenant[].datastores.<name>mapLogical datastore, bound to a pool by dbpool:
hives.tenant[].dbpools.<name>mapPhysical 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.

KeyTypeDefaultEffect
loadtenantslist of stringemptyTenant 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.

KeyTypeDefaultEffect
servicediscovery_urlURL""Registration endpoint. Empty means no registration at all
servicediscovery_apikeystring""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.

KeyTypeDefaultEffect
vault.urlURL""Endpoint. Empty means no client is built and every pool password: is used as a literal
vault.apikeystring""Credential
vault.filepath""File-backed secret source, used when vault.url is empty
vault.client.transportstringremoteremote, agent or both. An unknown value fails boot with vault-unknown-transport
vault.client.transport_canaryint 0–1000Share of reads served by the agent transport when the transport is both
vault.cache.enabledbooltrueCache resolved secrets in process
vault.cache.typestringinmemoryCache implementation
vault.cache.jwtttlduration58mToken cache lifetime
vault.cache.certttlduration6hCertificate cache lifetime
vault.cache.secretttlduration3hSecret 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.

Definitions come from the metadata service unless a schemasource: override is set. The endpoint is resolved per tenant, in order:

PriorityURL keyCredential key
1hives.tenant[].meta.urlhives.tenant[].meta.apikey
2meta.urlmeta.apikey
3properties.metaurlproperties.metaapikey
4hives.cluster[].api.key
KeyTypeEffect
meta.urlURLService-level metadata endpoint
meta.apikeystringService-level metadata credential
meta.dirpathLocal metadata directory for the service’s own metadata client, read only when meta.url is empty
hives.tenant[].meta.isexternalboolMarks 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.

KeyTypeDefaultEffect
schemasource.typestringabsentlocaldir is the only implemented value. Replaces the metadata-service fetch
schemasource.pathpathDirectory 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.

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.

KeyTypeDefaultEffect
ratelimit.store.kindstringmemorymemory or distrib
ratelimit.store.distrib.backendstring""Backend name for the distributed store
ratelimit.store.distrib.dsnstring""Connection string for the distributed store
ratelimit.profiles.<name>.algorithmstringBucket algorithm
ratelimit.profiles.<name>.capacityint0Burst capacity
ratelimit.profiles.<name>.refill_ratefloat0Sustained refill, requests per second
ratelimit.profiles.<name>.leak_ratefloat0Leak rate for leaky-bucket profiles
ratelimit.profiles.<name>.limitint0Fixed-window limit
ratelimit.profiles.<name>.maxint0Maximum counter value
ratelimit.profiles.<name>.windowduration0Window length. An unparseable value logs and becomes 0
ratelimit.profiles.<name>.ttlduration0Bucket 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 }
ProfileApplies to
v2_query_defaultGET routes
v2_mutate_defaultPOST, PUT, PATCH, DELETE on per-entity routes
v2_bulk_defaultThe bulk envelope
endpoint_query_defaultScripted endpoints that read and declare no limit
endpoint_mutate_defaultScripted 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.

KeyTypeDefaultEffect
responsecache.max_entriesint10000Entry ceiling for the whole service. Values ≤ 0 keep the default
responsecache.default_ttlduration30sLifetime 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.

One listener serves everything on host:port. Surfaces mount at unprefixed paths; behind a gateway the public path is /data/<service path>.

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

ProbeTenancy headersSuccessFailure
GET /readyno200 with ready:true500 with ready:false
GET /healthno200 with a JSON body500 with the same body, healthy:false
GET /checktenantyes200 {"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.

SIGINT or SIGTERM runs, in order:

  1. Stop the HTTP server. No new requests are accepted.
  2. Drain every per-tenant scope through tenancy: engines drain, materialized-refresh runners stop, script runtimes close, pools release.
  3. Stop every tenant metadata-client monitor and signal its done channel.
  4. 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.
  5. 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.

These are read from configuration but change nothing in data.svc, or are not read at all. Several appear in shipped example profiles.

KeyStatus
nameNot read. The service identity is the compiled-in name data
globaltimeoutNot a key. The per-request timeout is timeouts.defaultcontexttimeout
enablecacheNot a key. The cache switches are cache.active and its two children
cache.active, cache.query.active, cache.data.activeParsed, and each sets an internal flag that nothing reads. No cache is built
telemetry.enabledNot read. Telemetry initialisation is never invoked from the shared scaffold, so the engine’s metric instruments record into no-op providers
log.loglinenumbersNot read
timeouts.enabled at the top levelNot read. The toggle lives inside a timeouts.services[] entry
source.typeNot read
clusterstore.*Not read
config_url, config_apikey (flat)Not read for config-plane mode selection. Use the config: block
Message or codeCause
invalid/empty port providedNo port: and no -p
error: both svccert and svckey should be presentHalf a certificate pair
error: either servicediscovery_apikey or svccert should be presentservicediscovery_url with no credential
error: either config.apikey or svccert should be presentconfig.url with no credential
server start failed: failed to load TLSConfigzerotrust 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 failedThe 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_UNSETNo 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-outThe 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.