Skip to main content

Environment Setup

Closegram has three separate places that need environment configuration, each with its own file:

AppFileTemplate
apps/backend.env (also .env.dev for Docker, .env.test for the test stack).env.example
apps/frontend-nextjs.env.local.env.local.example
apps/iosNot a real .env file — variables are set directly in the Xcode scheme (see iOS below)apps/ios/app/.env.example (reference only)

This page is a full reference of every variable across all three. For step-by-step instructions on obtaining the actual key/secret values (Firebase, Google, Apple, Stripe, AWS, Twilio, etc.), see Obtaining API Keys. For how these variables differ across local/development/production and which values go in Railway's dashboard per environment, see Deployment Environments.

Quick start

# Backend
cd apps/backend
cp .env.example .env
# fill in the values — see the tables below and the API-keys guide

# Frontend
cd apps/frontend-nextjs
cp .env.local.example .env.local

Generate a secure random secret for any *_SECRET value with:

node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

You do not need real database, Redis, or LiveKit accounts to develop locally — apps/backend/docker-compose.yml spins up all three with baked-in dev credentials:

cd apps/backend
npm run docker:up # postgres + redis + redis-livekit + livekit, using .env.dev
npm run migrate # run migrations against the dockerized Postgres
npm run seed # optional — seed data
ServiceContainerDefault portDefault credentials
PostgreSQLclosegram-postgres5432user postgres / password closegram_postgres_pass / db closegram_dev
Redisclosegram-redis6382 (host) → 6379 (container)password closegram_redis_pass
Redis (LiveKit's dedicated instance)closegram-redis-livekit6381password livekit_redis_pass
LiveKit serverclosegram-livekit7880 (HTTP), 7881 (TCP), 7882 (UDP)API key devkey / secret dev1234567890abcdef1234567890abcdef (hardcoded in docker-compose.yml, dev-only)
Redis Commander (GUI, optional)closegram-redis-commandervia --profile guiadmin / admin

Since the LiveKit dev key is baked into the compose file, you can leave LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET in your .env pointed at ws://localhost:7880 / devkey / dev1234567890abcdef1234567890abcdef for local development — no LiveKit Cloud account needed until you deploy. Other useful scripts: npm run docker:down, npm run docker:reset (wipes volumes), npm run docker:up:test (separate test Postgres/Redis on different ports), npm run redis:cli / redis:cli:livekit.

What's actually required

Not every variable below needs a real value to run the app locally. Roughly:

  • Required to boot at all: Database (DB_*), JWT secrets (JWT_*). Without these the backend won't start or auth won't work.
  • Required for specific features, otherwise that feature silently degrades: Firebase (Google Sign-In verification + push), Apple Sign-In (APPLE_*), Stripe (payments — DISABLE_STRIPE=true to skip), AWS S3 (file uploads — DISABLE_S3=true uses mock responses), SMS/Twilio (phone OTP — DISABLE_SMS=true to skip), LiveKit (calls/live — see Docker section above, DISABLE_LIVEKIT=true to skip).
  • Optional / observability only: Redis can be skipped with DISABLE_REDIS=true (though several real-time features depend on it — see Infrastructure), Sentry/Datadog/New Relic/LogRocket/custom logger, BigQuery analytics (falls back to Postgres via ANALYTICS_DB_TYPE=postgres), PayPal, MessageBird.
  • Vestigial — defined in .env.example but not read by any backend code (confirmed by grepping the source, safe to leave blank or delete): DEEPAI_API_KEY, AWS_PINPOINT_PROJECT_ID, GMAIL_USER_NAME / GMAIL_USER_PASSWORD / GMAIL_SERVICE_HOST / GMAIL_SERVICE_PORT (there's no Gmail/nodemailer send path — EMAIL_PROVIDER only has an aws-ses implementation despite being nominally pluggable).

Backend — apps/backend/.env

Application

VariableExamplePurpose
NODE_ENVdevelopmentSelects which .env.<NODE_ENV> file loads
APP_NAMEClosegramDisplay name used in emails/logs
APP_VERSION / BACKEND_VERSION1.0.0Version tags for logging/Sentry release
BACKEND_PORT / PORT8000Server port
API_URLhttp://localhost:8000Backend's own public URL
FRONTEND_URLhttp://localhost:3000Used for building links in emails, CORS
TIMEZONEAmerica/Los_AngelesDefault server timezone

Database

VariablePurpose
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD / DB_DIALECTStandard Sequelize/Postgres connection pieces
DEV_DATABASE_URL / DATABASE_URLFull connection string alternative (used by some tooling)

Redis

VariablePurpose
REDIS_HOST / REDIS_PORT / REDIS_PASSWORDConnection — powers presence, chat cache, OTP cache, BullMQ, pub/sub subscriptions
DISABLE_REDISSet true to run without Redis (degrades the features above)
REDIS_COMMANDER_USER / REDIS_COMMANDER_PASSWORDLogin for the optional Redis Commander GUI

JWT

VariablePurpose
JWT_SECRETSigns regular user access tokens
JWT_REFRESH_SECRETSigns refresh tokens
JWT_ADMIN_SECRETSigns admin-panel session tokens
JWT_TOKEN_EXPIRYe.g. 8h
JWT_TEMP_TOKEN_EXPIRYe.g. 10m — short-lived tokens (2FA challenge, etc.)

Passkeys (WebAuthn)

Shared by both the main app's passkeys (services/passkey.service.js) and admin passkeys (services/admin-passkey.service.js) — there's no separate admin-only variable. Both default to localhost/FRONTEND_URL for local dev, but must be set explicitly in production or registration fails with The RP ID "localhost" is invalid for this domain (or, if only WEBAUTHN_ORIGIN is misconfigured, Unexpected registration response origin).

VariableExamplePurpose
WEBAUTHN_RP_IDclosegram.comThe registrable domain only — no scheme, no port, no path. Must be the origin's domain or a registrable parent of it (e.g. closegram.com is valid for admin.closegram.com too — no separate value needed for the admin panel). Defaults to localhost.
WEBAUTHN_ORIGINhttps://closegram.com,https://admin.closegram.comThe full origin(s) allowed to perform the ceremony, including the scheme. Comma-separated for multiple — must include both the client app's origin and the admin panel's origin (different subdomains), or admin passkey registration fails. No trailing slash. Defaults to FRONTEND_URL.
WEBAUTHN_RP_NAMEClosegramDisplay name shown in the OS passkey prompt. Defaults to APP_NAME.

See the WEBAUTHN_* block in apps/backend/.env.example for a worked prod example, and Admin accounts for the admin-specific origin gotcha.

AWS — S3, SES, SNS

VariablePurpose
AWS_REGIONe.g. us-east-1
AWS_ACCESS_KEY / AWS_ACCESS_KEY_ID, AWS_SECRET_KEY / AWS_SECRET_ACCESS_KEYIAM credentials (both naming variants are read in different places)
AWS_BUCKET_NAME / AWS_S3_URLS3 bucket for uploads
AWS_API_VERSION / AWS_PROFILE / AWS_TOKEN_KEYMisc AWS SDK config
STORAGE_PROVIDERCurrently only aws-s3 is implemented
DISABLE_S3true uses mock upload responses locally
AWS_EMAIL / AWS_EMAIL_NAMESES "from" address/name
AWS_S3_EMAIL_TEMPLATES_PATHS3 path for Pug email templates
AWS_CONFIGURATION_NAMESES configuration set (bounce/complaint tracking)
AWS_SNS_SENDER_IDSNS SMS sender ID
AWS_PINPOINT_PROJECT_IDNot referenced anywhere in the backend — vestigial

Email

VariablePurpose
EMAIL_PROVIDEROnly aws-ses has an implementation despite being nominally pluggable
DISABLE_EMAILSkip sending emails locally
VERIFIED_EMAIL_TIME_WAIT / VERIFIED_EMAIL_LIMIT_TIMES / VERIFIED_EMAIL_TIME_TRANSFORMEmail-verification-code throttling
GMAIL_USER_NAME / GMAIL_USER_PASSWORD / GMAIL_SERVICE_HOST / GMAIL_SERVICE_PORTVestigial — no Gmail/nodemailer send path exists

SMS / phone OTP

VariablePurpose
SMS_PROVIDERtwilio (default), aws-sns, or messagebird
SMS_ORIGINATOR_PHONESender label
DISABLE_SMSSkip sending SMS locally
VERIFIED_SMS_TIME_WAIT / VERIFIED_SMS_LIMIT_TIMES / VERIFIED_SMS_TIME_TRANSFORMOTP throttling
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_PHONE_NUMBER / TWILIO_VERIFY_SERVICE_SIDTwilio credentials
MESSAGEBIRD_API_KEY / MESSAGEBIRD_ORIGINATORMessageBird credentials (alternative provider)
PHONE_LOGIN_PROVIDERSwitch for how "log in / add account with phone number" works: firebase (default — web client uses Firebase Phone Auth, no SMS sent from this server), twilio (legacy requestPhoneOtp/loginWithPhone mutations, this server sends the OTP itself), or disabled (phone login off entirely, including via Firebase). Mirrored on the frontend by NEXT_PUBLIC_PHONE_LOGIN_PROVIDER.

Stripe / PayPal

VariablePurpose
STRIPE_API_KEY_PROD_SECRETStripe secret key
STRIPE_WEBHOOK_SECRETVerifies incoming Stripe webhook signatures
PAYMENT_PROVIDERCurrently stripe
DISABLE_STRIPESkip payment processing locally
PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRETPayPal app credentials
PAYPAL_API_BASEDefaults to the sandbox URL if unset

Firebase (push notifications + Google/phone auth verification)

VariablePurpose
FIREBASE_PROJECT_IDFirebase project ID
FIREBASE_PRIVATE_KEY_ID / FIREBASE_PRIVATE_KEY / FIREBASE_CLIENT_EMAIL / FIREBASE_CLIENT_IDService-account credentials (from the downloaded JSON key)
FIREBASE_AUTH_URI / FIREBASE_TOKEN_URI / FIREBASE_AUTH_PROVIDER_CERT_URL / FIREBASE_CLIENT_CERT_URL / FIREBASE_UNIVERSE_DOMAINStandard fields from the service-account JSON — copy as-is
FIREBASE_DATABASE_URL / FIREBASE_STORAGE_BUCKETProject URLs
DISABLE_PUSH_NOTIFICATIONSSkip sending push locally

Apple Sign-In

VariablePurpose
APPLE_CLIENT_IDThe native iOS app's Bundle ID (e.g. com.yourapp.bundle) — the aud claim on ID tokens minted by Sign in with Apple inside the iOS app. Not the Services ID below, despite the name.
APPLE_WEB_CLIENT_IDThe separate Services ID for Sign in with Apple on the web (registered in Apple Developer, tied to a verified domain + Return URLs) — not the same as APPLE_CLIENT_ID/Bundle ID above. Must match NEXT_PUBLIC_APPLE_CLIENT_ID in frontend-nextjs. Leave unset if web Apple Sign-In isn't set up.
APPLE_TEAM_IDApple Developer Team ID
APPLE_KEY_IDID of the Sign in with Apple private key
APPLE_REDIRECT_URIOAuth redirect URL (web flow)
APPLE_PRIVATE_KEYThe .p8 private key contents

Apple Push Notifications (APNs) — iOS calls/VoIP

VariablePurpose
APNS_PRIVATE_KEY / APNS_KEY_ID / APNS_TEAM_IDAPNs auth key (separate key from Sign in with Apple)
APNS_BUNDLE_IDiOS app's bundle ID
APNS_PRODUCTIONfalse for sandbox APNs, true for production

Google Cloud / BigQuery (analytics)

VariablePurpose
GOOGLE_CLOUD_PROJECT_ID / GCP_PROJECT_IDGCP project
BIGQUERY_DATASET_ID / BIGQUERY_LOCATIONDataset config
BIGQUERY_PRIVATE_KEY_ID / BIGQUERY_PRIVATE_KEY / BIGQUERY_CLIENT_EMAIL / BIGQUERY_CLIENT_IDService-account credentials
ANALYTICS_DB_TYPEpostgres (default, no GCP needed) or bigquery
ANALYTICS_DB_HOST / PORT / NAME / USER / PASSWORDOnly used when ANALYTICS_DB_TYPE=postgres and you want a separate analytics DB
ANALYTICS_ENABLEDMaster on/off switch

Social OAuth

There are two independent Google OAuth mechanisms in the codebase — both need to be filled in if you want Google Sign-In fully working outside of the Firebase-verification path:

VariablePurpose
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETPassport passport-google-token strategy
CLIENT_ID / CLIENT_ID_SECRET / REDIRECT_URIsA separate googleapis OAuth2Client used elsewhere in the auth service
FACEBOOK_APP_ID / FACEBOOK_APP_SECRETOnly checked for truthiness to decide whether to initialize the Facebook passport strategy at all
FACEBOOK_CLIENT_ID / FACEBOOK_CLIENT_SECRETThe actual credentials passed to passport-facebook-token

Note: Facebook Sign-In was removed from the product-facing Authentication docs (it isn't exposed to end users), but this backend OAuth plumbing still exists and is wired — leave these blank if you don't need it.

Image analysis (NSFW / content moderation)

VariablePurpose
IMAGE_ANALYSIS_PROVIDERgoogle-vision (the only implemented provider)
IMAGE_ANALYSIS_MIN_LABEL_CONFIDENCE0–1 confidence threshold
IMAGE_ANALYSIS_MAX_LABELS_PER_POSTCap on labels processed per upload
DISABLE_IMAGE_ANALYSISSkip analysis locally
DEEPAI_API_KEYVestigial — not referenced anywhere; the active provider is Google Vision, which authenticates via the same Google Cloud service-account credentials above

LiveKit (calls + live streams)

VariablePurpose
LIVEKIT_URL / LIVEKIT_WS_URLWebSocket URL of the LiveKit server
LIVEKIT_API_KEY / LIVEKIT_API_SECRETServer credentials
LIVEKIT_PORT / LIVEKIT_RTC_TCP / LIVEKIT_RTC_UDPOnly relevant if you're running the self-hosted Docker LiveKit server
DISABLE_LIVEKITSkip calls/live locally

Locally these can point at the Docker-provided LiveKit server (see the Docker section above) — no external account needed until production.

Logging / observability (all optional)

VariablePurpose
DATADOG_API_KEYEnables the Datadog log transport
SENTRY_DSN / SENTRY_ENVIRONMENT / SENTRY_RELEASEError tracking
NEW_RELIC_LICENSE_KEY / NEW_RELIC_APP_NAME / NEW_RELIC_ENABLEDAPM — index.js conditionally require('newrelic') only if the license key is set
LOGROCKET_APP_IDSession replay
CUSTOM_LOGGER_URL / CUSTOM_LOGGER_API_KEYShips logs to a custom HTTP endpoint

Admin panel, security, rate limiting, notifications

These generally have sane defaults and don't need external accounts — see .env.example for the full list: ADMIN_PANEL_URL, ADMIN_SESSION_TIMEOUT_MINUTES, ADMIN_REQUIRE_EMAIL_VERIFICATION, ADMIN_REQUIRE_2FA_SUPER_ADMIN, SESSION_EXPIRY_HOURS, MAX_FAILED_LOGIN_ATTEMPTS, ACCOUNT_LOCK_DURATION_MINUTES, the PASSWORD_* policy block, the RATE_LIMIT_* block, and NOTIFICATION_* / PUSH_NOTIFICATION_PROVIDER.

Frontend — apps/frontend-nextjs/.env.local

VariablePurpose
NEXT_PUBLIC_GRAPHQL_URLBackend GraphQL endpoint, e.g. http://localhost:4000/graphql (note: default backend port is 8000 — adjust to match your BACKEND_PORT)
NEXT_PUBLIC_WS_URLWebSocket endpoint for GraphQL subscriptions, e.g. ws://localhost:8000/web/graphql
NEXT_PUBLIC_SITE_URLPublic site URL — used for SEO metadata, canonical links, robots.txt, sitemap.xml. Must be the real production domain in production.
NEXT_PUBLIC_FIREBASE_API_KEY / _AUTH_DOMAIN / _PROJECT_ID / _STORAGE_BUCKET / _MESSAGING_SENDER_ID / _APP_IDFirebase web app config (from the Firebase console, distinct from the backend's service-account credentials)
NEXT_PUBLIC_FIREBASE_VAPID_KEYFirebase Web Push (VAPID) key — enables web push notifications. Optional.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYStripe's public key for client-side Elements
NEXT_PUBLIC_PAYPAL_CLIENT_IDPayPal app's public client id — for the "buy coins" PayPal buttons
NEXT_PUBLIC_ADSENSE_CLIENTGoogle AdSense publisher id (e.g. ca-pub-…). Activates the site-wide verification/Auto-ads loader in app/layout.tsx and AdSense rendering in the profile ad slot — see Ads & Revenue Share
NEXT_PUBLIC_GAM_NETWORK_CODEGoogle Ad Manager network code — enables the GPT-based profile ad slot (the compliant per-creator revenue route, see Ads & Revenue Share)
NEXT_PUBLIC_GA_MEASUREMENT_IDGA4 measurement id (e.g. G-XXXXXXXXXX), from Google Analytics → Admin → Data streams. Optional — the <GoogleAnalytics> tag in app/layout.tsx only renders when this is set, so dev traffic never pollutes production analytics.
NEXT_PUBLIC_GOOGLE_MAPS_API_KEYGoogle Places autocomplete for the "location" field in the post composer (needs the Places API + Maps JavaScript API enabled on the key). Optional — without it the field falls back to manual text entry + geolocation.
NEXT_PUBLIC_GIPHY_API_KEYGIPHY API key — powers the GIF picker
NEXT_PUBLIC_PHONE_LOGIN_PROVIDERMirrors the backend's PHONE_LOGIN_PROVIDER. Only disabled has any effect here — it hides the "Continue with phone" button in Login.tsx / AddAccountModal.tsx. Leave blank/unset (or firebase) to keep it shown.
NEXT_PUBLIC_APPLE_CLIENT_IDSign in with Apple on the web — a "Services ID" registered in Apple Developer (tied to a verified domain + Return URLs), not the native iOS app's Bundle ID. Must match the backend's APPLE_WEB_CLIENT_ID. Leave blank to hide/disable the "Continue with Apple" button (shows a "not available" message instead of silently failing).
NEXT_PUBLIC_APPLE_REDIRECT_URIReturn URL registered for the Services ID above. Required by Apple's init() even in popup mode. Defaults to the current origin if left unset.

All NEXT_PUBLIC_* variables are exposed to the browser bundle — never put a secret in one of these.

iOS — apps/ios/app

iOS doesn't load a .env file automatically. apps/ios/app/.env.example is a reference only; the actual values must be entered directly into the Xcode scheme:

Xcode → Product → Scheme → Edit Scheme → Run → Arguments → Environment Variables

VariablePurpose
STRIPE_PUBLISHABLE_KEYRead by Secrets.swift — falls back to a hardcoded test key in DEBUG builds, but fatalErrors in Release if unset
GIPHY_API_KEYSame pattern — hardcoded dev fallback in DEBUG, required in Release
API_HOSTOptional — defaults to 127.0.0.1. Set to your Mac's LAN IP (ipconfig getifaddr en0) when testing on a physical device so it can reach your locally-running backend

Two more pieces of iOS config that aren't environment variables at all:

  • app/GoogleService-Info.plist — already checked into the repo, pointing at the project's Firebase iOS app. If you're standing up a separate Firebase project, download your own from the Firebase console and replace this file (see Obtaining API Keys).
  • app/app.entitlements — declares the com.apple.developer.applesignin capability (needed for Sign in with Apple) and an app group (group.com.closegram, shared with the NotificationService extension for rich push notifications). If you fork this under your own bundle ID, update the app group identifier here and in your Apple Developer account.

See the iOS technical page for the rest of the setup (opening the project, resolving Swift packages, running tests).