Files
sure/.env.example
T
Joe Maples cc826df909 feat(ai): support OPENAI_EXTRA_HEADERS on the OpenAI-compatible provider (#3362)
* feat(ai): support OPENAI_EXTRA_HEADERS on OpenAI-compatible provider

Adds a fail-closed parser for the OPENAI_EXTRA_HEADERS env var (a JSON
object of header names to values) as a new Provider::Openai.extra_headers
class method. Malformed, non-object, blank, or unset values yield {}
with an error log and never the raw value, so chat keeps working on bad
config. Parsed headers are passed to the ruby-openai client at
construction, attaching them to every request the provider's client
makes (chat and batch flows alike).

ENV-only by design: no Setting fallback or settings-UI entry. Values are
stringified (nested JSON becomes Ruby-inspect strings) and blank values
are dropped.

Adds hosting docs and commented examples in .env.example and
.env.local.example, plus Minitest coverage mirroring the request_timeout
tests, including a docs-consistency test binding the knob to its docs.

* feat(ai): substitute {session_id} in OPENAI_EXTRA_HEADERS per chat request

Header values containing the literal {session_id} are now withheld at
client construction and merged onto the client at request time, with the
placeholder replaced by the chat's UUID. This identifies requests per
conversation rather than per install, for gateways that key sessions
(e.g. OpenCode Zen's x-opencode-session).

A session header is only merged when a session_id is present, so batch
flows (auto-categorize, merchant detection, PDF processing) — which
bypass chat_response — never send it; they receive static headers only.
The merge adds/overwrites without deleting managed headers.

Docs updated to cover both static and session-valued usage.

* fix(ai): keep OPENAI_EXTRA_HEADERS session values request-scoped

client.add_headers persists headers on the shared client in
ruby-openai 8.1.0, so a chat's resolved session header could survive
onto later requests made through the same provider instance. Session
headers are now merged onto a request-scoped dup of the client; the
shared client is never mutated. Batch flows and session-less chats
cannot observe another chat's session id.

Also updates the CodeRabbit-flagged tests to assert the shared client
stays untouched and the scoped copy is what issues the chat request.

* docs(ai): add YARD tags to OPENAI_EXTRA_HEADERS method docs

Converts the comment blocks on the four methods touched by this
feature (extra_headers, initialize, request_timeout, and
with_session_headers) into YARD docstrings with @param/@return tags,
satisfying CodeRabbit's docstring-coverage pre-merge check.
2026-09-03 21:38:23 +02:00

271 lines
11 KiB
Bash

# ================================ PLEASE READ ===========================================================
# This file outlines all the possible environment variables supported by the Sure app for self hosting.
#
# If you're a developer setting up your local environment, please use `.env.local.example` instead.
# ========================================================================================================
# Required self-hosting vars
# --------------------------------------------------------------------------------------------------------
# Enables self hosting features (should be set to true unless you know what you're doing)
SELF_HOSTED=true
# Controls onboarding flow (valid: open, closed, invite_only)
ONBOARDING_STATE=open
# Secret key used to encrypt credentials (https://api.rubyonrails.org/v7.1.3.2/classes/Rails/Application.html#method-i-secret_key_base)
# Has to be a random string, generated eg. by running `openssl rand -hex 64`
SECRET_KEY_BASE=secret-value
# Optional self-hosting vars
# --------------------------------------------------------------------------------------------------------
# Optional: OpenAI-compatible API endpoint config
OPENAI_ACCESS_TOKEN=
OPENAI_MODEL=
OPENAI_URI_BASE=
# Optional: LLM token budget (applies to chat, auto-categorize, merchant detection, PDF processing).
# Lower these for small-context local models (Ollama, LM Studio, LocalAI).
# For larger local models, raise the context window to match the model you actually run.
# Example: Gemma 3/4, Qwen, and other large-context models often need `LLM_CONTEXT_WINDOW=8192` or higher.
# LLM_CONTEXT_WINDOW=2048
# LLM_MAX_RESPONSE_TOKENS=512
# LLM_MAX_HISTORY_TOKENS=
# LLM_SYSTEM_PROMPT_RESERVE=256
# LLM_MAX_ITEMS_PER_CALL=25
# Optional: how long the chat waits for an assistant response before showing a
# "no response" error. Raise for slow local models — OpenAI-compatible providers
# are not streamed, so nothing renders until the whole reply is generated.
#
# This clock starts when the message is queued and covers the whole turn, so it
# is a SUM, not a maximum. The worst case is:
#
# (1 + ASSISTANT_MAX_TOOL_CALL_ITERATIONS) * OPENAI_REQUEST_TIMEOUT
# + tool execution + queue wait
#
# At the defaults that bound is 6 * 60 = 360s plus overhead. The 90s default is
# sized for typical latency rather than that bound — cloud models answer in
# seconds, so a turn rarely approaches it. Size against the formula once your
# per-call latency is genuinely near OPENAI_REQUEST_TIMEOUT, which is the case
# for local models. Set too low, you get a generic "no response" instead of the
# specific timeout error, and the job keeps burning tokens after the chat gave up.
# Minimum 30; also settable on the Self-Hosting settings page.
# AI_RESPONSE_TIMEOUT=90
#
# Lowering the tool-call cap is often the better lever on slow hardware: it cuts
# the first term of that sum instead of requiring a very long timeout.
# ASSISTANT_MAX_TOOL_CALL_ITERATIONS=5
# Optional: OpenAI-compatible capability flags
# OPENAI_REQUEST_TIMEOUT=60 # HTTP timeout in seconds; raise for slow local models
# OPENAI_EXTRA_HEADERS='{"x-opencode-session":"{session_id}"}' # Extra JSON headers for chat requests; value containing {session_id} becomes the chat UUID
# OPENAI_SUPPORTS_PDF_PROCESSING=true # Set to false for endpoints without vision support
# OPENAI_SUPPORTS_RESPONSES_ENDPOINT= # Override Responses-API vs chat.completions routing
# LLM_JSON_MODE= # auto | strict | json_object | none
# Optional: document-search vector store. Hosted OpenAI is selected by default
# when OPENAI_ACCESS_TOKEN is configured. For a local OpenAI-compatible LLM,
# use pgvector plus a separate OpenAI-compatible embeddings endpoint.
# VECTOR_STORE_PROVIDER=pgvector # openai | pgvector | qdrant (scaffolded only)
# EMBEDDING_URI_BASE=http://ollama:11434/v1
# EMBEDDING_MODEL=mxbai-embed-large
# EMBEDDING_DIMENSIONS=1024 # Must match the embedding model
# EMBEDDING_ACCESS_TOKEN= # Optional; falls back to OPENAI_ACCESS_TOKEN
# AI_HEALTH_PROBE_TIMEOUT=5 # Per-request timeout for admin live checks
# AI_HEALTH_PROBE_CACHE_TTL=60 # Cache live results and deduplicate failure logs
# Optional: External AI Assistant — delegates chat to a remote AI agent
# instead of calling LLMs directly. The agent calls back to Sure's /mcp endpoint.
# See docs/hosting/ai.md for full details.
# ASSISTANT_TYPE=external
# EXTERNAL_ASSISTANT_URL=https://your-agent-host/v1/chat/completions
# EXTERNAL_ASSISTANT_TOKEN=your-api-token # pipelock:ignore
# EXTERNAL_ASSISTANT_AGENT_ID=main
# EXTERNAL_ASSISTANT_SESSION_KEY=agent:main:main
# EXTERNAL_ASSISTANT_ALLOWED_EMAILS=user@example.com
# Optional: MCP server endpoint — enables /mcp for external AI assistants.
# Both values are required. MCP_USER_EMAIL must match an existing user's email.
# MCP_API_TOKEN=your-random-bearer-token # pipelock:ignore
# MCP_USER_EMAIL=user@example.com
# Optional: Langfuse config
LANGFUSE_HOST=https://cloud.langfuse.com
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
# Optional: Twelve Data API Key for exchange rates + stock prices
# (you can also set this in your self-hosted settings page)
# Get it here: https://twelvedata.com/
TWELVE_DATA_API_KEY=
# Optional: Provider selection for exchange rates and securities data
# Options: twelve_data (default), yahoo_finance
# EXCHANGE_RATE_PROVIDER=twelve_data
# SECURITIES_PROVIDER=twelve_data
# Alternative: Use Yahoo Finance as provider (free, no API key required)
EXCHANGE_RATE_PROVIDER=yahoo_finance
SECURITIES_PROVIDER=yahoo_finance
# Brandfetch to grab logos for banks and merchants
BRAND_FETCH_CLIENT_ID=
# Custom port config
# For users who have other applications listening at 3000, this allows them to set a value puma will listen to.
PORT=3000
# SMTP Configuration
# This is only needed if you intend on sending emails from your Sure instance (such as for password resets or email financial reports).
# Resend.com is a good option that offers a free tier for sending emails.
SMTP_ADDRESS=
SMTP_PORT=465
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_ENABLED=true
SMTP_TLS_SKIP_VERIFY=false
# Address that emails are sent from
EMAIL_SENDER=
# Database Configuration
DB_HOST=localhost # May need to be changed to `DB_HOST=db` if using devcontainer
DB_PORT=5432
POSTGRES_PASSWORD=postgres # pipelock:ignore
POSTGRES_USER=postgres
# Redis configuration
# Standard Redis URL (for direct connection)
REDIS_URL=redis://localhost:6379/1
# Redis Sentinel configuration (for high availability)
# When REDIS_SENTINEL_HOSTS is set, it takes precedence over REDIS_URL
# REDIS_SENTINEL_HOSTS=sentinel1:26379,sentinel2:26379,sentinel3:26379
# REDIS_SENTINEL_MASTER=mymaster
# REDIS_SENTINEL_USERNAME=default
# REDIS_PASSWORD=your-redis-password # pipelock:ignore
# Sidekiq Web UI (/sidekiq)
# The queue dashboard is reachable in production only by signed-in super admins
# (the first user created on the instance). Optionally set BOTH variables below
# to require basic-auth credentials as a second layer on top of that.
# SIDEKIQ_WEB_USERNAME=
# SIDEKIQ_WEB_PASSWORD=
# App Domain
# This is the domain that your Sure instance will be hosted at. It is used to generate links in emails and other places.
APP_DOMAIN=
# WebAuthn / passkey configuration
# RP ID is usually the registrable domain (example.com), not a full URL.
# Allowed origins is a comma-separated list of the full origins where users
# access Sure (scheme included), e.g. https://sure.example.com,https://app.example.com.
WEBAUTHN_RP_ID=
WEBAUTHN_ALLOWED_ORIGINS=
# Set to false to keep passkeys as a second factor only, instead of also
# allowing passwordless sign-in. See docs/hosting/webauthn.md.
# AUTH_PASSKEY_LOGIN_ENABLED=true
# OpenID Connect configuration
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_ISSUER=
OIDC_REDIRECT_URI=
# Product/Brand Name
PRODUCT_NAME=
BRAND_NAME=
# PostHog configuration
POSTHOG_KEY=
POSTHOG_HOST=
# Disable enforcing SSL connections
# DISABLE_SSL=true
# Customizations to outbound SSL/TLS connections
# Path to custom CA certificate (PEM format)
# SSL_CA_FILE=
# Enable/disable SSL verification
# SSL_VERIFY=true
# Enable verbose SSL logging
# SSL_DEBUG=false
# Active Record Encryption Keys (Optional)
# These keys are used to encrypt sensitive data like API keys in the database.
# For managed mode: Set these environment variables to provide encryption keys.
# For self-hosted mode: If not provided, they will be automatically generated based on your SECRET_KEY_BASE.
# You can generate your own keys by running: rails db:encryption:init
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
# ======================================================================================================
# Active Storage Configuration - responsible for storing file uploads
# ======================================================================================================
#
# * Defaults to disk storage but you can also use Amazon S3, Cloudflare R2, or Google Cloud Storage
# * Set the appropriate environment variables to use these services.
# * Ensure libvips is installed on your system for image processing - https://github.com/libvips/libvips
#
# Amazon S3
# ==========
# ACTIVE_STORAGE_SERVICE=amazon <- Enables Amazon S3 storage
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_REGION= # defaults to `us-east-1` if not set
# S3_BUCKET=
#
# Cloudflare R2
# =============
# ACTIVE_STORAGE_SERVICE=cloudflare <- Enables Cloudflare R2 storage
# CLOUDFLARE_ACCOUNT_ID=
# CLOUDFLARE_ACCESS_KEY_ID=
# CLOUDFLARE_SECRET_ACCESS_KEY=
# CLOUDFLARE_BUCKET=
#
# Generic S3
# ==========
# ACTIVE_STORAGE_SERVICE=generic_s3 <- Enables Generic S3 storage
# GENERIC_S3_ACCESS_KEY_ID=
# GENERIC_S3_SECRET_ACCESS_KEY=
# GENERIC_S3_REGION=
# GENERIC_S3_BUCKET=
# GENERIC_S3_ENDPOINT=
# GENERIC_S3_FORCE_PATH_STYLE= <- defaults to false
#
# Google Cloud Storage
# ====================
# ACTIVE_STORAGE_SERVICE=google <- Enables Google Cloud Storage
# GCS_PROJECT=
# GCS_BUCKET=
# GCS_KEYFILE_JSON= <- JSON content of service account key (preferred)
# GCS_KEYFILE= <- path to service account JSON key file
# Skylight
# ========
SKYLIGHT_AUTHENTICATION=
SKYLIGHT_ENABLED=
# ======================================================================================================
# Database Backup Configuration (Optional)
# ======================================================================================================
#
# When using the "backup" container profile, these settings control how backups are saved.
# Uses rclone to sync to any storage provider (S3, Cloudflare R2, Google Drive, SFTP, etc).
#
# BACKUP_SCHEDULE="0 2 * * *" # Cron schedule (default: every day at 2am)
# BACKUP_OVERWRITE="false" # true = backup_latest.sql.gz, false = backup_YYYY-MM-DD_HH-MM-SS.sql.gz
# BACKUP_KEEP_DAYS="7" # Delete backups older than this many days (when BACKUP_OVERWRITE is false)
# BACKUP_DESTINATION="s3:my-bucket" # Rclone remote format (remote:bucket/path)
# INSTANCE_ID="default" # Scopes backups under BACKUP_DESTINATION/<INSTANCE_ID> to isolate retention lifecycle operations per deployment
#
# Example Rclone configuration for an S3 compatible provider (e.g., Cloudflare R2):
# RCLONE_CONFIG_S3_TYPE=s3
# RCLONE_CONFIG_S3_PROVIDER=Cloudflare
# RCLONE_CONFIG_S3_ACCESS_KEY_ID=
# RCLONE_CONFIG_S3_SECRET_ACCESS_KEY=
# RCLONE_CONFIG_S3_ENDPOINT=