The source of truth for environment configuration is the repo-root .env.example file. This page walks through it grouped by domain.
| Variable | Purpose |
|---|
DATABASE_URL | Pooled connection for app queries (port 6543, pgbouncer=true). |
DIRECT_URL | Direct DB connection (port 5432). |
| Variable | Purpose |
|---|
BETTER_AUTH_SECRET | 32+ character secret. Generate with openssl rand -base64 32. |
BETTER_AUTH_URL | Origin for cookie scope. http://localhost:5173 in dev. |
COOKIE_DOMAIN | Leave 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_CIDRS | Comma-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_SCHEMES | true (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_ORIGINS | Comma-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_SECRET | OAuth credentials from Google Cloud Console. |
| Variable | Purpose |
|---|
PORT | Listen port. Default 4000. |
CORS_ORIGINS | Comma-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_API | Override BETTER_AUTH_URL specifically for apps/api. Falls back to BETTER_AUTH_URL when unset. |
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.
| Variable | Purpose |
|---|
FIREBASE_APP_ID_IOS_USER / _IOS_AGENT / _ANDROID_USER / _ANDROID_AGENT | Per-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_GROUPS | Default tester group alias(es), comma-separated (e.g. qa). Override per run with --groups. |
APPLE_TEAM_ID | Apple Developer Team ID — required for iOS ad-hoc archive signing. |
See Mobile distribution → Sending test builds for the workflow.
| Variable | Purpose |
|---|
PUBLIC_API_URL | Origin 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). |
| Variable | Purpose |
|---|
RESEND_API_KEY | From resend.com/api-keys. Required for OTP delivery + Knock email channel. |
RESEND_FROM_EMAIL | Sender address (e.g. noreply@<domain>). |
| Variable | Purpose |
|---|
VALKEY_URL | redis://... or rediss://... for TLS. Used for caching, property search index, BullMQ queues, rate limiting. |
VALKEY_TLS | true for self-signed TLS (Proxmox). |
VALKEY_TLS_REJECT_UNAUTHORIZED | false 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_CERT | Optional PEM CA cert for verifying a private TLS Valkey endpoint. |
QUEUE_SUFFIX | Suffix 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_DAYS | Days 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_PASSWORD | HTTP 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. |
| Variable | Purpose |
|---|
AWS_ENDPOINT_URL | S3-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_NAME | Bucket name (e.g. uploads). |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | Credentials. |
AWS_DEFAULT_REGION | auto for Cloudflare R2 (dev env). us-east-1 for local SeaweedFS / MinIO (conventional default). |
| Variable | Purpose / placement |
|---|
AI_ROOM_STAGING_ENABLED | Fail-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_TOKEN | Worker only. Creates, polls, and cancels predictions for the official openai/gpt-image-2 model. |
REPLICATE_WEBHOOK_BASE_URL | Worker only. Public HTTPS apps/api origin; the worker appends /api/webhooks/replicate/staging/{providerAttemptId}. |
REPLICATE_WEBHOOK_SIGNING_SECRET | apps/api only. Default Standard Webhooks secret returned by Replicate; verifies the exact raw request body and rejects stale replays. |
AI_STAGING_LEGACY_ALIAS_SUNSET_AT | svelte-web + apps/api. ISO timestamp ending the 90-day compatibility window for the six legacy style names. |
AI_STAGING_QUALITY | svelte-web + apps/api, and worker during the one-release legacy bridge. low, medium, high, or auto; captured on each new job. |
AI_STAGING_ASPECT_RATIO | svelte-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_COMPRESSION | svelte-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_MINUTE | Worker only. Independent BullMQ controls for the dedicated ai-staging queue. |
AI_DESCRIPTION_CONCURRENCY | Worker only. Description queue concurrency; intentionally separate from staging. |
AI_STAGING_USER_HASH_SECRET | Worker 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 / _AUTO | Worker only, optional. Operator-verified cost estimates in micro-USD. Leave unset instead of using stale pricing. |
AI_STAGING_PRICING_VERSION | Worker only, optional. Date/version attached when a configured estimate is recorded. |
See the AI room staging rollout and smoke-test runbook.
| Variable | Purpose |
|---|
IMGPROXY_URL | Public origin serving signed images (e.g. https://assets.<domain>). |
IMGPROXY_KEY / IMGPROXY_SALT | Hex-encoded HMAC key + salt. Match the imgproxy container config. |
IMGPROXY_URL_TTL_SECONDS | Signed 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). |
| Variable | Purpose |
|---|
MEILISEARCH_HOST | Internal hostname (http://meilisearch:7700 on the dev dokploy-network). |
MEILISEARCH_ADMIN_API_KEY | Write access for sync scripts and BullMQ worker. |
MEILISEARCH_SEARCH_API_KEY | Read-only for client autocomplete. |
| Variable | Purpose |
|---|
GOOGLE_MAPS_API_KEY | Server-side: Places, Geocoding. Browser: Maps JS API. |
GOOGLE_MAPS_MAP_ID | Cloud-styled map. From Google Cloud Console Maps Studio. |
| Variable | Purpose |
|---|
PUBLIC_GETSTREAM_API_KEY | Public-by-design — client embedding. Set in repo-root .env.local; consumed by apps/api (token mint) and apps/svelte-web. |
GETSTREAM_API_SECRET | Server-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. |
| Variable | Purpose |
|---|
PUBLIC_KNOCK_API_KEY | Public — used by the Knock Feed SDK. |
KNOCK_SIGNING_KEY | Server-only. Signs the userToken for Knock Feed authenticate(). Required in production — signKnockUserToken throws when missing. Enable “Require identification token verification” in the Knock dashboard. |
KNOCK_FCM_CHANNEL_ID | Server-only. Android FCM push (shared by both apps). |
KNOCK_APNS_CHANNEL_ID | Server-only. iOS APNs for the agent app. |
KNOCK_APNS_USER_CHANNEL_ID | Server-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_TOKEN | CLI/CI only — not read by the app. Authenticates the Knock CLI for knock push/pull against the knock/ source of truth. |
| Variable | Purpose |
|---|
PUBLIC_POSTHOG_KEY / PUBLIC_POSTHOG_HOST | Consent-gated product analytics. |
SENTRY_DSN / VITE_SENTRY_DSN | Error + 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_TOKEN | Source-map / symbol upload during build (svelte-web; native apps upload symbols from their own build pipelines). |
| Variable | Purpose |
|---|
STRIPE_SECRET_KEY | Server-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_SECRET | Verifies 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.
| Variable | Purpose |
|---|
FACTURAPI_SECRET_KEY | Server-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_FORM | SAT 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.
| Variable | Purpose |
|---|
ORIGIN | Canonical 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. |
| Variable | Purpose |
|---|
E2E_TEST_USER_EMAIL / E2E_TEST_USER_PASSWORD | Seeded test account. |
PLAYWRIGHT_BASE_URL | Target origin. http://localhost:5173 for local. |
| Env | Where 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 + Production | GCP us-central1 — Secret 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.