Skip to content

Environment Variables

Variable Used by Default Description
API_KEY API - Bearer token required for authenticated /v1/* API requests.
DATABASE_URL API/worker Local test database URL PostgreSQL connection string. Set explicitly outside local development.

hub-worker requires an explicit DATABASE_URL; it does not use the local test default.

Variable Default Description
PORT 8080 HTTP port used inside the API process/container.
PUBLIC_BASE_URL - Public API root advertised in runtime OpenAPI specs when Hub is behind ingress, TLS, or a path prefix.
LOG_LEVEL info Log level, such as debug, info, warn, or error.
SHUTDOWN_TIMEOUT_SECONDS 30 Graceful shutdown timeout in seconds.

LOG_LEVEL also applies to the job-queue lines hub-worker logs for its background jobs. A failed job attempt is logged at info, with the job id, kind, and error, so warn or error hides failures that a job does not report itself. At info the job queue also logs a status line per busy queue at most every 5 seconds, and a summary when its maintenance work (scheduling retries, cleaning up finished jobs) did something. debug adds the job queue’s internal tracing (fetches, completions, notifications), whose volume grows with job throughput.

So that an outage cannot flood the logs, hub-worker samples repeated job-queue lines. Once one job kind logs the same line more than 10 times in a second, it keeps 1 in 100 for the rest of that second. Job-queue lines that carry no job kind are sampled the same way, per message. The dropped lines are counted: the next time that line is logged, or when hub-worker shuts down, a similar log lines suppressed by sampling line reports the count and the window (since, until) it covers. hub-worker’s other lines are never sampled.

The Quick Start Docker Compose file also supports HUB_PORT, POSTGRES_PORT, and HUB_IMAGE_TAG as Compose conveniences. They are compose variables, not Hub runtime settings.

PUBLIC_BASE_URL must be an absolute http or https URL. It may include a path prefix, such as https://example.com/hub, but must not include query strings, fragments, or credentials. Hub serves runtime OpenAPI specs at /openapi.yaml and /openapi.json.

Variable Default Description
DATABASE_MAX_CONNS 25 Maximum PostgreSQL pool connections.
DATABASE_MIN_CONNS 0 Minimum PostgreSQL pool connections.
DATABASE_MAX_CONN_LIFETIME_SECONDS 3600 Maximum connection lifetime.
DATABASE_MAX_CONN_IDLE_TIME_SECONDS 1800 Maximum idle time before a connection is closed.
DATABASE_HEALTH_CHECK_PERIOD_SECONDS 60 Pool health-check interval.
DATABASE_CONNECT_TIMEOUT_SECONDS 10 Database connection timeout.
Variable Default Description
RIVER_JOB_TIMEOUT_SECONDS 0 Max job runtime before cancellation; 0 uses the River default.
RIVER_RESCUE_STUCK_JOBS_AFTER_SECONDS 0 Stuck-job rescue interval; 0 uses the River default.
RIVER_COMPLETED_JOB_RETENTION_SECONDS 86400 Completed job retention period; -1 disables deletion.
RIVER_CLIENT_ID - Optional client identifier for logs and River coordination.
Variable Default Description
WEBHOOK_DELIVERY_MAX_CONCURRENT 100 Max concurrent webhook delivery workers.
WEBHOOK_DELIVERY_MAX_ATTEMPTS 3 Max delivery attempts per webhook job.
WEBHOOK_MAX_FAN_OUT_PER_EVENT 500 Max webhook jobs inserted for a single event batch.
WEBHOOK_MAX_COUNT 500 Max total webhook endpoints allowed.
WEBHOOK_HTTP_TIMEOUT_SECONDS 15 Outbound webhook request timeout.
WEBHOOK_ENQUEUE_MAX_RETRIES 3 Retries when enqueueing webhook jobs fails.
WEBHOOK_ENQUEUE_INITIAL_BACKOFF_MS 100 Initial enqueue retry backoff in milliseconds.
WEBHOOK_ENQUEUE_MAX_BACKOFF_MS 2000 Maximum enqueue retry backoff in milliseconds.
WEBHOOK_BLACKLIST localhost,127.0.0.1,::1,169.254.169.254 Comma-separated hostnames blocked for webhook URLs.
Variable Default Description
MESSAGE_PUBLISHER_QUEUE_MAX_SIZE 16384 In-memory event channel buffer size.
MESSAGE_PUBLISHER_PER_EVENT_TIMEOUT_SECONDS 10 Per-event publish timeout in seconds.

Hub runs four independent text enrichments — embeddings, translation, sentiment, and emotions — processed asynchronously by hub-worker. Each is enabled per type by setting its *_PROVIDER and *_MODEL; if either is unset, that enrichment is disabled and no jobs are enqueued.

Each enrichment also has a *_MAX_INPUT_BYTES cap on the text it sends to its provider, in UTF-8 bytes (0 disables it). No accepted record can exceed the 30,000-rune ingest ceiling — roughly 120,000 bytes, and about 90,000 for CJK — so a value at or above that has no effect whatever; the defaults are deliberately far below it, because the caps exist to bound cost and, for embeddings, to stay inside the model’s own limit.

All four accept the same providers: openai, google (Gemini Developer API / Google AI Studio), and google-gemini (Gemini Enterprise Agent Platform API). openai and google authenticate with the enrichment’s *_PROVIDER_API_KEY; google-gemini uses Google Cloud Application Default Credentials and reads *_GOOGLE_CLOUD_PROJECT / *_GOOGLE_CLOUD_LOCATION, falling back to the global GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION (set GOOGLE_APPLICATION_CREDENTIALS to a service-account key path when running outside Google Cloud). A custom *_BASE_URL is honored only with openai.

Variable Default Description
EMBEDDING_PROVIDER - Embedding provider (openai, google, or google-gemini; legacy google-vertex maps to google-gemini). Set with EMBEDDING_MODEL to enable.
EMBEDDING_PROVIDER_API_KEY - API key for openai / google.
EMBEDDING_MODEL - Embedding model name.
EMBEDDING_BASE_URL - OpenAI-compatible embeddings root URL. Use with EMBEDDING_PROVIDER=openai to target a self-hosted endpoint. See Self-Hosted Embeddings.
EMBEDDING_MAX_CONCURRENT 5 Max concurrent embedding jobs.
EMBEDDING_MAX_ATTEMPTS 3 Max embedding attempts.
EMBEDDING_MAX_INPUT_BYTES 12000 Max bytes of a record sent to the embedding provider; over it the input is trimmed to its first N bytes, so an over-long record still gets a vector. 0 disables the cap.
EMBEDDING_NORMALIZE false Whether to normalize embeddings before storage.
EMBEDDING_GOOGLE_CLOUD_PROJECT - Google Cloud project (for google-gemini).
EMBEDDING_GOOGLE_CLOUD_LOCATION - Google Cloud location (for google-gemini).
Variable Default Description
TRANSLATION_PROVIDER - Translation provider (openai, google, or google-gemini; legacy google-vertex maps to google-gemini). Set with TRANSLATION_MODEL to enable.
TRANSLATION_PROVIDER_API_KEY - API key for openai / google.
TRANSLATION_MODEL - Translation model name.
TRANSLATION_BASE_URL - OpenAI-compatible base URL; honored only with openai.
TRANSLATION_DEFAULT_LANGUAGE - Fallback target language (BCP-47, e.g. en-US) for tenants with no target_language. Empty makes translation per-tenant opt-in.
TRANSLATION_MAX_CONCURRENT 5 Max concurrent translation jobs.
TRANSLATION_RECONCILE_MAX_CONCURRENT 2 Max concurrent translation jobs on the reconcile lane (see Reconciliation).
TRANSLATION_MAX_ATTEMPTS 3 Max translation attempts.
TRANSLATION_MAX_INPUT_BYTES 24000 Max bytes of a record sent to the translation provider. Over it the record is skipped — not trimmed, since a partial translation stored as complete is wrong — and marked as a terminal failure, without calling the provider. An edit that brings the text back under the cap re-runs it; raising this value does not clear markers already written (a tenant clears them with the retry endpoint). 0 disables the cap.
TRANSLATION_GOOGLE_CLOUD_PROJECT - Google Cloud project (for google-gemini).
TRANSLATION_GOOGLE_CLOUD_LOCATION - Google Cloud location (for google-gemini).

Each record is translated into its tenant’s target_language (set via the tenant settings API); TRANSLATION_DEFAULT_LANGUAGE supplies the target for tenants that have not set one.

Variable Default Description
SENTIMENT_PROVIDER - Sentiment provider (openai, google, or google-gemini). Set with SENTIMENT_MODEL to enable.
SENTIMENT_PROVIDER_API_KEY - API key for openai / google.
SENTIMENT_MODEL - Sentiment model name.
SENTIMENT_BASE_URL - OpenAI-compatible base URL; honored only with openai.
SENTIMENT_MAX_CONCURRENT 5 Max concurrent sentiment jobs.
SENTIMENT_RECONCILE_MAX_CONCURRENT 2 Max concurrent sentiment jobs on the reconcile lane (see Reconciliation).
SENTIMENT_MAX_ATTEMPTS 3 Max sentiment attempts.
SENTIMENT_MAX_INPUT_BYTES 24000 Max bytes of a record sent to the sentiment provider; over it the input is trimmed to its first N bytes. 0 disables the cap.
SENTIMENT_GOOGLE_CLOUD_PROJECT - Google Cloud project (for google-gemini).
SENTIMENT_GOOGLE_CLOUD_LOCATION - Google Cloud location (for google-gemini).
Variable Default Description
EMOTIONS_PROVIDER - Emotion provider (openai, google, or google-gemini). Set with EMOTIONS_MODEL to enable.
EMOTIONS_PROVIDER_API_KEY - API key for openai / google.
EMOTIONS_MODEL - Emotion model name.
EMOTIONS_BASE_URL - OpenAI-compatible base URL; honored only with openai.
EMOTIONS_MAX_CONCURRENT 5 Max concurrent emotion jobs.
EMOTIONS_RECONCILE_MAX_CONCURRENT 2 Max concurrent emotion jobs on the reconcile lane (see Reconciliation).
EMOTIONS_MAX_ATTEMPTS 3 Max emotion attempts.
EMOTIONS_MAX_INPUT_BYTES 24000 Max bytes of a record sent to the emotion provider; over it the input is trimmed to its first N bytes. 0 disables the cap.
EMOTIONS_GOOGLE_CLOUD_PROJECT - Google Cloud project (for google-gemini).
EMOTIONS_GOOGLE_CLOUD_LOCATION - Google Cloud location (for google-gemini).

Sentiment and emotions additionally honor per-directory switches (sentiment_enabled, emotions_enabled) set through the tenant settings API; both default to on when the enrichment is configured.

Enrichment is enqueued by events as records arrive, which is fast but not complete: a record created while a provider was unconfigured has no job at all, a job can be lost to a crash, and a transient outage can exhaust a record’s retries. The reconciler closes that gap. It periodically finds every record that is eligible for an enrichment, has not received one, and has not failed terminally, and enqueues the difference. Events remain the fast path; this is what makes coverage eventually complete.

It runs on hub-worker, on River’s elected leader, so a deployment with several workers sweeps once between them rather than once each. Disabling it does not affect the event path — it only stops coverage from healing itself.

Variable Default Description
ENRICHMENT_RECONCILE_ENABLED true Master switch for the periodic sweep. Set it identically on hub-api and hub-worker — see below.
ENRICHMENT_RECONCILE_INTERVAL_SECONDS 300 How often the sweep runs.
ENRICHMENT_RECONCILE_TARGET_DEPTH 1000 Per-queue top-up target: each tick adds only enough to bring a reconcile queue to this depth. Clamped to 100000.

Reconciled work runs on its own *_reconcile queue per enrichment, with its own worker budget (*_RECONCILE_MAX_CONCURRENT above), so draining a large backlog cannot crowd out a record submitted a moment ago. Topping up to a depth rather than enqueueing at a rate makes the sweep self-regulating — a tick can only add what the workers have already drained — so the job table stays bounded whether the backlog is a thousand records or millions.

ENRICHMENT_RECONCILE_ENABLED must carry the same value on both hub-api and hub-worker. The sweep itself runs on the worker, but the API reads the flag too, to decide whether clearing terminal failures could ever lead to anything. Disable it on the worker alone — the natural thing to do for a worker-only feature, since that is where the Helm chart’s worker.extraEnv applies — and the API still accepts a retry: it deletes the markers, spends the tenant’s cooldown, drops those records out of the status endpoint’s failed counts, and no sweep ever picks them up. Set it on the API alone and every retry is refused with a 409 while the worker sweeps normally.

Records that failed terminally are excluded. A terminal failure means retrying cannot change the outcome, because the outcome is a property of the record’s own text: the provider rejected it on structural grounds — a content-policy block, a refusal, input past the model’s limit — or the record is over the enrichment’s own configured cap, which Hub refuses before calling anything (TRANSLATION_MAX_INPUT_BYTES; the other three trim instead). A sweep that re-enqueued them would never terminate, and for the provider-rejected case every attempt would spend a call that cannot succeed. To give those records another chance after the underlying reason has changed (the text was edited, a provider relaxed a policy, or you raised the cap — raising it does not clear markers already written), clear their markers with POST /v1/tenants/{tenant_id}/enrichments/retry. That endpoint is rate-limited by a per-tenant, per-enrichment cooldown, because clearing the markers is what makes the next sweep spend money on records already known to fail.

Taxonomy clusters embedded feedback into a tree of topics via a standalone service. It requires embeddings (see Embeddings) to be configured.

Variable Default Description
TAXONOMY_SERVICE_URL - Absolute http(s) URL Hub calls to run clustering. Unset disables taxonomy.
TAXONOMY_SERVICE_TOKEN - Bearer token Hub sends to the taxonomy service.
HUB_INTERNAL_API_TOKEN - Token the taxonomy service presents to call Hub’s internal callback endpoints. Required to enable the internal routes.
TAXONOMY_MIN_EMBEDDED_RECORDS 20 Minimum embedded records a scope needs before a run may start.

The translation, sentiment, and emotion enqueue providers each resolve a per-tenant setting (target_language, sentiment_enabled, emotions_enabled) on every feedback event. They share a small in-process LRU cache over tenant settings so that resolution does not hit the database on each event; a settings write evicts the tenant’s entry so a change is visible immediately.

Variable Default Description
TENANT_SETTINGS_CACHE_SIZE 2048 Maximum number of tenants cached (LRU). Set to 0 to disable the cache.
TENANT_SETTINGS_CACHE_TTL_SECONDS 60 How long a cached settings entry stays fresh, in seconds.
Variable Default Description
OTEL_METRICS_EXPORTER - OpenTelemetry metrics exporter.
OTEL_TRACES_EXPORTER - OpenTelemetry traces exporter.

With OTEL_METRICS_EXPORTER set, Hub emits the metrics listed in the Metrics reference.