# Logging # LOG_LEVEL=INFO # Data directory for per-user workspace files (default: data/users) # DATA_DIR=data/users # PostgreSQL connection URL # DATABASE_URL=postgresql://clawbolt:clawbolt@localhost:5432/clawbolt # Settings store backend: "db" persists runtime settings in app_settings # (default, recommended). "file" keeps the legacy data/config.json behavior # for deployments that prefer not to add a Postgres dependency for config. # SETTINGS_STORE=db # CORS — comma-separated list of allowed origins (no wildcard with credentials) # Default: http://localhost:3000,http://localhost:8000 (local dev only) # Production: set to your actual frontend origin(s) CORS_ORIGINS=http://localhost:3000,http://localhost:8000 # Auth # AUTH_MODE=single_user is the self-hosted default: no credentials, every # request resolves to the one user in the database. # # AUTH_MODE=multi_user turns on the hosted, multi-tenant surface: Google # OAuth sign-in, JWT sessions, the admin console, per-tenant quotas, and # operator monitoring. Everything under "Multi-user deployment" at the # bottom of this file applies only in that mode. # AUTH_MODE=single_user JWT_SECRET=change-me-in-production JWT_EXPIRY_MINUTES=15 # Messaging MESSAGING_PROVIDER=telegram TELEGRAM_BOT_TOKEN= # TELEGRAM_WEBHOOK_SECRET= # Auto-derived from bot token if not set # Your numeric Telegram user ID, or "*" to allow all. # Empty = deny all incoming messages (safe default). # Only a single ID is allowed per user. TELEGRAM_ALLOWED_CHAT_ID= # Linq (iMessage/RCS/SMS via linqapp.com) # LINQ_API_TOKEN= # Linq Partner API V3 bearer token # LINQ_FROM_NUMBER= # Your Linq-provisioned phone number (E.164) # LINQ_WEBHOOK_SIGNING_SECRET= # From webhook subscription creation # LINQ_ALLOWED_NUMBERS= # E.164 phone number, "*" for all, empty = deny # LINQ_PREFERRED_SERVICE=iMessage # iMessage, SMS, or RCS # BlueBubbles (self-hosted iMessage bridge via Mac) # BLUEBUBBLES_SERVER_URL= # e.g. https://my-mac.ngrok.io # BLUEBUBBLES_PASSWORD= # BlueBubbles server password # BLUEBUBBLES_ALLOWED_NUMBERS= # E.164 phone or email, "*" for all, empty = deny # BLUEBUBBLES_SEND_METHOD=apple-script # "apple-script" (default) or "private-api" # BLUEBUBBLES_IMESSAGE_ADDRESS= # iCloud email or phone shown in the UI for users to text # BLUEBUBBLES_BACKFILL_LOOKBACK_MINUTES=30 # On startup, replay iMessages from the last N minutes whose webhook never reached us. 0 disables. # BLUEBUBBLES_BACKFILL_INTERVAL_SECONDS=300 # Re-run the backfill on this cadence (also runs at boot). 0 disables the recurring sweep. # BLUEBUBBLES_HEALTH_CHECK_INTERVAL_SECONDS=120 # Re-probe /api/v1/server/info so the dashboard reachability light reflects current state. 0 disables. # Twilio (RCS via Messaging Service, with SMS/MMS fallback). Register an # RCS Agent in the Twilio console (4-6 week onboarding), attach it to a # Messaging Service, and point TWILIO_MESSAGING_SERVICE_SID at the # service. Twilio routes capable recipients over RCS and falls back to # SMS automatically for everyone else. # TWILIO_ACCOUNT_SID= # Twilio account SID, e.g. "AC..." # TWILIO_AUTH_TOKEN= # Required for inbound webhook signature validation only (Twilio signs webhooks with this; API keys cannot sign webhooks). # TWILIO_API_KEY_SID= # Required for outbound REST: Standard API key SID, e.g. "SK..." # TWILIO_API_KEY_SECRET= # Required for outbound REST. Console: Account > API Keys & Tokens > Create API Key (Standard). # TWILIO_PHONE_NUMBER= # E.164 fallback sender for SMS-only deployments, e.g. "+15551234567". Ignored when TWILIO_MESSAGING_SERVICE_SID is set. # TWILIO_MESSAGING_SERVICE_SID= # "MG..." Required for RCS. Attach an RCS Agent and any sender numbers to this service in the Twilio console. # TWILIO_ALLOWED_NUMBERS= # E.164 phone, "*" for all, empty = deny all # TWILIO_VALIDATE_SIGNATURES=true # Set to false only for local dev. Production must keep this on. # E2E testing # TELEGRAM_TEST_CHAT_ID= # LLM — set your provider and model (any provider supported by any-llm) LLM_PROVIDER= LLM_MODEL= # LLM_API_BASE= # Custom API base URL (e.g. http://localhost:1234/v1 for LM Studio) # LLM_ENDPOINT= # Named endpoint (see Settings > Model); supersedes LLM_PROVIDER and LLM_API_BASE # LLM_PROMPT_CACHE=auto # auto, never: Anthropic cache_control breakpoints. # # auto marks for natively-Messages providers, including # # through a gateway. Set never only if your gateway runs # # any-llm < 1.24, which rejects a marked request instead # # of dropping the marker. # REASONING_EFFORT=auto # none, minimal, low, medium, high, xhigh, auto # VISION_MODEL= # empty = fall back to LLM_MODEL # VISION_PROVIDER= # empty = fall back to LLM_PROVIDER # VISION_ENDPOINT= # setting either this or VISION_PROVIDER opts vision out of LLM_ENDPOINT # LLM token limits # LLM_MAX_TOKENS_AGENT=8192 # LLM_MAX_TOKENS_HEARTBEAT=12000 # LLM_MAX_TOKENS_VISION=12000 # LLM Provider API Keys — uncomment and set the key for your chosen provider. # The any-llm SDK reads these from the environment automatically. # OPENAI_API_KEY= # ANTHROPIC_API_KEY= # GROQ_API_KEY= # GEMINI_API_KEY= # Any LLM Managed Platform (optional) — set this to use the any-llm.ai key vault # instead of individual provider keys above. See https://any-llm.ai # ANY_LLM_KEY= # Storage — per-user Google Drive via OAuth. # Each user grants drive.file scope through manage_integration in chat; # files land in their own Drive under a top-level "Clawbolt" folder. # Without these credentials set, file storage stays disabled and users # see no upload/find/analyze tools. # GOOGLE_DRIVE_CLIENT_ID= # GOOGLE_DRIVE_CLIENT_SECRET= # Inbound media staging — bytes for photos/files the user sends over the # messaging channel land here until the agent uploads them durably (Drive, # CompanyCam) or the 7-day TTL expires. Point this at a persistent volume in # production so the bytes survive process restarts. Metadata lives in the # ``staged_media`` Postgres table. # MEDIA_STAGING_BASE_DIR=data/staged_media # Agent loop # APPROVAL_TIMEOUT_SECONDS=120 # Seconds to wait for user approval before denying # AGENT_PROCESSING_TIMEOUT_SECONDS=300 # Max seconds for agent processing (includes lock wait) # MESSAGE_BATCH_WINDOW_MS=1500 # Batch rapid-fire messages per user # INBOUND_RECOVERY_LOOKBACK_MINUTES=30 # Sweep for inbound messages persisted but never run on startup (0 disables) # COMPACTION_RETRY_LOOKBACK_MINUTES=10080 # Retry compaction events stuck in 'pending' on startup (0 disables) # MAX_TOOL_ROUNDS=10 # MAX_INPUT_TOKENS=600000 # Hard ceiling on the input token budget; the trim trigger must stay at or below this # CONTEXT_TRIM_TARGET_TOKENS=120000 # Drop-to token budget after trimming (the primary trim governor) # CONTEXT_TRIM_TRIGGER_TOKENS=150000 # Trim fires when the prompt exceeds this and drops to CONTEXT_TRIM_TARGET_TOKENS, leaving headroom (token-side hysteresis) # CONTEXT_TRIM_TARGET_TURNS=200 # Turn-count backstop (tokens bind first). Older turns roll into MEMORY.md / USER.md / SOUL.md via compaction # CONTEXT_TRIM_TRIGGER_TURNS= # Turn backstop fires when user-turn count exceeds this and drops to CONTEXT_TRIM_TARGET_TURNS. Defaults to target+16 if unset # COMPACTION_EVENT_SNAPSHOT_MAX_BYTES_PER_FILE=100000 # Per-file truncation cap for memory diff snapshots stored on compaction_events rows # LLM_MAX_RETRIES=3 # LLM_CACHE_EXTENDED_TTL=true # Use Anthropic's 1h cache TTL (vs default 5min); set false on providers that reject the ttl field # LLM_EVAL_CONCURRENCY=4 # Parallel turns in an admin model-comparison run. Competes with live traffic for the same provider rate limit # LLM_EVAL_MAX_SAMPLES=200 # Ceiling on turns one comparison run may replay (each turn costs two LLM calls) # LLM_EVAL_MAX_CONCURRENT_RUNS=2 # Evaluations in flight across all users; they share the provider rate limit with live traffic # Conversation & memory # CONVERSATION_HISTORY_LIMIT=500 # MEMORY_RECALL_LIMIT=20 # COMPACTION_ENABLED=true # COMPACTION_MODEL= # empty = fall back to LLM_MODEL # COMPACTION_PROVIDER= # empty = fall back to LLM_PROVIDER # COMPACTION_ENDPOINT= # setting either this or COMPACTION_PROVIDER opts compaction out of LLM_ENDPOINT # COMPACTION_MAX_TOKENS=16000 # Rate limiting # WEBHOOK_RATE_LIMIT_MAX_REQUESTS=30 # WEBHOOK_RATE_LIMIT_WINDOW_SECONDS=60 # RATE_LIMIT_TRUST_PROXY=false # Unknown-sender reply (sent to numbers not in the allowlist; rate-limited # per (channel, sender) so we cannot be used as a spam relay). # UNKNOWN_SENDER_SIGNUP_URL= # link offered to unknown senders; empty = generic copy # UNKNOWN_SENDER_REPLY_COOLDOWN_SECONDS=86400 # seconds before we'll reply to the same number again # Media # MAX_MEDIA_SIZE_BYTES=20971520 # 20 MB # MEDIA_DOWNLOAD_MAX_SECONDS=60.0 # Hard wall-time deadline per media download # HTTP timeouts # HTTP_TIMEOUT_SECONDS=30.0 # CLOUDFLARED_METRICS_TIMEOUT_SECONDS=5.0 # TELEGRAM_WEBHOOK_TIMEOUT_SECONDS=10.0 # QuickBooks Online # Set client_id and client_secret to enable OAuth flow for QuickBooks. # Users connect via the web dashboard. # QUICKBOOKS_CLIENT_ID= # QUICKBOOKS_CLIENT_SECRET= # QUICKBOOKS_ENVIRONMENT=sandbox # Google Calendar # Set client_id and client_secret to enable OAuth flow for Google Calendar. # Users connect via the web dashboard. # GOOGLE_CALENDAR_CLIENT_ID= # GOOGLE_CALENDAR_CLIENT_SECRET= # Gmail # Set client_id and client_secret to enable the Gmail integration. Use a # separate Google OAuth client from Calendar/Drive so the gmail.readonly # and gmail.send scopes can be approved independently. Users connect via # manage_integration(action='connect', target='gmail'). # GMAIL_CLIENT_ID= # GMAIL_CLIENT_SECRET= # CompanyCam OAuth 2.0 # Register your app at https://docs.companycam.com/docs/oauth # COMPANYCAM_CLIENT_ID= # COMPANYCAM_CLIENT_SECRET= # Web app base URL used to build clickable receipt URLs. Override only if # CompanyCam ever ships EU / sandbox hosts. Default: https://app.companycam.com # COMPANYCAM_WEB_BASE= # AppFolio Vendor Portal — passwordless magic-link auth, no client ID/secret. # Override only for staging or test environments. # APPFOLIO_VENDOR_API_BASE=https://vendor.appf.io # APPFOLIO_VENDOR_WEB_BASE=https://vendor.appfolio.com # ServiceTitan. OAuth 2.0 client-credentials. Each tenant pastes their own # tenant ID, client ID, and client secret via the connect_servicetitan tool; # the operator wires the app-level App Key here. SERVICETITAN_USE_FAKE=true # routes every call to the in-process fake (no real network calls); flip to # false once a real sandbox or production tenant is available. ServiceTitan # serves /connect/token on a separate auth host: auth.servicetitan.io for # production, auth-integration.servicetitan.io for the integration sandbox. # Both API_BASE_URL and AUTH_BASE_URL must be overridden together when # pointing at the integration environment. # SERVICETITAN_APP_KEY= # SERVICETITAN_API_BASE_URL=https://api.servicetitan.io # SERVICETITAN_AUTH_BASE_URL=https://auth.servicetitan.io # SERVICETITAN_USE_FAKE=true # Web Search # General web search: material prices, product specs, code requirements. # Without a key the web_search tool does not load and the agent has no # search capability. Get a key at https://brave.com/search/api/ # (free tier: 2,000 queries/month). # WEB_SEARCH_API_KEY= # Search backend. Only "brave" is implemented today. # WEB_SEARCH_PROVIDER=brave # WEB_SEARCH_TIMEOUT_SECONDS=10 # How long identical queries are served from cache, in seconds. # WEB_SEARCH_CACHE_TTL_SECONDS=900 # Results returned when the agent does not request a specific count. # The agent can ask for 1-20 per call; roughly 800 tokens per result. # WEB_SEARCH_MAX_RESULTS=3 # OAuth # Public URL where this server is reachable (used for OAuth callback URLs). # APP_BASE_URL=http://localhost:8000 # Encryption — encrypts OAuth tokens at rest in the database. # Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))" # Required for production. Without this, tokens are stored unencrypted. # ENCRYPTION_KEY= # Heartbeat (proactive check-ins) HEARTBEAT_ENABLED=true HEARTBEAT_DEFAULT_FREQUENCY=30m HEARTBEAT_INTERVAL_MINUTES=30 HEARTBEAT_MAX_DAILY_MESSAGES=5 # HEARTBEAT_MODEL= # empty = fall back to LLM_MODEL # HEARTBEAT_PROVIDER= # empty = fall back to LLM_PROVIDER # HEARTBEAT_ENDPOINT= # setting either this or HEARTBEAT_PROVIDER opts heartbeat out of LLM_ENDPOINT # HEARTBEAT_CONCURRENCY=5 # HEARTBEAT_RECENT_MESSAGES_COUNT=5 # Minutes since the user's last message to skip the heartbeat LLM call. # Avoids burning tokens on "skip" decisions during an active conversation. # Set to 0 to disable the throttle. # HEARTBEAT_USER_QUIET_PERIOD_MINUTES=5 # Seconds the scheduler sleeps before its first tick after process start. # Lets in-flight work from the previous container settle and queued inbound # messages drain through normal processing, avoiding double-execution of # side-effecting tools. Set to 0 to disable. # HEARTBEAT_STARTUP_WARMUP_SECONDS=60 # -- Observability -- # LOG_REQUEST_TIMING=false # Log rendering: "text" (default) or "json". JSON adds the request # correlation ID as a top-level key for log aggregators. # LOG_FORMAT=text # -- Web chat UI -- # Show the paperclip / file-input affordance on the chat page. Set to false # behind proxies or CDNs that cap request body size below the typical photo # upload (e.g. CloudFront's 1 MB default) so users do not see an attach button # that silently 413s. Defaults to true; premium flips this off automatically. # CHAT_WEB_ATTACHMENTS_ENABLED=true # --------------------------------------------------------------------------- # Multi-user deployment # # Everything below is inert unless AUTH_MODE=multi_user (see "Auth" above). # A single-user self-host can ignore this whole block. # --------------------------------------------------------------------------- # Google OAuth — the sign-in flow. Create credentials at # https://console.cloud.google.com/apis/credentials and register # /api/auth/oauth/google/callback as an authorized redirect URI. # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= # The callback URL is derived from APP_BASE_URL. This setting is kept only # for deployments that pinned a non-default value before that change. # GOOGLE_REDIRECT_URI=http://localhost:8000/api/auth/oauth/google/callback # Session tokens. JWT_SECRET is shared with the Auth block above. # JWT_ACCESS_TOKEN_EXPIRE_MINUTES=15 # JWT_REFRESH_TOKEN_EXPIRE_DAYS=30 # JWT_ALGORITHM=HS256 # Who may register. "restricted" (default) admits only addresses in the # allowed_emails table plus ADMIN_EMAIL; "open" admits anyone with a Google # account. ADMIN_EMAIL is promoted to admin on first login. # REGISTRATION_MODE=restricted # ADMIN_EMAIL= # Legacy env-var admin list. No longer consulted at request time; kept so # `python -m backend.app.cli promote-env-admins` can migrate it into the # database. Remove it once that has run. # ADMIN_USER_IDS_RAW= # Sign-in rate limiting (per client IP) # AUTH_RATE_LIMIT_MAX_REQUESTS=10 # AUTH_RATE_LIMIT_WINDOW_SECONDS=60 # OAUTH_STATE_EXPIRY_MINUTES=5 # Global daily message cap across all free-tier users. 0 disables it. # FREE_TIER_DAILY_GLOBAL_CAP=0 # Inactive account sweep (free tier only): warn at the first threshold, # delete at the second. Driven by `python -m backend.app.cli cleanup`. # INACTIVE_WARN_MONTHS=11 # INACTIVE_DELETE_MONTHS=12 # SMTP for transactional email (waitlist approvals, operator alerts). # Set both SMTP_HOST and SMTP_FROM_EMAIL or neither: a partial config is # rejected at startup rather than silently no-opping. # SMTP_HOST= # SMTP_PORT=587 # SMTP_USERNAME= # SMTP_PASSWORD= # SMTP_FROM_EMAIL= # SMTP_TIMEOUT_SECONDS=10 # Operator error alerting. Routes ERROR-level logs to an inbox, grouped by # fingerprint so one broken integration cannot send thousands of emails. # Dormant unless SMTP is configured and a recipient resolves (ALERT_EMAIL, # else ADMIN_EMAIL). # ALERTS_ENABLED=true # ALERT_EMAIL= # ALERT_FLUSH_INTERVAL_SECONDS=60 # ALERT_DEDUPE_MINUTES=30 # ALERT_MAX_EMAILS_PER_HOUR=20 # Proactive health monitoring. Probes each dependency on a timer and emails # on state transitions (OK -> DOWN, DOWN -> OK), not on every failing tick. # HEALTH_MONITOR_ENABLED=true # HEALTH_CHECK_INTERVAL_SECONDS=300 # HEALTH_FAILURE_THRESHOLD=2 # The LLM probe spends a single token per tick. Opt out if you would rather # not pay for it. # HEALTH_PROBE_LLM=true # HEALTH_PROBE_MAX_USERS=50 # HEALTH_PROBE_TIMEOUT_SECONDS=45 # AWS KMS envelope encryption for the DEKs that protect encrypted columns. # Unset, the deployment wraps DEKs locally with a key derived from # ENCRYPTION_KEY. Setting KMS_KEY_ARN switches to KMS on the next restart. # Changing this after rows exist makes them unreadable: it is a data # migration, not a config change. # KMS_KEY_ARN= # AWS_ACCESS_KEY_ID= # AWS_SECRET_ACCESS_KEY=