Skip to content

Environment Variables

Applies to: All subscriptions

Purpose

Provide the complete reference for host-level configuration: what each setting controls, what happens if it is wrong, and which subsystem stops working. This is the page an administrator opens when something is misconfigured and the log is unhelpful.

Audience

System administrators and implementation engineers. Integration developers, for the authentication and write-back groups.

Prerequisites

Reference

Settings live in an environment file read by the API and worker at startup. Every change requires a restart of the affected process.

Treat this file as a credential store: restrict its permissions to the service account and administrators, keep it out of source control, and own the rotation of what it holds.

Core application

Setting Purpose Note
NODE_ENV Runtime mode production in production. Development mode changes error verbosity and logging
PORT API listening port 5003 in the reference deployment. A built-in fallback exists and must not be relied on — if the proxy and service disagree, the symptom is HTTP 502 with a silent application log
APP_NAME Display name in logs Cosmetic
APP_BASE_URL The application's own public URL Used to build links. Wrong here means emails and documents point at the wrong environment
CORS_ORIGINS Browser origins permitted to call the API Only needed when the front end is on a different origin. Never a wildcard
LOG_LEVEL Log verbosity info in production. debug is for diagnosis and generates a lot of volume
LOG_PRETTY Human-readable log formatting Off in production, where logs are machine-collected

Secret resolution (Azure Key Vault)

Setting Purpose Note
KEY_VAULT_URL Switches where the Tier 1–2 secrets in this file come from — Azure Key Vault instead of the values set directly below (DB_PASSWORD, JWT_SECRET, AGENT_GATEWAY_SECRET, GSP credentials, and others; the exact list is gst-backend/services/secrets/mapping.js) Absent (the default everywhere today): every setting in this file is read exactly as written below — nothing changes. Set: at startup, each mapped secret is fetched from the named vault and overrides the value below under the same variable name, so no other config module needs to change. If a Tier 1 secret (DB_PASSWORD, JWT_SECRET, AGENT_GATEWAY_SECRET) fails to resolve — or resolves to an unmigrated placeholder — startup fails loudly instead of continuing with a broken or wrong credential. A non-Tier 1 secret still holding its placeholder is left unset in the environment (a warning is logged), never set to the placeholder text — so REDIS_PASSWORD stays unset until a real value is in the vault

As of this writing, KEY_VAULT_URL is not set on any deployed environment — every server still runs entirely off the values in this file, unchanged. The corresponding Key Vaults (complifly-kv-dev/qa/prod) are populated with real values ahead of time, but nothing reads from them yet; that cutover is a separate, later step per environment.

Database

Setting Purpose Failure if wrong
DB_SERVER Host or address, with instance name if applicable API refuses to start
DB_PORT Database port Connection timeout
DB_DATABASE Database name Startup failure, often misread as a credential problem
DB_USER / DB_PASSWORD Application login Authentication failure at startup
DB_ENCRYPT Encrypt the connection Should be enabled. Disabling puts credentials and data in clear text
DB_TRUST_SERVER_CERT Whether to accept the server certificate without validation A deliberate, recorded decision. Acceptable only inside a controlled network
DB_POOL_MIN / DB_POOL_MAX Connection pool bounds Too low starves under load; too high exhausts database connections. Tune only against measurement
DB_CONNECTION_TIMEOUT_MS Connection establishment timeout Too low produces spurious startup failures on a busy instance
DB_REQUEST_TIMEOUT_MS Per-query timeout Too low fails long reports; too high hides a runaway query
DB_POOL_IDLE_TIMEOUT_MS Idle connection lifetime Rarely needs changing
DB_APP_NAME Application name in database sessions Useful for identifying Complifly connections when diagnosing

Authentication and identity

Setting Purpose Failure if wrong
JWT_SECRET Signs and verifies session tokens Users cannot sign in. Changing it invalidates every existing session — a deliberate act, not a routine one. Must be long, random, and different per environment
HOSTNAME Identifies which node wrote a given JWT key-rotation audit count, when more than one server is running Set automatically by the OS/shell on every Linux host — never set this by hand. Absent (unlikely): the audit event records the node as unknown, which still works but makes a multi-node rotation harder to read
JWT_KEY_ID, JWT_SECRET_PREVIOUS, JWT_PREVIOUS_KEY_ID, JWT_PREVIOUS_VALID_UNTIL Key rotation for JWT_SECRET without forcing every user to sign in again. New tokens carry JWT_KEY_ID in their header and are signed with JWT_SECRET. A token signed with the previous key (JWT_SECRET_PREVIOUS, identified by JWT_PREVIOUS_KEY_ID) is still accepted, but only until JWT_PREVIOUS_VALID_UNTIL (an ISO timestamp) passes — after that it is rejected even if the value is still configured All four absent (the default): behaves exactly as JWT_SECRET alone always has. Set during a rotation: both keys are valid for a bounded window (recommended 48 hours — covers the 8-hour token lifetime plus a rolling restart, restart delays and clock skew). Each successful validation is logged as an aggregated count (not per-request) under JWT_KEY_CURRENT_VERIFIED / JWT_KEY_PREVIOUS_VERIFIED in the audit trail, with no token claims or key values — this is how an operator confirms traffic has moved off the previous key before retiring it
AZURE_TENANT_ID Your own identity tenant — used for user sign-in, and to recognise ERP app registrations that live in it. It is not the tenant an ERP token must come from; that is recorded per connection User sign-in fails
AZURE_CLIENT_ID Application registration identifier for user sign-in Sign-in token validation fails
AZURE_API_AUDIENCE Accepted token audience values for machine tokens Valid tokens rejected. Both the application identifier URI and the bare identifier may be needed, depending on token version
AZURE_REQUIRED_ROLE Application role a machine token must carry Authenticated calls rejected as unauthorised
AZURE_API_APP_ID The API application registration customers grant admin consent to. That registration must be multi-tenant, or no customer directory can consent Consent links cannot be generated. The administration screen says so rather than failing later
AGENT_GATEWAY_SECRET Shared secret between the backend and whatever terminates client certificates for the Complifly Agent (nginx in production, scripts/dev-mtls/local-mtls-gateway.js in development). The terminator sends it in X-Gateway-Secret; the backend ignores the client-certificate headers on any request without it. No default, and it must not have one — with it unset the backend treats no request as certificate-verified, so every agent call fails with AGENT_CERT_REQUIRED Agent calls rejected with AGENT_CERT_REQUIRED even with a valid certificate. If the value differs between the backend and the terminator the symptom is identical, so check both. Anyone who learns it can impersonate any agent whose certificate serial they know — treat it like JWT_SECRET
ERP_CONSENT_REDIRECT_URI Where Microsoft Entra returns a customer's administrator after consent Consent fails at the customer's end with an AADSTS error you never see
GOOGLE_CLIENT_ID Social sign-in, where enabled Social sign-in fails
OTP_EXPIRY_MINUTES One-time code lifetime Too short frustrates users; too long weakens the control
OTP_MAX_ATTEMPTS Attempts before lockout Balance usability against brute-force resistance
OTP_RESEND_COOLDOWN_SECONDS Minimum interval between code requests Prevents message flooding
SESSION_MAX_CONCURRENT How many devices a user may be signed in on at once. Default 1 — a new sign-in beyond the limit signs the oldest device out automatically Set higher (e.g. 2) for a workforce that legitimately works from a desktop and a phone at once. Setting it very high approaches the old (pre-session-registry) behaviour of effectively unlimited concurrent sign-ins — see User Authentication
SESSION_TTL_HOURS How long a session token is valid for before re-authentication is required Longer eases usability; shorter reduces the window a lost or stolen token remains usable
SESSION_TOUCH_THROTTLE_MS How often the Active Sessions "last activity" timestamp is written, per session A performance knob only — never affects whether a session is valid; lowering it writes more often but reflects activity slightly more precisely
PAN_ENCRYPTION_KEY Encrypts stored identity numbers Losing this makes existing encrypted values unreadable. Back it up separately from the database, or a restore recovers rows nobody can read

