config.yaml Reference
Full schema for the file you pass to definite init. See examples/ for working starting points.
Top-level shape
deployment: { ... } # required
broker: { ... } # optional, used by default brokered DNS/license setup
postgres: { ... } # required
object_store: { ... } # required
lakehouse: { ... } # optional, defaults to postgres_catalog mode
events: { ... } # optional, defaults to enabled
fi: { ... } # optional, defaults to disabled
auth: { ... } # optional, defaults to local auth
llm: { ... } # required
resources: { ... } # optional
license: { ... } # required for a usable deployment (see `license`)
telemetry: { ... } # optional, defaults to disabled
deployment
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name | string | yes | Kubernetes-safe name. Lowercase alphanumeric + hyphens, max 63 chars. | |
namespace | string | no | definite | Created if missing. |
dns_mode | enum | no | definite-brokered | definite-brokered or customer-owned. |
hostname | string | conditional | Required only for dns_mode: customer-owned; brokered installs receive this from Definite during definite init. | |
tls | enum | no | cert_manager | cert_manager, manual, or disabled. |
Default installs use Definite-brokered DNS, so the customer does not need to
create a DNS record before definite init. To manage DNS yourself, set
dns_mode: customer-owned and provide hostname, or pass
--dns-mode customer-owned --hostname analytics.customer.com to definite init.
tls: cert_manager uses the production Let's Encrypt issuer created by
definite bootstrap. For customer-owned DNS, use a real DNS hostname that you
control. Do not use *.nip.io with tls: cert_manager: Let's Encrypt applies a global
registered-domain rate limit to nip.io, so new certs for any nip.io
hostname are commonly rejected. The CLI rejects that combination before
install; for local/no-trust testing use tls: disabled or bring your own cert
with tls: manual.
broker
Used by the default deployment.dns_mode: definite-brokered flow. The CLI
discovers the ingress controller's load-balancer target, calls the Definite
broker, waits for brokered DNS readiness, then uses the returned FQDN and
license key for the Helm install.
broker:
api_url: https://api.definite.app
setup_token: { env: DEFINITE_ONPREM_SETUP_TOKEN }
requested_slug: analytics # optional
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
api_url | string | no | https://api.definite.app | Broker API base URL. Can also be overridden with DEFINITE_BROKER_API_URL. |
setup_token | SecretRef | no | Optional one-time setup token issued by Definite. If omitted, the CLI falls back to DEFINITE_ONPREM_SETUP_TOKEN, then DEFINITE_SETUP_TOKEN, then automatic cloud attestation. Never printed by the CLI. | |
requested_slug | string | no | Optional preferred subdomain slug. Can also be passed with --requested-slug or DEFINITE_BROKER_REQUESTED_SLUG. |
If no setup token is configured, brokered installs automatically acquire one
from cloud credentials. On GCP, set
DEFINITE_ONPREM_GCP_ATTESTATION_SERVICE_ACCOUNT or gcloud
auth/impersonate_service_account when the active account must impersonate a
service account to mint an audience-bound identity token with an email claim.
postgres
| Field | Type | Required | Default |
|---|---|---|---|
url | string | yes | |
pool_size | uint | no | 20 |
schema | string | no | definite |
Backups are a prerequisite, not a config field. Definite does not back up the application database — the managed instance behind
urlmust have automatic daily backups and point-in-time recovery enabled before you deploy. The bundled AWS Terraform andscripts/install-eks.shdo this for RDS by default; for Cloud SQL and Azure seeprerequisites.mdand the backup runbookbackup-restore.md.
object_store
Tagged union by type:
S3
object_store:
type: s3
bucket: my-bucket
region: us-east-1
endpoint: null # optional, for S3-compatible non-AWS
credentials:
mode: irsa
role_arn: arn:aws:iam::123456789012:role/definite-lakehouse-irsa
# Static fallback:
# credentials:
# key_id: { env: S3_ACCESS_KEY_ID }
# secret: { env: S3_SECRET_ACCESS_KEY }
GCS
object_store:
type: gcs
bucket: my-bucket
credentials: null # optional; prefer Workload Identity
Azure
object_store:
type: azure
account: mystorageaccount
container: definite
credentials: null
MinIO
object_store:
type: minio
bucket: definite-lake
endpoint: https://minio.internal:9000
credentials:
literal: "accesskey:secretkey"
lakehouse
Optional. The lakehouse is postgres_catalog: the DuckLake catalog lives
in a dedicated Postgres database, query plans run client-side in embedded
DuckDB (ATTACH 'ducklake:postgres:<DSN>'), and parquet is read from the object
store over httpfs. There is no separate query server. This block only tunes the
catalog connection and a few persistence knobs — omit it entirely to keep
defaults.
lakehouse:
data_inlining_row_limit: 1000
# catalog_dsn: derived from postgres.url by default; set only to override.
| Field | Type | Default | Notes |
|---|---|---|---|
catalog_dsn | string | derived from postgres.url | libpq keyword/value connection string for the DuckLake catalog database, e.g. dbname=ducklake_catalog host=H port=5432 user=U password=P. When omitted, the CLI derives it from postgres.url by changing only the database name to ducklake_catalog. Reuse the deployment's existing Postgres server but keep this as a dedicated database (not the application database). Supports ${ENV_VAR} substitution when provided explicitly. |
data_inlining_row_limit | uint | 1000 | DuckLake row threshold for inlining small inserts/deletes into the metadata catalog. Keep this at least as high as events.max_batch_rows if event batches should inline; 0 disables inlining. |
prefix | string | lake/ | Object-store prefix where lakehouse parquet data lives. |
events
Small-batch event ingestion endpoint (POST /api/v1/events/{schema}/{table}).
Defaults are tuned so event writes fit under DuckLake's inlining threshold.
events:
enabled: true
max_batch_rows: 1000
max_payload_bytes: 1048576
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | Enables the event ingest API. |
max_batch_rows | uint | 1000 | Maximum events accepted in one request. Keep this less than or equal to lakehouse.data_inlining_row_limit for inline writes. |
max_payload_bytes | uint | 1048576 | Maximum request body size. |
fi
Fi is disabled by default. Enabling it requires kubernetes-sigs Agent Sandbox
v0.4.5 CRDs/controller to already be installed in the cluster; definite doctor verifies that prerequisite.
fi:
enabled: true
web_access:
enabled: true # optional; default true
web_search:
provider: duckduckgo # optional; default duckduckgo
api_key: { ... } # SecretRef; required for brave/tavily/serpapi
url: https://searxng.internal # searxng provider only
sandbox:
template_name: definite-fi
image: ghcr.io/definite-app/fi-sandbox:0.0.4
runtime_class_name: gvisor # optional; omit for default runtime
workspace_size: 10Gi
storage_class_name: gp3 # optional
max_active_threads: 5
idle_ttl_seconds: 604800
resources:
request_cpu: "1"
request_memory: 2Gi
limit_cpu: "4"
limit_memory: 8Gi
egress:
public_ports: [] # optional; [] = all ports allowed
private_cidrs: [] # optional; exact private CIDRs to allow
private_ports: [5432] # optional; TCP ports for private_cidrs
The API creates one Agent Sandbox SandboxClaim per Fi thread and reuses its
PVC-backed /workspace across runs. Interactive Fi chat sandboxes do not
receive object-store or Postgres credentials; query/catalog access goes through
the API and lakehouse. Compute-profile SQL workers use the same sandbox
template, but receive the lakehouse/object-store query environment they need to
run SQL inside the worker pod. For v1, sandbox pods do receive the configured
LLM credentials so the Fi agent can call the customer's provider directly from
inside the sandbox.
fi.sandbox.egress
A Kubernetes NetworkPolicy governs what the Fi sandbox can reach. In-cluster
traffic (the API pod, DNS, the LiteLLM proxy, the GCP metadata server) is fixed.
The default tunable knob is which ports the sandbox may open to the public
internet — any public IP outside the RFC1918 ranges, which stay blocked so the
sandbox cannot dial internal cluster pods/services on arbitrary ports. Operators
can also opt in to exact private CIDRs when a sandbox or compute-profile worker
must reach a private dependency such as the DuckLake Postgres catalog.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
egress.public_ports | list[int] | no | [] | Ports the sandbox may reach on public IPs. Empty list = all ports allowed, so the agent can connect directly to external databases (Postgres 5432, MySQL 3306, etc.) and arbitrary APIs. Set an explicit allowlist (e.g. [443]) to restrict it. |
egress.private_cidrs | list[string] | no | [] | RFC1918 IPv4 or IPv6 ULA CIDRs the sandbox may reach. Empty keeps private ranges blocked. Set exact CIDRs such as ["10.224.0.3/32"] when compute-profile workers need a private catalog endpoint. |
egress.private_ports | list[int] | no | [5432] | TCP ports allowed for private_cidrs. Defaults to Postgres. |
The default is intentionally open: the Fi agent often needs a direct connection
to a customer's source database. To lock the sandbox down to HTTPS only, set
public_ports: [443].
fi.web_access / fi.web_search
Fi can search the public web and fetch URLs via two tools — definite_web_search
and definite_fetch. Both run server-side in the API pod, not the sandbox,
so the API owns egress, size caps, and timeouts.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
web_access.enabled | bool | no | true | Set false for air-gapped installs; the tools are then not registered. |
web_search.provider | enum | no | duckduckgo | duckduckgo | searxng | brave | tavily | serpapi. |
web_search.api_key | SecretRef | for keyed providers | — | Required for brave / tavily / serpapi. |
web_search.url | string | for searxng | — | Base URL of a self-hosted SearXNG instance. |
duckduckgo is the default because it is free and needs no API key — web search
works on a fresh install with nothing to configure.
auth
Tagged union by mode:
Local
auth:
mode: local
initial_admin_email: admin@example.com # optional
initial_admin_password: # optional; enables first admin password login
env: INITIAL_ADMIN_PASSWORD
OIDC
auth:
mode: oidc
issuer: https://issuer.example.com
client_id: my-client-id
client_secret:
env: OIDC_CLIENT_SECRET
scopes: [openid, email, profile] # optional
email
Optional transactional email settings. When configured, /login shows a
self-service password reset flow, and automation pipelines can send rendered
data-app reports with attachments. When unset, /login shows the
workspace-admin and operator recovery paths instead.
email:
mode: smtp # disabled | smtp | definite_cloud
enabled: true
from: "Definite <no-reply@example.com>"
reply_to: "support@example.com" # optional
smtp:
host: smtp.example.com
port: 587
username: smtp-user # optional for trusted relays
password:
env: EMAIL_SMTP_PASSWORD # optional for trusted relays
starttls: true
ssl: false
definiteCloud:
centralApiUrl: https://api.definite.app
deploymentId: "" # defaults to license.deploymentId
from: hello@definite.app
definite_cloud is for Definite-managed hosted deployments. Self-managed
installs should use smtp with a customer-owned relay.
See password-recovery.md for behavior and fallback recovery details.
llm
Tagged union by provider:
llm:
provider: anthropic
model: claude-sonnet-5
api_key: { env: ANTHROPIC_API_KEY }
# or
llm:
provider: bedrock
region: us-east-1
model: claude-sonnet-4-6
inference_profile_prefix: us # geo prefix of the cross-region profile;
# us (default) | eu | apac | au | ...
# or
llm:
provider: vertex
project: my-project
region: global
model: claude-sonnet-5
# or
llm:
provider: azure_openai
endpoint: https://acme.openai.azure.com
deployment: gpt-4o
api_key: { env: AZURE_OPENAI_API_KEY }
serviceAccount
Optional Helm ServiceAccount settings. The chart names the shared
ServiceAccount after deployment.name; annotations here are rendered onto that
shared ServiceAccount. On EKS Bedrock installs, set the IRSA role annotation
from Terraform:
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/definite-bedrock-irsa
When S3 uses object_store.credentials.mode: irsa, the chart also creates a
dedicated <deployment.name>-lakehouse ServiceAccount for the API and
job-runner pods and annotates it with object_store.credentials.role_arn.
resources
Per-component sizing. All fields optional.
resources:
api: { replicas: 2, cpu: "1", memory: 4Gi }
frontend: { replicas: 2, cpu: 500m, memory: 512Mi }
job_runner: { replicas: 1, cpu: 500m, memory: 2Gi }
There is no separate lakehouse component to size: lakehouse queries run as
embedded DuckDB inside the API and job-runner pods. Size api.memory for
interactive Query-page / data-app SQL and job_runner.memory for automation
and maintenance SQL. For heavy bursty SQL, prefer a
compute profile over permanently fattening these pods.
Sizing for heavy SQL
Lakehouse queries run as embedded DuckDB inside the API and job-runner
pods (ATTACH 'ducklake:postgres:<DSN>', parquet over httpfs) — there is no
separate lakehouse query pod to size or pin. For steady-state query memory,
raise resources.api.memory (interactive Query-page / data-app SQL) and
resources.job_runner.memory (automation + maintenance SQL).
For bursty heavy SQL — one-off migrations, big joins, full-table
re-materializations — prefer a compute_profiles block
over permanently fattening the always-on pods. A burst profile spawns a fresh
sandbox pod per call (so the upstream DuckDB connection-scoped memory leak does
not accumulate) and can pin that pod to a dedicated, scale-to-zero node pool.
See compute-profiles.md for the per-cloud node-pool
setup (GKE / EKS / Karpenter / AKS) and when to raise the always-on pods vs.
add a burst profile.
compute_profiles
Named burst tiers a user, Fi thread, or pipeline Python step can pick
at runtime to land on a bigger pod. Each entry renders its own
SandboxTemplate and an optional SandboxWarmPool. Empty / omitted uses the
default sandbox settings from fi.sandbox.*.
compute_profiles:
large:
resources:
requests: { cpu: "4", memory: "16Gi" }
limits: { cpu: "8", memory: "32Gi" }
node_selector:
cloud.google.com/gke-nodepool: definite-burst # EKS / AKS keys differ
tolerations:
- { key: definite-app/burst, operator: Equal, value: "true", effect: NoSchedule }
warm_pool_size: 0 # cold-start; node pool can scale to zero
max_concurrent: 2 # attached SQL worker slots
worker_idle_ttl_seconds: 300
| Field | Type | Default | Notes |
|---|---|---|---|
resources | K8s resources map | inherit fi.sandbox.resources | Standard {requests, limits}. GPUs etc. flow through. |
node_selector | {label: value} | none | Pins to a specific node pool. See compute-profiles.md for per-cloud label keys. |
tolerations | list | none | Required when the pool carries a taint. |
warm_pool_size | int | fi.sandbox.warmPoolSize | 0 = cold-start; bigger numbers reserve idle pods at the profile's cost. |
worker_idle_ttl_seconds | int | fi.workerTier.idleTtlSeconds | How long an attached SQL worker stays warm after the last query before the API reaper deletes its claim. |
workspace_size | string | fi.sandbox.workspaceSize | Per-pod ephemeral PVC. |
storage_class_name | string | fi.sandbox.storageClassName | StorageClass for the workspace PVC. |
max_concurrent | int | none | Maximum attached SQL workers for the profile when worker tier is enabled. Excess queries queue briefly, then return 503. |
Profile names must be DNS-label safe (lowercase alphanumeric + hyphens,
max 40 chars). The default profile is always present — declare it
explicitly to override defaults, or omit it to fall back to
fi.sandbox.*. See compute-profiles.md for the
full guide including node-pool setup and a TPC-H recipe.
license
license:
key: { env: DEFINITE_LICENSE_KEY }
# Optional. Leave unset unless Definite support tells you to preserve a
# known deployment identity during restore or test automation.
deploymentId: "00000000-0000-4000-8000-000000000000"
# Optional restore/test hook. Must be signed by a key trusted through
# telemetry.publicKey or the image-bundled Definite public keys.
initialEntitlementJwt: { env: DEFINITE_INITIAL_ENTITLEMENT_JWT }
Required for customer-owned/manual licensing. In the default brokered install
flow, definite init receives a license key from the broker and uses it for
the install without printing it. Otherwise, the CLI renders license.key into the
definite-secrets Kubernetes Secret; the definite-api pod reads the
Secret's license-key entry as LICENSE_KEY and activates against the
central Definite API. Without a license block or externally managed
license-key Secret entry, the deployment still boots, but it comes up
unlicensed and every product API route returns HTTP 403. definite init
will print a warning, and definite doctor flags the missing block.
Advanced installs that use an external secret store may leave license.key
unset and sync license-key into the chart-managed definite-secrets Secret
instead. The chart wires LICENSE_KEY and
LICENSE_INITIAL_ENTITLEMENT_JWT from that Secret with optional
secretKeyRefs, so an unlicensed install still starts. Kubernetes reads env
vars only at container startup; make sure your external secret controller or a
reloader rolls the API pod when the synced license changes.
The key is issued by Definite (internally, via pybe onprem issue-license).
key is a SecretRef — supply it as an env
var, a file, or (not recommended) an inline literal. The k8s_secret SecretRef
form is not supported for license fields; external secret store installs should
sync license-key into definite-secrets instead. To license or re-license a
running deployment, add or update this block and run definite upgrade. Check
the live entitlement with definite license status.
deploymentId is an advanced restore/test hook. On a fresh application
database, the API adopts it as the stable deployment UUID before activation.
Once the deployment has activated, changing deploymentId is ignored so the
central license binding is not silently moved.
initialEntitlementJwt is also advanced. On a fresh database, the API verifies
the JWT and persists it before trying central activation. This is for restore
or controlled test automation, not the normal customer licensing path.
telemetry
telemetry:
enabled: false # opt-in
image
Override the container image source for all Definite components. Useful when you've mirrored images into your own registry (Artifact Registry, ECR, ACR, Harbor, etc.). All fields are optional; omit the block entirely to use the release defaults.
| Field | Type | Default | Notes |
|---|---|---|---|
registry | string | release default | Registry + repo prefix; the chart appends /<component>:<tag>. |
tag | string | CLI version | Image tag. Defaults to the version of the definite CLI itself, so chart and CLI ship in lockstep. |
pull_policy | string | IfNotPresent | Kubernetes imagePullPolicy. |
pull_secrets | string[] | [] | Names of imagePullSecrets in the deployment namespace. |
image:
registry: us-central1-docker.pkg.dev/acme-prod/definite
tag: 0.0.4
pull_policy: IfNotPresent
pull_secrets:
- artifact-registry-pull
SecretRef (used by many fields)
A SecretRef is a tagged union telling the CLI where to find a sensitive value:
# Read from an environment variable at deploy time
field: { env: SOME_ENV_VAR }
# Read from a file at deploy time
field: { file: /etc/secrets/postgres-password }
# Reference an existing Kubernetes Secret
field:
k8s_secret:
name: my-existing-secret
key: password
# Inline literal (NOT RECOMMENDED — value ends up in Helm release)
field: { literal: "plaintext-value" }
For env, file, and literal refs the CLI resolves the value to plaintext at
deploy time and the chart writes it to a Kubernetes Secret it manages.
k8s_secret refs point the chart at an existing Secret you already manage with
your own tooling (External Secrets Operator, Vault sync, etc.) — note that
end-to-end passthrough for k8s_secret is partial today; some fields still
require env / file / literal.
Environment variable substitution
Anywhere in the config, ${VAR} is replaced by the value of VAR at config load time. Missing variables fail loud — they don't silently produce empty values.
postgres:
url: postgres://definite:${POSTGRES_PASSWORD}@db.internal:5432/definite