Configuration reference
The backend is configured through environment variables, loaded by backend/app/core/config.py (Pydantic settings) from backend/.env. Variable names are the field names in upper case. The frontend reads a small set of NEXT_PUBLIC_* build-time variables. This page lists them all.
Copy backend/.env.example to backend/.env and fill it in. Never commit .env.
AI services
| Variable | Description | Default | Required |
|---|---|---|---|
OPENROUTER_API_KEY | Vision and analysis via OpenRouter (Claude). The pipeline's main AI. | empty | Yes |
DEEPGRAM_API_KEY | Transcription via Deepgram Nova-2. Without it, inventories are visual-only. | empty | Yes (for narration/audio) |
ANTHROPIC_API_KEY | Anthropic key. Present for compatibility; vision goes through OpenRouter. | empty | Situational |
OPENROUTER_BASE_URL | OpenRouter API base. | https://openrouter.ai/api/v1 | No |
OPENROUTER_MODEL | The vision/analysis model id. | anthropic/claude-opus-4-5 | No |
OPENROUTER_TEMPERATURE | Sampling temperature for every AI call. Pinned to 0 so repeated uploads of the same video/photos return the same items and names (AS-719). Raise only to trade repeatability for variety. | 0 | No |
OPENROUTER_CONCURRENCY | Max simultaneous AI calls per job. | 6 | No |
OPENROUTER_GLOBAL_CONCURRENCY | Max simultaneous AI calls across all in-flight jobs in one process (a shared limiter on top of the per-job cap, so concurrent jobs don't collectively trip the provider rate limit). Keep at or below the httpx max_connections (20). | 16 | No |
CLAUDE_MODEL | Model id reported by /health. | claude-opus-4-7 | No |
DEEPGRAM_MODEL | Transcription model. | nova-2 | No |
AdjustSquare SSO (login)
| Variable | Description | Default | Required |
|---|---|---|---|
ADJUSTSQUARE_JWT_SECRET | Shared HS256 secret used to validate login tokens. If unset, all logins fail. | empty | Yes |
ADJUSTSQUARE_LOGIN_URL | Where unauthenticated users are redirected to sign in. | https://demo.adjustsquare.com/login | Yes |
ADJUSTSQUARE_COOKIE_NAME | Cookie name AdjustSquare sets on shared subdomains. | bt_shared_data | No |
ADMIN_EXTERNAL_USER_IDS | Allowlist of Adjust Square user IDs (matched against users.external_id) that may see the internal admin dashboard. JSON list, e.g. [7, 10]. | {7, 10} (Dan, Nathan) | No |
Adjust Square API (claim submission)
| Variable | Description | Default | Required |
|---|---|---|---|
ADJUST_SQUARE_BASE_URL | Base URL of the Adjust Square API. One host per deployment. | empty | Yes (to submit) |
ADJUST_SQUARE_BEARER_TOKEN | Optional global Adjust Square API token, used only as a fallback for a call made without a team token. Submission authenticates as the signed-in user's team.team_token (from the SSO JWT), so this is not required and is not the configured-gate. | empty | No (optional fallback) |
ADJUST_SQUARE_APP_URL | The Adjust Square web app origin (distinct from the API base above), used only to deep-link a submitted claim into its assignment: {app_url}/lkq/{claim_key}. Empty renders the claim key as plain text (no link). | empty | No |
Claim submission authenticates as the signed-in user's team (team.team_token, captured from the SSO JWT's current_team_token) and attributes the claim to the user's user.external_id (see the Adjust Square integration), both read from the database at submit time. Only ADJUST_SQUARE_BASE_URL must be set for the integration to count as "configured"; ADJUST_SQUARE_BEARER_TOKEN is an optional fallback for calls made without a team token and is not required. There is no longer a shared adjuster-id environment variable (the old ADJUST_SQUARE_ADJUSTER_ID was removed; the adjuster is the signed-in user).
Contents Estimation (claim push)
Contents Estimation is the clean-stack rebuild of Adjust Square. A finished claim can be pushed to it in one call, which creates the claim there and starts pricing. This is a separate action from the Adjust Square submission above; both can be used independently on the same claim.
| Variable | Description | Default | Required |
|---|---|---|---|
CONTENTS_ESTIMATION_BASE_URL | Base URL of the Contents Estimation API. One host per deployment. Prod: https://api-production-36ce3.up.railway.app. Local: http://localhost:8000. | empty | Yes (to send) |
CONTENTS_ESTIMATION_API_KEY | Optional global fallback intake key (<key_id>.<secret>, cek_...), used only for a request whose team has no per-team key yet. Auth is normally per-team (Team.contents_estimation_key, from the SSO JWT). Mirrors ADJUST_SQUARE_BEARER_TOKEN; not required and not the configured-gate. | empty | No (optional fallback) |
CONTENTS_ESTIMATION_APP_URL | The Contents Estimation web app origin (distinct from the API base above), used only to deep-link a pushed claim: {app_url}/claims/{claim_uuid}. | https://contentsestimation.com | No |
PUBLIC_BASE_URL | ContentsVision's own public origin (e.g. https://app.contentsvision.com), used to turn item photo paths (/api/photos/...) into absolute URLs, since the intake takes photos as URLs, not bytes. Empty pushes the inventory without photos. | empty | No |
Only CONTENTS_ESTIMATION_BASE_URL must be set for the integration to count as "configured" (settings.contents_estimation_configured), exactly like ADJUST_SQUARE_BASE_URL. The intake key is per-team: captured from the AdjustSquare SSO JWT's current_team_ce_intake_key claim into Team.contents_estimation_key and resolved from the signed-in user's team at submit, so each team's claims land under its own Contents Estimation tenant. CONTENTS_ESTIMATION_API_KEY is only a fallback for a team with no key yet. A team with neither gets a clear 409. See the Contents Estimation integration.
Contents Capture (media import)
| Variable | Description | Default | Required |
|---|---|---|---|
CONTENTS_CAPTURE_API_BASE_URL | Base URL of the Contents Capture API. Empty disables the integration entirely (the Integrations tab reports it unavailable). Dev/stage: https://contents-capture-api.dev-908.workers.dev. | empty | Yes (to import) |
CONTENTS_CAPTURE_WEBHOOK_SECRET | HMAC secret for Contents Capture webhooks. Optional; webhooks are a later phase. | empty | No |
CONTENTS_CAPTURE_IMPORT_CONCURRENCY | Parallel media downloads per import run. | 4 | No |
The org integration token (cci_..., minted by a Capture org admin) is pasted
by a team under Settings, then Integrations, and stored on that team's
storage_connections row. It is not an environment variable. See
the Contents Capture integration.
Storage and database
| Variable | Description | Default | Required |
|---|---|---|---|
DATABASE_URL | SQLAlchemy async DB URL. | sqlite+aiosqlite:///./storage/inventory.db | No |
UPLOAD_DIR | Where raw uploads are streamed. | ./uploads | No |
STORAGE_DIR | Per-job files (frames, photos, crops), the SQLite DB, and the settings JSON files. | ./storage | No |
MAX_UPLOAD_SIZE_MB | Per-file upload size cap for video/audio (returns 413 if exceeded). | 2000 | No |
MAX_PHOTO_UPLOAD_MB | Running-total cap for one photo job across all its batched requests. Far higher than a single video because one room can hold hundreds of photos (a 757-photo phone upload is ~3GB). | 8000 | No |
STUCK_JOB_MINUTES | Minutes a processing job's row may go without an update before the recovery watchdog requeues (or fails) it. See troubleshooting. | 15 | No |
Pipeline tuning
| Variable | Description | Default |
|---|---|---|
FRAME_EXTRACTION_FPS | Frames extracted from video per second. | 1.0 |
CHUNK_DURATION_SECONDS | Length of each analyzed time chunk. | 30.0 |
FRAMES_PER_MINUTE | Target frame density per chunk. | 60 |
MAX_FRAMES_PER_CALL | Hard cap on frames per vision call. | 60 |
VIDEO_DEDUP_WINDOW_SECONDS | Boundary window for cross-chunk dedup. | 3.0 |
MAX_PHOTOS_PER_CALL | Photos per call in the legacy batched photo pipeline. | 8 |
MAX_PHOTOS_PER_ITEM | Hard cap on photos grouped into one item. | 20 |
MAX_FRAMES_PER_ROOM | Legacy, unused after the time-chunk pipeline. | 8 |
These defaults are tuned together; see the AI pipeline before changing them.
Billing rates (credits)
| Variable | Description | Default |
|---|---|---|
COST_PER_VIDEO_MINUTE | Credits per minute of video. | 10 |
COST_PER_AUDIO_MINUTE | Credits per minute of audio. | 3 |
COST_PER_PHOTO_REQUEST | Credits per photo AI request (one per group). | 1 |
COST_PER_CONTENTS_ESTIMATION_ITEM | Credits charged when a claim is sent to Contents Estimation, per active item (charged once per claim, at send). Only applies with ENABLE_BILLING on. | 1 |
DEFAULT_TEAM_CREDIT_RATE | Fallback dollars of revenue per credit, used for any team with no per-team Team.credit_rate. Feeds the Revenue field of the Slack notification. | 0.15 |
Prepaid billing (external service)
Optional, instance-wide. Off by default; invoiced / enterprise instances leave it off and the app only meters usage. When on, uploads are gated on a team's prepaid balance held by an external billing service, and completed jobs are charged against it. See billing.
| Variable | Description | Default |
|---|---|---|
ENABLE_BILLING | Master switch for the prepaid balance gate + charge. | false |
BILLING_APP_DOMAIN | Base URL of the external billing service (no trailing slash). | (empty) |
BILLING_APP_API_KEY | Bearer key for the billing service. | (empty) |
BILLING_EXTERNAL_APP | Identifier sent to the billing service as external_app. | contentsvision |
When ENABLE_BILLING is true but the domain/key are missing, the integration degrades to fail-open (no balance read, no charge) and logs a warning, so a misconfigured instance never blocks uploads.
Product analytics (PostHog)
Optional. Off by default: with no key the backend no-ops and the frontend ships with no tracking, so neither touches the network. See analytics.
| Variable | Description | Default | Required |
|---|---|---|---|
POSTHOG_API_KEY | Backend project key (phc_…). Enables server-side capture of every user action and job outcome. Empty = off. | empty | No |
POSTHOG_HOST | Backend ingestion host: https://us.i.posthog.com, https://eu.i.posthog.com, or a self-hosted URL. | https://us.i.posthog.com | No |
NEXT_PUBLIC_POSTHOG_KEY | Frontend project key (use the same project as the backend). Enables page views, autocapture, session replay, and identify. Build-time (see below). Empty = off. | empty | No |
NEXT_PUBLIC_POSTHOG_HOST | Frontend ingestion host. App defaults to US Cloud when blank. Build-time. | empty (US Cloud) | No |
The backend keys are read at runtime (a restart picks up a change). The two NEXT_PUBLIC_* keys are public and build-time: compose passes them to the frontend image as build args and Next.js inlines them into the bundle, so changing them requires a rebuild, not just a restart.
Slack notifications (media processing)
Optional. Off by default: with no webhook the backend no-ops and never calls out. When set, a message is posted to a Slack channel every time a media-processing job finishes (success and failure), carrying company, user, media type, credits used, and revenue (credits used × the team credit rate). See billing.
| Variable | Description | Default | Required |
|---|---|---|---|
SLACK_WEBHOOK_URL | Slack Incoming Webhook URL (https://hooks.slack.com/services/…). Empty = off. | empty | No |
DEFAULT_TEAM_CREDIT_RATE | Fallback dollars per credit for the Revenue field (see billing rates); a per-team Team.credit_rate overrides it. | 0.15 | No |
Read at runtime, so a restart picks up a change.
Networking
| Variable | Description | Default |
|---|---|---|
CORS_ORIGINS | Comma-separated allowed origins (no trailing slash). | http://localhost:3000 |
Error tracking (Sentry)
Sentry is off until a DSN is set, on both halves of the stack, so local dev and the test suite never ship anything. See observability for what reports from where.
| Variable | Description | Default | Required |
|---|---|---|---|
SENTRY_DSN | Backend DSN (API process and ARQ worker). Empty = error tracking off. | empty | No |
SENTRY_TRACES_SAMPLE_RATE | Backend performance tracing sample rate. 0.0 keeps tracing off (OTel owns tracing); errors still report. | 0.0 | No |
NEXT_PUBLIC_SENTRY_DSN | Frontend DSN. Public and build-time: passed as a compose build arg and baked into the bundle, so changing it needs a frontend rebuild. Empty = off. | empty | No |
SENTRY_AUTH_TOKEN | Optional, build-time. When present the frontend build uploads source maps so stack traces are readable. Needs SENTRY_ORG and SENTRY_PROJECT alongside it. | empty | No |
Observability (OpenTelemetry)
Telemetry (traces, metrics, logs to the Grafana/Loki/Prometheus/Tempo stack) is off until OTEL_ENABLED=true and an endpoint are both set, so local dev and the test suite never ship off-box. config.py reads OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT, APP_ENV, and COMMIT_SHA; the rest are read by the OpenTelemetry SDK and the frontend build directly. See observability for what flows where.
| Variable | Description | Default | Required |
|---|---|---|---|
OTEL_ENABLED | Master switch for backend telemetry. | false | No |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP HTTP collector URL. One endpoint receives all three signals; the SDK appends /v1/traces, /v1/metrics, /v1/logs. | empty | When telemetry is on |
OTEL_EXPORTER_OTLP_PROTOCOL | OTLP transport. Keep http/protobuf: it traverses proxies and Railway TLS more reliably than gRPC. | http/protobuf | No |
OTEL_EXPORTER_OTLP_HEADERS | Collector auth, comma-separated key=value (e.g. Authorization=Basic <b64>). | empty | If the collector requires auth |
OTEL_SERVICE_NAME | Overrides the default contentsvision-backend service name. | contentsvision-backend | No |
NEXT_PUBLIC_OTEL_ENABLED | Master switch for browser telemetry. Baked at build time. | false | No |
NEXT_PUBLIC_OTEL_BROWSER_ENDPOINT | Same-origin path the browser posts OTLP to, forwarded to the collector by the backend. | /api/telemetry | No |
APP_ENV | Stamped on all telemetry as deployment.environment. | local | No |
COMMIT_SHA | Stamped on all telemetry as service.version. | dev | No |
Deployment and frontend (env-only, not in config.py)
| Variable | Description | Where |
|---|---|---|
APP_ENV | production, stage, or local. Drives the env badge and title. | compose / build arg |
NEXT_PUBLIC_APP_ENV | The above, exposed to the frontend at build time. Also gates the imgproxy thumbnail CDN: when unset it defaults to production, so a stage build that forgets it enables the prod CDN and breaks thumbnails (see imgproxy rows below). The stage and prod compose files set it explicitly. | frontend build |
NEXT_PUBLIC_IMGPROXY_ENABLED | true/false to force the thumbnail CDN on or off. Unset = on only when NEXT_PUBLIC_APP_ENV is production. | frontend build arg |
NEXT_PUBLIC_IMGPROXY_URL | imgproxy base host for server-resized thumbnails. | frontend build arg (default https://cdn.adjustsquare.com) |
NEXT_PUBLIC_PHOTO_PUBLIC_ORIGIN | Public origin imgproxy prefixes onto relative /api/photos paths so it can fetch the source. Must point at the origin that actually serves this environment's photos. | frontend build arg (default https://contentsvision.com) |
NEXT_PUBLIC_API_URL | Where the frontend proxies /api (e.g. http://backend:8000). Must be a build arg: the standalone build freezes the rewrite destination at build time, so a runtime-only value is ignored. | frontend build arg |
NEXT_PUBLIC_POSTHOG_KEY | Frontend PostHog project key. Build arg (inlined into the bundle). Empty = analytics off. See analytics. | frontend build arg |
NEXT_PUBLIC_POSTHOG_HOST | Frontend PostHog ingestion host. Build arg. Defaults to US Cloud in app code. | frontend build arg |
CLOUDFLARE_TUNNEL_TOKEN | Token for the cloudflared service. | prod compose |
COMMIT_SHA / COMMIT_TIME | Optional build metadata baked into the frontend. | build args |
How settings are loaded
config.py reads .env once at import, exposing a settings singleton. Auto-approval rules and AI prompt presets are not environment variables; they are stored as JSON files and edited from the Settings page. They are scoped per team, one file per team under STORAGE_DIR/team_settings/<team_id>/ (automation_settings.json, prompt_settings.json). See automation and prompts.