PAN_ENCRYPTION_KEY deserves particular care. It is the one setting whose loss is not recoverable from a database backup, because the backup contains only the ciphertext.

ERP_CONSENT_REDIRECT_URI must match Entra character for character. This value is registered as a redirect URI on the API application in Microsoft Entra. If the two disagree by so much as a trailing slash, consent fails inside the customer's directory, where the only diagnostic is an AADSTS code on their screen.

It is therefore set explicitly rather than derived from the domain map: a hostname change must be a deliberate, paired edit — this variable and the Entra registration, in the same change. Moving the public address without updating Entra breaks onboarding for every new customer while leaving existing ones working, so nothing alerts you. An ERP token is not validated against AZURE_TENANT_ID. Each connection records the customer's own directory, and the token is checked against that. This is the setting most often misread when an integration is refused — widening or changing it does not admit a customer's ERP, and never should.

GSP providers

Setting group Purpose
PROVIDER The default provider for this deployment
WEBTEL_* Webtel credentials and per-operation endpoint URLs
MI_* Masters India credentials, base URLs and timeout
EINV_USER / EINV_PASS e-Invoice service credentials
EF_USER / EF_PASS e-Way Bill service credentials
GSP_CONFIG_CACHE_TTL_MS How long provider configuration is cached

Provider endpoint URLs are supplied by the provider and differ between sandbox and production. Mixing them is the mechanism by which a test document reaches the live government system.

See GSP Configuration for assignment and testing.

Mail

All outbound mail — password reset, one-time codes, e-Way Bill expiry alerts, ERP sync failures, platform alerts — goes through Azure Communication Services. There is no SMTP path: a prior raw-SMTP implementation (SMTP_HOST/SMTP_USER/SMTP_PASS/etc., authenticating as a Microsoft 365 mailbox) was retired after Microsoft disabled tenant-wide SMTP basic auth, which broke it outright regardless of the password's correctness. Azure Communication Services authenticates with an access key in a connection string instead of a mailbox login, so that class of failure does not apply to it.

Setting Purpose Failure if wrong
AZURE_COMMUNICATION_CONNECTION_STRING Endpoint + access key for the Azure Communication Services resource that sends all mail No notifications, no password reset, no one-time codes. Users are locked out with no visible cause
AZURE_EMAIL_SENDER_ADDRESS Sender address, e.g. DoNotReply@complifly.in. Must be a MailFrom address verified under a domain connected to the Communication Services resource (Azure Portal → the resource → Email → Domains) Send fails — the address is not authorized to send from that resource

Mail failures are the most under-appreciated configuration fault in the product. They do not surface at startup, they produce no error in the interface, and the symptom reaching support is "a user cannot log in".

Redis — cache, rate limiting, and (reserved) the job queue

One Redis instance serves three separate purposes, kept apart by database index — never share an index across them, or a job-queue key and a cached value could collide.

Setting Purpose Note
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD Connection, shared across all three uses below Wrong here affects caching and rate limiting immediately — not just the queue
REDIS_CACHE_DB Database index for services/cache.js (feature flags, theme, session policy, GSP config, master-data caching) Default 0. Fails open: any Redis error is treated as a cache miss and the app reads SQL Server directly — degraded performance, never a 500
REDIS_CACHE_OP_TIMEOUT_MS How long a single cache read/write may take before it's treated as failed Default 250. Too high delays the fail-open fallback under a real outage
MASTER_CACHE_TTL_MS How long a whole small master-data list (states, countries, UQC, etc.) is cached Default 24 hours. Explains why a bulk master-data import doesn't appear immediately — see the import's own cache-invalidation step
MASTER_QUERY_CACHE_TTL_MS How long one HSN/GST-rate search result is cached Default 12 hours — shorter than the whole-table TTL because these two tables are large and/or searched, not held whole in memory
REDIS_RATELIMIT_DB Database index for authLimiter/apiLimiter in server.js Default 2. This is what makes the limit genuinely shared across every app server — see Rate Limits and Errors. Fails open the same way as the cache: a Redis error lets the request through unlimited rather than returning an error page
JOBS_USE_QUEUE Whether background work is queue-backed (BullMQ, database index 1) Off in production today. When enabled, Redis becomes a hard dependency for job processing specifically — a queue failure here is a silent one: jobs enqueue and are never consumed, and the interface looks entirely healthy
WORKER_CONCURRENCY Parallel jobs per worker, once the queue is enabled Higher throughput, higher memory. Raise only against measurement
FILE_UPLOAD_DIR Where uploaded files are written Must be writable by the service account and included in backup planning
MAX_FILE_SIZE_MB Largest accepted upload Must align with the proxy's body limit, or the proxy rejects first and the message is unhelpful

