Skip to content

Environment variables

The source of truth for environment configuration is the repo-root .env.example file. This page walks through it grouped by domain.

VariablePurpose
DATABASE_URLPooled connection for app queries (port 6543, pgbouncer=true).
DIRECT_URLDirect DB connection (port 5432).
VariablePurpose
BETTER_AUTH_SECRET32+ character secret. Generate with openssl rand -base64 32.
BETTER_AUTH_URLOrigin for cookie scope. http://localhost:5173 in dev.
COOKIE_DOMAINLeave unset for single-origin dev/prod. Set to a parent domain (.<domain>) once apps/api is on a sibling subdomain — adds Domain + crossSubDomainCookies for sharing. Cookies stay SameSite=Lax (web↔api are same-site).
TRUSTED_PROXY_CIDRSComma-separated IPv4 CIDR ranges trusted to set X-Forwarded-For / X-Real-IP. Consumed by getTrustedClientIp() for rate limiting and audit. Dev (Dokploy): leave unset (headers honored as-is). Production: set on both apps/api and apps/svelte-web to your edge proxy ranges (e.g. Cloudflare’s published IP ranges). When unset in production, getTrustedClientIp fails closed (ignores forwarded headers, uses socket peer) so clients can’t spoof their IP into auth rate limits.
AUTH_TRUST_CUSTOM_SCHEMEStrue (default) keeps the native apps’ custom URL schemes in Better Auth trustedOrigins. Set false only after verified App Links are live — otherwise mobile sign-in breaks.
MOBILE_APP_LINK_ORIGINSComma-separated verified HTTPS origins for Universal Links / App Links (e.g. https://app.<domain>). Drives the native apps’ associatedDomains (iOS) / intentFilters (Android) deep-link config.
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETOAuth credentials from Google Cloud Console.
VariablePurpose
PORTListen port. Default 4000.
CORS_ORIGINSComma-separated browser origin allowlist. Used by both the CORS layer and the CSRF Origin allowlist (enforceOrigin + avatar guard) — a browser request with a non-listed Origin is rejected 403. Mobile (RN fetch) sends no Origin and is exempt.
BETTER_AUTH_URL_APIOverride BETTER_AUTH_URL specifically for apps/api. Falls back to BETTER_AUTH_URL when unset.

Mobile distribution (Firebase App Distribution)

Section titled “Mobile distribution (Firebase App Distribution)”

Local-tooling only — consumed by scripts/firebase-distribute.ts (Bun auto-loads .env.local from the repo root), never by an app build or a deployed service. CLI auth is a one-time firebase login, not an env var.

VariablePurpose
FIREBASE_APP_ID_IOS_USER / _IOS_AGENT / _ANDROID_USER / _ANDROID_AGENTPer-app Firebase App ID (Firebase console → Project settings → Your apps → “App ID”, e.g. 1:1234567890:ios:abcd…). Not the bundle id. Not secret.
FIREBASE_DISTRIBUTION_GROUPSDefault tester group alias(es), comma-separated (e.g. qa). Override per run with --groups.
APPLE_TEAM_IDApple Developer Team ID — required for iOS ad-hoc archive signing.

See Mobile distribution → Sending test builds for the workflow.

VariablePurpose
PUBLIC_API_URLOrigin of the standalone tRPC host for browser-side fetches. Build-time — baked into the browser bundle via a Vite define (a runtime env change does nothing; rebuild). Required on the deployed dev env (https://api-dev.real-estate-core.com): svelte-web serves no /api/trpc route, so an empty value makes browser tRPC hit svelte-web’s HTML 404 (Unexpected token '<'). In Dokploy, set it as a Build Argument (the Dockerfile consumes an ARG; Build-time Secrets never reach it). SSR / form actions are unaffected (use createCaller(locals) direct in-process).
VariablePurpose
RESEND_API_KEYFrom resend.com/api-keys. Required for OTP delivery + Knock email channel.
RESEND_FROM_EMAILSender address (e.g. noreply@<domain>).
VariablePurpose
VALKEY_URLredis://... or rediss://... for TLS. Used for caching, property search index, BullMQ queues, rate limiting.
VALKEY_TLStrue for self-signed TLS (Proxmox).
VALKEY_TLS_REJECT_UNAUTHORIZEDfalse to accept self-signed certs over TLS (default verifies). Honored by @repo/cache, the shared queue connection, and the worker (apps/worker/src/connection.ts no longer hardcodes rejectUnauthorized: false).
VALKEY_CA_CERTOptional PEM CA cert for verifying a private TLS Valkey endpoint.
QUEUE_SUFFIXSuffix appended to every BullMQ queue name. Production leaves empty; local dev uses -dev to prevent shared-Valkey deployed workers from stealing dev jobs. Producer and worker must use the same suffix.
DLQ_RETENTION_DAYSDays to retain failed_lead_ingestions rows before the worker dlq-purge job deletes them (default 90). Resolved / replayed / discarded rows are purged regardless of age.
BULL_BOARD_USER / BULL_BOARD_PASSWORDHTTP Basic auth for the worker’s Bull Board dashboard. DEV/STG only — Bull Board is not deployed in PRD (a Cloud Run worker pool has no URL; PRD uses the queue:admin scripts — see Incident guides). Required wherever the dashboard runs, in every environment — the worker refuses to start it without both (it grants full queue control + exposes lead PII, and binds 0.0.0.0). Leaving them unset is what keeps the dashboard off PRD; there is no NODE_ENV gate, and none would work, because apps/worker/Dockerfile pins NODE_ENV=production into DEV and STG images too. The credential gate itself used to key off NODE_ENV=production, which served the dashboard unauthenticated wherever the platform env said development/staging. Failed auth attempts are limited to 10/min per client IP (resolved through getTrustedClientIp, so admins behind a proxy don’t share one bucket), after which the IP is refused for 5 minutes before its password is compared. During a Valkey outage the counter still denies but the lockout is not remembered — degraded, not absent.
VariablePurpose
AWS_ENDPOINT_URLS3-compatible endpoint. Cloudflare R2 (https://<ACCOUNT_ID>.r2.cloudflarestorage.com, region auto) on the deployed dev env; local SeaweedFS / MinIO for local dev. The S3 client keeps forcePathStyle.
AWS_S3_BUCKET_NAMEBucket name (e.g. uploads).
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYCredentials.
AWS_DEFAULT_REGIONauto for Cloudflare R2 (dev env). us-east-1 for local SeaweedFS / MinIO (conventional default).
VariablePurpose / placement
AI_ROOM_STAGING_ENABLEDFail-closed paid-prediction switch. Only exact true enables submissions/new provider attempts; unset or false stays disabled. Set identically on svelte-web, apps/api, and worker. Persisted active attempts remain recoverable/cancelable.
REPLICATE_API_TOKENWorker only. Creates, polls, and cancels predictions for the official openai/gpt-image-2 model.
REPLICATE_WEBHOOK_BASE_URLWorker only. Public HTTPS apps/api origin; the worker appends /api/webhooks/replicate/staging/{providerAttemptId}.
REPLICATE_WEBHOOK_SIGNING_SECRETapps/api only. Default Standard Webhooks secret returned by Replicate; verifies the exact raw request body and rejects stale replays.
AI_STAGING_LEGACY_ALIAS_SUNSET_ATsvelte-web + apps/api. ISO timestamp ending the 90-day compatibility window for the six legacy style names.
AI_STAGING_QUALITYsvelte-web + apps/api, and worker during the one-release legacy bridge. low, medium, high, or auto; captured on each new job.
AI_STAGING_ASPECT_RATIOsvelte-web + apps/api, and worker during the legacy bridge. Official GPT Image 2 value: 1:1, 3:2, or 2:3; defaults to 1:1.
AI_STAGING_OUTPUT_FORMAT / AI_STAGING_OUTPUT_COMPRESSIONsvelte-web + apps/api, and worker during the legacy bridge. jpeg, png, or webp; integer compression from 0–100. Captured on each new job.
AI_STAGING_CONCURRENCY / AI_STAGING_RATE_LIMIT_PER_MINUTEWorker only. Independent BullMQ controls for the dedicated ai-staging queue.
AI_DESCRIPTION_CONCURRENCYWorker only. Description queue concurrency; intentionally separate from staging.
AI_STAGING_USER_HASH_SECRETWorker only. Independent HMAC secret used to pseudonymize Replicate’s optional user_id. Generate with openssl rand -hex 32.
AI_STAGING_COST_MICROUSD_LOW / _MEDIUM / _HIGH / _AUTOWorker only, optional. Operator-verified cost estimates in micro-USD. Leave unset instead of using stale pricing.
AI_STAGING_PRICING_VERSIONWorker only, optional. Date/version attached when a configured estimate is recorded.

See the AI room staging rollout and smoke-test runbook.

VariablePurpose
IMGPROXY_URLPublic origin serving signed images (e.g. https://assets.<domain>).
IMGPROXY_KEY / IMGPROXY_SALTHex-encoded HMAC key + salt. Match the imgproxy container config.
IMGPROXY_URL_TTL_SECONDSSigned URL lifetime (default 86400 = 24 hours, matching the image routes’ browser max-age). Expiry is part of the HMAC payload — leaked URLs stop working after this window. Empty, unset, or non-numeric all fall back to the default; only a literal 0 disables expiry (not recommended — the URL then never stops working).
VariablePurpose
MEILISEARCH_HOSTInternal hostname (http://meilisearch:7700 on the dev dokploy-network).
MEILISEARCH_ADMIN_API_KEYWrite access for sync scripts and BullMQ worker.
MEILISEARCH_SEARCH_API_KEYRead-only for client autocomplete.
VariablePurpose
GOOGLE_MAPS_API_KEYServer-side: Places, Geocoding. Browser: Maps JS API.
GOOGLE_MAPS_MAP_IDCloud-styled map. From Google Cloud Console Maps Studio.
VariablePurpose
PUBLIC_GETSTREAM_API_KEYPublic-by-design — client embedding. Set in repo-root .env.local; consumed by apps/api (token mint) and apps/svelte-web.
GETSTREAM_API_SECRETServer-only. Used to mint client user tokens via HMAC. Never ship to the browser or mobile. Lives in repo-root .env.local only, consumed by apps/api.
VariablePurpose
PUBLIC_KNOCK_API_KEYPublic — used by the Knock Feed SDK.
KNOCK_SIGNING_KEYServer-only. Signs the userToken for Knock Feed authenticate(). Required in productionsignKnockUserToken throws when missing. Enable “Require identification token verification” in the Knock dashboard.
KNOCK_FCM_CHANNEL_IDServer-only. Android FCM push (shared by both apps).
KNOCK_APNS_CHANNEL_IDServer-only. iOS APNs for the agent app.
KNOCK_APNS_USER_CHANNEL_IDServer-only. iOS APNs for the customer app (separate bundle id / auth key). registerDevice’s app field routes customer tokens here; leave empty until the channel is provisioned (push then no-ops).
KNOCK_SERVICE_TOKENCLI/CI only — not read by the app. Authenticates the Knock CLI for knock push/pull against the knock/ source of truth.
VariablePurpose
PUBLIC_POSTHOG_KEY / PUBLIC_POSTHOG_HOSTConsent-gated product analytics.
SENTRY_DSN / VITE_SENTRY_DSNError + replay. One project per deployable, so each service (svelte-web, apps/api, apps/worker) sets SENTRY_DSN to its own project’s DSN; VITE_SENTRY_DSN is the svelte-web client DSN (same project as the server). Native-app DSNs are inlined in each app’s Monitoring init (publishable, not env).
SENTRY_ORG / SENTRY_PROJECT / SENTRY_AUTH_TOKENSource-map / symbol upload during build (svelte-web; native apps upload symbols from their own build pipelines).
VariablePurpose
STRIPE_SECRET_KEYServer-only. Read by @repo/server-services, apps/api (the webhook at POST /api/webhooks/stripe) and apps/worker (the daily stripe-reconcile). Unset disables checkout and the billing pages rather than erroring. Test and live are separate keys — each tier needs its own.
STRIPE_WEBHOOK_SIGNING_SECRETVerifies the webhook signature before any event is trusted. It is per endpoint, not per account, so a new endpoint means a new secret even in the same Stripe account.

Checkout is Stripe-hosted, so no publishable key and no Stripe.js reach the browser — which is also why nothing here changes the web CSP.

VariablePurpose
FACTURAPI_SECRET_KEYServer-only. Stamps CFDI 4.0 documents at the SAT through FacturAPI’s PAC. Unsetting it is the clean stop: the facturación card hides and the 15-minute cfdi-scan no-ops, leaving the rest of billing untouched. Live mode additionally needs CSD certificates uploaded in the FacturAPI dashboard — a live key with no certificates stamps nothing.
FACTURAPI_PRODUCT_KEY / FACTURAPI_UNIT_KEY / FACTURAPI_PAYMENT_FORMSAT catalog codes (c_ClaveProdServ, c_ClaveUnidad, c_FormaPago), not secrets. The defaults are defensible; which code actually applies is the issuer’s fiscal call and their contador’s to confirm.

PII_ENCRYPTION_KEY encrypts the fiscal identity both of these integrations exchange — see Settings → Secrets, because it is the one secret in the repo that cannot be re-issued.

VariablePurpose
ORIGINCanonical site origin used in OpenGraph tags + sitemap. Also read by apps/api and apps/worker to absolutize billing-email CTAs — unset in a deployed tier, those degrade to relative paths silently.
VariablePurpose
E2E_TEST_USER_EMAIL / E2E_TEST_USER_PASSWORDSeeded test account.
PLAYWRIGHT_BASE_URLTarget origin. http://localhost:5173 for local.
EnvWhere it lives
Local dev.env.local (gitignored, per-developer).
Dev (deployed)Dokploy — per-app Environment tab (runtime) + Build Arguments for svelte-web’s build-time values (PUBLIC_API_URL, VITE_SENTRY_DSN, SENTRY_ORG/SENTRY_PROJECT; SENTRY_AUTH_TOKEN omitted in dev). Mobile builds point at https://api-dev.real-estate-core.com.
Staging + ProductionGCP us-central1Secret Manager, injected into Cloud Run; QUEUE_SUFFIX=-stg on staging and empty on production. Decided, not provisioned yet — see Production landscape.

See Settings → Secrets for the policy on how secrets are stored and rotated.