Pitchbar reads its configuration from the standard Laravel
.env, with overrides for some keys available via the
platform admin's App Settings page so you can swap
Stripe / mail / LLM credentials without redeploying.
Required
Variable
Why
APP_KEY
Encryption master key. Generate with php artisan key:generate. Never rotate without a re-encrypt migration.
APP_URL
Public URL, used to build widget snippets, OAuth callbacks, signed URLs.
DB_*
Postgres connection.
REDIS_HOST
Cache, sessions, queue, hot-path retrieval cache, conversation history cache.
QUEUE_CONNECTION
Set to redis in production.
SESSION_DRIVER
redis in production.
CACHE_DRIVER
redis.
WIDGET_JWT_SECRET
HS256 signing secret for visitor JWTs. ≥ 32 random bytes.
LLM provider
At least one of the following:
Variable
Provider
CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN
Cloudflare Workers AI (preferred). Auto-binds Llama 3.3 70B + bge-base-en-v1.5.
OPENAI_API_KEY
OpenAI direct.
OPENROUTER_API_KEY
OpenRouter (router across many providers).
Set LLM_PROVIDER to force a binding (cloudflare,
openai, openrouter). When unset, the resolver
picks based on which keys are available, in the priority above.
Vector store
Variable
Provider
CLOUDFLARE_VECTORIZE_INDEX
Cloudflare Vectorize index name. Uses CLOUDFLARE_ACCOUNT_ID + token. Default pitchbar-chunks.
QDRANT_URL + QDRANT_API_KEY
Qdrant.
Set VECTOR_PROVIDER to force. Auto-binds based on
available keys (Vectorize preferred).
Crawler
Variable
Strategy
(uses CLOUDFLARE_*)
Cloudflare Browser Rendering. Preferred.
BROWSERLESS_TOKEN + BROWSERLESS_URL
Browserless fallback.
(none)
Plain HTTP. Free; no JS rendering.
Variable
Purpose
CRAWL_MAX_PAGES_PER_SOURCE
Max pages fan-out per Source. Default 500 (raised from 25 — buyers adding 100-URL sitemaps were silently losing 75 pages). Cap applies after sitemap-index recursion + dedupe.
CRAWL_PAGE_DISPATCH_DELAY
Per-page delay (seconds) when fanning out sitemap pages so Cloudflare Browser Rendering's small per-account concurrency doesn't 429. Default 2.
CLOUDFLARE_BROWSER_RENDERING
Set to false to force the Browserless / plain-HTTP path even when Cloudflare keys are present. Default true.
RAG / retrieval
Variable
Purpose
RAG_CONFIDENCE_THRESHOLD
Default similarity threshold for newly created agents. 0.5 for Cloudflare bge-base-en-v1.5 (its ANN cosine peaks lower than OpenAI's); 0.78 for OpenAI text-embedding-3-small. Auto-resolves based on LLM_PROVIDER.
RAG_QUERY_REWRITE
Hands the retrieval query to a small LLM instead of the built-in stitch. Off by default, and the safe default — the stitch is deterministic and costs nothing, while the rewrite adds a round-trip to every context-dependent turn. If you enable it, set RAG_QUERY_REWRITE_MODEL to a small fast model; leaving it empty runs the rewrite on your main chat model. Whatever it returns, any product name it dropped is added back before the search runs. php artisan pitchbar:diagnose-widget reports the current state.
RAG_CURATED_BOOST
How much a source you authored (pasted text, Google Doc/Sheet, Notion, uploaded file, SQL) outranks a page crawled from your own website on a near-tie. Default 0.10; 0 weighs every source equally. Precedence only — crawled pages still answer everything your documents don't cover.
RAG_RERANK_ENABLED
Set to false to disable the bge-reranker-base cross-encoder pass. Default true when Cloudflare keys are present.
ATTRIBUTION_DIRECT_WINDOW_DAYS
Revenue attribution: max days between a recommended product's click and an order containing that product for the order to count as direct. Default 7. Changes apply to new orders only — recorded matches are never revisited. See Revenue attribution.
ATTRIBUTION_INFLUENCED_WINDOW_DAYS
Revenue attribution: max days between a conversation and an order for the order to count as influenced (last touch wins). Default 30.
LLM_STREAM_READ_TIMEOUT_SECONDS
Max seconds a single streamed-response read may block (default 30). A healthy token stream delivers bytes subsecond, so this can never truncate a real answer — it turns a provider that stops sending mid-stream into a clean retryable timeout instead of a request that hangs until PHP's execution limit kills the Octane worker (which then serves 500s until it recycles).
FAKE_LLM_FIRST_BYTE_MS / FAKE_LLM_PER_TOKEN_MS
Load testing only — leave unset in production. When no provider keys are configured the app falls back to a built-in fake LLM that answers instantly. That makes it useless for capacity testing, because a real turn holds an Octane worker for seconds while an instant one never reveals the concurrency ceiling. These two make the fake wait before its first token and between tokens, so you can measure how many simultaneous visitors an instance really serves without spending a cent on a provider. Both default to 0. See Concurrency and worker sizing.
VECTOR_DIM
Embedding dimension override. Leave unset to auto-resolve from the configured embed model via the known-model map (bge-base=768, bge-m3=1024, text-embedding-3-small=1536, etc.). Set explicitly only when pointing at an unlisted model. Changing this on an existing install needs php artisan vector:rebuild-index — the Vectorize index is provisioned at the dim active when it was first created and cannot be resized in place.
Cloudflare AI Gateway (optional)
Variable
Purpose
CLOUDFLARE_AI_GATEWAY_URL
If set, Workers AI calls route through Cloudflare's AI Gateway — observability, caching, rate limits.
CLOUDFLARE_CHAT_MODEL
Default @cf/meta/llama-3.3-70b-instruct-fp8-fast.
CLOUDFLARE_EMBED_MODEL
Default @cf/baai/bge-base-en-v1.5.
CLOUDFLARE_VECTORIZE_INDEX
Vectorize index name. Default pitchbar-chunks.
OPENROUTER_CHAT_MODEL
OpenRouter chat model slug. Default meta-llama/llama-3.3-70b-instruct:free.
Qdrant collection name when using the Qdrant vector store fallback. Default pitchbar_chunks.
S3 / R2 (object storage for file uploads)
Variable
Purpose
AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
Bucket credentials. For Cloudflare R2, use the R2 token's key/secret pair.
AWS_DEFAULT_REGION
Region of the bucket. Use auto for R2.
AWS_BUCKET
Bucket name.
AWS_ENDPOINT
Custom S3-compatible endpoint. Required for R2 (e.g. https://<accountid>.r2.cloudflarestorage.com). Leave blank for native AWS S3.
AWS_USE_PATH_STYLE_ENDPOINT
true for R2 and most non-AWS S3 implementations; false for native AWS.
Frontend (Vite)
Variable
Purpose
VITE_WIDGET_CDN_URL
Base URL the visitor widget loader uses. Defaults to http://localhost:5173 in dev. In production, point at the public origin serving /widget/widget.js.
Shared inbox the delivery team monitors for one-time add-on purchases (Professional AI Setup, etc.). Leave blank to fall back to MAIL_FROM_ADDRESS. Consumed by NotifyTeamOfAddonPurchaseJob.
Branding
Variable
Purpose
BRANDING_LABEL
Default "Powered by Pitchbar" label. Override in app_settings for white-label.
BRANDING_URL
Where the label links to.
BRANDING_FOOTER_LOGO_PATH
Storage path to the footer logo image.
Observability
Variable
Purpose
SENTRY_LARAVEL_DSN
Error reporting. (Some hosts still ship the legacy SENTRY_DSN name; the Laravel SDK reads SENTRY_LARAVEL_DSN as of v4.x — keep both in .env for back-compat if you migrated.)
Google OAuth (Drive + Docs + Sheets) — register an OAuth client in the GCP console with drive.readonly, documents.readonly, and spreadsheets.readonly scopes. Omitting spreadsheets.readonly blocks the Google Sheets source type.
Marketing demo widget
Variable
Purpose
MARKETING_DEMO_AGENT_ID
UUID of a published agent that powers the live demo widget on the marketing pages. Add the marketing site URL to that agent's allowed_origins.
DEMO
Set to true to open Login / Get started links in a new tab on the marketing site — useful for reviewers exploring a CodeCanyon live demo without losing the marketing page when they hop into the app.
Queue tick (Cloudflare Worker cron)
Variable
Purpose
INTERNAL_QUEUE_TOKEN
Shared bearer secret protecting POST /api/v1/internal/queue-tick. The Cloudflare Worker cron pings this every 60s to drive the queue when in-cluster scheduling isn't available. Only set it when this host has no queue daemon. If PM2 or supervisor already runs queue:work, the tick is redundant — leave the token empty. The tick runs the worker in its own subprocess, so it can no longer take the web worker down with it, but two things draining the same queues buys you nothing.
App Settings overrides
The app_settings singleton row stores plaintext-encrypted
overrides for:
Stripe secret + webhook secret + publishable key.
Mail driver settings.
Cloudflare / OpenAI / OpenRouter keys.
Branding (label, URL, logo).
The AppSettingsOverrideServiceProvider reads this on boot
and merges into config(). Setting things via the admin
panel is preferred for production — you don't have to re-deploy when a
key rotates.
APP_KEY rotation requires a manual migration. The
encrypted columns in app_settings were sealed with the
old key; rotating without re-encrypting renders them unreadable.