IRN submission

Setting Purpose
IRN_SUBMIT_MAX_ATTEMPTS Retry attempts against the provider
IRN_SUBMIT_BASE_DELAY_MS Base delay for backoff between attempts
INGEST_IN_FLIGHT_TIMEOUT_MS How long an in-flight ingest may remain unresolved

Retries protect against transient network faults, not against rejections. A rejected document is rejected deterministically; retrying it consumes billed calls to no purpose.

e-Way Bill

Setting Purpose
EWB_EXPIRY_ALERTS_ENABLED Whether approaching-expiry alerts are raised
EWB_EXPIRY_HORIZON_HOURS How far ahead to look. Set to the customer's real dispatch lead time
EWB_PRINT_HORIZON_MONTHS How far back printed bills are retrievable
EWB_GENERATION_GUARD ENFORCE (the default) or OFF. While enforced, only one generation runs per document at a time on a server, and a document that already has a live e-way bill is refused before any provider call. OFF restores the previous behaviour
EWB_SUBMIT_MAX_ATTEMPTS Total attempts for a request that provably never reached the provider (a refused connection, an unresolvable name, HTTP 429 or 503). Default 3; 1 disables retry
EWB_SUBMIT_BASE_DELAY_MS First backoff step, doubled each attempt with jitter. Default 1500
EWB_PERSIST_MAX_ATTEMPTS Attempts to record an issued e-way bill after a transient database fault (a deadlock, a timeout). Default 3

What is and is not retried. A request that may have reached NIC (a timeout, a reset, an HTTP 500, 502 or 504, or an answer Complifly could not read) is never resubmitted automatically: NIC may already have issued the bill, and a repeat would be refused as a duplicate and recorded as a failure against a document that has a live bill. Such a request is recorded as in doubt, with the code EWB_IN_DOUBT. See e-Way Bill failures.

Housekeeping scheduling and authentication throttling

Setting Purpose Note
HOUSEKEEPING_RUN_ON_STARTUP Run every enabled retention/archive job once shortly after the server starts Default true. Safe on every restart: a job that already ran today exits as "already claimed"
HOUSEKEEPING_STARTUP_DELAY_MS Wait before that first run Default 60000 (60 s), so start-up is not slowed
HOUSEKEEPING_RETRY_MAX How many times a failed pass is retried Default 3. 0 disables retry
HOUSEKEEPING_RETRY_DELAY_MS First retry delay; each further retry doubles it Default 900000 (15 min), then 30 min, 60 min
AUTH_RATE_LIMIT_ENABLED Throttle repeated failed requests to /api/auth Default true. Setting false disables brute-force protection — for development only; the Observability Security panel shows when it is off
AUTH_RATE_LIMIT_MAX Failed attempts allowed per client address per 15 minutes Default 20. Successful sign-ins do not count

A job runs only when both its environment switch (SESSION_REAPER_ENABLED, QUEUE_RETENTION_ENABLED, …) is on and its row in the retention policy has enabled = 1. See Housekeeping.

Attention Center checks

All of these are off or at their default when absent. With the first one off, the Attention API has no findings and reports sweepHealthy: false — users are told the checks are not running rather than shown an all-clear.

Setting Purpose Default and what happens when absent
COMPLIANCE_SWEEP_ENABLED Run the scheduled check that opens, refreshes and closes Attention Center findings for every registration Off. No findings are created or closed. Set true on every application server; they elect one runner per interval between themselves, so it is safe on all of them
COMPLIANCE_SWEEP_INTERVAL_MS How often the check runs 900000 (15 minutes). Values below 60000 are ignored. A finding is reported as stale after two intervals without a completed pass
COMPLIANCE_HOOKS_ENABLED Re-check a document's findings within seconds of it changing (IRN generated, e-way bill extended), instead of waiting for the next scheduled pass Off. Findings still close — at the next scheduled pass
FINDING_RETENTION_ENABLED Schedule the job that purges closed findings past their retention window Off. Closed findings are kept indefinitely. Also needs the two compliance_finding.* retention policy rows enabled — see Housekeeping
FINDING_RETENTION_INTERVAL_MS How often that job runs 86400000 (daily)
ATTENTION_NOTIFICATIONS_ENABLED Run the job that sends the emails the notification settings ask for (assigned, escalated, reopened, daily and weekly summaries). This job emails real people. It is the server-wide stop: off, no company is emailed. Inside it, each company also needs the NOTIFICATION_DISPATCH_ENABLED flag on (default off, Feature Flags), which only a Complifly Super Admin can set Off. Notification settings can be saved but no email goes out, for any company; the in-app bell is unaffected. Also needs Azure email configured (AZURE_COMMUNICATION_CONNECTION_STRING and AZURE_EMAIL_SENDER_ADDRESS); without them it logs that it did not start. Safe on every application server: a ledger row written before each send stops two servers sending the same email
ATTENTION_NOTIFICATIONS_INTERVAL_MS How often it looks for something to send 900000 (15 minutes). Values below 60000 are ignored. It does not run at start-up, so a restart loop cannot replay the last two days

The checks evaluate the rules installed in the database (GET /api/attention/rules lists them). A rule is installed by its migration, which Complifly runs during rollout.

Changing any of these needs a restart of the application server. Setting them requires access to the server's environment; a Complifly administrator role inside the application cannot change them.

Observability (MVP, non-Azure)

Off by default — flip the two _ENABLED switches to turn this on, no deploy required. See Logs and Observability for what each piece covers and eng-portal ADR-0020 for what is deliberately deferred to Phase 2 (Azure Log Analytics, Application Insights, distributed tracing).

