Closegram has three separate places that need environment configuration, each with its own file:
| App | File | Template |
|---|
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/ios | Not 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'))"
Local infrastructure via Docker (recommended)
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
| Service | Container | Default port | Default credentials |
|---|
| PostgreSQL | closegram-postgres | 5432 | user postgres / password closegram_postgres_pass / db closegram_dev |
| Redis | closegram-redis | 6382 (host) → 6379 (container) | password closegram_redis_pass |
| Redis (LiveKit's dedicated instance) | closegram-redis-livekit | 6381 | password livekit_redis_pass |
| LiveKit server | closegram-livekit | 7880 (HTTP), 7881 (TCP), 7882 (UDP) | API key devkey / secret dev1234567890abcdef1234567890abcdef (hardcoded in docker-compose.yml, dev-only) |
| Redis Commander (GUI, optional) | closegram-redis-commander | via --profile gui | admin / 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
| Variable | Example | Purpose |
|---|
NODE_ENV | development | Selects which .env.<NODE_ENV> file loads |
APP_NAME | Closegram | Display name used in emails/logs |
APP_VERSION / BACKEND_VERSION | 1.0.0 | Version tags for logging/Sentry release |
BACKEND_PORT / PORT | 8000 | Server port |
API_URL | http://localhost:8000 | Backend's own public URL |
FRONTEND_URL | http://localhost:3000 | Used for building links in emails, CORS |
TIMEZONE | America/Los_Angeles | Default server timezone |
Database
| Variable | Purpose |
|---|
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD / DB_DIALECT | Standard Sequelize/Postgres connection pieces |
DEV_DATABASE_URL / DATABASE_URL | Full connection string alternative (used by some tooling) |
Redis
| Variable | Purpose |
|---|
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD | Connection — powers presence, chat cache, OTP cache, BullMQ, pub/sub subscriptions |
DISABLE_REDIS | Set true to run without Redis (degrades the features above) |
REDIS_COMMANDER_USER / REDIS_COMMANDER_PASSWORD | Login for the optional Redis Commander GUI |
JWT
| Variable | Purpose |
|---|
JWT_SECRET | Signs regular user access tokens |
JWT_REFRESH_SECRET | Signs refresh tokens |
JWT_ADMIN_SECRET | Signs admin-panel session tokens |
JWT_TOKEN_EXPIRY | e.g. 8h |
JWT_TEMP_TOKEN_EXPIRY | e.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).
| Variable | Example | Purpose |
|---|
WEBAUTHN_RP_ID | closegram.com | The 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_ORIGIN | https://closegram.com,https://admin.closegram.com | The 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_NAME | Closegram | Display 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
| Variable | Purpose |
|---|
AWS_REGION | e.g. us-east-1 |
AWS_ACCESS_KEY / AWS_ACCESS_KEY_ID, AWS_SECRET_KEY / AWS_SECRET_ACCESS_KEY | IAM credentials (both naming variants are read in different places) |
AWS_BUCKET_NAME / AWS_S3_URL | S3 bucket for uploads |
AWS_API_VERSION / AWS_PROFILE / AWS_TOKEN_KEY | Misc AWS SDK config |
STORAGE_PROVIDER | Currently only aws-s3 is implemented |
DISABLE_S3 | true uses mock upload responses locally |
AWS_EMAIL / AWS_EMAIL_NAME | SES "from" address/name |
AWS_S3_EMAIL_TEMPLATES_PATH | S3 path for Pug email templates |
AWS_CONFIGURATION_NAME | SES configuration set (bounce/complaint tracking) |
AWS_SNS_SENDER_ID | SNS SMS sender ID |
AWS_PINPOINT_PROJECT_ID | Not referenced anywhere in the backend — vestigial |
Email
| Variable | Purpose |
|---|
EMAIL_PROVIDER | Only aws-ses has an implementation despite being nominally pluggable |
DISABLE_EMAIL | Skip sending emails locally |
VERIFIED_EMAIL_TIME_WAIT / VERIFIED_EMAIL_LIMIT_TIMES / VERIFIED_EMAIL_TIME_TRANSFORM | Email-verification-code throttling |
GMAIL_USER_NAME / GMAIL_USER_PASSWORD / GMAIL_SERVICE_HOST / GMAIL_SERVICE_PORT | Vestigial — no Gmail/nodemailer send path exists |
SMS / phone OTP
| Variable | Purpose |
|---|
SMS_PROVIDER | twilio (default), aws-sns, or messagebird |
SMS_ORIGINATOR_PHONE | Sender label |
DISABLE_SMS | Skip sending SMS locally |
VERIFIED_SMS_TIME_WAIT / VERIFIED_SMS_LIMIT_TIMES / VERIFIED_SMS_TIME_TRANSFORM | OTP throttling |
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_PHONE_NUMBER / TWILIO_VERIFY_SERVICE_SID | Twilio credentials |
MESSAGEBIRD_API_KEY / MESSAGEBIRD_ORIGINATOR | MessageBird credentials (alternative provider) |
PHONE_LOGIN_PROVIDER | Switch 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
| Variable | Purpose |
|---|
STRIPE_API_KEY_PROD_SECRET | Stripe secret key |
STRIPE_WEBHOOK_SECRET | Verifies incoming Stripe webhook signatures |
PAYMENT_PROVIDER | Currently stripe |
DISABLE_STRIPE | Skip payment processing locally |
PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET | PayPal app credentials |
PAYPAL_API_BASE | Defaults to the sandbox URL if unset |
Firebase (push notifications + Google/phone auth verification)
| Variable | Purpose |
|---|
FIREBASE_PROJECT_ID | Firebase project ID |
FIREBASE_PRIVATE_KEY_ID / FIREBASE_PRIVATE_KEY / FIREBASE_CLIENT_EMAIL / FIREBASE_CLIENT_ID | Service-account credentials (from the downloaded JSON key) |
FIREBASE_AUTH_URI / FIREBASE_TOKEN_URI / FIREBASE_AUTH_PROVIDER_CERT_URL / FIREBASE_CLIENT_CERT_URL / FIREBASE_UNIVERSE_DOMAIN | Standard fields from the service-account JSON — copy as-is |
FIREBASE_DATABASE_URL / FIREBASE_STORAGE_BUCKET | Project URLs |
DISABLE_PUSH_NOTIFICATIONS | Skip sending push locally |
Apple Sign-In
| Variable | Purpose |
|---|
APPLE_CLIENT_ID | The 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_ID | The 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_ID | Apple Developer Team ID |
APPLE_KEY_ID | ID of the Sign in with Apple private key |
APPLE_REDIRECT_URI | OAuth redirect URL (web flow) |
APPLE_PRIVATE_KEY | The .p8 private key contents |
Apple Push Notifications (APNs) — iOS calls/VoIP
| Variable | Purpose |
|---|
APNS_PRIVATE_KEY / APNS_KEY_ID / APNS_TEAM_ID | APNs auth key (separate key from Sign in with Apple) |
APNS_BUNDLE_ID | iOS app's bundle ID |
APNS_PRODUCTION | false for sandbox APNs, true for production |
Google Cloud / BigQuery (analytics)
| Variable | Purpose |
|---|
GOOGLE_CLOUD_PROJECT_ID / GCP_PROJECT_ID | GCP project |
BIGQUERY_DATASET_ID / BIGQUERY_LOCATION | Dataset config |
BIGQUERY_PRIVATE_KEY_ID / BIGQUERY_PRIVATE_KEY / BIGQUERY_CLIENT_EMAIL / BIGQUERY_CLIENT_ID | Service-account credentials |
ANALYTICS_DB_TYPE | postgres (default, no GCP needed) or bigquery |
ANALYTICS_DB_HOST / PORT / NAME / USER / PASSWORD | Only used when ANALYTICS_DB_TYPE=postgres and you want a separate analytics DB |
ANALYTICS_ENABLED | Master 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:
| Variable | Purpose |
|---|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Passport passport-google-token strategy |
CLIENT_ID / CLIENT_ID_SECRET / REDIRECT_URIs | A separate googleapis OAuth2Client used elsewhere in the auth service |
FACEBOOK_APP_ID / FACEBOOK_APP_SECRET | Only checked for truthiness to decide whether to initialize the Facebook passport strategy at all |
FACEBOOK_CLIENT_ID / FACEBOOK_CLIENT_SECRET | The 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)
| Variable | Purpose |
|---|
IMAGE_ANALYSIS_PROVIDER | google-vision (the only implemented provider) |
IMAGE_ANALYSIS_MIN_LABEL_CONFIDENCE | 0–1 confidence threshold |
IMAGE_ANALYSIS_MAX_LABELS_PER_POST | Cap on labels processed per upload |
DISABLE_IMAGE_ANALYSIS | Skip analysis locally |
DEEPAI_API_KEY | Vestigial — not referenced anywhere; the active provider is Google Vision, which authenticates via the same Google Cloud service-account credentials above |
LiveKit (calls + live streams)
| Variable | Purpose |
|---|
LIVEKIT_URL / LIVEKIT_WS_URL | WebSocket URL of the LiveKit server |
LIVEKIT_API_KEY / LIVEKIT_API_SECRET | Server credentials |
LIVEKIT_PORT / LIVEKIT_RTC_TCP / LIVEKIT_RTC_UDP | Only relevant if you're running the self-hosted Docker LiveKit server |
DISABLE_LIVEKIT | Skip 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)
| Variable | Purpose |
|---|
DATADOG_API_KEY | Enables the Datadog log transport |
SENTRY_DSN / SENTRY_ENVIRONMENT / SENTRY_RELEASE | Error tracking |
NEW_RELIC_LICENSE_KEY / NEW_RELIC_APP_NAME / NEW_RELIC_ENABLED | APM — index.js conditionally require('newrelic') only if the license key is set |
LOGROCKET_APP_ID | Session replay |
CUSTOM_LOGGER_URL / CUSTOM_LOGGER_API_KEY | Ships 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
| Variable | Purpose |
|---|
NEXT_PUBLIC_GRAPHQL_URL | Backend GraphQL endpoint, e.g. http://localhost:4000/graphql (note: default backend port is 8000 — adjust to match your BACKEND_PORT) |
NEXT_PUBLIC_WS_URL | WebSocket endpoint for GraphQL subscriptions, e.g. ws://localhost:8000/web/graphql |
NEXT_PUBLIC_SITE_URL | Public 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_ID | Firebase web app config (from the Firebase console, distinct from the backend's service-account credentials) |
NEXT_PUBLIC_FIREBASE_VAPID_KEY | Firebase Web Push (VAPID) key — enables web push notifications. Optional. |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Stripe's public key for client-side Elements |
NEXT_PUBLIC_PAYPAL_CLIENT_ID | PayPal app's public client id — for the "buy coins" PayPal buttons |
NEXT_PUBLIC_ADSENSE_CLIENT | Google 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_CODE | Google 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_ID | GA4 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_KEY | Google 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_KEY | GIPHY API key — powers the GIF picker |
NEXT_PUBLIC_PHONE_LOGIN_PROVIDER | Mirrors 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_ID | Sign 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_URI | Return 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
| Variable | Purpose |
|---|
STRIPE_PUBLISHABLE_KEY | Read by Secrets.swift — falls back to a hardcoded test key in DEBUG builds, but fatalErrors in Release if unset |
GIPHY_API_KEY | Same pattern — hardcoded dev fallback in DEBUG, required in Release |
API_HOST | Optional — 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).