Configuration¶
toise-server resolves its configuration from four layers. From lowest to
highest precedence:
- Built-in defaults — loopback listeners,
toise-datadata dir, no retention cap. Running with nothing set behaves exactly as the historical flag defaults. - YAML file —
--config <path>or theTOISE_CONFIGenvironment variable. - Environment variables —
TOISE_*. Override the file. Secrets belong here (never on the command line). - Command-line flags — override everything; useful for ad-hoc overrides.
Each layer overrides only what it sets, so you can keep a committed base file and override one value with an env var or a flag for a one-off run. See ADR 0023 for the rationale.
Settings¶
| YAML key | Env var | Flag | Default | Meaning |
|---|---|---|---|---|
listen |
TOISE_LISTEN |
--listen |
127.0.0.1:8080 |
GraphQL/HTTP + MCP + debug UI address |
otlp_listen |
TOISE_OTLP_LISTEN |
--otlp-listen |
127.0.0.1:4317 |
OTLP/gRPC ingestion address |
data_dir |
TOISE_DATA_DIR |
--data-dir |
toise-data |
Pebble event-log directory |
relation_buffer_ttl |
TOISE_RELATION_BUFFER_TTL |
--relation-buffer-ttl |
30s |
hold an out-of-order edge waiting for its endpoints (0 = disabled) |
liveness_sweep_interval |
TOISE_LIVENESS_SWEEP_INTERVAL |
--liveness-sweep-interval |
30s |
how often to expire entities past their heartbeat interval (0 = disabled) |
retention_max_age |
TOISE_RETENTION_MAX_AGE |
--retention-max-age |
0 |
max age of retained events (0 = unlimited) |
retention_compaction_interval |
TOISE_RETENTION_COMPACTION_INTERVAL |
--retention-compaction-interval |
1h |
heartbeat-coalescing compaction cadence |
snapshot_interval |
TOISE_SNAPSHOT_INTERVAL |
--snapshot-interval |
5m |
projection snapshot cadence for fast restart and liveness survival across restarts (0 = disabled) |
backup_dir |
TOISE_BACKUP_DIR |
--backup-dir |
(empty) | directory for periodic online backups (with backup_interval); empty = off (ADR 0029) |
backup_interval |
TOISE_BACKUP_INTERVAL |
--backup-interval |
0 |
interval between online backups of every tenant's event log (0 = off) |
log_shipping_dir |
TOISE_LOG_SHIPPING_DIR |
--log-shipping-dir |
(empty) | directory to ship event-log segments to (with log_shipping_interval); may be a mounted bucket/NFS; empty = off (ADR 0029) |
log_shipping_interval |
TOISE_LOG_SHIPPING_INTERVAL |
--log-shipping-interval |
0 |
interval between event-log segment ships of every tenant (0 = off) |
log_shipping_s3_bucket |
TOISE_LOG_SHIPPING_S3_BUCKET |
— | (empty) | ship segments to this S3 bucket instead of a directory (selects S3 mode); AWS S3 or any S3-compatible store (ADR 0029) |
log_shipping_s3_endpoint |
TOISE_LOG_SHIPPING_S3_ENDPOINT |
— | (empty) | S3 endpoint host[:port], no scheme (e.g. s3.amazonaws.com, minio.internal:9000) |
log_shipping_s3_region |
TOISE_LOG_SHIPPING_S3_REGION |
— | (empty) | optional S3 region; ignored by many compatible stores |
log_shipping_s3_prefix |
TOISE_LOG_SHIPPING_S3_PREFIX |
— | (empty) | optional key prefix under the bucket |
log_shipping_s3_use_ssl |
TOISE_LOG_SHIPPING_S3_USE_SSL |
— | true |
use HTTPS to the endpoint |
| (secret) | TOISE_LOG_SHIPPING_S3_ACCESS_KEY |
— | (empty) | S3 access key — env-only (never config file or flag) |
| (secret) | TOISE_LOG_SHIPPING_S3_SECRET_KEY |
— | (empty) | S3 secret key — env-only |
identity_confidence_threshold |
TOISE_IDENTITY_CONFIDENCE_THRESHOLD |
--identity-confidence-threshold |
0.9 |
same_as confidence (0,1] at/above which an alias joins an entity's canonical view on get_entity (ADR 0020); read-time only, never merges storage |
log_format |
TOISE_LOG_FORMAT |
--log-format |
text |
log output format: text or json |
log_level |
TOISE_LOG_LEVEL |
--log-level |
info |
debug, info, warn, or error |
production |
TOISE_PRODUCTION |
--production |
false |
hardening profile — forces the three below off |
graphql_introspection |
TOISE_GRAPHQL_INTROSPECTION |
--graphql-introspection |
true |
expose GraphQL introspection |
playground |
TOISE_PLAYGROUND |
--playground |
true |
serve the GraphQL playground at /playground |
debug_ui |
TOISE_DEBUG_UI |
--debug-ui |
true |
serve the debug UI at / |
allowed_origins |
TOISE_ALLOWED_ORIGINS |
--allowed-origins |
(empty) | comma-separated browser Origin allowlist (WebSocket/CORS); empty = same-origin only |
auth_tokens |
TOISE_AUTH_TOKENS |
(none — secret) | (empty) | comma-separated bearer tokens, full role (read + ingest), valid for every tenant; empty = auth disabled |
read_tokens |
TOISE_READ_TOKENS |
(none — secret) | (empty) | bearer tokens valid only on the read surfaces (GraphQL, MCP, debug UI) — rejected on OTLP ingest |
ingest_tokens |
TOISE_INGEST_TOKENS |
(none — secret) | (empty) | bearer tokens valid only on OTLP ingest — rejected on the read surfaces |
tenant_tokens |
TOISE_TENANT_TOKENS |
(none — secret) | (empty) | comma-separated tenant:token pairs — full role, authorized only for its tenant (HTTP 403 / gRPC PermissionDenied elsewhere) |
tenant_read_tokens |
TOISE_TENANT_READ_TOKENS |
(none — secret) | (empty) | tenant:token pairs — read-only role, that tenant's read surfaces only (per-tenant RBAC, ADR 0028) |
tenant_ingest_tokens |
TOISE_TENANT_INGEST_TOKENS |
(none — secret) | (empty) | tenant:token pairs — ingest-only role, that tenant's OTLP ingest only |
accept_unknown_types |
TOISE_ACCEPT_UNKNOWN_TYPES |
--accept-unknown-types |
false |
accept entity/relation types outside the built-in registry (shape still validated; counted on /metrics) |
tenant_auto_create |
TOISE_TENANT_AUTO_CREATE |
--tenant-auto-create |
true |
allow a first write to a new tenant id to create its stack; off = only pre-existing tenants (and default) are served |
tenant_allowlist |
TOISE_TENANT_ALLOWLIST |
--tenant-allowlist |
(empty) | comma-separated tenant ids allowed to be created; empty = any (subject to auto-create and the cap) |
max_tenants |
TOISE_MAX_TENANTS |
--max-tenants |
0 |
cap on open tenants; 0 = unbounded. Reading an unknown tenant never creates it (404) |
tenant_trust_mode |
TOISE_TENANT_TRUST_MODE |
--tenant-trust-mode |
trust-header |
how a request's tenant is decided. trust-header: from X-Scope-OrgID / tenant.id (the edge is trusted). derive-only: a tenant-scoped token's tenant is derived from its binding and any client-supplied X-Scope-OrgID / tenant.id is ignored (anti-spoofing for multi-tenant SaaS, ADR 0028); global (operator) tokens keep header-based cross-tenant selection |
tls_cert_file |
TOISE_TLS_CERT_FILE |
--tls-cert-file |
(empty) | PEM certificate; with the key, serves HTTP + OTLP over TLS |
tls_key_file |
TOISE_TLS_KEY_FILE |
--tls-key-file |
(empty) | PEM private key (pairs with the cert) |
tls_client_ca_file |
TOISE_TLS_CLIENT_CA_FILE |
--tls-client-ca-file |
(empty) | PEM CA bundle; when set, requires + verifies a client certificate on OTLP ingest (mTLS, ADR 0028) — needs TLS; the HTTP surfaces are unaffected |
ingest_mtls_only |
TOISE_INGEST_MTLS_ONLY |
--ingest-mtls-only |
false |
authenticate OTLP ingest by mutual TLS alone (no bearer required or consulted on ingest) while the read surfaces keep requiring their scoped tokens / OIDC; decouples ingest auth from read auth (ADR 0028). Requires tls_client_ca_file |
audit_log |
TOISE_AUDIT_LOG |
--audit-log |
(empty) | path to an append-only JSON-line audit file for operator writes (annotate_entity), per tenant; empty = off (ADR 0028) |
oidc_issuer |
TOISE_OIDC_ISSUER |
--oidc-issuer |
(empty) | OIDC issuer URL; when set, JWT bearers are verified on the read surfaces (discovery + JWKS). Empty = OIDC off (ADR 0028) |
oidc_audience |
TOISE_OIDC_AUDIENCE |
--oidc-audience |
(empty) | expected JWT aud |
oidc_tenant_claim |
TOISE_OIDC_TENANT_CLAIM |
--oidc-tenant-claim |
tenant |
JWT claim carrying the tenant id |
oidc_role_claim |
TOISE_OIDC_ROLE_CLAIM |
--oidc-role-claim |
(empty) | JWT claim carrying the role (read/ingest/full); empty = every valid token is full |
Durations are Go-duration strings ("30s", "5m", "1h30m"). Unknown YAML
keys are rejected — a typo fails at startup rather than being silently ignored.
--mcp-stdio (serve only the MCP server over stdio, for Claude Desktop) is a run
mode rather than steady-state config; pass it as a flag when needed — see
MCP for AI assistants.
Examples¶
A complete annotated file lives at
examples/toise-server.yaml.
A minimal one:
listen: 0.0.0.0:8080
otlp_listen: 0.0.0.0:4317
data_dir: /var/lib/toise
retention_max_age: 720h # 30 days
Run it:
toise-server --config /etc/toise/toise-server.yaml
# or
TOISE_CONFIG=/etc/toise/toise-server.yaml toise-server
Override one value for a single run without editing the file:
# env wins over the file...
TOISE_DATA_DIR=/tmp/scratch toise-server --config /etc/toise/toise-server.yaml
# ...and a flag wins over both
toise-server --config /etc/toise/toise-server.yaml --listen 127.0.0.1:9999
Authentication & TLS¶
Both are off by default — the server binds to 127.0.0.1 and trusts the
network (private datacenter segment or VPN; ADR 0014). Exposing it to other hosts
(0.0.0.0:...) is an explicit choice; when you make it, turn these on.
- Bearer tokens. Set
auth_tokensvia the environment only — they are secrets and must never appear on the command line or in a committed file:
Clients then send Authorization: Bearer <token> on both HTTP and gRPC. The
operational probes (/healthz, /readyz) and the metrics scrape (/metrics)
stay public so a load balancer and Prometheus can reach them without a token.
Configured tokens are hashed at rest (SHA-256): the server holds only the
hashes, never the plaintext, so a heap dump never yields a usable token
(ADR 0028). Matching is constant-time. Rotation is operational — add the new
token alongside the old, then drop the old one; revocation is removing a token
and reloading.
- Token roles (least privilege). auth_tokens are full (read + ingest). Use
read_tokens for a token that may query but never ingest (a dashboard, an
assistant), and ingest_tokens for a producer that may ingest but never read.
A read-only token is rejected on OTLP ingest; an ingest-only token is rejected
on GraphQL/MCP/debug. These roles are global (every tenant). For per-tenant
RBAC, the same roles exist scoped to one tenant: tenant_tokens (full),
tenant_read_tokens (read-only), tenant_ingest_tokens (ingest-only) — each a
tenant:token pair authorized only for its tenant and surface (ADR 0028).
- TLS. Point tls_cert_file and tls_key_file at a PEM cert/key pair to serve
the HTTP surfaces and OTLP ingestion over TLS.
- mTLS on ingest (optional). Set tls_client_ca_file to a PEM CA bundle to
require and verify a client certificate on the OTLP ingest listener — for
regulated producers, on top of the bearer token. It applies to ingest only; the
HTTP read surfaces keep bearer/OIDC auth. Requires TLS to be enabled (ADR 0028).
- Audit log. Set audit_log to a file path to record an append-only JSON-line
entry for every operator write (annotate_entity, on MCP and GraphQL) — the time,
tenant, surface, and target entity. It is distinct from the producer event log,
exportable, and off by default. A write failure is logged (never silent), and
never fails the audited operation (ADR 0028).
- OIDC / JWT (read surfaces). Set oidc_issuer (and oidc_audience) to verify
JWT bearers on GraphQL/MCP/debug as a second path after the static tokens: the
token is validated against the issuer (discovery + JWKS, signature, audience,
expiry), and its oidc_tenant_claim / oidc_role_claim map to a tenant and role.
The client X-Scope-OrgID is ignored for a verified JWT (the claim is the
authority). Off by default; the static bearer tokens stay the baseline (ADR 0028).
Hardening for production¶
The development surfaces — GraphQL introspection, the /playground, and the debug
UI — are on by default for a friendly local experience. For an exposed deployment,
turn them off with a single switch:
production: true forces graphql_introspection, playground, and debug_ui
off, winning over any individual setting. Set allowed_origins to the browser
Origins permitted for WebSocket subscriptions and CORS (empty = same-origin only).
Multi-tenancy¶
One Toise instance can serve multiple tenants with fully isolated graphs — a
query scoped to tenant A never sees tenant B (ADR 0025). Each tenant gets its own
store + projection + change engine under <data_dir>/<tenant>/.
The tenant id is generic and vendor-neutral, resolved (in order) from:
- the
X-Scope-OrgIDrequest metadata — the de-facto standard used by Mimir/Loki/Tempo/VictoriaMetrics (an HTTP header on the query surfaces, gRPC metadata on OTLP ingest); - a
tenant.idresource attribute on the OTLP request (set perResourceLogs, it overrides the request metadata — so one OTLP stream can carry several tenants); - otherwise the
defaulttenant.
# Query tenant "acme" over HTTP
curl -H 'X-Scope-OrgID: acme' http://127.0.0.1:8080/graphql \
-d '{"query":"{ entities { totalCount } }"}'
Single-tenant deployments need no configuration: with no tenant id ever set,
everything lives under default and behaves as a single-graph build. A
pre-existing data directory is migrated to <data_dir>/default/ automatically on
first start (take a backup first, as with any upgrade). /metrics reports the sum
across tenants, so existing dashboards are unchanged.
Boot quarantine: if a tenant's store cannot be opened at startup (a corrupt
or half-written store), the server does not abort — it logs a warning, skips
that tenant, counts it in the toise_tenants_quarantined gauge, and serves the
healthy tenants. The default tenant is the exception: it is required, so its
failure is fatal. A quarantined tenant's directory is left on disk under
<data_dir>/<tenant>/ for recovery; restore or remove it and restart.
Deleting a tenant is a cold, destructive operation — run it with the server stopped (it holds the pebble lock while serving):
It removes <data_dir>/acme/ (event log and snapshot) entirely and frees a slot
under max_tenants. The default tenant cannot be deleted.
Metrics and tenants: the Toise gauges on /metrics (toise_entities,
toise_events_total, toise_store_disk_bytes, …) are aggregated across
tenants — a sum, except toise_snapshot_seq and toise_store_prune_horizon_seconds
which report the high-water mark. They carry no per-tenant label, so the endpoint
stays single-series and dashboards are unchanged. The maintenance metrics
(toise_maintenance_*) are the exception: they label by tenant (and op,
outcome) so a stuck sweep/prune can be traced to its tenant.
Authentication is not yet bound to a tenant
A valid bearer token may set any X-Scope-OrgID. Isolation therefore relies on
the upstream OTel Collector authenticating each client and stamping its tenant;
do not expose the ports directly to untrusted multi-tenant clients. Per-token
tenant binding is planned.
Operational endpoints¶
Always on, on the listen address: /healthz (liveness), /readyz (readiness —
green only when every tenant store is healthy), and Prometheus /metrics (sampled
at scrape time). Wire these into your orchestrator and scrape config.
See also Storage sizing for choosing
retention_max_age, and Performance for the
sweep/compaction cadences.