Setting Purpose Note
PLATFORM_HEALTH_SWEEP_ENABLED Master switch for the 5-minute health sweep Sends real alert email when a P1/P2 condition is detected
PLATFORM_ALERT_RECIPIENTS Comma-separated email addresses for P1/P2 alerts Required for the sweep to actually notify anyone — a missing value means breaches are detected but silent
PLATFORM_ALERT_RECIPIENTS_P1 / _P2 Severity-specific recipient overrides (Phase 1.1) Each falls back to PLATFORM_ALERT_RECIPIENTS if unset — fully optional, backward compatible. Read dynamically (process.env[\PLATFORM_ALERT_RECIPIENTS_${severity}`]`), so this row is hand-maintained — the automated env-var completeness gate cannot see a dynamically-constructed key
PLATFORM_EVENT_LOG_RETENTION_ENABLED Master switch for the daily platform_event_log purge job
PLATFORM_EVENT_LOG_RETENTION_DAYS Rows older than this are deleted Default 90
PLATFORM_EVENT_LOG_RETENTION_INTERVAL_MS How often the purge job runs Default daily
PLATFORM_EVENT_LOG_PURGE_BATCH Rows deleted per batch Bounded to avoid a long-held table lock on a busy table
PLATFORM_ALERT_MIN_SAMPLE_IRN / _EWB / _UPLOAD Minimum attempts in the alert window before the IRN, e-Way Bill or upload failure-rate alert may fire Default 5 each. Below it, a high percentage (1 failure out of 1) is ignored. Customer-data validation rejections are excluded from the IRN rate regardless
LOGIN_FAILURE_ALERT_THRESHOLD Failed sign-ins in 15 minutes (all accounts) that raise the LOGIN_FAILURE_SPIKE P2 alert Default 25
LOGIN_FAILURE_ALERT_PER_USER_THRESHOLD Failed sign-ins in 15 minutes against one account that raise the same alert Default 10
SECURITY_REPORT_ENABLED Whether the daily npm audit snapshot (Security Health widget) runs Read-only — visibility only, not the full Vulnerability Management program. A snapshot that could not run is shown as unknown, never as zero
POLICY_GATE_EVENTS_ENABLED Whether a policy or tenant-lifecycle gate that let a request through without applying its control writes a POLICY_GATE_DEGRADED platform event Default on. The count and the log line are unaffected by this switch. See Integration policies
POLICY_GATE_EVENT_THROTTLE_MS Minimum time between events for the same gate and reason Default 900000 (15 minutes), so a database outage is one row with a count, not a flood. 0 records every occurrence
COMMERCIAL_CAPACITY_ALERTS_ENABLED Master switch for the scheduled capacity-alert run: it notices when a customer crosses 70, 85 or 100 % of a plan limit (or goes over), records each crossing once in that customer's commercial history, and emails the support matrix's commercial contact one digest of what is new Default off. Records and reports only: nothing reads it as a gate and nothing is ever blocked. Until the business supplies a commercial contact (config/support/supportMatrix.js), crossings are recorded and nobody is emailed; the support-readiness check says so. A Super Admin can also run one on request with POST /api/admin/commercial/capacity-alerts/run (a dry run unless apply is true)
COMMERCIAL_CAPACITY_ALERTS_INTERVAL_MS How often the scheduled capacity-alert run happens Default 3600000 (an hour). Never faster than 300000 (five minutes); a smaller or unreadable value is the default. Only used when the setting above is on
COMMERCIAL_PROMOTIONS_ENABLED Whether a Complifly Super Admin can apply a voucher (a free term, a longer trial or an optional module for a limited time) to a customer Default off: applying is refused with PROMOTIONS_DISABLED and nothing is written. Only the string true turns it on. It switches off applying only: reading a customer's vouchers, taking back a mistake, and the rule that a voucher trial needs the customer's recorded acceptance before it becomes a paid term all work either way
COMMERCIAL_PROMOTION_SWEEP_ENABLED Master switch for the scheduled voucher expiry follow-up: it finds a voucher-funded free period that has ended without being converted to a paid term, records it once in the customer's commercial history, and emails the support matrix's commercial contact one digest of the ones they have not yet been told about (it is retried until it has been sent) Default off. The flag on the Commercial tab does not depend on it. It records and reports only: the customer keeps working and nothing statutory (IRN, e-way bill, cancellation) is ever blocked. Without a commercial contact in the support matrix it records and emails nobody, and the first digest after one is supplied names every expiry still unconverted
COMMERCIAL_PROMOTION_SWEEP_INTERVAL_MS How often the scheduled voucher expiry follow-up runs Default 3600000 (an hour). Never faster than 300000 (five minutes); a smaller or unreadable value is the default. Only used when the setting above is on
SENTRY_TRACES_SAMPLE_RATE Sentry performance-transaction sample rate Defaults to 0 (errors only) — deliberately, to avoid a second tracing pipeline before OpenTelemetry's own destination is decided

ERP write-back

Off by default. When enabled, these govern delivery behaviour:

Setting Purpose
ERP_WRITEBACK_ENABLED Master switch for the deployment. Enforced at emission — with this off, no outbound event is stored for anyone, and enabling it does not backfill history
ERP_WRITEBACK_BATCH_SIZE Events dispatched per cycle
ERP_WRITEBACK_DISPATCH_INTERVAL_MS How often dispatch runs
ERP_WRITEBACK_LEASE_SEC How long a worker holds a claim on an event
ERP_WRITEBACK_CIRCUIT_THRESHOLD Consecutive failures before the circuit opens
ERP_WRITEBACK_CIRCUIT_COOLDOWN_SEC / ..._CAP_SEC Cooldown after opening, and its ceiling
ERP_WRITEBACK_RETENTION_ENABLED / ..._DAYS How long delivered events are retained
ERP_WRITEBACK_DEAD_RETENTION_DAYS How long dead-lettered events are retained. Must exceed your realistic reaction time, or evidence is purged before anyone investigates
ERP_WRITEBACK_RAW_PAYLOAD_DAYS Retention of raw payloads

Three things must all be true before a single event is recorded. This variable is only the first of them, and the other two are not environment settings at all:

  1. ERP_WRITEBACK_ENABLED for the deployment (here).
  2. The ERP_WRITEBACK_ENABLED feature flag on the specific GSTIN — see Feature Flags.
  3. An active subscription for that ERP connection and GSTIN — see Write-back API.

All three are enforced at emission, so a gap in any one of them means the event is never written — not queued, not skipped, not delayed. Nothing errors on either side. If an ERP reports that it receives no events while everything "looks enabled", check all three before investigating delivery. | ERP_WRITEBACK_ALERTS_ENABLED / ..._SUPPORT_EMAIL | Failure alerting and its recipient | | ERP_WRITEBACK_ALLOW_INSECURE_URL | Permits a non-HTTPS endpoint. Never enable in production | | ERP_WRITEBACK_EMIT_ONLY | Emit events without dispatching, for a pull-based consumer | | ERP_WRITEBACK_RECONCILE_ENABLED / ..._INTERVAL_MS / ..._LOOKBACK_DAYS / ..._CAP | Reconciliation sweep for events that should have been emitted |

Verification caching

Setting Purpose
VERIFY_CACHE_ENABLED Whether verification results are cached rather than re-fetched
VERIFY_CACHE_TTL_GSTIN_DAYS Staleness window for registration status
VERIFY_CACHE_TTL_PAN_DAYS Staleness window for identity verification
VERIFY_CACHE_TTL_UDYAM_DAYS Staleness window for enterprise registration

These directly control the trade-off between provider cost and data freshness. A counterparty's registration status can change after it was cached — shorten the window where that risk matters, lengthen it where it does not.

Other

Setting Purpose
FEATURE_CACHE_TTL_MS How long a registration's resolved feature flags are cached (default 60 seconds). A change made through Operational Controls clears the cache at once; this is the upper bound if that clearing fails
FEATURE_DEFINITION_CACHE_TTL_MS How long each server keeps the catalogue of operational controls in memory (default 5 minutes). The catalogue changes only when a release's database migration changes it; leave this alone
FEATURE_CATALOGUE_CHECK Emergency recovery only — leave unset. At start-up each server checks that its list of operational controls agrees with the database. Default strict: a disagreement (a control the product reads has no database row, or a default, type or scope differs) stops the server and names the problem. Set to warn to start the server anyway while the catalogue is repaired, then remove it and restart — while it is set, Operational Controls may show a control whose default or scope disagrees with the product. Any other value is treated as strict. A control in the database that this build does not know is never a failure in either mode: the server starts and that control is hidden. See Operational Controls cutover
THEME_CACHE_TTL_MS How long an organization's resolved theme is cached. Same 60-second default and same "why did my change take a moment" behavior as FEATURE_CACHE_TTL_MS — see Branding and Theming
THEME_ASSET_MAX_MB Maximum upload size for a logo, favicon, or login hero image. Default 5 MB
FEATURE_CATALOGUE_CHECK Emergency recovery only — leave unset. At start-up each server checks that its list of operational controls agrees with the database. Default strict: a disagreement (a control the product reads has no database row, or a default, type or scope differs) stops the server and names the problem. Set to warn to start the server anyway while the catalogue is repaired, then remove it and restart — while it is set, Operational Controls may show a control whose default or scope disagrees with the product. Any other value is treated as strict. A control in the database that this build does not know is never a failure in either mode: the server starts and that control is hidden. See Operational Controls cutover
THEME_SVG_SANITIZE_ENABLED Whether uploaded SVG theme assets (logo/favicon/hero) are sanitized before storage. Leave enabled (default) — this closes a stored-XSS path; disabling it is an emergency rollback switch only, for if a legitimate logo is ever incorrectly rejected, not a setting to turn off routinely
SCHEMA_GUARD Startup schema verification. Leave enabled — disabling it restores the failure mode it exists to prevent
FVU_JAR_PATH / TDS_OUT_DIR TDS return validation tooling and output directory
DOTENV_PATH Alternative configuration file location
SENTRY_DSN Sentry project DSN for exception aggregation. Absent by default — error reporting to Sentry is a true no-op (no network calls) until this is set. See Logging & Observability
OTEL_ENABLED Enables OpenTelemetry distributed tracing. Off everywhere today — non-production bootstrap only; production rollout is a separate, deliberate step (requires -r ./otel-bootstrap.js / PM2 node_args preloading, not just setting this flag, for HTTP/Express spans to instrument correctly). See Logging & Observability
OTEL_EXPORTER_OTLP_ENDPOINT Where traces are sent when tracing is enabled. Absent falls back to a console exporter (for local verification only)

Dev/POC tooling — mTLS bridge over ngrok

gst-backend/scripts/dev-mtls/ws-bridge-server.js and ws-bridge-client.js are a development-only pair of scripts that let the Complifly Agent's mTLS connection travel through a free ngrok HTTP tunnel (which cannot carry raw mTLS, since ngrok ends TLS at its own edge on the free tier) by carrying the TLS bytes inside a WebSocket instead. Not used in any deployed environment.

Setting Purpose
BRIDGE_PORT Server side (ws-bridge-server.js). Port the bridge listens on. Default 8444
BRIDGE_TARGET Server side. The local mTLS gateway to forward decoded traffic to. Default 127.0.0.1:8443
BRIDGE_URL Client side (ws-bridge-client.js). The ngrok WebSocket address to connect to. No default — required
BRIDGE_LISTEN Client side. Local port the client listens on, for the agent script to point at. Default 9443

Complete reference

The sections above group the settings you will actually reach for, with the reasoning behind each. This table is the exhaustive list — every variable the backend reads, generated from the source so it cannot fall behind a release.

Treat an unfamiliar variable here as a default you are accepting rather than a setting you are missing. Most of them should stay untouched.

Every non-GSP variable this build reads, with the default that applies when you do not set it. GSP endpoints and credentials are in GSP Configuration.

Variable Default Read by
AGENT_GATEWAY_SECRET Secret — no default middleware/agentAuth.middleware.js, scripts/dev-mtls/local-mtls-gateway.js
ANTHROPIC_API_KEY Secret — no default config/compass.config.js, services/compass/agent/config.js
APP_BASE_URL http://localhost:8080 routes/attentionNotifications.routes.js, routes/auth.routes.js +2
APP_NAME GST Backend API serverStart.js
ATTENTION_NOTIFICATIONS_ENABLED none services/attention/notifications/dispatcher.js
ATTENTION_NOTIFICATIONS_INTERVAL_MS none services/attention/notifications/dispatcher.js
AUDIT_ARCHIVE_ENABLED false services/housekeeping/auditArchive.service.js
AUDIT_ARCHIVE_INTERVAL_MS none services/housekeeping/auditArchive.service.js
AUTH_RATE_LIMIT_ENABLED none config/authRateLimit.js, scripts/preflight_commercial_stack.js
AUTH_RATE_LIMIT_MAX none config/authRateLimit.js
AZURE_API_APP_ID none routes/erp-connection.routes.js
AZURE_API_AUDIENCE empty string middleware/erpAuth.middleware.js, routes/erp-connection.routes.js
AZURE_CLIENT_ID none services/social-auth.service.js
AZURE_COMMUNICATION_CONNECTION_STRING none routes/attentionNotifications.routes.js, services/attention/notifications/dispatcher.js +2
AZURE_EMAIL_SENDER_ADDRESS none routes/attentionNotifications.routes.js, services/attention/notifications/dispatcher.js +2
AZURE_REQUIRED_ROLE Invoice.Post middleware/erpAuth.middleware.js
AZURE_SEARCH_API_KEY Secret — no default config/compass.config.js
AZURE_SEARCH_ENDPOINT empty string config/compass.config.js
AZURE_SEARCH_INDEX complifly-knowledge config/compass.config.js
AZURE_SEARCH_USE_VECTORS none config/compass.config.js
AZURE_TENANT_ID empty string middleware/erpAuth.middleware.js, services/social-auth.service.js
BRIDGE_LISTEN 9443 scripts/dev-mtls/ws-bridge-client.js
BRIDGE_PORT 8444 scripts/dev-mtls/ws-bridge-server.js
BRIDGE_TARGET 127.0.0.1:8443 scripts/dev-mtls/ws-bridge-server.js
BRIDGE_URL none scripts/dev-mtls/ws-bridge-client.js
CERT_DIR none scripts/dev-mtls/machineA-agent-poc.js
COMMERCIAL_CAPACITY_ALERTS_ENABLED none services/commercial/capacityAlerts.service.js
COMMERCIAL_CAPACITY_ALERTS_INTERVAL_MS none services/commercial/capacityAlerts.service.js
COMMERCIAL_PROMOTION_SWEEP_INTERVAL_MS none services/commercial/promotionSweep.service.js
COMPASS_MODEL none config/compass.config.js
COMPASS_RETRIEVAL_BACKEND local config/compass.config.js
COMPLIANCE_HOOKS_ENABLED none services/attention/config.js
COMPLIANCE_SWEEP_ENABLED none services/attention/config.js
COMPLIANCE_SWEEP_INTERVAL_MS none services/attention/config.js
CORS_EXTRA_ORIGINS none config/domains.js
CORS_ORIGINS http://localhost:8080 serverStart.js
DB_APP_NAME einvoice-service config/db.config.js
DB_CONNECTION_TIMEOUT_MS 15000 config/db.config.js
DB_DATABASE empty string config/db.config.js, scripts/audit_role_audience.js +2
DB_ENCRYPT false config/db.config.js
DB_NAME (see .env) scripts/check_stgheaderid_integrity.js
DB_PASSWORD Secret — no default config/db.config.js
DB_POOL_IDLE_TIMEOUT_MS 30000 config/db.config.js
DB_POOL_MAX 20 config/db.config.js
DB_POOL_MIN 4 config/db.config.js
DB_PORT 1433 config/db.config.js
DB_REQUEST_TIMEOUT_MS 30000 config/db.config.js
DB_SERVER empty string config/db.config.js, scripts/audit_role_audience.js +3
DB_TRUST_SERVER_CERT true config/db.config.js
DB_USER empty string config/db.config.js
DOMAIN_API https://api.complifly.in config/domains.js
DOMAIN_APP https://app.complifly.in config/domains.js
DOMAIN_DOCS https://docs.complifly.in config/domains.js
DOMAIN_FILES https://files.complifly.online config/domains.js
DOMAIN_HELP https://help.complifly.in config/domains.js
DOMAIN_MARKETING https://www.complifly.info config/domains.js
DOMAIN_STATUS https://status.complifly.tech config/domains.js
DOMAIN_TUNNEL none config/domains.js
DOTENV_PATH none config/db.config.js, config/mastersIndia.config.js +13
EF_PASS none config/nic.config.js
EF_USER none config/nic.config.js
EINV_PASS none config/nic.config.js
EINV_USER none config/nic.config.js
ERP_CONSENT_REDIRECT_URI none routes/erp-connection.routes.js
ERP_WRITEBACK_ALERTS_ENABLED false services/integration/alerter.service.js
ERP_WRITEBACK_ALLOW_INSECURE_URL false services/integration/egressGuard.js
ERP_WRITEBACK_BACKFILL_MAX_DOCS none services/integration/backfill.service.js
ERP_WRITEBACK_BATCH_SIZE none services/integration/dispatcher.service.js
ERP_WRITEBACK_CIRCUIT_COOLDOWN_CAP_SEC none services/integration/circuit.service.js
ERP_WRITEBACK_CIRCUIT_COOLDOWN_SEC none services/integration/circuit.service.js
ERP_WRITEBACK_CIRCUIT_THRESHOLD none services/integration/circuit.service.js
ERP_WRITEBACK_DEAD_RETENTION_DAYS none services/integration/retention.service.js
ERP_WRITEBACK_DISPATCH_INTERVAL_MS none services/integration/dispatcher.service.js
ERP_WRITEBACK_EMIT_ONLY false routes/writeback-subscription.routes.js, services/integration/dispatcherRunner.js +1
ERP_WRITEBACK_ENABLED false routes/writeback-subscription.routes.js, services/integration/dispatcherRunner.js +1
ERP_WRITEBACK_LEASE_SEC none services/integration/dispatcher.service.js
ERP_WRITEBACK_PURGE_BATCH none services/integration/retention.service.js
ERP_WRITEBACK_RAW_PAYLOAD_DAYS none services/integration/retention.service.js
ERP_WRITEBACK_RECONCILE_CAP none services/integration/reconciler.service.js
ERP_WRITEBACK_RECONCILE_ENABLED false services/integration/reconciler.service.js
ERP_WRITEBACK_RECONCILE_INTERVAL_MS none services/integration/reconciler.service.js
ERP_WRITEBACK_RECONCILE_LOOKBACK_DAYS none services/integration/reconciler.service.js
ERP_WRITEBACK_REQUEUE_STAGGER_SEC none services/integration/recovery.service.js
ERP_WRITEBACK_RETENTION_DAYS none services/integration/retention.service.js
ERP_WRITEBACK_RETENTION_ENABLED false services/integration/retention.service.js
ERP_WRITEBACK_RETENTION_INTERVAL_MS none services/integration/retention.service.js
ERP_WRITEBACK_SUPPORT_EMAIL empty string services/integration/alerter.service.js
ERP_WRITEBACK_WORKER_ID none services/integration/dispatcher.service.js
EWB_EXPIRY_ALERTS_ENABLED none services/notifications/ewbExpiryAlert.service.js
EWB_EXPIRY_HORIZON_HOURS none services/notifications/ewbExpiryAlert.service.js
EWB_GENERATION_GUARD ENFORCE services/ewb/ewbSubmit.service.js
EWB_PERSIST_MAX_ATTEMPTS none services/ewb/ewbSubmit.service.js
EWB_PRINT_HORIZON_MONTHS 2 services/ewb/printEwbDoc.service.js
EWB_SUBMIT_BASE_DELAY_MS none services/ewb/ewbSubmit.service.js
EWB_SUBMIT_MAX_ATTEMPTS none services/ewb/ewbSubmit.service.js
FEATURE_CACHE_TTL_MS 60000 services/effectiveFeature.service.js, services/feature.service.js
FEATURE_CATALOGUE_CHECK strict services/featureDefinition.service.js
FEATURE_DEFINITION_CACHE_TTL_MS 300000 services/featureDefinition.service.js
FILE_UPLOAD_DIR none routes/theme.routes.js, routes/upload.routes.js
FINDING_RETENTION_ENABLED false services/housekeeping/findingRetention.service.js
FINDING_RETENTION_INTERVAL_MS none services/housekeeping/findingRetention.service.js
FVU_JAR_PATH none services/tds/fvuService.js
GOOGLE_CLIENT_ID none services/social-auth.service.js
GROWTH_METRICS_ENABLED false services/housekeeping/growthMetrics.service.js
GROWTH_METRICS_INTERVAL_MS none services/housekeeping/growthMetrics.service.js
GROWTH_METRICS_PURGE_BATCH none services/housekeeping/growthMetrics.service.js
GROWTH_METRICS_RETENTION_DAYS none services/housekeeping/growthMetrics.service.js
GSP_CONFIG_CACHE_TTL_MS 60000 services/GSP/gspResolver.service.js
HEALTH_DEEP_TIMEOUT_MS none routes/health.routes.js
HOSTNAME unknown services/jwtKeys.js
HOUSEKEEPING_RUN_ON_STARTUP none services/housekeeping/jobScheduler.js
INGEST_IN_FLIGHT_TIMEOUT_MS none services/integration/ingestProtocol.service.js
INSIGHTS_ENABLED none services/insights/config.js
IRN_SUBMIT_BASE_DELAY_MS none services/IRN/irnSubmit.service.js
IRN_SUBMIT_MAX_ATTEMPTS none services/IRN/irnSubmit.service.js
JOBS_USE_QUEUE false services/queue/index.js
JWT_KEY_ID Secret — no default services/jwtKeys.js
JWT_PREVIOUS_KEY_ID Secret — no default services/jwtKeys.js
JWT_PREVIOUS_VALID_UNTIL empty string services/jwtKeys.js
JWT_SECRET Secret — no default routes/auth.routes.js, scripts/test_consent_direct.js +3
JWT_SECRET_PREVIOUS Secret — no default services/jwtKeys.js
KEY_VAULT_URL none services/secrets/index.js, services/secrets/provider.js
LOGIN_FAILURE_ALERT_PER_USER_THRESHOLD none services/observability/healthSweep.service.js
LOGIN_FAILURE_ALERT_THRESHOLD none services/observability/healthSweep.service.js
LOG_LEVEL info config/logger.js
LOG_PRETTY empty string config/logger.js
MASTER_CACHE_TTL_MS none services/mastersCache.service.js
MASTER_QUERY_CACHE_TTL_MS none services/mastersCache.service.js
MAX_FILE_SIZE_MB none routes/upload.routes.js
MTLS_CA_CERT none scripts/dev-mtls/local-mtls-gateway.js
MTLS_GATEWAY_CERT none scripts/dev-mtls/local-mtls-gateway.js
MTLS_GATEWAY_KEY Secret — no default scripts/dev-mtls/local-mtls-gateway.js
MTLS_GATEWAY_PORT 8443 scripts/dev-mtls/local-mtls-gateway.js
MTLS_UPSTREAM_URL http://127.0.0.1:5003 scripts/dev-mtls/local-mtls-gateway.js
NOTIFICATION_RETENTION_ENABLED false services/housekeeping/notificationCleanup.service.js
NOTIFICATION_RETENTION_INTERVAL_MS none services/housekeeping/notificationCleanup.service.js
ORG_SESSION_POLICY_CACHE_TTL_MS 60000 services/orgSessionPolicy.service.js
OTEL_ENABLED 0 otel-bootstrap.js
OTEL_EXPORTER_OTLP_ENDPOINT none otel-bootstrap.js
OTP_EXPIRY_MINUTES 5 services/otp.service.js
OTP_MAX_ATTEMPTS 5 services/otp.service.js
OTP_RESEND_COOLDOWN_SECONDS 60 services/otp.service.js
PAN_ENCRYPTION_KEY Secret — no default services/privacy/privacyService.js
PDF_CACHE_RETENTION_ENABLED false services/housekeeping/pdfCacheRetention.service.js
PDF_CACHE_RETENTION_INTERVAL_MS none services/housekeeping/pdfCacheRetention.service.js
PLATFORM_ALERT_MIN_SAMPLE_EWB none services/observability/healthSweep.service.js
PLATFORM_ALERT_MIN_SAMPLE_IRN none services/observability/healthSweep.service.js
PLATFORM_ALERT_MIN_SAMPLE_UPLOAD none services/observability/healthSweep.service.js
PLATFORM_ALERT_RECIPIENTS empty string services/observability/healthSweep.service.js
PLATFORM_EVENT_LOG_PURGE_BATCH none services/observability/eventLogRetention.service.js
PLATFORM_EVENT_LOG_RETENTION_DAYS none services/observability/eventLogRetention.service.js
PLATFORM_EVENT_LOG_RETENTION_ENABLED false services/observability/eventLogRetention.service.js
PLATFORM_EVENT_LOG_RETENTION_INTERVAL_MS none services/observability/eventLogRetention.service.js
PLATFORM_HEALTH_SWEEP_ENABLED none services/observability/healthSweep.service.js
POLICY_GATE_EVENTS_ENABLED none services/policy/gateHealth.js
POLICY_GATE_EVENT_THROTTLE_MS none services/policy/gateHealth.js
PORT 5003 scripts/test_consent_direct.js, scripts/test_consent_gate.js +2
QUARANTINE_ARCHIVE_ENABLED false services/housekeeping/quarantineArchive.service.js
QUARANTINE_ARCHIVE_INTERVAL_MS none services/housekeeping/quarantineArchive.service.js
QUEUE_RETENTION_ENABLED false services/housekeeping/queueRetention.service.js
QUEUE_RETENTION_INTERVAL_MS none services/housekeeping/queueRetention.service.js
REDIS_CACHE_DB 0 services/cache.js
REDIS_CACHE_OP_TIMEOUT_MS 250 services/cache.js
REDIS_HOST 127.0.0.1 config/rateLimitRedis.js, services/cache.js +1
REDIS_PASSWORD Secret — no default config/rateLimitRedis.js, services/cache.js +1
REDIS_PORT 6379 config/rateLimitRedis.js, services/cache.js +1
REDIS_RATELIMIT_DB 2 config/rateLimitRedis.js
SCHEMA_GUARD empty string config/schemaGuard.js
SECURITY_REPORT_ENABLED none serverStart.js
SENTRY_DSN none config/sentry.js
SENTRY_TRACES_SAMPLE_RATE none config/sentry.js
SESSION_MAX_CONCURRENT 1 services/orgSessionPolicy.service.js, services/session.service.js
SESSION_REAPER_ENABLED false services/housekeeping/sessionCleanup.service.js
SESSION_REAPER_INTERVAL_MS none services/housekeeping/sessionCleanup.service.js
SESSION_TOUCH_THROTTLE_MS 60000 services/session.service.js
SESSION_TTL_HOURS 8 services/session.service.js
SHUTDOWN_TIMEOUT_MS none serverStart.js
TDS_CSI_RETENTION_DAYS none services/housekeeping/fileLifecycle.service.js
TDS_CSI_RETENTION_ENABLED false services/housekeeping/fileLifecycle.service.js
TDS_CSI_RETENTION_INTERVAL_MS none services/housekeeping/fileLifecycle.service.js
TDS_OUT_DIR none services/tds/fvuService.js
THEME_ASSET_MAX_MB none routes/theme.routes.js
THEME_CACHE_TTL_MS 60000 services/theme.service.js
THEME_SVG_SANITIZE_ENABLED empty string routes/theme.routes.js
USER operator scripts/opcontrols_phase3.js
USERNAME none scripts/opcontrols_phase3.js
USER_SESSION_POLICY_CACHE_TTL_MS 60000 services/userSessionPolicy.service.js
VERIFY_CACHE_ENABLED empty string services/verificationCache.service.js
VERIFY_CACHE_TTL_GSTIN_DAYS 7 services/verificationCache.service.js
VERIFY_CACHE_TTL_PAN_DAYS 90 services/verificationCache.service.js
VERIFY_CACHE_TTL_UDYAM_DAYS 90 services/verificationCache.service.js
WEBSITE_API_KEY_HASH Secret — no default routes/privacy.routes.js, scripts/setup_website_dpdp.js +1
WEBSITE_NOTICE_VERSION_ID none routes/privacy.routes.js, scripts/setup_website_dpdp.js +1
WEBSITE_PLAINTEXT_KEY Secret — no default scripts/test_website_dpdp.js
WEBSITE_TENANT_ID COMPLIFLY_WEBSITE routes/privacy.routes.js, scripts/test_website_dpdp.js
WORKER_CONCURRENCY 4 worker.js

196 variables. Generated from the process.env reads in the backend — do not edit by hand; run node tools/docs/gen-env-reference.js --write.

Validation

After editing the environment file:

Check Method Pass condition
File is valid Restart the API and read the startup log Starts cleanly
Restart actually happened Compare the process start time Recent. Editing without restarting is the most common "the change did nothing" cause
Worker restarted too Check the worker Restarted, if the setting affects background work
Database settings correct Application starts and screens load Connected
Port matches the proxy Request through the proxy HTTP 200, not 502
Provider settings correct Run the connection test Success
Mail works Trigger a password reset to a real mailbox Delivered
Queue works, if enabled Submit an upload Job completes rather than queuing indefinitely
Permissions restricted Inspect the file's permissions Readable only by the service account and administrators
Not in source control Check the repository Absent
Sandbox and production separated Inspect provider URLs per environment Non-production points only at sandbox

Troubleshooting

Symptom Cause Action
Change had no effect No restart, or the wrong process restarted Restart the API, and the worker if relevant
API will not start after an edit Malformed file, or a required value missing Read the startup log; it usually names the setting
HTTP 502 through the proxy Port mismatch between service and proxy Align them
Users cannot sign in after a change The token signing secret changed Every existing session is invalidated. Expected, but only if intended
Encrypted identity values unreadable The encryption key changed or was lost Not recoverable from a database backup. Restore the original key
No emails at all Mail settings wrong, or the mail path blocked Test from the application host. This locks users out silently
Uploads never process Queue-backed jobs enabled with the queue unreachable Check the queue. No error appears in the interface
Master data or a feature flag change takes a moment to appear everywhere Normal — the relevant TTL hasn't elapsed on every app server yet Expected. Shorten the TTL if the delay genuinely matters for this deployment
Rate limits seem looser than documented across a multi-server deployment Redis unreachable — the limiter fails open rather than blocking the request Restore the Redis connection. This is deliberate: a caching outage must never turn into an availability outage
Provider calls fail after working Credentials rotated, endpoint changed, or egress address changed Run the connection test; check the egress address the provider sees
Write-back stops after a burst of failures The circuit breaker opened Expected protective behaviour. Fix the endpoint; delivery resumes after cooldown
Dead-lettered events disappeared before investigation Dead-letter retention shorter than reaction time Raise it. Evidence purged before anyone looks is evidence lost
Verification data is stale Cache window longer than the business needs Shorten the relevant window, accepting the extra provider cost
Environments diverge Configuration drift Compare files between environments regularly