Skip to content

Configuration

Configuration comes from the environment through one model, jdlib.TenancyConfig (a pydantic-settings BaseSettings). There is no configuration file: a library that reads files has to answer where the file is, and every deployment answers differently.

The model

Field Type Default What it is
control_dsn SecretStr \| None None the control-plane connection string; wrapped, so it cannot be logged by accident
context ContextConfig required the context envelope: signing_key (required, no default), envelope_ttl, operator_ttl, allow_test_contexts
resolvers ResolverConfig ResolverConfig() order, base_domain, path_prefix, header_name, jwt_claim
oidc OidcConfig \| None None issuer, audience, algorithms, jwks_url, jwks_ttl, leeway, email_trusted
api_keys ApiKeyConfig ApiKeyConfig() prefix — the shape of an API key the library will parse
rls RlsConfig RlsConfig() enabled, app_role — the row-level-security policy and the role it applies to

Every field is settable as an environment variable with the JDLIB_ prefix; __ nests:

JDLIB_CONTROL_DSN=postgresql+psycopg://jd:secret@localhost:5432/app
JDLIB_CONTEXT__SIGNING_KEY=<32 bytes of hex>
JDLIB_CONTEXT__ENVELOPE_TTL=900
JDLIB_RESOLVERS__ORDER=jwt,subdomain,path,header,api_key
JDLIB_RESOLVERS__BASE_DOMAIN=example.com
JDLIB_RESOLVERS__JWT_CLAIM=tenant
JDLIB_OIDC__ISSUER=https://login.example.com/
JDLIB_OIDC__AUDIENCE=my-api
JDLIB_OIDC__ALGORITHMS=RS256,ES256
JDLIB_RLS__ENABLED=true
JDLIB_RLS__APP_ROLE=jdlib_app

What fails fast, and why

TenancyConfig() raises at construction when JDLIB_CONTEXT__SIGNING_KEY is missing. That is deliberate: the key signs the envelope a job or a delegation carries, and a default would be a shared secret across every deployment that forgot to set one. The minimal example has no settings module at all — the library's JDLIB_* contract is the entire surface — and one of its tests asserts that a process started without a DSN refuses to start rather than serving unconfigured.

The resolver order

JDLIB_RESOLVERS__ORDER is the list of places the tenant may come from, in the order they are tried. Each name is a resolver the library ships:

Name Reads Notes
jwt the configured claim in a verified token the claim name is JDLIB_RESOLVERS__JWT_CLAIM
subdomain the host, against JDLIB_RESOLVERS__BASE_DOMAIN acme.example.com → acme
path the first segment after JDLIB_RESOLVERS__PATH_PREFIX useful for /t/{slug}/… APIs
header JDLIB_RESOLVERS__HEADER_NAME only meaningful behind a gateway that sets it; a client can send any header, which is why it is not first by default
api_key the tenant an API key belongs to resolved from the control plane, not from the key's text

The order is configuration because it is a deployment decision; the rule is not: the resolved tenant is the only source, and a request field that claims one is ignored — asserted by examples/enterprise/tests/security/test_tenant_isolation.py.

Secrets

Secrets arrive as SecretStr and are unwrapped at the point of use, never logged, never placed in a span attribute (the tracing allow-list drops them), and never cached — jdlib.caching refuses a credential object outright, including inside a container. Where a secret comes from is a provider chain: the environment, a file, or your own provider, composed in order.

Configuration that is not environment

Three things are deliberately code rather than configuration, because they are claims about the process rather than settings for it:

  • the span attribute allow-list (SPAN_ATTRIBUTE_ALLOWLIST) — what may leave the process;
  • the error envelope's shape — one contract for every failure;
  • the capability each route or tool declares — a permission is a decision, not a knob.

Reference

The model itself, including every validator and default, is src/jdlib/config.py: the table above is that file read at this commit, and the generated API reference covers the same surface.