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 5 Max embedding attempts.
EMBEDDING_JOB_TIMEOUT_SECONDS 60 Deadline for one embedding job.
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).
EMBEDDING_RECONCILE_ENABLED false Periodically repair embeddings on hub-worker (see Embedding repair). Opt in per deployment.
EMBEDDING_RECONCILE_INTERVAL_SECONDS 300 How often the repair sweep runs.
EMBEDDING_RECONCILE_RETRY_AFTER_SECONDS 900 Cooldown before a record whose attempts ran out on a transient error is retried.
EMBEDDING_RECONCILE_TARGET_DEPTH 100 Each sweep tops the embeddings_reconcile queue up to this many runnable jobs, shared fairly between the search and taxonomy embeddings. See How long recovery takes.
EMBEDDING_RECONCILE_MAX_CONCURRENT 1 Worker concurrency reserved for repaired records, separate from live embedding work.

hub-api queues a record’s embedding jobs in the same transaction that writes it, so a crash cannot lose them; if the jobs cannot be queued, the write fails and the client can retry. What can still leave a record without a current vector is a job that runs out of attempts. With EMBEDDING_RECONCILE_ENABLED=true and an embedding provider configured, a sweep on hub-worker (on River’s elected leader) re-queues every record whose vector is:

  • missing: the record has text but no vector for the current model;
  • stale: the text the vector was computed from has changed since (for the taxonomy embedding, this includes the translation);
  • orphaned: the record no longer has any text. The repair deletes the vector without calling the provider.

This covers the search embedding always, and the taxonomy embedding when TAXONOMY_SERVICE_URL is set. A repair job that finds the vector already current (because a live job got there first) skips the provider. Records whose attempts ran out on a transient error are retried after EMBEDDING_RECONCILE_RETRY_AFTER_SECONDS. Records the provider rejected outright are not retried until the record is written again (the rejection is tied to the record’s revision, so an edit, or an enrichment such as translation or sentiment landing on it, makes it eligible once more).

Vectors written before Hub recorded what they were computed from are trusted as current. Changing the model’s document prefix or EMBEDDING_MAX_INPUT_BYTES does not mark existing vectors stale either: those are deployment settings, not record content.

How long recovery takes. This applies only with EMBEDDING_RECONCILE_ENABLED=true; without it, nothing repairs a record automatically. A record whose attempts ran out is re-queued at the first sweep after its cooldown. Each sweep tops the repair lane up to EMBEDDING_RECONCILE_TARGET_DEPTH jobs, shared between the search and taxonomy embeddings, so with both backlogged each model drains about TARGET_DEPTH ÷ 2 records per sweep (with the defaults, 100 every 300 seconds: roughly 600 an hour per model); with only one model backlogged it gets the whole depth. The lane also runs only EMBEDDING_RECONCILE_MAX_CONCURRENT jobs at a time, so when a provider call takes longer than about interval ÷ (TARGET_DEPTH ÷ MAX_CONCURRENT) (three seconds with the defaults), worker throughput sets the pace instead. A job left running by a worker that died is not retried until River’s stuck-job rescue (RIVER_RESCUE_STUCK_JOBS_AFTER_SECONDS, one hour when unset), and the sweep leaves that record alone until then; set it a little above EMBEDDING_JOB_TIMEOUT_SECONDS to shorten that. A provider answering 429 snoozes a job without using an attempt for up to an hour from when the job was created, then lets it fail normally. For a faster drain, use the backfill below.

For incident recovery, backfill-embeddings queues the same work on demand, on the live embeddings queue so it drains at full worker concurrency. It selects what the sweep would, except that it does not wait out the transient-failure cooldown: it is meant to run once the provider has recovered. Records with a job already in flight (including the sweep’s) and records the provider rejected outright are skipped, so running it twice queues nothing the second time.

Flag Description
-tenant-id <id> Restrict to one tenant (feedback directory). Without it the backfill runs across the deployment.
-taxonomy Backfill the taxonomy embedding instead of the search embedding.
-max-records <n> Stop after n records, to run in bounded batches (for example 100, then 500 at a time).
-count-only Print how many records are owed work without queueing anything.
-stale Also re-embed vectors written before Hub recorded what they were computed from. Needs -tenant-id.
-retry-terminal Also retry records the provider rejected outright, once the cause is fixed (say, a lower input cap). Needs -tenant-id.
-cooldown <d> Skip records whose attempts ran out on a transient error less than d ago (for example 24h), to keep a record that keeps failing out of repeated batch runs. Default 0: retry them at once.

Each run logs one line when it starts and one when it finishes, with its scope and counts, as the record of what was done.

Upgrading to the release that adds repair of stale vectors. Vectors from before it carry no record of what they were computed from. While old and new hub-worker pods overlap during the rollout, an old pod can rewrite a vector without updating that record, so a record edited and then edited back inside the rollout window can keep the vector of its middle text. To rule this out, stop the old workers before the new ones start (or let the rollout finish first and then clear the record for vectors written during it: UPDATE embeddings SET source_hash = NULL WHERE updated_at BETWEEN <rollout start> AND <rollout end>, followed by backfill-embeddings -tenant-id <id> -stale for the affected tenants). Search-embedding failures are recorded for the repair sweep but do not appear in hub_enrichment_failed_records, which counts taxonomy embedding failures only.

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.