Files
Ruslan KonviserandClaude Opus 5 3a62724c23 fix(security): Social-login audience verification + purpose-bound tokens (GHSA-58x4, GHSA-28wv) (#10241)
* fix(security): verify social-login token audience and bind every purpose token

GHSA-58x4-7mw9-gmqg (critical): POST /auth/signin.email.social accepted any
provider access token that resolved to a victim's email — another app's token or
a GitHub PAT — and signed the caller in as that user. Each provider is now
introspected against an allow-list of OAuth client ids (Google tokeninfo aud/azp
plus email_verified, GitHub /applications/{client_id}/token, Facebook
debug_token app_id) and fails closed when no client is configured. Twitter/X is
refused, since it exposes no verified email. One normaliser rejects an empty id
or email, so an undefined value can no longer reach a find() and be dropped by
TypeORM's undefined:'ignore' behaviour, which returned every user in every
tenant.

GHSA-28wv-vrxj-rp4q (medium): tokens signed with JWT_SECRET were interchangeable.
New signPurposeToken/verifyPurposeToken pin a purpose claim, required non-empty
claims and HS256. Workspace sign-in, invoice share, estimate, invite, team-join,
appointment and password-reset tokens are typed; public invoice and estimate
links are bound to the stored row and the URL id; access-token consumers
(JwtStrategy, RegisterAuthorizationGuard, Zapier, Plane) reject a token whose
purpose says it is something else. Untyped legacy tokens are accepted only where
they are also bound to a stored row, and never on signin.workspace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(cspell): add the new vocabulary and use US spellings

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(auth): review follow-ups on the purpose-token hardening

Addresses the bot review round on PR #10241. No security behaviour is relaxed;
every change either tightens a check or is a readability fix with the same
runtime semantics, and the affected suites were re-run (9 suites / 155 tests).

- Password reset now goes through `verifyPurposeToken(TokenPurposeEnum.PASSWORD_RESET)`
  instead of a bare `verify()` plus a string-literal purpose comparison
  (CodeRabbit). It additionally requires a non-empty `id` claim: an `undefined`
  id reaching `findOneByIdString` widens the lookup instead of failing closed,
  which is exactly the class of bug this PR exists to remove. The stored
  password_reset row still binds the token, so this is defence in depth.
  `verify` and `JWT_ALGORITHMS` are no longer imported there.
- `JwtStrategy.validate` moves the employeeId/organizationId claim checks into
  `attachEmployeeAndOrganizationContext` (SonarCloud: cognitive complexity 16 >
  15). The helper RETURNS the UnauthorizedException instead of throwing, so the
  exact exceptions and messages the callback received before are unchanged, and
  three specs now cover the organization branch (member missing, employee in
  another organization, happy path). Control: inverting the rejection makes 12
  of the 30 tests in that suite fail.
- `normalizeSocialIdentity` extracts its nested ternary into
  `normalizeProviderAccountId` (SonarCloud), same accepted values as before:
  trimmed string, or a positive safe integer stringified for GitHub.
- `Number.NaN` over `NaN` in the reschedule-token lifetime (SonarCloud), and the
  two unused `catch (error)` bindings in PublicInvoiceService are now bare
  `catch`.

Not changed, deliberately: Greptile's P1 "mixed-case emails fail lookup". The
social lookup already queries BOTH the normalised (lowercased) address and the
provider's exact spelling, which is a strict superset of what this code did
before the PR, so nothing regressed. Matching stored emails case-insensitively
would let the holder of `a@x.com` sign into an account stored as `A@x.com` —
a widening of an authentication lookup that password login does not perform —
and belongs in a repo-wide email-normalisation change, not in this advisory fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:20:57 +02:00

947 lines
36 KiB
Bash

# The name of the application.
APP_NAME="Gauzy"
# The URL for the application logo.
APP_LOGO="http://localhost:4200/assets/images/logos/logo_Gauzy.png"
# The signature or tagline for the application.
APP_SIGNATURE="Gauzy"
# The link to the application.
APP_LINK="http://localhost:4200"
# The URL for an email confirmation in the application.
APP_EMAIL_CONFIRMATION_URL="http://localhost:4200/#/auth/confirm-email"
# The URL for magic sign-in in the application.
APP_MAGIC_SIGN_URL="http://localhost:4200/#/auth/magic-sign-in"
# Set true if running inside the Docker container
IS_DOCKER=false
# Deployed release version (git tag, e.g. v111.2.10) and full commit SHA, shown in the
# web UI footer and returned by GET /api/version. Normally injected automatically at
# Docker build time (build-args, see .deploy/*/Dockerfile); leave empty for source runs.
GAUZY_APP_VERSION=
GAUZY_APP_COMMIT=
# Format: https://hook.{region}.make.com/{webhook-id}
GAUZY_MAKE_WEBHOOK_URL=
# Make.com Platforms integration
GAUZY_MAKE_API_URL="https://hook.us2.make.com/api/v2"
GAUZY_MAKE_BASE_URL="https://www.make.com"
GAUZY_MAKE_CLIENT_ID=
GAUZY_MAKE_CLIENT_SECRET=
GAUZY_MAKE_REDIRECT_URL="${API_BASE_URL}/api/integration/make-com/oauth/callback"
GAUZY_MAKE_POST_INSTALL_URL="${CLIENT_BASE_URL}/#/pages/integrations/make"
GAUZY_MAKE_DEFAULT_SCOPES="offline_access"
# ActivePieces platforms integration
ACTIVEPIECES_BASE_URL="https://cloud.activepieces.com"
GAUZY_ACTIVEPIECES_API_KEY=
#SIM platform integration
SIM_DEFAULT_BASE_URL="https://www.sim.ai"
GAUZY_SIM_API_KEY=
# Set true if running as a Demo
DEMO=false
# DO (DIGITALOCEAN), AWS, AZURE, CIVO, CW (COREWEAVE), HEROKU, LINODE, LOCAL, OVH, SCALEWAY, VULTR, etc
CLOUD_PROVIDER=
ALLOW_SUPER_ADMIN_ROLE=true
# Set to Gauzy API base URL
API_BASE_URL=http://localhost:3000
# Set to Gauzy UI base URL
CLIENT_BASE_URL=http://localhost:4200
# Set to Website Platform
PLATFORM_WEBSITE_URL=https://gauzy.co
PLATFORM_WEBSITE_DOWNLOAD_URL=https://gauzy.co/downloads
# DB_ORM: typeorm | mikro-orm
DB_ORM=typeorm
# DB_TYPE: sqlite | postgres | better-sqlite3 | mysql
DB_TYPE=better-sqlite3
# DB Connection Parameters
# DB_HOST=localhost
## DB Port. The default for PostgreSQL - 5432, for MySQL - 3306
# DB_PORT=5432
# DB_NAME=gauzy
## DB Username. The default for PostgreSQL is 'postgres', for MySQL it's 'root'
# DB_USER=postgres
# DB_PASS=root
# DB_LOGGING=all
# DB_POOL_SIZE=40
# DB_POOL_SIZE_KNEX=10
# DB_CONNECTION_TIMEOUT=5000
# DB_IDLE_TIMEOUT=10000
# DB_SLOW_QUERY_LOGGING_TIMEOUT=10000
# DB_SSL_MODE=false
## If you want to use SSL and set DB_SSL_MODE=true, set the following environment variable
## with base64 encoded SSL certificate for DB
# DB_CA_CERT=
# Configuration for Worker Queue and Scheduler
WORKER_QUEUE_ENABLED=false
WORKER_SCHEDULER_ENABLED=false
WORKER_DEFAULT_QUEUE=gauzy_worker_default_queue
WORKER_TIMEZONE=UTC
# Redis Connection Parameters
REDIS_ENABLED=false
REDIS_HOST=
REDIS_PASSWORD=
REDIS_PORT=
REDIS_USER=
REDIS_TLS=false
# redis[s]://[[username][:password]@][host][:port][/db-number]
REDIS_URL=redis://localhost:6379
# ============================================================================
# SECURITY: Authentication & session secrets
# Set each of these to a strong, UNIQUE, random value before deploying, e.g.:
# openssl rand -hex 64
# The API refuses to start in production (NODE_ENV=production and DEMO != true)
# while any of these is empty or left at a well-known default value.
# Outside production (and outside DEMO=true) an empty value makes the API use a
# random secret generated for that process only: sign-ins, emailed links and
# sessions then stop working on every restart, and every OTHER process that must
# accept the same tokens (API replicas, `yarn seed`, which signs seeded invite and
# estimate links) cannot verify them. Set explicit values shared by all of them.
# ============================================================================
EXPRESS_SESSION_SECRET=
# JWT Configuration
JWT_SECRET=
JWT_TOKEN_EXPIRATION_TIME=86400
# JWT Refresh Token Configuration
JWT_REFRESH_TOKEN_SECRET=
JWT_REFRESH_TOKEN_EXPIRATION_TIME=86400
# Email Verification Config
JWT_VERIFICATION_TOKEN_SECRET=
JWT_VERIFICATION_TOKEN_EXPIRATION_TIME=86400
# Password Less Authentication Configuration
MAGIC_CODE_EXPIRATION_TIME=600
# Join Request Organization Team Configuration
TEAM_JOIN_REQUEST_EXPIRATION_TIME=86400
# Rate Limiting
THROTTLE_ENABLED=true
THROTTLE_TTL=60000 # 1 minute
THROTTLE_LIMIT=60000
# ============================================================================
# SECURITY: Seeded account credentials
# The FIRST boot against an empty database creates three accounts from these
# variables. Their shipped defaults (admin@ever.co / admin, local.admin@ever.co
# / admin, employee@ever.co / 12345678) are published in this repository, so a
# deployment that leaves the passwords unset is exposed to publicly known
# passwords. The API therefore REFUSES TO SEED in production (NODE_ENV=production
# or a production build, DEMO != true, non-Electron) while any of the three
# *_PASSWORD variables is unset or left at its default; the *_EMAIL variables
# are optional. The `yarn seed:ever` / `yarn seed:all` seeds are refused there
# outright, because they also create fixture accounts with a hard-coded password.
# The check runs only when a seed runs: an existing database (users present) is
# never seeded again, so it is never refused. Demo deployments (DEMO=true) and
# the desktop Gauzy Server are exempt.
# ============================================================================
DEMO_SUPER_ADMIN_EMAIL=
DEMO_SUPER_ADMIN_PASSWORD=
DEMO_ADMIN_EMAIL=
DEMO_ADMIN_PASSWORD=
DEMO_EMPLOYEE_EMAIL=
DEMO_EMPLOYEE_PASSWORD=
# Emergency escape hatch; seeds the well-known defaults anyway. Strongly discouraged.
# ALLOW_INSECURE_SEED_CREDENTIALS=false
# ============================================================================
# SECURITY: Proxy trust & brute-force controls
# TRUST_PROXY is the Express `trust proxy` setting and decides what `req.ip`
# resolves to - which is what the rate limiter counts against. Use the NUMBER OF
# PROXY HOPS in front of the API (1 for a single nginx/ingress, 2 for
# Cloudflare Tunnel -> ingress -> api), or a comma-separated list of trusted proxy
# CIDRs. `true` trusts the whole X-Forwarded-For chain and lets any client pick
# its own rate-limit bucket - do not use it. A hop count trusts whoever connects
# to the API socket to write one X-Forwarded-For entry, so if the API port is
# reachable WITHOUT going through your proxy (e.g. docker-compose publishing
# 3000), use your proxy CIDRs instead.
TRUST_PROXY=1
# Set to true when every request reaches the API through Cloudflare (including
# a Cloudflare Tunnel) and nothing else can reach the origin; the
# CF-Connecting-IP header is client-writable otherwise. Behind Cloudflare with
# this left false, every client shares the proxy's address and therefore ONE
# rate-limit bucket.
THROTTLE_TRUST_CF_CONNECTING_IP=false
# Per-ACCOUNT brute-force control, independent of the client IP. Once this many
# consecutive failed sign-ins for one account have come from at least TWO
# different client addresses, that account is blocked for AUTH_LOCKOUT_SECONDS
# (HTTP 429 with Retry-After). Failures from a single address never block the
# account - the per-address throttle covers that - so one client cannot lock a
# known email out. At most this many checks per account may run concurrently.
# Trade-off: an attacker with several addresses can still block a known account
# for AUTH_LOCKOUT_SECONDS at a time. Set AUTH_MAX_FAILED_ATTEMPTS=0 to switch
# the per-account control off and rely on the per-address throttle alone.
AUTH_MAX_FAILED_ATTEMPTS=10
AUTH_LOCKOUT_SECONDS=900
# CORS Allowed Origins (comma-separated list of trusted origins)
# If not set, all origins (*) are allowed. In production, set this to your trusted domains.
# ALLOWED_ORIGINS=https://app.gauzy.co,https://gauzy.co
# Twitter OAuth Configuration
TWITTER_CLIENT_ID=XXXXXXX
TWITTER_CLIENT_SECRET=XXXXXXX
TWITTER_CALLBACK_URL=http://localhost:3000/api/auth/twitter/callback
# Google OAuth Configuration
GOOGLE_CLIENT_ID=XXXXXXX
GOOGLE_CLIENT_SECRET=XXXXXXX
GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback
# Facebook OAuth Configuration
FACEBOOK_CLIENT_ID=XXXXXXX
FACEBOOK_CLIENT_SECRET=XXXXXXX
FACEBOOK_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback
FACEBOOK_GRAPH_VERSION=v3.0
# Github OAuth App Integration
GAUZY_GITHUB_OAUTH_CLIENT_ID=XXXXXXX
GAUZY_GITHUB_OAUTH_CLIENT_SECRET=XXXXXXX
GAUZY_GITHUB_OAUTH_CALLBACK_URL="http://localhost:3000/api/auth/github/callback"
# Social sign-in by provider access token (POST /api/auth/signin.email.social, /api/auth/signup.link.account).
# Tokens are only accepted when issued to an allowed OAuth client. Gauzy's own apps above (GOOGLE_CLIENT_ID,
# GAUZY_GITHUB_OAUTH_CLIENT_ID/SECRET, FACEBOOK_CLIENT_ID/SECRET) are always allowed; list OTHER first-party
# clients (e.g. Ever Teams) here. Google: comma-separated client ids. GitHub/Facebook: comma-separated
# clientId:clientSecret pairs (the secret is needed to introspect the token). A provider with no client rejects all tokens.
GAUZY_SOCIAL_AUTH_GOOGLE_CLIENT_IDS=
GAUZY_SOCIAL_AUTH_GITHUB_APPS=
GAUZY_SOCIAL_AUTH_FACEBOOK_APPS=
# LinkedIn OAuth Configuration
LINKEDIN_CLIENT_ID=XXXXXXX
LINKEDIN_CLIENT_SECRET=XXXXXXX
LINKEDIN_CALLBACK_URL=http://localhost:3000/api/auth/linkedin/callback
# Microsoft OAuth Configuration
MICROSOFT_GRAPH_API_URL=https://graph.microsoft.com/v1.0
MICROSOFT_AUTHORIZATION_URL=https://login.microsoftonline.com/common/oauth2/v2.0/authorize
MICROSOFT_TOKEN_URL=https://login.microsoftonline.com/common/oauth2/v2.0/token
MICROSOFT_CLIENT_ID=XXXXXXX
MICROSOFT_CLIENT_SECRET=XXXXXXX
MICROSOFT_CALLBACK_URL=http://localhost:3000/api/auth/microsoft/callback
# Github Apps Integration
GAUZY_GITHUB_CLIENT_ID=XXXXXXX
GAUZY_GITHUB_CLIENT_SECRET=XXXXXXX
# Github App Install Integration
GAUZY_GITHUB_APP_NAME=
GAUZY_GITHUB_APP_ID=XXXXXXX
GAUZY_GITHUB_APP_PRIVATE_KEY=
# Github Webhook Configuration
# GAUZY_GITHUB_WEBHOOK_SECRET is REQUIRED whenever the GitHub App integration is enabled: the
# receiver at POST /api/integration/github/webhook verifies GitHub's `x-hub-signature-256` HMAC and
# rejects every delivery (403) when the secret is unset or does not match. Set it to the exact
# "Webhook secret" configured on the GitHub App (Settings -> Developer settings -> GitHub Apps).
GAUZY_GITHUB_WEBHOOK_URL=http://localhost:3000/api/auth/github/webhook
GAUZY_GITHUB_WEBHOOK_SECRET=XXXXXXX
# Github Redirect URL
GAUZY_GITHUB_REDIRECT_URL=http://localhost:3000/api/integration/github/callback
GAUZY_GITHUB_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/github/setup/installation"
GAUZY_GITHUB_API_VERSION="2022-11-28"
# Zapier Apps Integration
GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
# Comma-separated list of domains allowed for OAuth redirects (security feature)
GAUZY_ZAPIER_ALLOWED_DOMAINS=gauzy.co,*.gauzy.co,ever.co,*.ever.co,zapier.com,*.zapier.com,localhost
# Maximum number of OAuth authorization codes to store in memory
GAUZY_ZAPIER_MAX_AUTH_CODES=1000
# Number of server instances (affects auth code cleanup behavior)
GAUZY_ZAPIER_INSTANCE_COUNT=1
FIVERR_CLIENT_ID=XXXXXXX
FIVERR_CLIENT_SECRET=XXXXXXX
AUTH0_CLIENT_ID=XXXXXXX
AUTH0_CLIENT_SECRET=XXXXXXX
AUTH0_DOMAIN=XXXXXXX
# Keycloak OAuth
KEYCLOAK_CLIENT_ID=XXXXXXX
KEYCLOAK_CLIENT_SECRET=XXXXXXX
KEYCLOAK_REALM=
KEYCLOAK_COOKIE_KEY=XXXXXXX
KEYCLOAK_AUTH_SERVER_URL=https://keycloak.example.com/auth
KEYCLOAK_CALLBACK_URL=http://localhost:3000/api/auth/keycloak/callback
INTEGRATED_HUBSTAFF_USER_PASS=hubstaffPassword
# Upwork Integration Config
UPWORK_API_KEY=XXXXXXX
UPWORK_API_SECRET=XXXXXXX
UPWORK_REDIRECT_URL="http://localhost:3000/api/integrations/upwork/callback"
UPWORK_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/upwork"
# Hubstaff Integration Configuration
HUBSTAFF_CLIENT_ID=XXXXXXX
HUBSTAFF_CLIENT_SECRET=XXXXXXX
HUBSTAFF_REDIRECT_URL="http://localhost:3000/api/integration/hubstaff/callback"
HUBSTAFF_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/hubstaff"
# File System: LOCAL | S3 | WASABI | CLOUDINARY
FILE_PROVIDER=LOCAL
# AWS Config
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=us-east-1
AWS_S3_BUCKET=gauzy
# WASABI Config (optional)
WASABI_ACCESS_KEY_ID=
WASABI_SECRET_ACCESS_KEY=
WASABI_REGION=us-east-1
WASABI_SERVICE_URL=https://s3.wasabisys.com
WASABI_S3_BUCKET=gauzy
WASABI_S3_FORCE_PATH_STYLE=true
# DIGITALOCEAN Spaces Config (optional)
DIGITALOCEAN_ACCESS_KEY_ID=
DIGITALOCEAN_SECRET_ACCESS_KEY=
DIGITALOCEAN_REGION=us-east-1
DIGITALOCEAN_SERVICE_URL=
DIGITALOCEAN_CDN_URL=
DIGITALOCEAN_S3_BUCKET=gauzy
DIGITALOCEAN_S3_FORCE_PATH_STYLE=false
# Cloudinary Config (optional)
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
CLOUDINARY_API_SECURE=true
CLOUDINARY_CDN_URL=https://res.cloudinary.com
# Gauzy AI Endpoints (optional, do not set unless you subscribed to Gauzy AI)
GAUZY_AI_GRAPHQL_ENDPOINT=http://localhost:3005/graphql
GAUZY_AI_REST_ENDPOINT=http://localhost:3005/api
# Gauzy AI Key/Secret pair authentication
GAUZY_AI_API_KEY=
GAUZY_AI_API_SECRET=
# Gauzy Cloud
GAUZY_CLOUD_ENDPOINT=https://api.gauzy.co
GAUZY_CLOUD_APP=https://app.gauzy.co
# SMTP Mail Config
MAIL_FROM_ADDRESS=gauzy@ever.co
MAIL_HOST=smtp.gmail.com
MAIL_PORT=465
MAIL_USERNAME=
MAIL_PASSWORD=
# Sentry Client Key
SENTRY_DSN=
SENTRY_HTTP_TRACING_ENABLED=
SENTRY_POSTGRES_TRACKING_ENABLED=
SENTRY_PROFILING_ENABLED=
SENTRY_TRACES_SAMPLE_RATE=
SENTRY_PROFILE_SAMPLE_RATE=
# PostHog Configuration
POSTHOG_KEY=
POSTHOG_HOST=
POSTHOG_ENABLED=
POSTHOG_FLUSH_INTERVAL=
# Default Currency
DEFAULT_CURRENCY=USD
# Default Country
DEFAULT_COUNTRY=US
# Google Maps API Key
GOOGLE_MAPS_API_KEY=
# Chatwoot SDK Token
CHATWOOT_SDK_TOKEN=
# Restrict Access to Google Place Autocomplete
GOOGLE_PLACE_AUTOCOMPLETE=false
# Nebular CHAT API key for a map message type (which is required by Google Maps)
CHAT_MESSAGE_GOOGLE_MAP=
# Default Latitude and Longitude
DEFAULT_LATITUDE=
DEFAULT_LONGITUDE=
# Keymetrics settings (optional)
WEB_CONCURRENCY=1
WEB_MEMORY=4096
# Unleash Configuration for Features management (optional)
UNLEASH_APP_NAME=Gauzy
UNLEASH_API_URL=
UNLEASH_INSTANCE_ID=
UNLEASH_REFRESH_INTERVAL=15000
UNLEASH_METRICS_INTERVAL=60000
UNLEASH_API_KEY=
# Defines feature flags and settings related to user authentication methods.
FEATURE_EMAIL_PASSWORD_LOGIN=true
FEATURE_MAGIC_LOGIN=true
FEATURE_GITHUB_LOGIN=true
FEATURE_FACEBOOK_LOGIN=true
FEATURE_GOOGLE_LOGIN=true
FEATURE_TWITTER_LOGIN=true
FEATURE_MICROSOFT_LOGIN=true
FEATURE_LINKEDIN_LOGIN=true
# Features Toggles
FEATURE_DASHBOARD=true
FEATURE_TIME_TRACKING=true
FEATURE_ESTIMATE=true
FEATURE_ESTIMATE_RECEIVED=true
FEATURE_INVOICE=true
FEATURE_INVOICE_RECURRING=true
FEATURE_INVOICE_RECEIVED=true
FEATURE_INCOME=true
FEATURE_EXPENSE=true
FEATURE_PAYMENT=true
FEATURE_PROPOSAL=true
FEATURE_PROPOSAL_TEMPLATE=true
FEATURE_PIPELINE=true
FEATURE_PIPELINE_DEAL=true
FEATURE_DASHBOARD_TASK=true
FEATURE_TEAM_TASK=true
FEATURE_MY_TASK=true
FEATURE_JOB=true
FEATURE_EMPLOYEES=true
FEATURE_EMPLOYEE_TIME_ACTIVITY=true
FEATURE_EMPLOYEE_TIMESHEETS=true
FEATURE_EMPLOYEE_APPOINTMENT=true
FEATURE_EMPLOYEE_APPROVAL=true
FEATURE_EMPLOYEE_APPROVAL_POLICY=true
FEATURE_EMPLOYEE_LEVEL=true
FEATURE_EMPLOYEE_POSITION=true
FEATURE_EMPLOYEE_TIMEOFF=true
FEATURE_EMPLOYEE_RECURRING_EXPENSE=true
FEATURE_EMPLOYEE_CANDIDATE=true
FEATURE_MANAGE_INTERVIEW=true
FEATURE_MANAGE_INVITE=true
FEATURE_ORGANIZATION=true
FEATURE_ORGANIZATION_EQUIPMENT=true
FEATURE_ORGANIZATION_INVENTORY=true
FEATURE_ORGANIZATION_TAG=true
FEATURE_ORGANIZATION_VENDOR=true
FEATURE_ORGANIZATION_PROJECT=true
FEATURE_ORGANIZATION_DEPARTMENT=true
FEATURE_ORGANIZATION_TEAM=true
FEATURE_ORGANIZATION_DOCUMENT=true
FEATURE_ORGANIZATION_EMPLOYMENT_TYPE=true
FEATURE_ORGANIZATION_RECURRING_EXPENSE=true
FEATURE_ORGANIZATION_HELP_CENTER=true
FEATURE_CONTACT=true
FEATURE_GOAL=true
FEATURE_GOAL_REPORT=true
FEATURE_GOAL_SETTING=true
FEATURE_REPORT=true
FEATURE_USER=true
FEATURE_ORGANIZATIONS=true
FEATURE_APP_INTEGRATION=true
FEATURE_SETTING=true
FEATURE_EMAIL_HISTORY=true
FEATURE_EMAIL_TEMPLATE=true
FEATURE_IMPORT_EXPORT=true
FEATURE_FILE_STORAGE=true
FEATURE_PAYMENT_GATEWAY=true
FEATURE_SMS_GATEWAY=true
FEATURE_SMTP=true
FEATURE_ROLES_PERMISSION=true
# Email Verification
FEATURE_EMAIL_VERIFICATION=false
# Set the environment variable to enable/disable the global stats endpoint
FEATURE_OPEN_STATS=false
# Mac Code Sign
# Required for signing the macOS app with your Developer ID certificate
#
# CSC_LINK: Base64 encoded .p12 certificate file
# Generate with: base64 -i /path/to/certificate.p12 | tr -d '\n'
# The certificate must include both the Developer ID Application certificate
# and its private key (exported from Keychain Access as .p12)
CSC_LINK=
# CSC_KEY_PASSWORD: Password for the .p12 file
# Set to empty string if no password was used when exporting the .p12
CSC_KEY_PASSWORD=
# Notarize MacOS
# Required for notarization to avoid Gatekeeper warnings on macOS 10.15+
#
# APPLE_API_KEY: Base64 encoded .p8 key file from App Store Connect
APPLE_API_KEY=
# APPLE_API_KEY_ID: The Key ID from App Store Connect (e.g., AB12CD34EF)
APPLE_API_KEY_ID=
# APPLE_API_ISSUER: The Issuer ID from App Store Connect
APPLE_API_ISSUER=
# GitHub App Integration
GITHUB_INTEGRATION_APP_ID=
GITHUB_INTEGRATION_CLIENT_ID=
GITHUB_INTEGRATION_CLIENT_SECRET=
GITHUB_INTEGRATION_PRIVATE_KEY=
# NOTE: nothing reads GITHUB_INTEGRATION_WEBHOOK_SECRET. The GitHub App webhook receiver reads
# GAUZY_GITHUB_WEBHOOK_SECRET (above) — setting this one instead leaves the receiver unconfigured,
# which now rejects every delivery.
GITHUB_INTEGRATION_WEBHOOK_SECRET=
# HubStaff Integration
HUBSTAFF_CLIENT_ID=
HUBSTAFF_CLIENT_SECRET=
HUBSTAFF_PERSONAL_ACCESS_TOKEN=
# Jitsu Browser Configuration
JITSU_BROWSER_URL=
JITSU_BROWSER_WRITE_KEY=
# Jitsu Server Configuration
JITSU_SERVER_URL=
JITSU_SERVER_WRITE_KEY=
JITSU_SERVER_DEBUG=
JITSU_SERVER_ECHO_EVENTS=
# Tracing Configuration
OTEL_ENABLED=false
OTEL_PROVIDER=zipkin
OTEL_SERVICE_NAME=
OTEL_EXPORTER_OTLP_PROTOCOL=
OTEL_EXPORTER_OTLP_HEADERS=
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=
OTEL_EXPORTER_OTLP_ENDPOINT=
ASPECTO_API_KEY=
HONEYCOMB_API_KEY=
HONEYCOMB_ENABLE_LOCAL_VISUALIZATIONS=
# Platform Logo resource URL (SVG is Recommended)
PLATFORM_LOGO='assets/images/logos/logo_Gauzy.svg'
# Desktop App 512x512 icon
GAUZY_DESKTOP_LOGO_512X512='assets/icons/icon_512x512.png'
# Platform Privacy URL
PLATFORM_PRIVACY_URL='https://gauzy.co/privacy'
# Platform terms of Services URL
PLATFORM_TOS_URL='https://gauzy.co/tos'
# Platform no internet logo
NO_INTERNET_LOGO='assets/images/logos/logo_Gauzy.svg'
# Company Information
COMPANY_NAME='Ever Co. LTD'
COMPANY_LINK='https://ever.co'
COMPANY_SITE_NAME='Gauzy'
COMPANY_SITE_LINK='https://gauzy.co'
COMPANY_GITHUB_LINK='https://github.com/ever-co'
COMPANY_GITLAB_LINK='https://gitlab.com/ever-co'
COMPANY_FACEBOOK_LINK='https://www.facebook.com/gauzyplatform'
COMPANY_TWITTER_LINK='https://twitter.com/gauzyplatform'
COMPANY_IN_LINK='https://www.linkedin.com/company/everhq'
# Desktop download links
DESKTOP_APP_DOWNLOAD_LINK_APPLE='https://gauzy.co/downloads#desktop/apple'
DESKTOP_APP_DOWNLOAD_LINK_WINDOWS='https://gauzy.co/downloads#desktop/windows'
DESKTOP_APP_DOWNLOAD_LINK_LINUX='https://gauzy.co/downloads#desktop/linux'
MOBILE_APP_DOWNLOAD_LINK='https://gauzy.co/downloads#mobile'
EXTENSION_DOWNLOAD_LINK='https://gauzy.co/downloads#extensions'
# Desktop Timer Application Configuration
PROJECT_REPO='https://github.com/ever-co/ever-gauzy.git'
DESKTOP_TIMER_APP_NAME='gauzy-desktop-timer'
DESKTOP_TIMER_APP_DESCRIPTION='Gauzy Desktop Timer'
DESKTOP_TIMER_APP_ID='com.ever.gauzydesktoptimer'
DESKTOP_TIMER_APP_REPO_NAME='ever-gauzy-desktop-timer'
DESKTOP_TIMER_APP_REPO_OWNER='ever-co'
DESKTOP_TIMER_APP_WELCOME_TITLE=
DESKTOP_TIMER_APP_WELCOME_CONTENT=
DESKTOP_TIMER_APP_PROTOCOL='gauzy-timer'
# Desktop Application Configuration
DESKTOP_APP_NAME='gauzy-desktop'
DESKTOP_APP_DESCRIPTION='Gauzy Desktop'
DESKTOP_APP_ID='com.ever.gauzydesktop'
DESKTOP_APP_REPO_NAME='ever-gauzy-desktop'
DESKTOP_APP_REPO_OWNER='ever-co'
DESKTOP_APP_WELCOME_TITLE=
DESKTOP_APP_WELCOME_CONTENT=
DESKTOP_APP_PROTOCOL='gauzy-desktop'
# Desktop Server Application Configuration
DESKTOP_SERVER_APP_NAME='gauzy-server'
DESKTOP_SERVER_APP_DESCRIPTION='Gauzy Server'
DESKTOP_SERVER_APP_ID='com.ever.gauzyserver'
DESKTOP_SERVER_APP_REPO_NAME='ever-gauzy-server'
DESKTOP_SERVER_APP_REPO_OWNER='ever-co'
DESKTOP_SERVER_APP_WELCOME_TITLE=
DESKTOP_SERVER_APP_WELCOME_CONTENT=
DESKTOP_SERVER_APP_PROTOCOL='gauzy-server'
# Desktop API Server Application Configuration
DESKTOP_API_SERVER_APP_NAME='gauzy-api-server'
DESKTOP_API_SERVER_APP_DESCRIPTION='Gauzy API Server'
DESKTOP_API_SERVER_APP_ID='com.ever.gauzyapiserver'
DESKTOP_API_SERVER_APP_REPO_NAME='ever-gauzy-api-server'
DESKTOP_API_SERVER_APP_REPO_OWNER='ever-co'
DESKTOP_API_SERVER_APP_WELCOME_TITLE=
DESKTOP_API_SERVER_APP_WELCOME_CONTENT=
DESKTOP_API_SERVER_APP_PROTOCOL='gauzy-api-server'
#AGENT
AGENT_APP_PROTOCOL='gauzy-agent'
REGISTER_URL='https://app.gauzy.co/#/auth/register'
FORGOT_PASSWORD_URL='https://app.gauzy.co/#/auth/request-password'
# I18N Translation Files URL
I18N_FILES_URL=
# MCP Server Configuration
API_TIMEOUT=30000
GAUZY_AUTH_EMAIL=your-email@example.com
GAUZY_AUTH_PASSWORD=your-secure-password
GAUZY_AUTO_LOGIN=false
GAUZY_MCP_DEBUG=false
MCP_APP_ID=co.gauzy.mcp-server
MCP_APP_NAME="Gauzy MCP Server"
NODE_ENV=development
# MCP Transport Configuration
# Options: stdio | http | websocket
MCP_SERVER_MODE="stdio" # Internal server runtime mode
MCP_TRANSPORT="stdio" # Exposed transport (used by TransportFactory)
# HTTP Transport Settings
MCP_AUTH_BASE_URL=http://localhost:3003
MCP_AUTH_PORT=3003
MCP_CORS_CREDENTIALS=true
MCP_CORS_ORIGIN=http://localhost:3000,http://localhost:4200,http://127.0.0.1:3000,http://127.0.0.1:4200
MCP_HTTP_HOST=localhost
MCP_HTTP_PORT=3001
# Session Management
MCP_AUTH_SESSION_SECRET=your-secure-session-secret
MCP_SESSION_COOKIE_NAME=mcp-session-id
MCP_SESSION_ENABLED=true
MCP_SESSION_TTL=1800000
MCP_TRUSTED_PROXIES=
# WebSocket Transport Settings
MCP_WS_ALLOWED_ORIGINS="*" # DEV ONLY; set specific origins in production
MCP_WS_CERT_PATH=path/to/your/certs/cert.pem
MCP_WS_COMPRESSION=true
MCP_WS_HOST=localhost
MCP_WS_KEY_PATH=path/to/your/certs/key.pem
# Default 1MB is safer; increase only if you must handle large messages
MCP_WS_MAX_PAYLOAD=1048576
MCP_WS_PATH="/sse" # Realtime endpoint path (WS server behind /sse by default)
MCP_WS_PER_MESSAGE_DEFLATE=true
MCP_WS_PORT=3002
MCP_WS_SESSION_COOKIE_NAME=mcp-ws-session-id
MCP_WS_SESSION_ENABLED=true
MCP_WS_TLS=false
MCP_WS_TRUSTED_PROXIES=
# OAuth 2.0 Authorization Configuration (Development)
MCP_AUTH_ENABLED=false
MCP_AUTH_RESOURCE_URI=http://localhost:3001/sse
MCP_AUTH_REQUIRED_SCOPES=mcp.read,mcp.write
# JWT validation for development using RS256 with JWKS
MCP_AUTH_JWT_ALGORITHMS=RS256
MCP_AUTH_JWT_AUDIENCE=http://localhost:3001/sse
MCP_AUTH_JWT_ISSUER=http://localhost:3003
MCP_AUTH_JWT_JWKS_URI=http://localhost:3003/.well-known/jwks.json
# If you switch to HS*, use MCP_AUTH_JWT_SECRET and set ALGORITHMS=HS256
# Cache settings
# 5 minutes
MCP_AUTH_TOKEN_CACHE_TTL=300
# 1 hour
MCP_AUTH_METADATA_CACHE_TTL=3600
# Authorization server configuration (mock for development)
MCP_AUTH_SERVERS='[{"issuer":"http://localhost:3003","authorizationEndpoint":"http://localhost:3003/oauth2/authorize","tokenEndpoint":"http://localhost:3003/oauth2/token","grantTypesSupported":["authorization_code","client_credentials","refresh_token"],"responseTypesSupported":["code"],"scopesSupported":["mcp.read","mcp.write","mcp.admin"],"codeChallengeMethodsSupported":["S256"]}]'
# -----------------------------------------------------------------------------
# AI CHAT (embedded AI agent) — @gauzy/plugin-ai-chat
# -----------------------------------------------------------------------------
# Master switch (chat is also hidden automatically when no provider is configured)
GAUZY_AI_CHAT_ENABLED=true
# SSRF egress guard for BYOK provider endpoints.
# A tenant admin can store a custom provider base URL and the server then fetches it (model
# catalogue, dictation, chat completions, document embeddings). By default any loopback, private
# (RFC 1918), link-local or cloud-metadata target is REFUSED, both when the URL is saved and when it
# is used, and redirects are not followed.
# Set this to `true` ONLY on single-tenant / self-hosted installs (and desktop local-server builds)
# whose users enter a LocalAI, Speaches, vLLM, Ollama or whisper.cpp address on localhost or a LAN in
# the AI settings page. Leave it unset on shared/multi-tenant hosting: there it would let any tenant
# reach the operator's internal network. It governs everything a TENANT credential leads to: the base
# URL a tenant entered AND the built-in local default a key-less row for Speaches/LocalAI/whisper.cpp
# falls back to (GHSA-w3mx-m5cr-3gxp). Only an operator's own `*_BASE_URL` value is trusted without it,
# so a zero-config local provider needs either this flag or that variable.
# GAUZY_AI_CHAT_ALLOW_PRIVATE_BASE_URLS=false
# Default provider/model when the tenant has not chosen one in Settings (BYOK).
# Providers are contributed by @gauzy/plugin-ai-provider-* plugins:
# anthropic | openai | openrouter | vercel-gateway | gauzy-ai | gemini | grok | groq | mistral |
# localai | openai-compatible (chat) — plus the voice-only ones listed under AI VOICE below.
GAUZY_AI_CHAT_DEFAULT_PROVIDER=anthropic
GAUZY_AI_CHAT_DEFAULT_MODEL=claude-sonnet-5
# BYOK credentials entered in Settings are encrypted at rest with this base64
# 32-byte key (shared with the core EncryptionService). Generate one with:
# node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# ENCRYPTION_KEY=
# Server-wide provider API keys (tenant BYOK credentials from Settings take precedence)
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
OPENROUTER_API_KEY=
AI_GATEWAY_API_KEY=
# Optional custom base URLs (proxies / self-hosted compatible endpoints)
# ANTHROPIC_BASE_URL=
# OPENAI_BASE_URL=
# OPENROUTER_BASE_URL=
# AI_GATEWAY_BASE_URL=
# Free tier: set this to make the AI agent work with NO per-tenant setup.
#
# It is resolved LAST — after a tenant's own key from Settings, and after OPENROUTER_API_KEY above —
# and it is the only source restricted to OpenRouter's free (":free") models. That separation is the
# point: if you set OPENROUTER_API_KEY you are bringing your own account and keep full access, while
# this variable is for a shared key that should never be spent on paid models.
#
# Rate limits on free models are low and shared across everyone using the key. When a user is rate
# limited the chat tells them to connect their own OpenRouter account or configure another provider.
# OPENROUTER_PLATFORM_API_KEY=
#
# Optional: pin the free model list instead of fetching it from OpenRouter. Free slugs are retired
# without notice, so this lets you correct drift with a restart rather than a release. Comma
# separated; leave unset to use the live catalogue (cached, with a pinned fallback).
# OPENROUTER_FREE_MODELS=deepseek/deepseek-r1:free,meta-llama/llama-3.3-70b-instruct:free
# Optional: attach the Gauzy MCP server's tools to the chat agent.
# Only point this at an MCP server that honors the per-request bearer token
# (see packages/plugins/ai-chat README — security note).
# Google Gemini + xAI Grok provider keys (BYOK per tenant also supported in Settings -> AI)
# GEMINI_API_KEY=
# XAI_API_KEY=
# Groq + Mistral: OpenAI-compatible chat AND speech-to-text (Whisper / Voxtral) — usable as the
# tenant's voice (dictation) provider as well as for chat.
# GROQ_API_KEY=
# GROQ_BASE_URL=
# MISTRAL_API_KEY=
# MISTRAL_BASE_URL=
# -----------------------------------------------------------------------------
# AI VOICE (dictation / speech-to-text) — voice providers for the AI chat
# -----------------------------------------------------------------------------
# Dictation uses the tenant's chosen voice provider (Settings -> AI Providers -> "Use as default
# voice provider"), and otherwise the first speech-capable provider that has credentials, in this
# order: openai (50), groq (80), mistral (90), speaches (100), localai (101), whisper-cpp (102),
# openai-compatible (103), deepgram (110), elevenlabs (120). Any provider below that is configured
# — even one that cannot chat — makes dictation work.
#
# Cloud speech-to-text only providers (no chat models):
# DEEPGRAM_API_KEY=
# DEEPGRAM_BASE_URL=
# ELEVENLABS_API_KEY=
# ELEVENLABS_BASE_URL=
#
# LOCAL / self-hosted speech (no API key needed — setting the base URL is the whole credential;
# a tenant can also enter the URL in Settings -> AI Providers instead):
# Speaches (faster-whisper-server): docker run -p 8000:8000 ghcr.io/speaches-ai/speaches:latest-cpu
# SPEACHES_BASE_URL=http://localhost:8000/v1
# SPEACHES_API_KEY=
# LocalAI (chat + whisper): docker run -p 8080:8080 localai/localai:latest
# LOCALAI_BASE_URL=http://localhost:8080/v1
# LOCALAI_API_KEY=
# whisper.cpp whisper-server: ./build/bin/whisper-server -m models/ggml-base.en.bin --convert
# WHISPER_CPP_BASE_URL=http://localhost:8080
# WHISPER_CPP_API_KEY=
# Any OpenAI-compatible endpoint (vLLM, LM Studio, Ollama /v1, LiteLLM …) for chat and/or STT:
# OPENAI_COMPATIBLE_BASE_URL=http://localhost:11434/v1
# OPENAI_COMPATIBLE_API_KEY=
# GAUZY_AI_CHAT_MCP_URL=
# Optional: base URL the chat agent's tools use to call this API itself
# (defaults to API_BASE_URL). Useful in k8s to short-circuit via localhost.
# GAUZY_AI_CHAT_SELF_API_URL=
# --------------------------------------------------------------------------------------------------
# Documents (@gauzy/plugin-docs)
# --------------------------------------------------------------------------------------------------
# The Documents hub works out of the box with no configuration: files upload, extract text, and become
# searchable. Everything below is optional tuning. AI features are OFF by default — turn them on only
# after an AI provider is configured (see the AI chat section above; Documents reuses those providers
# and honors per-tenant BYOK keys).
# Master switch for AI classification, summaries and embeddings in Documents. ON by default.
# It is safe to leave on with no AI provider configured: the AI stages are additionally gated on a
# provider having credentials, so with none registered the pipeline skips them and uploads still
# extract text and stay fully searchable (lexical search only). Set to false to keep the AI stages
# off even where a provider IS configured.
# GAUZY_DOCS_AI_ENABLED=true
# Model overrides. Classification falls back to the AI chat default model when unset.
# GAUZY_DOCS_CLASSIFY_MODEL=
# GAUZY_DOCS_EMBEDDING_MODEL=text-embedding-3-small
# GAUZY_DOCS_EMBEDDING_DIMS=1536
# GAUZY_DOCS_EMBED_BATCH_SIZE=64
# GAUZY_DOCS_CLASSIFY_SAMPLE_CHARS=12000
# Text recognition (OCR) for scanned PDFs and image uploads, using the same AI provider (and the same
# per-tenant BYOK keys) as classification — no separate OCR service is involved. Off by default because
# it costs one model call per page. ON by default, but doubly gated — it needs GAUZY_DOCS_AI_ENABLED
# *and* a provider with credentials, so a deployment with no AI configured never transcribes anything.
# While unavailable, a PDF with no text layer and an image upload both fail with a clear "OCR is not
# available" message and land in the review queue.
# OCR'd documents are always flagged for review: a transcription can drop or garble text silently.
# GAUZY_DOCS_OCR_ENABLED=true
# GAUZY_DOCS_OCR_MAX_PAGES=20
# Vector store used for semantic retrieval. 'pgvector' requires PostgreSQL with the vector extension;
# on MySQL/SQLite (or when the extension is missing) retrieval degrades to lexical search automatically.
# GAUZY_DOCS_VECTOR_STORE=pgvector
# GAUZY_DOCS_RETRIEVAL_TOPK_MAX=12
# Chunking (how extracted text is split before embedding).
# GAUZY_DOCS_CHUNK_TOKENS=512
# GAUZY_DOCS_CHUNK_OVERLAP_TOKENS=64
# Re-index every document automatically when the embedding model or dimensions change. Off by default
# because a fleet-wide re-embed costs money — trigger it deliberately from Documents settings instead.
# GAUZY_DOCS_AUTO_REINDEX_ON_MODEL_CHANGE=false
# Upload limits and processing.
# GAUZY_DOCS_MAX_FILE_SIZE=52428800
# GAUZY_DOCS_MAX_EXTRACTED_CHARS=2000000
# GAUZY_DOCS_MAX_BINARY_BYTES=10485760
# GAUZY_DOCS_QUEUE_CONCURRENCY=2
# GAUZY_DOCS_STUCK_THRESHOLD_MINUTES=1440
# Per-organization storage quota in bytes. 0 or unset means unlimited (an organization-level setting
# in Documents settings overrides this).
# GAUZY_DOCS_ORG_QUOTA_BYTES=0
# Minutes between automatic version snapshots while a page is being edited.
# GAUZY_DOCS_VERSION_DEBOUNCE_MINUTES=10
# Structured logging of knowledge searches (query length, result counts, latency — never query text).
# GAUZY_DOCS_RETRIEVAL_LOG_ENABLED=true
# Inbound email capture: documents emailed to a per-organization address land in Documents for review.
# ON by default, but the route accepts NOTHING until a delivery can prove itself. Captured documents
# are never added to AI knowledge automatically — they always go through review first.
#
# Two kinds of capture address:
# PLATFORM docs-<128-bit hex>@$GAUZY_DOCS_INBOUND_DOMAIN — minted automatically for every
# organization on first read. Requires GAUZY_DOCS_INBOUND_DOMAIN; without it there
# is no address to mint and none is invented.
# CUSTOM_DOMAIN <mailbox>@<the tenant's own domain>, registered through the Documents settings UI.
# Inert until the tenant publishes _gauzy-docs.<domain> IN TXT with the value the UI
# shows — a chosen mailbox name is guessable, so ownership must be proven.
#
# A delivery is authenticated by EITHER of two proofs:
# 1. The deployment-wide HMAC below. Signature is hex(HMAC_SHA256(secret, "<timestamp>.<rawBody>"))
# in x-gauzy-docs-signature with x-gauzy-docs-timestamp. Fails closed when unset, and enforces a
# timestamp tolerance, a constant-time compare and single-use replay consumption.
# 2. A per-address relay secret in x-gauzy-docs-address-secret, issued once when a custom-domain
# address is created or rotated. Prefer this for tenant-owned domains: the deployment-wide
# secret can post as ANY tenant, a per-address secret only as one.
# GAUZY_DOCS_INBOUND_EMAIL_ENABLED=true
# GAUZY_DOCS_INBOUND_DOMAIN=
# GAUZY_DOCS_INBOUND_WEBHOOK_SECRET=
# GAUZY_DOCS_INBOUND_MAX_MESSAGE_BYTES=26214400
# BullMQ-backed dispatch for the Documents pipeline. Defaults to ON exactly where a BullMQ root is
# registered — i.e. REDIS_ENABLED=true and SCHEDULER_QUEUE_ENABLED not set to false. Every process
# that loads plugins (API, `yarn seed`, worker) registers that root under the same condition, so
# the default cannot register a @Processor without a connection. Set it explicitly to force either
# direction; with it off the pipeline runs in-process, which stays a fully supported mode.
# GAUZY_DOCS_QUEUE_ENABLED=false
# Whether THIS process also runs the docs-processing consumer, or only enqueues. Defaults to true
# wherever the queue is on. Set false on the API when a dedicated worker deployment exists, so the
# extraction / OCR / embedding work happens only there.
# GAUZY_DOCS_QUEUE_WORKER_ENABLED=false
# Fleet-wide kill switch for the BullMQ root itself (API + worker + seeder). `false` removes the
# root everywhere and puts every queue-backed pipeline back on its in-process fallback.
# SCHEDULER_QUEUE_ENABLED=false
# BILLING (Stripe) — hosted deployments only.
#
# Leave everything here unset for self-hosting: registration behaves exactly as it always has, no
# Stripe call is ever made, no subscription is required, and the billing UI is absent. The presence
# of STRIPE_SECRET_KEY is the only switch that turns any of it on.
#
# Server-side only — never expose these to the browser.
#
# Per-environment expectation:
# demo no key at all. DEMO=true also disables billing outright even if a key is present,
# so a demo deployment cannot reach Stripe by accident.
# staging a sk_test_ key. Test mode; no real card is ever charged.
# prod a sk_live_ key AND STRIPE_LIVE_MODE=true. A live key without that second opt-in is
# refused, so copying the production secret bundle onto staging cannot start taking
# real payments.
STRIPE_SECRET_KEY=
# Required alongside a sk_live_ key. Leave unset everywhere except production.
STRIPE_LIVE_MODE=
# Signing secret for the Stripe webhook endpoint (POST /api/billing/webhook).
STRIPE_WEBHOOK_SECRET=
# Where an unsubscribed visitor is sent to pick a plan. Shared by every Ever product.
EVER_CHECKOUT_URL=https://ever.co/checkout