Skip to content

Environment variables ​

Every variable the AlianHub server and web app read, grouped the way the code groups them: 317 in all. Most installs set five or six (see Requirements and Install with Docker); everything else has a sensible default or switches on an optional feature.

Where the values come from ​

WhereUsed byNotes
The process environment (Docker environment:, systemd Environment=, cPanel variables)the serverAlways wins.
.env in the app folder (next to package.json)the serverRead once at boot by Config/applyEnv.js; a key already in the environment keeps the environment's value. The Docker image has no .env: docker compose reads the .env next to docker-compose.yml and passes it in.
Settings › Instance › Settings (stored in global.instance_settings)the serverOnly the keys the console lists. A key also set in the environment or .env shows as env and is locked. See The Instance console.
frontend/.env (VUE_APP_*)the web app buildBaked in when you run npm run build in frontend/; changing one means rebuilding.

So the order is: environment, then .env, then the Instance console, then the built-in default. A change to .env needs a restart. A change in the console applies on save, except the few marked "restart".

Secrets

Variables marked (secret) are keys and passwords. Keep them out of version control and out of backups you share. The Instance console stores the secrets it holds encrypted with a key derived from JWT_SECRET, so keep JWT_SECRET stable: rotating it makes those stored secrets unreadable and they read as unset.

How this page is made ​

The reference below is a copy of docs/ENV.md in the repository. That file is generated by node scripts/env-doc.js from the variables the code actually reads, with descriptions from scripts/env-doc.meta.json; the same script writes the generated block of .env.example and frontend/.env.example. npm run env:doc:check fails when any of them is behind the code. "Read by" names the files that read each variable, relative to the repository root. A default shown here is the code's own default; the Docker image and npm run setup set a few differently (STORAGE_TYPE=server, NODE_ENV=production in the image).

Sections: Server · Database · Auth · Mail · Storage · AI · Calling · Integrations · Billing · Security · Logging · Limits · Jobs · Frontend · General

Server ​

VariableRequiredDefaultDescriptionRead by
APIURLyesPublic base URL of this API, with a trailing slash (used in emails, OAuth callbacks and share links).Config/config.js, Config/contentSecurityPolicy.js and others
APP_NAMEAlian HubProduct name shown in emails and page titles.Config/config.js, Modules/DesktopTracker/controller.js and others
BUILD_INFO_GIT_TIMEOUT_MS10000Per-command timeout, in ms, for the git calls that resolve the running version. On timeout the server reports channel unknown and retries in the background.scripts/build-info.js
NODE_ENVdevelopmentdevelopment | productionConfig/config.js, Config/cookies.js and others
PORTyes4000TCP port the HTTP server listens on.Config/config.js
SERVER_HEADERS_TIMEOUT_MS66000How long the HTTP server waits for a full request header block; always kept above the keep-alive timeout (default 66000).Modules/Agents/engine/timeouts.js
SERVER_KEEP_ALIVE_TIMEOUT_MS65000Idle keep-alive timeout of the HTTP server; keep it above the idle timeout of any proxy in front (default 65000).Modules/Agents/engine/timeouts.js
SERVER_REQUEST_TIMEOUT_MS300000How long the HTTP server waits to receive a whole request (default 300000). Server-sent event streams are exempt.Modules/Agents/engine/timeouts.js
TRUST_PROXYloopbackExpress trust proxy setting; loopback by default so X-Forwarded-For is honoured behind a local reverse proxy.index.js
UNDER_MAINTENANCEfalsetrue | falseConfig/config.js
WEBURLhttp://localhost:4000Public base URL of the web app, used in emails and CORS.Config/config.js, Config/contentSecurityPolicy.js and others

Database ​

VariableRequiredDefaultDescriptionRead by
MONGO_CONNECT_TIMEOUT_MS60000How long a new MongoDB connection may take to establish (default 60000).Modules/Agents/engine/timeouts.js
MONGO_MIN_POOL_SIZEMinimum connections kept open per company database.utils/mongo-handler/mongoConnector.js
MONGO_POOL_SIZEMaximum connections per company database.utils/mongo-handler/mongoConnector.js
MONGO_SERVER_SELECTION_TIMEOUT_MS60000How long the MongoDB driver waits to find a usable server before failing an operation (default 60000).Modules/Agents/engine/timeouts.js
MONGO_SOCKET_TIMEOUT_MS300000Inactivity timeout on a MongoDB socket before it is closed (default 300000).Modules/Agents/engine/timeouts.js
MONGO_WAIT_QUEUE_TIMEOUT_MSHow long a query waits for a free connection before failing.utils/mongo-handler/mongoConnector.js

Auth ​

VariableRequiredDefaultDescriptionRead by
JWT_ALGORITHMHS256Algorithm used to sign JWTs.Config/jwt.js, Modules/Auth/helpers/refreshTokenRules.js and others
JWT_EXP24hAccess token lifetime (jsonwebtoken duration string).Config/jwt.js, Modules/Auth/controller/loginSession.js and others
JWT_SECRET (secret)yesyour_jwt_secret_hereSecret used to sign JWTs and derive encryption keys. Also keys audit redaction pseudonyms when AUDIT_CHAIN_KEY is unset or invalid.Config/hostingMode.js, Config/instanceSettings.js and others
MAGIC_LINK_ENABLEDtrueSet to false to disable passwordless magic-link sign-in.Modules/Auth/controller/magicLink.js
REFRESH_TOKEN_REUSE_GRACE_SECONDS10Seconds after a refresh token is rotated during which presenting the old one again is refused without revoking the session, so parallel requests or tabs do not log the user out. Past this window a reused token revokes the session.Modules/Auth/helpers/refreshTokenRules.js
SESSIONEXPIREDTIME172800Idle time after which a login session expires.Modules/Auth/controller/loginSession.js, Modules/Auth/helpers/refreshTokenRules.js and others
SESSION_COOKIE_HTTPONLYSet to on to mark the session cookies httpOnly so page JavaScript can no longer read them: the server reads the access token from the cookie when no Authorization header is sent, the socket handshake and the refresh endpoint do the same, and the web app authenticates by cookie instead of copying tokens into headers. Tracker, CLI and API-token clients are unaffected (they send headers and bodies, never cookies). Unset or off keeps readable cookies as before.Config/cookies.js
TRACKER_PKCE_LEGACY_UNTILISO date from which desktop tracker builds that sign in without a PKCE verifier are refused. Unset (the default) keeps those builds working; an unparseable value reads as unset.Modules/Auth/helpers/trackerCode.js
TWO_FACTOR_ENC_KEY (secret)key material for encrypting TOTP secrets at restModules/Auth/helpers/twoFactorRules.js
TWO_FACTOR_ISSUERAlianHublabel shown in the user's authenticator appModules/Auth/helpers/twoFactorRules.js
TWO_FACTOR_TEMP_SECRET (secret)`${process.env.JWT_SECRET''}::2fa-temp`

Mail ​

VariableRequiredDefaultDescriptionRead by
AWS_SES_FROM_DEFAULTFrom address for mail sent through Amazon SES, on a domain verified in SES. The sender name is APP_NAME.Config/config.js
AWS_SES_KEY (secret)Access key ID of an IAM user allowed to send through Amazon SES. With AWS_SES_SECRET and AWS_SES_REGION set, SES sends all mail, ahead of Resend and SMTP.Config/config.js
AWS_SES_REGIONAWS region of the Amazon SES account, e.g. us-east-1.Config/config.js
AWS_SES_SECRET (secret)Secret access key for AWS_SES_KEY.Config/config.js
EMAIL_IN_DOMAINinbox.alianhub.comDomain of the inbound-email addresses that create tasks.Modules/EmailIn/controller.js
NODEMAILER_ALLOW_SELF_SIGNEDAccept self-signed certificates from the SMTP server.Modules/mailProvider.js
NODEMAILER_EMAILyour@email.comFrom address; also the SMTP login when NODEMAILER_USER is unset.Config/config.js
NODEMAILER_HOSTsmtp.gmail.comSMTP host.Config/config.js
NODEMAILER_USERSMTP login, only when it differs from NODEMAILER_EMAIL (e.g. an Amazon SES Mail Manager ingress endpoint ID). NODEMAILER_EMAIL stays the From address.Config/config.js
SUPPORT_MAILMailbox that gets a copy of each message the support account (VUE_APP_USEREMAIL) posts in the support project (VUE_APP_SUPPORT_PROJECTID). Unset: no support mail is sent.Modules/EmailNotification/supportMail.js

Storage ​

VariableRequiredDefaultDescriptionRead by
CLOUD_STORAGE_ENC_KEY (secret)Key used to encrypt cloud-storage provider credentials at rest.Modules/CloudStorage/helpers/cloudCrypto.js
IAM_ENDPOINThttps://iam.wasabisys.comWasabi IAM endpoint used when provisioning per-company storage users.Config/aws.js
S3_ACCESS_KEY (secret)Access key for object storage. Falls back to WASABI_ACCESS_KEY. A new key applies to the next request, with no restart.common-storage/s3Config.js
S3_BUCKETOne bucket for every file (prefix mode). Set: files go to <S3_PREFIX><companyId>/… and avatars and public files to <S3_PREFIX>public/…, and no bucket is ever created or deleted. Empty: each workspace keeps a bucket named after its id and public files go to USERPROFILEBUCKET, as before. Move an install with npm run storage:migrate -- --to-prefix.common-storage/s3Config.js
S3_CONNECTION_TIMEOUT_MSConnection timeout for S3-compatible storage calls.Config/config.js
S3_ENDPOINTAddress of the S3-compatible service, e.g. https://blr1.digitaloceanspaces.com, https://s3.ap-southeast-1.wasabisys.com or http://minio:9000. Falls back to WASABIENDPOINT; empty uses Amazon S3. Applied without a restart.Config/contentSecurityPolicy.js, common-storage/s3Config.js
S3_FORCE_PATH_STYLEfalsetrue puts the bucket in the URL path instead of the host name, for MinIO and similar servers.common-storage/s3Config.js
S3_PREFIXOptional key prefix inside S3_BUCKET, e.g. ins_01jd3k8w/ or prod/. A trailing slash is added when missing; empty, "." and ".." parts are refused.common-storage/s3Config.js
S3_PUBLIC_BASE_URLOptional address (a CDN) that serves public files such as email images, as <S3_PUBLIC_BASE_URL>/<key>. In prefix mode it should serve <S3_BUCKET>/<S3_PREFIX>public/; the bucket itself is never made public.Config/contentSecurityPolicy.js, common-storage/s3Config.js
S3_REGIONRegion of the S3-compatible service, e.g. blr1 or us-east-1. Falls back to WASABI_REGION.common-storage/s3Config.js
S3_SECRET_KEY (secret)Secret key for object storage. Falls back to WASABI_SECRET_ACCESS_KEY.common-storage/s3Config.js
S3_SOCKET_TIMEOUT_MSSocket timeout for S3-compatible storage calls.Config/config.js
STORAGE_DOWNLOAD_SCOPEreportWhich stored files a signed download link is issued for. report (the default): every link is issued as before, and each one enforce would refuse is logged as 'stored-file download would be refused' with the key's layout type (task_attachment, channel_comment, form_upload, unknown, ...), a reason category (not_found, no_access, invalid_path, unknown) and a running count per pair, never the key or path. enforce: a link is issued only for a file attached to a record the caller may open (a task, project, comment, channel, direct message, form, clip, reminder, timesheet screenshot, or a company settings image); any other key answers 404 with code STORED_FILE_NOT_AVAILABLE. The same mode governs task attachment writes: under report a task attachment that names a file not stored for that task is written and logged as 'attachment write would be refused' with a reason category (other_task, form_upload, clip, unknown) and a count; under enforce it is refused with 400 ATTACHMENT_KEY_NOT_OWN. An attachment url that is not text is refused in both modes. Removing or uploading a stored file does not follow this switch: those routes always check that the caller may change the record the file belongs to, and refuse a key outside every layout (logged as 'stored-file remove refused' or 'stored-file upload refused' with the layout type and the key's shape, never an id or a file name). A file on a doc comment does not follow it either: its link is issued only to someone who can read the doc, in both modes. Anything else reads as report. Switch plan: run one release on report, check the logged counts, and set enforce once no layout other than unknown shows refusals that a legitimate screen caused.common-storage/storedFileScope.js
STORAGE_TYPEyeswasabis3 | wasabi | server. s3 and wasabi are the same S3-compatible driver (see the S3_* variables). With server, uploads are served from the app's own origin, and whatever CSP_MODE is: every upload response carries X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy; images (including SVG), PDF, plain text, audio and video still preview inline; every other file (scripts, HTML, XML, unknown types) downloads as application/octet-stream.Config/config.js, Config/contentSecurityPolicy.js and others
TRACKSHOT_RETENTION_DAYS180Days to keep time-tracker screenshots before deletion.Modules/storage/server/helpers/bucket.helper.js, Modules/storage/wasabi/controller.js
USERPROFILEBUCKETyour-bucket-nameWith a bucket per workspace (S3_BUCKET empty): the bucket that holds avatars and public files. Not used in prefix mode.Config/aws.js, Config/config.js and others
WASABIENDPOINThttps://s3.wasabisys.comOlder name of S3_ENDPOINT, still read when S3_ENDPOINT is empty.Config/aws.js, common-storage/s3Config.js
WASABI_ACCESS_KEY (secret)Older name of S3_ACCESS_KEY, still read when S3_ACCESS_KEY is empty.Config/aws.js, common-storage/s3Config.js
WASABI_REGIONus-east-1Older name of S3_REGION, still read when S3_REGION is empty.Config/aws.js, common-storage/s3Config.js
WASABI_SECRET_ACCESS_KEY (secret)Older name of S3_SECRET_KEY, still read when S3_SECRET_KEY is empty.Config/aws.js, common-storage/s3Config.js
WASABI_USERIDWasabi account user id used for IAM policies.Config/aws.js

AI ​

VariableRequiredDefaultDescriptionRead by
AGENT_ACT_BUDGET_MS300000Wall-clock budget for the act phase of an agent run, added to the model timeout when sizing the job lock (default 5 min).Modules/Agents/engine/timeouts.js
AGENT_PERFORMANCE_READoffThe performance.read agent action: "on" registers it and offers it over MCP, so an agent can read logged time, estimate against actual, sprint velocity and cumulative flow for up to 5 projects over at most 120 days, as the person behind the agent and with each query and its numbers kept in the replay record; default "off" leaves the registry and the MCP tool list as they were.Modules/Agents/performanceFlag.js
AI_API_KEY (secret)API key for the configured LLM provider (LLM_PROVIDER).Config/config.js
AI_BUDGET_CACHE_MS60000How long a workspace's AI budget settings are kept in memory before the next call reads them again; a change made in settings is seen at once on this server.Modules/Agents/budget.js
AI_ENABLEDtrueThe instance-wide AI switch. Set to false to stop every AI call on this instance: no prompt, text or audio is sent to any model provider, AI screens say AI is turned off, agent runs are refused and knowledge chunks are written without vectors. Each workspace also has its own switch (Settings, owners and admins), stored on the company as aiSwitch.enabled; either one being off is enough. Also settable under Instance > Settings > AI.Modules/AICore/aiSwitch.js
AI_FIELD_BULK_MAX200Most tasks one AI field bulk fill may take; a larger selection is refused before any model call.Modules/CustomField/aiFields/limits.js
AI_FIELD_DAILY_LIMIT500AI field fills a workspace may make per UTC day, counted from the spend ledger; a bulk fill stops when it is reached.Modules/CustomField/aiFields/limits.js
AI_FIELD_REFILL_DEBOUNCE_MS60000How long an AI field with auto-refill waits after the last change to a task part it reads before it fills again.Modules/CustomField/aiFields/autoRefill.js
AI_MAX_TASK_MINUTESUpper bound, in minutes, for a task the AI project generator may plan.Modules/AIProjectGenerator/planRules.js
AI_MODELgpt-4.1recommended: gpt-4o or gpt-4.1 for best instruction-following on complex plansConfig/config.js
AI_MODEL_ROUTERoffmodel router: "on" lets a caller pick the provider and model per call and lets the workspace routing policy name a model per task class; default "off" keeps the single configured provider answering everything, so a saved policy changes nothingModules/AICore/llmProvider/normalise.js
AI_REPLAYagentWhich model calls keep a replay record (prompt, raw response, model, revisions; redacted): off, agent (agent runs only) or all.Modules/AICore/replay.js
AI_REPLAY_RETENTION_DAYS30Days a replay record is kept before the TTL index deletes it.Modules/AICore/replay.js
AI_RESERVATION_TTL_MS900000How long a budget reservation holds a workspace's money before it is treated as stranded and stops counting; a process killed mid-call leaves one behind.Modules/AICore/reservation.js
AI_ROUTER_BACKOFF_MAX_MS8000ceiling for one backoff wait, and for a retry-after hint the provider returnsModules/AICore/llmProvider/router.js
AI_ROUTER_BACKOFF_MS500base delay for the jittered exponential backoff between retries of a transient provider errorModules/AICore/llmProvider/router.js
AI_ROUTER_BREAKER_COOLDOWN_MS30000how long a breaker stays open before it half-opens for one probe; doubles on each failed probeModules/AICore/llmProvider/health.js
AI_ROUTER_BREAKER_MAX_COOLDOWN_MS300000ceiling for the doubling breaker cooldownModules/AICore/llmProvider/health.js
AI_ROUTER_BREAKER_THRESHOLD0.5share of provider-caused failures in the window that opens the breaker (0..1)Modules/AICore/llmProvider/health.js
AI_ROUTER_BREAKER_VOLUME5calls needed in the health window before the circuit breaker may open, so one bad call cannot trip itModules/AICore/llmProvider/health.js
AI_ROUTER_DEFAULT_RPM0requests per minute for a provider with no entry in AI_ROUTER_RATE_LIMITS; 0 means no bucketModules/AICore/llmProvider/rateLimit.js
AI_ROUTER_HEALTH_SAMPLES50most recent calls kept per provider and model in the health windowModules/AICore/llmProvider/health.js
AI_ROUTER_HEALTH_WINDOW_MS300000how far back the per-provider health window looks when computing success rate, latency and the breakerModules/AICore/llmProvider/health.js
AI_ROUTER_MAX_ATTEMPTS2attempts on one provider before the router fails over to the next candidate (router flag on only)Modules/AICore/llmProvider/router.js
AI_ROUTER_RATE_LIMITSper-provider request-per-minute budgets as token buckets, e.g. "openai:500,anthropic:200"; empty means no bucketModules/AICore/llmProvider/rateLimit.js
AI_ROUTER_RATE_WAIT_MS2000longest the router waits for a rate-limit token before taking the next candidate insteadModules/AICore/llmProvider/rateLimit.js
ANTHROPIC_API_KEY (secret)Anthropic API key, used when LLM_PROVIDER is anthropic.Modules/AICore/llmProvider/anthropicProvider.js, Modules/AICore/providerKeys.js
ANTHROPIC_MODELclaude-sonnet-4-5-20250929current Sonnet 4.5 model idModules/AICore/llmProvider/anthropicProvider.js
ANTHROPIC_TIMEOUT_MS600000optional: override the Anthropic SDK timeout (default LLM_TIMEOUT_MS, 10 min); the agent job lock is derived from the largest provider timeoutModules/Agents/engine/timeouts.js
DEEPSEEK_API_KEY (secret)DeepSeek API key, used when LLM_PROVIDER is deepseek.Config/config.js
DEEPSEEK_BASE_URLhttps://api.deepseek.comoptional: override DeepSeek base URL (e.g. for a compatible proxy)Modules/AICore/llmProvider/deepseekProvider.js
DEEPSEEK_MODELdeepseek-v4-flashdeepseek-chat / deepseek-reasoner / deepseek-v4-flash / deepseek-v4-pro (all V4: 1M ctx, up to 384K output)Config/config.js
DEEPSEEK_TIMEOUT_MS600000optional: override the DeepSeek axios timeout (default: LLM_TIMEOUT_MS for reasoner, 4 min for chat); the agent job lock is derived from the largest provider timeoutModules/Agents/engine/timeouts.js
DISPATCHERoffThe dispatcher: "on" lets a project's owner or admin switch its dispatcher to suggest or apply (Project > Agents). Each task created or changed in such a project is then routed to a role from Modules/Agents/roles: the project's routing rules first, in order (task type, tags, priority, status, list and custom field values), a model guess only when one is plugged in, a model is configured and the guess reaches the project's confidence, otherwise the task waits in the project's Needs routing list. Suggest shows the role on the task for a lead to accept, dismiss or change; apply puts the task in the role's agent work queue at once. A role off for the project, a paused project and paused connected agents route nothing. Every routing and every lead's choice is written to the audit log. Default "off" routes nothing and closes the dispatcher's write routes.Modules/AssignmentRules/dispatcher/flag.js, Modules/Workflows/flag.js
ESTIMATE_SAMPLESNumber of historical tasks sampled when the AI estimates effort.Modules/EstimatedTime/aiTaskEstimator.js
ESTIMATE_TARGETTarget accuracy band the AI estimator calibrates against.Modules/EstimatedTime/aiTaskEstimator.js
EXTERNAL_AGENT_SESSIONSoffSet to on (true, 1 and yes mean the same) to let a person delegate a task to an approved outside OAuth client (MCP_OAUTH). The person must be able to edit the task and hold a live grant to that client in the workspace; the assignee stays, and an unassigned task is given to the delegating person, who is notified. A task in a private sprint can be delegated only by a member of that sprint and only to a client whose workspace approval has the private-sprint opt-in; without the approvals collection it is refused. An owner or admin sets each client's delivery URL under PUT /api/v2/agent-sessions/endpoints (https on a public host, or http to a host in WEBHOOK_ALLOWED_PRIVATE_HOSTS, and on the workspace egress allowlist when AGENT_EGRESS_ALLOWLIST is on) and receives its signing secret once. Delegating sends a webhook through the agents' egress gateway, with X-AlianHub-Timestamp (Unix seconds) and X-AlianHub-Signature, sha256= the HMAC-SHA256 of <timestamp>.<body> under that secret; receivers should reject a timestamp more than 300 seconds from their clock (replayWindowSeconds in the body), carrying the session id, the task and a five-minute session handle, never a token; the ten-second first-activity clock starts when the client answers 2xx, and an untaken offer is marked unresponsive then, a late first activity is refused however it arrives (a sweep every 15 seconds and on boot decides timers lost to a restart). The client reports over /mcp with the tools session.activity (thought, action, elicitation, response or error, 4000 characters, 60 a minute), session.complete and session.fail, each needing tasks:write, the session's own client and grant, an open session and a person behind the grant who can still open the task; the first call carries the handle. Sessions count as tainted and are audited as the client acting for the person. Revoking the grant, the client or its workspace approval, withdrawing the private-sprint opt-in, or moving the task into a private sprint the delegating person is not on marks open sessions revoked. An unassigned task can be delegated only by someone who may assign it. The task panel's agent strip shows the session and its latest activities, pushed over the task's socket room to people who can open the task. Unset or off registers none of this (no routes, no JWT prefix, no session tools or scopes, no socket relay) and the strip, the MCP tool list and the socket rooms are as before.Modules/AgentSessions/config.js
EXTERNAL_AGENT_STEPSoffSet to on (true, 1 and yes mean the same), with EXTERNAL_AGENT_SESSIONS and WORKFLOW_ENGINE on, to register the external_agent workflow step type: its config names an outside OAuth client approved in the workspace (clientId), a task (taskId, or the task the run was started on) and an optional deadline (deadlineMs, default one hour, never past the run's own). On its first claim the step delegates the task to that client as the person who started the run, one session per run and step however often it is claimed, and waits without holding a worker; it succeeds with the session's response when the client completes it, and fails, without retrying, when the client reports an error, the session goes unresponsive or is revoked, or the deadline passes, which also closes the session. Every session call and every tool call from the grant of a session bound to a waiting step is checked live: the session open, the run running, the step still waiting for that session, and the grant and client approval live; a failed check refuses the call and closes the session. A call with an access token whose grant was revoked is answered 401, revokes the sessions that grant held on waiting steps, fails those steps with the reason and records the refusal as the client acting for the delegating person. The step never hands its step credential to the client. Unset or off, the step type is not registered, offered to the builder or accepted in a definition.Modules/Workflows/flag.js
GOOGLE_API_KEY (secret)Google AI Studio API key, used when LLM_PROVIDER is google.Modules/AICore/llmProvider/googleProvider.js, Modules/AICore/providerKeys.js
GOOGLE_BASE_URLhttps://generativelanguage.googleapis.comoptional: override the Google generative-language base URL (e.g. for a compatible proxy)Modules/AICore/llmProvider/googleProvider.js
GOOGLE_MODELgemini-3-flashGemini model id, e.g. gemini-3-flash / gemini-3-pro / gemini-2.5-flashModules/AICore/llmProvider/googleProvider.js
GOOGLE_TIMEOUT_MS600000optional: override the Google axios timeout (default: LLM_TIMEOUT_MS for thinking models, 4 min for the rest)Modules/Agents/engine/timeouts.js
KNOWLEDGE_AGENT_MEMORYoffAgent memory in the knowledge index: "off" publishes and indexes nothing, so agent notes stay in the LangGraph store only; "on", for a company whose knowledge indexer is on, indexes each agent note (agent_memory, namespace agent/<agentId>/note) into knowledge_chunks under scope "agent" off the memory events, backfills existing notes in the knowledge.memory.backfill job, and lets an agent's own runs retrieve its notes once that backfill is complete: only within the run's visible set (the starter's projects, narrowed to the agent's), only when every project the note was formed in is in that set, ranked below what people wrote, and with origin "external" when a tainted run formed it. People asking through Ask never see agent notes.Modules/Knowledge/memory/flag.js
KNOWLEDGE_ATLAS_CANDIDATE_RATIO10With KNOWLEDGE_VECTOR_STORE=atlas, how many candidates Atlas considers for each chunk it returns (numCandidates is at least this times the limit, at most 10000).Modules/Knowledge/adapters/atlas.js
KNOWLEDGE_ATLAS_INDEX_BUDGET_MS30000With KNOWLEDGE_VECTOR_STORE=atlas, how long, in milliseconds, the knowledge.backfill job gives one tenant's index check before moving on; tenants are checked four at a time.Modules/Knowledge/vectorStore.js
KNOWLEDGE_ATLAS_INDEX_TIMEOUT_MS5000With KNOWLEDGE_VECTOR_STORE=atlas, how long, in milliseconds, each step of a tenant's index check (listing, creating or updating the search index, reading a stored vector) may take before the check records the index state as timed out. Questions never wait on a check.Modules/Knowledge/adapters/atlas.js
KNOWLEDGE_ATLAS_MAX_TIME_MS3000With KNOWLEDGE_VECTOR_STORE=atlas, how long, in milliseconds, one vector search may run before the question is answered from the lexical side (the backend then reads "lexical (vector store timed out)").Modules/Knowledge/adapters/atlas.js
KNOWLEDGE_ATLAS_NUM_CANDIDATES1000With KNOWLEDGE_VECTOR_STORE=atlas, the fewest nearest chunks Atlas considers per source type for a question ($vectorSearch numCandidates); raised to KNOWLEDGE_ATLAS_CANDIDATE_RATIO times the limit when that is more, and at most 10000.Modules/Knowledge/adapters/atlas.js
KNOWLEDGE_ATLAS_THIN_RESULTS3With KNOWLEDGE_VECTOR_STORE=atlas, when Atlas returns a full limit of chunks but fewer than this many passages survive the access check, the answer's backend reads "lexical+vector-atlas(thin)", so a vector side crowded out by chunks the asker may not see is visible.Modules/Knowledge/adapters/atlas.js
KNOWLEDGE_ATLAS_VECTOR_LIMIT100With KNOWLEDGE_VECTOR_STORE=atlas, how many chunks per source type Atlas returns for a question ($vectorSearch limit) before they are narrowed by access and collapsed to one passage per source; never fewer than the passages asked for.Modules/Knowledge/adapters/atlas.js
KNOWLEDGE_EMBEDDING_DIMENSIONSWith KNOWLEDGE_VECTOR_STORE=atlas, the vector size the Atlas index is built for. Leave unset to take it from a stored vector of KNOWLEDGE_EMBEDDING_MODEL, or from the model (1536 for text-embedding-3-small, 3072 for text-embedding-3-large); set it for any other model. A value that disagrees with a stored vector of the model, or with the model's known size, builds no index: the console shows the index as the wrong size and questions stay lexical.Modules/Knowledge/adapters/atlas.js
KNOWLEDGE_EMBEDDING_MODELtext-embedding-3-smallThe OpenAI embedding model a hybrid company's chunks and questions are embedded with. It must have a price on file (text-embedding-3-small and text-embedding-3-large do; LLM_PRICING adds another); a change re-embeds each chunk on its next write and, a batch per run, from the recurring knowledge.backfill job.Modules/Knowledge/embeddings.js
KNOWLEDGE_EMBED_QUERY_TIMEOUT_MS2000How long a question waits for its embedding before Ask answers from the lexical side alone (the backend then reads "lexical (embed timed out)"). Embedding at ingest keeps the provider's own timeout.Modules/Knowledge/embeddings.js
KNOWLEDGE_FIGURES_MAX_TIME_MS10000How long, in milliseconds, the database may spend reading one workspace's knowledge figures for the instance console before the console says the figures timed out.Modules/Knowledge/figures.js
KNOWLEDGE_FILE_MAX_BYTES10485760The largest task attachment, in bytes, whose text the knowledge indexer extracts. A larger file is recorded as skipped and never read.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FILE_MAX_CHARS200000How many characters of one file's text the knowledge indexer keeps; the rest is cut and a truncation marker added.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FILE_MAX_PAGES200How many pages of a PDF the knowledge indexer reads.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FILE_MAX_PARSE_MEMORY_BYTES2 x KNOWLEDGE_FILE_MAX_UNZIPPED_BYTES + 134217728How much memory one file's parser thread may hold (its heap plus off-heap buffers, measured by the thread itself where the parser yields, with the process's resident growth watched as a backstop for a parser that never yields) before the knowledge indexer stops it and records the file for a retry. By default twice KNOWLEDGE_FILE_MAX_UNZIPPED_BYTES (the rebuilt archive plus one entry being copied into it) plus 128 MB for the parser.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FILE_MAX_ROWS5000How many rows of each sheet or CSV the knowledge indexer reads.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FILE_MAX_SHEETS20How many sheets of a spreadsheet the knowledge indexer reads.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FILE_MAX_UNZIPPED_BYTES104857600The most a DOCX or XLSX may inflate to. The knowledge indexer refuses one that declares more without opening it, and inflates every entry itself against this budget, whatever the archive declares, before a parser sees it.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FILE_TIMEOUT_MS20000How long the knowledge indexer lets one file's parser run before stopping its thread and recording the file as timed out. A failed or timed-out file is retried at most twice.Modules/Knowledge/ingest/extract/limits.js
KNOWLEDGE_FLAG_CACHE_TTL_SECONDS30Seconds a company's knowledgeRetrieval and knowledgeIndexer modes are kept after one read, so a question, a sync and a retry share it; 0 reads the row on every call, which the e2e harness sets so a test sees a switch it just flipped.Modules/Knowledge/flag.js
KNOWLEDGE_INDEXERoffKnowledge indexer: "off" subscribes to nothing and reads or writes no chunk; "tenant" ingests page, comment, call-notes, project-guide and task-attachment events into knowledge_chunks (pages and guides split on headings; attachment text extracted off the request path within the KNOWLEDGE_FILE_* limits; tombstoned on delete, on a deleted task or a removed attachment for comments and files, on project trash for pages, comments, guides and files, and on a member's departure for private pages), backfills each source separately, and lets retrieval read a source's passages from the chunks once that source's backfill is complete, for a company whose knowledgeIndexer.mode is "on"; "all" does so for every company except one set to "off".Modules/Knowledge/flag.js
KNOWLEDGE_RETRIEVALoffKnowledge retrieval for Ask: "off" gathers sources exactly as before (task fields and page titles by regular expression); "tenant" searches tasks, page bodies and comments through the full-text indexes, and the caller's own call transcripts, with access control applied at query time, for a company whose knowledgeRetrieval.mode is "on" or "hybrid"; "all" does so for every company except one set to "off". A company in "hybrid" also has its chunks embedded as they are indexed (KNOWLEDGE_EMBEDDING_MODEL, with the OpenAI instance key) and each question embedded once, and its answers fuse the lexical and vector candidates by reciprocal rank under the same access filter; "on" stays lexical.Modules/Knowledge/flag.js
KNOWLEDGE_TOMBSTONE_RETENTION_DAYS30Days a tombstoned knowledge chunk (a deleted, trashed, departed or removed source) is kept before the knowledge.backfill job removes it, once a day per company. A tombstone the indexer reads back (a deleted task's comments and files, a departed member's agent notes, a file's skip or failure outcome) keeps its row with the text and vector emptied; a file still owed an extraction is left alone. 0 keeps every tombstone.Modules/Knowledge/ingest/purge.js
KNOWLEDGE_VECTOR_STORElocalWhere hybrid knowledge retrieval searches vectors: "local" scores the newest chunks of each source in process from the tenant database (any MongoDB); "atlas", for hosted deployments on MongoDB Atlas, searches the same chunk rows with Atlas Vector Search through one knowledge_chunks_vector index in the database of each tenant whose knowledgeRetrieval mode is "hybrid", created or checked in the background at tenant creation and by the knowledge.backfill job (which also creates it once a tenant switches to hybrid), with the index state shown in the instance knowledge console. Atlas caps the number of search indexes a cluster may hold, lower on shared and free tiers, so check the tier's limit against the number of hybrid tenants. While a tenant's index is being checked, missing, building or the wrong size, or Atlas cannot answer, questions are answered from the lexical side. Any other value is read as "local".Modules/Knowledge/vectorStore.js
LLM_MAX_TOKENS_PLAN (secret)32000cap for the planning call (raise if you see "ran out of output token budget")Modules/AIProjectGenerator/controller.js
LLM_PRICINGJSON map of model name to per-token prices used to report AI spend.Modules/AICore/usage.js
LLM_PROVIDERopenai | anthropic | deepseekModules/AICore/llmProvider/embeddingChoice.js, Modules/AICore/llmProvider/index.js and others
LLM_REGIONoptional: the inference region the provider is pinned to; shown read-only in the agent settings (GET /api/v2/agents/settings)Modules/Agents/budget.js
LLM_TIMEOUT_MS600000Model call timeout every provider inherits unless overridden per provider (default 10 min). The agent job lock is model timeout + AGENT_ACT_BUDGET_MS + 2 min.Modules/Agents/engine/timeouts.js
MCP_ROLE_PROMPTSoffRole playbooks for a connected AI: "on" offers each role playbook in Modules/Agents/roles as a ready-made MCP prompt (work_as_<role>, with an optional request), listed only to a connection that may run every tool the role's playbook names, and lets a signed-in member of a workspace download a role as a Claude skill (a zip holding <name>/SKILL.md) from /api/v2/agents/roles/<blueprint>/<role>/skill. A prompt and a skill are fixed text: they give the AI no tool and no right it did not have. Default "off" leaves the MCP prompt list as it was and refuses the download.Modules/Mcp/rolesFlag.js
MCP_TOOLS_DATAonMCP data tools: "on" registers and offers projects.list, project.get, sprints.list, statuses.list, comments.list, pages.search, page.get and timesheet.read (reads), and comment.create and timelog.create (the caller's own time), each a rated registry action that answers only what the person behind the token can open in the web app and needs its own OAuth scope. Unset means on for a new install; set it to off (false, 0 or no) to turn the tools off, which leaves the registry and the MCP tool list as they were. Every write still needs the person's own access in the web app and, for an outside AI app, the scopes that person granted it on the consent screen. An install that already had a workspace before this default changed keeps it off: migration 074 records off in the instance settings (Settings, Instance, AI), where an owner can turn it on.Modules/Mcp/dataFlag.js
MCP_TOOLS_MANAGEonMCP management tools: "on" registers and offers, to a personal access token created with the tasks:manage grant or an OAuth token holding the tasks:manage scope, fields.list, subtasks.list, members.list, task.history and task.links.list (reads), task.update, task.assign, task.field.set, task.move, task.archive, task.restore, comment.update and tasks.batch (up to 50 writes in one call that names more than one task and so waits for a person as one proposal; a call on one task, which is applied at once, takes 25), and fuller forms of tasks.search, task.get, task.create, subtask.create, task.comment and task.status.set (any status of the task's project, Done included, recorded as closed through the agent and unchecked); and, to a token created with the docs:manage grant or an OAuth token holding the docs:manage scope, page.create and page.update. Each write runs through the web app's own route preparation and handlers as the person behind the token. A grant is chosen when the token is created and never added afterwards, so a token made earlier lists and runs exactly what it did before; an OAuth client is given a manage scope only as MCP_OAUTH describes. Unset means on for a new install; set it to off (false, 0 or no) to turn the tools off, which leaves the registry and the MCP tool list as they were. Every write still needs the person's own access in the web app and, for an outside AI app, the scopes that person granted it on the consent screen. An install that already had a workspace before this default changed keeps it off: migration 074 records off in the instance settings (Settings, Instance, AI), where an owner can turn it on.Modules/Mcp/manageFlag.js
MCP_TOOLS_V2offMCP tools v2: "on" puts human names and a ref next to the ids in tool results (only names the caller may see), pages every list tool with an opaque cursor (default 25, at most 100 per page), annotates every tool with readOnlyHint, destructiveHint, idempotentHint and openWorldHint from its action rating, and turns a destructive call (an irreversible or workspace-scope write) into a proposal a person approves in the Inbox instead of acting. Default "off" keeps the tool list and result shapes as they were.Modules/Mcp/v2Flag.js
MCP_TOOLS_WORKonMCP everyday work tools: "on" registers and offers tags.list, task.relations.list, task.lists.list, lists.list, page.comments.list, goals.list and goal.get (reads), and task.tags.add, task.tags.remove, task.relation.add, task.relation.remove, list.create, list.rename, list.move, page.comment.create, page.comment.reply, page.comment.assign, goal.target.set, goal.target.sources.add, goal.target.sources.remove, task.lists.add and task.lists.remove (writes), and lets tasks.search take a list (sprintId) for every token. A read needs the read scope and a write the write scope; none needs a manage grant, so every token that reads or writes gains them. Each answers only what the person behind the token can open in the web app, stays inside the projects a token was narrowed to, and each write runs through the web app's own route handlers as that person, under the permission the route asks for. Nothing here deletes or archives. For an OAuth client under AGENT_TAINT_ROUTING, a write that reaches past one task (a link, a list change, a task added to or taken out of another list, a doc comment, a change to a goal) is refused, or filed as a proposal when the token holds tasks:manage (links, lists and goals) or docs:manage (doc comments). A goal belongs to no project: a token narrowed to some projects reads a goal only when everything the goal counts is inside them, and changes no goal. A list a task is added to gives nobody access to the task: a search by list answers an added task only to a caller who can open its home list. Unset means on for a new install; set it to off (false, 0 or no) to turn the tools off, which leaves the registry and the MCP tool list as they were. Every write still needs the person's own access in the web app and, for an outside AI app, the scopes that person granted it on the consent screen. An install that already had a workspace before this default changed keeps it off: migration 074 records off in the instance settings (Settings, Instance, AI), where an owner can turn it on.Modules/Mcp/workFlag.js
OPENAI_BASE_URLhttps://api.openai.com/v1Base URL for every OpenAI call (chat completions, embeddings, speech-to-text and the Test connection probe), for a proxy or a regional endpoint. OPENAI_EMBEDDINGS_URL still wins for embeddings when set. For a self-hosted model use the OpenAI-compatible provider instead.Modules/AICore/llmProvider/openaiProvider.js
OPENAI_COMPATIBLE_API_KEYOptional bearer key for the OpenAI-compatible server; leave empty for Ollama or LM Studio. A workspace can bring its own under Settings > Routing policy when TENANT_PROVIDER_KEYS is on.Modules/AICore/providerKeys.js
OPENAI_COMPATIBLE_BASE_URLBase URL of a server that speaks the OpenAI Chat Completions and Embeddings API on your own network: Ollama (http://localhost:11434/v1), vLLM (http://host:8000/v1), LM Studio (http://host:1234/v1), LiteLLM or a company gateway. Select it with LLM_PROVIDER=openai_compatible. Loopback and private addresses are allowed for this one URL (it is set by the instance owner, never by a workspace user) through a dedicated client that refuses link-local and cloud metadata addresses, credentials or a query in the URL, and every redirect; agent fetches and webhooks keep their own private-address rules. While this provider answers chat, embeddings and speech-to-text go to it too and never to OpenAI, and it never fails over to another provider.Modules/AI/transcribe.js, Modules/AICore/llmProvider/openaiCompatibleProvider.js and others
OPENAI_COMPATIBLE_EMBEDDINGS_MODELEmbeddings model on the OpenAI-compatible server, used for knowledge search when that provider answers chat (e.g. nomic-embed-text, 768 dimensions). Priced at 0 like the chat model. Vectors are stored with their model name and a search only compares vectors of the current model, so a model with a different vector size is fine; changing the model means re-embedding each workspace under Instance > Knowledge. A server that starts answering the same model name with a different vector size is refused with that advice. With KNOWLEDGE_VECTOR_STORE=atlas set KNOWLEDGE_EMBEDDING_DIMENSIONS, or let the first stored vector decide. Without it, knowledge search stays lexical: embeddings never fall back to OpenAI.Modules/AICore/llmProvider/openaiCompatibleProvider.js
OPENAI_COMPATIBLE_MAX_OUTPUT_TOKENS8192Output token ceiling sent to the OpenAI-compatible server as max_tokens. Local models often have an 8k context and vLLM refuses a max_tokens that does not fit; raise it for a model with a larger context.Modules/AICore/llmProvider/openaiCompatibleProvider.js
OPENAI_COMPATIBLE_MODELChat model name the OpenAI-compatible server knows (e.g. llama3.1:8b). With OPENAI_COMPATIBLE_BASE_URL set it is priced at 0 unless LLM_PRICING names a price, so it is never refused as unpriced; a model id that already has a vendor price keeps it.Modules/AICore/llmProvider/openaiCompatibleProvider.js
OPENAI_COMPATIBLE_TIMEOUT_MSPer-request timeout for the OpenAI-compatible server. Defaults to LLM_TIMEOUT_MS, since local inference is slow.Modules/Agents/engine/timeouts.js
OPENAI_EMBEDDINGS_URLhttps://api.openai.com/v1/embeddingsThe endpoint the OpenAI provider posts embeddings to. Point it at an Azure deployment, a proxy or a self-hosted server that speaks the same API; the e2e harness points it at a stub. Chat completions are unaffected.Modules/AICore/llmProvider/openaiProvider.js
OPENAI_TIMEOUT_MS600000optional: override the OpenAI axios timeout (default: LLM_TIMEOUT_MS for reasoning/gpt-5, 4 min for others); the agent job lock is derived from the largest provider timeoutModules/Agents/engine/timeouts.js
TENANT_PROVIDER_KEYSSet to on to let each workspace pay for model calls with its own provider keys: owners and admins set one key per provider under Settings, kept in the secrets store by handle with only the handle on the company row, and every model call and embedding run for that workspace is billed to its key, or to the instance key while none is set. A set key that will not open refuses the call rather than billing the instance owner. Needs SECRETS_STORE with SECRETS_KEY; unset or off keeps every call on the instance keys.Modules/AICore/providerContext.js
WHISPER_MODELwhisper-1Speech-to-text model for Talk to text. Audio goes to the OpenAI-compatible server when it answers chat (its /audio/transcriptions), otherwise to OpenAI at OPENAI_BASE_URL.Modules/AI/transcribe.js
WHISPER_USD_PER_MINUTE0.006What a minute of Talk to text audio costs, in USD, booked to the workspace AI budget as feature transcription. Unset, audio sent to OpenAI is booked at whisper-1's list price (0.006) and audio sent to the OpenAI-compatible server at 0. Set it when WHISPER_MODEL names a model with another price.Modules/AI/transcribe.js
WORKSPACE_GUIDE_AUTO_CREATEtrueWhether each workspace created from now on gets the built-in Workspace Guide agent. "true" (the default) adds it at creation; "false" adds none until an owner or admin adds it from the Team board. Existing workspaces are never changed by this setting, and a guide an admin removed is not made again.Modules/Agents/workspaceGuide.js

Calling ​

VariableRequiredDefaultDescriptionRead by
TURN_CREDENTIAL_TTL_SECONDS (secret)3600Lifetime of TURN credentials minted from TURN_STATIC_AUTH_SECRET.Modules/Calls/iceConfig.js

Integrations ​

VariableRequiredDefaultDescriptionRead by
APIKEY (secret)Firebase web API key used by the server for push notifications.Config/config.js, Config/contentSecurityPolicy.js
APPIDFirebase app id.Config/config.js
APP_CONNECTIONSfalseSet to true to switch on App connections: the Integrations > App connections page, and a job that every 5 minutes reads the apps an owner or admin has connected and linked to projects, one company at a time. Default off: no job runs and the page says so. With the GitHub app connected, the job lists the pull requests of its owner/repo updated since the last check, finds the task keys (such as AP-479) in each title, branch name and description among the projects linked to that repo and open to the person who connected it, adds the pull request link and a comment to the task when it opens, and a comment and the move to In review when it merges (never Done). Each event is acted on once per task. A failing app is tried again after 5 minutes, then 10, doubling up to 6 hours, and the page shows the last error.Modules/Integrations/appConnections/flag.js
AUTODOMAINFirebase auth domain.Config/config.js
CONNECTORSoffAgent connectors to switch on, as a list separated by commas: slack and google_calendar. Default off: no connector route, no connector action in the agent registry and no section on the Integrations screen. With slack named, an owner or admin connects the workspace's Slack app under Integrations > Slack (the bot token and the signing secret go straight into the secrets store under the kind connector and are never shown again) and chooses the channels agents may post to; an agent can then propose a Slack message (slack.message.post), and nothing is sent until an owner or admin approves the exact channel and text in the AI Inbox. Needs SECRETS_STORE with a usable SECRETS_KEY and AGENT_TAINT_ROUTING on: without them the connector stays off, the server says why in one startup log line and the Integrations screen shows the same reason. While AGENT_EGRESS_ALLOWLIST is on and the workspace has a host list, slack.com must be on it. With google_calendar named, each member connects their own Google account on the Connections page: this slice ships the connection only (no calendar is read or written yet). Its access and refresh tokens go to the secrets store, the connection ends when the member disconnects, an owner or admin ends it, or the member is removed, and nobody else can use it. It needs the same two settings plus CONNECTOR_GOOGLE_CLIENT_ID and CONNECTOR_GOOGLE_CLIENT_SECRET, and APIURL, from which the redirect address <APIURL>/api/v1/connector-oauth/google/callback is built; that host list must then also hold oauth2.googleapis.com.Modules/Agents/connectors/flag.js
CONNECTOR_GOOGLE_CLIENT_IDClient id of the Google OAuth client the connectors use (CONNECTORS=google_calendar). A separate client from the sign-in one: never reuse GOOGLE_CLIENT_ID, because the sign-in exchange answers to the browser and connector tokens must never leave the server. Create it in the Google Cloud console as a Web application with the redirect address <APIURL>/api/v1/connector-oauth/google/callback. Not a secret.Modules/Agents/connectors/flag.js
CONNECTOR_GOOGLE_CLIENT_SECRET (secret)Client secret of that Google OAuth client. Type it into .env yourself; it is sent only to Google's token endpoint and is never logged, stored in the database or shown. Without both this and CONNECTOR_GOOGLE_CLIENT_ID the google_calendar connector stays off and the server says so in one startup log line.Modules/Agents/connectors/flag.js
GITHUB_BASE_OAUTH_URLhttps://github.com/login/oauthauthorize + access_token baseModules/Integrations/appConnections/github/oauth.js, Modules/githubOAuth/controller.js
GITHUB_CLIENT_IDGitHub OAuth app client id.Modules/Integrations/appConnections/github/oauth.js, Modules/githubOAuth/controller.js
GITHUB_CLIENT_SECRET (secret)GitHub OAuth app client secret.Modules/Integrations/appConnections/github/oauth.js, Modules/githubOAuth/controller.js
GITHUB_CONNECT_CLIENT_IDGitHub OAuth app client id for one-click GitHub on App connections; its callback URL is APIURL + /api/v1/github-connect/callback. Falls back to GITHUB_CLIENT_ID.Modules/Integrations/appConnections/github/oauth.js
GITHUB_CONNECT_CLIENT_SECRET (secret)Client secret of the one-click GitHub OAuth app. Falls back to GITHUB_CLIENT_SECRET.Modules/Integrations/appConnections/github/oauth.js
GITHUB_TOKEN (secret)Token used to read release notes from GitHub for the changelog page.Modules/Changelog/controller.js
GITLAB_BASE_API_URLhttps://gitlab.com/api/v4used to verify the identity (/user)Modules/Auth/helpers/socialIdentity.js
GITLAB_BASE_OAUTH_URLhttps://gitlab.com/oauthauthorize + token baseModules/gitlabOAuth/controller.js
GITLAB_CLIENT_IDGitLab OAuth app client id.Modules/gitlabOAuth/controller.js
GITLAB_CLIENT_SECRET (secret)GitLab OAuth app client secret.Modules/gitlabOAuth/controller.js
GOOGLE_CLIENT_IDused for token exchange AND id-token verificationModules/googleOAuth/controller.js
GOOGLE_CLIENT_SECRET (secret)Google OAuth client secret.Modules/googleOAuth/controller.js
GOOGLE_OAUTH_CLIENT_IDGoogle OAuth client id accepted when verifying Google sign-in tokens.Modules/Auth/helpers/socialIdentity.js
GOOGLE_OAUTH_URLhttps://oauth2.googleapis.com/tokenGoogle token endpointModules/googleOAuth/controller.js
MEASUREMENTIDFirebase analytics measurement id.Config/config.js
MESSAGINGSENDERIDFirebase messaging sender id.Config/config.js
PROJECTIDFirebase project id.Config/config.js
SERVICE_FILE./firebase-adminsdk.jsonPath to Firebase service account JSON (must be inside project root)Config/config.js, utils/safeServiceFile.js
STORAGEBUCKETFirebase storage bucket.Config/config.js

Billing ​

VariableRequiredDefaultDescriptionRead by
FREE_COMPANY_COUNTCompanies a user may create on the free plan.Modules/Company/controller.js

Security ​

VariableRequiredDefaultDescriptionRead by
AGENT_EGRESS_ALLOWLISTfalseSet to true to route every agent fetch through the workspace egress gateway: a workspace whose list under Instance > Egress names hosts lets its agents fetch those hosts only, checked on every redirect hop and again against the resolved address, with each refusal audited as agent.egress_refused naming the host. Entries are exact hostnames or .suffix patterns with an optional port; an entry without a port allows that host on any port and over plain http as well as https, so write host:443 to allow https on its default port only. Wildcards on shared-hosting and wildcard-DNS suffixes (.github.io, *.nip.io, ...) are refused. A workspace with no list, and every workspace while this is unset or false, fetches as before: public hosts allowed, private, metadata and link-local hosts refused. Private hosts stay refused even when listed. Turning this on also judges NAT64, 6to4 and IPv4-compatible IPv6 addresses by the IPv4 address they carry, and that rule is shared, so it applies to webhook URL checks on save and to the Integrations public-URL check as well. Not behind the gateway: model provider and embeddings calls, SMTP, file storage, and webhooks fired by task changes an agent makes.Modules/Agents/engine/egressContext.js
AGENT_TAINT_ROUTINGonSet to on (or true) to mark an agent run as tainted once its context takes in content from outside the workspace's own members (a fetched page, an email-in body, a form submission, a webhook payload, uploaded file text or a retrieved passage whose origin is external) or when the instruction guard flags instruction-shaped text in what reaches the model (recorded as instruction task:<id>; the text still reaches the model inside the data block), and to route a tainted run's risky writes (any escalation under the risk rating, or a write outside the run's own project) to approval with a reason that names the taint. The run, its replay rows, its audit rows and its proposals record where the content came from (a host, an id or a hash), never the content. Unset, it is on while any of MCP_TOOLS_DATA, MCP_TOOLS_MANAGE, MCP_TOOLS_WORK or MCP_OAUTH is on (all four are by default), so a risky write by an outside AI app waits for a person. An explicit value here always wins, and only here: the instance settings page (Security) shows its state but cannot change it; off leaves runs, decisions and records exactly as they were.Modules/Agents/taint.js
API_TOKEN_MAX_DAYS365Longest lifetime, in days, of an API token while API_TOKEN_STRICT is on. Can only lower the 365-day maximum: a value above 365 or not a whole number of days is ignored with a warning at boot. Ignored while API_TOKEN_STRICT is off.Modules/ApiTokens/helpers/apiTokenRules.js
API_TOKEN_STRICTfalseSet to true to require an expiry and at least one scope on every new API token, and to allow a token without an expiry only to be revoked. A token without an expiry keeps working for 30 days from the later of its creation and when this was first on for the instance (recorded once as apiTokenStrictSince on the instance settings document and kept across backup restores), then stops. A new token may last at most API_TOKEN_MAX_DAYS (365 by default); an older token that expires later works for that many days from the later of its creation and when the maximum first applied (recorded once as apiTokenMaxLifetimeSince, also kept across restores), then stops. Unset or false keeps the old rules.Modules/ApiTokens/helpers/apiTokenRules.js
APP_SHELL_WORKERonSet to off to withdraw the web app's service worker from every browser: /sw.js then answers with a worker that deletes every cache it holds for this address, unregisters itself and handles no requests, so the next page load of each browser runs without one. People keep working; the app no longer opens without a network until this is unset and they load the page again. Unset or on serves the worker of the build. Read on each request, so a change through the environment needs a restart and nothing else.Config/appShellWorker.js
AUDIT_CHAINfalseSet to true to chain every new audit row per company: each row carries a sequence number, the previous row's hash and an HMAC-SHA256 keyed with AUDIT_CHAIN_KEY, a change to a row is appended as a new row instead of editing it, retention anchors the rows it deletes and keeps audit rows at least as long as agent runs, and the audit log reports each row as verified, broken or unchained. Names, email, IP address and user agent are outside the hash. Needs AUDIT_CHAIN_KEY; without a valid key the chain stays off and an error is logged. Unset or false keeps rows unchained and edited in place.Modules/Audit/helpers/chainRules.js
AUDIT_CHAIN_KEY (secret)Secret of at least 32 characters that keys the audit chain hash, so someone who can write to the database cannot recompute it. Rows chained under one key only verify under that key; keep it out of the database and its backups. It also keys the pseudonyms that redacting a person in audit rows writes (erased-user-<hash of the user id>), falling back to JWT_SECRET when this key is unset or invalid; after the key changes a person gets a new pseudonym, and running the redaction again changes nothing already redacted. The key cannot change once AUDIT_CHAIN has been on: rows, chain heads and anchors do not record which key signed them, so under a new key every older chained row would read as broken. The first start with AUDIT_CHAIN on and a valid key stores a fingerprint of the key (an HMAC-SHA256 of a fixed label under it, never the key) in the audit_chain_key collection of the global database; every later start with AUDIT_CHAIN on and a different key refuses to start, logging a fatal error and exiting with status 1, until the previous key is set back. With AUDIT_CHAIN off nothing is checked or stored, and a missing or short key still only turns the chain off.Modules/Audit/helpers/chainRules.js
AUDIT_EXPORT_MAX_ROWS100000Most rows one audit-log CSV export streams; a last row says when the filter held more. Can only lower the hard cap.Modules/Audit/controller.js
AUDIT_RETENTION_DAYSDays to keep audit-log entries before the recorder prunes them.Modules/Audit/recorder.js
AUTH_RATE_LIMIT_BLOCK_MS1800000How long an IP stays blocked after too many failed logins.Modules/Auth/helper.js
AUTH_RATE_LIMIT_MAX_ATTEMPTS5Failed login attempts allowed per window before blocking.Modules/Auth/helper.js
AUTH_RATE_LIMIT_WINDOW_MS900000Window over which failed login attempts are counted.Modules/Auth/helper.js
CORS_ORIGINShttp://localhost:8080Extra origins allowed by CORS, comma-separated (WEBURL and APIURL are always allowed).utils/cors.js
CSP_EXTRA_CONNECT_SRCExtra hosts for the connect-src directive of the content security policy (API, XHR and websocket calls), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_FONT_SRCExtra hosts for the font-src directive of the content security policy (fonts), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_FORM_ACTIONExtra hosts for the form-action directive of the content security policy (form posts), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_FRAME_ANCESTORSExtra hosts for the frame-ancestors directive of the content security policy (sites that may frame AlianHub), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_FRAME_SRCExtra hosts for the frame-src directive of the content security policy (framed pages), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_IMG_SRCExtra hosts for the img-src directive of the content security policy (images), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_MEDIA_SRCExtra hosts for the media-src directive of the content security policy (audio and video), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_SCRIPT_SRCExtra hosts for the script-src directive of the content security policy (scripts), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. In script-src and worker-src, sources must be https, may not be on a domain where anyone can publish (github.io, googleusercontent.com, githubusercontent.com and similar) must name exact hosts (no wildcard), and on a package CDN (cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com) must name one package and its version. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_STYLE_SRCExtra hosts for the style-src directive of the content security policy (style sheets), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. Read at boot.Config/contentSecurityPolicy.js
CSP_EXTRA_WORKER_SRCExtra hosts for the worker-src directive of the content security policy (workers), separated by spaces and written with their scheme, such as https://cdn.example.com or https://*.example.com. Quotes, keywords such as 'unsafe-eval', semicolons, commas, schemes on their own, a bare * and a wildcard over a top-level domain, a public suffix or a shared hosting domain stop the server at boot, whatever CSP_MODE is. In script-src and worker-src, sources must be https, may not be on a domain where anyone can publish (github.io, googleusercontent.com, githubusercontent.com and similar) must name exact hosts (no wildcard), and on a package CDN (cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com) must name one package and its version. Read at boot.Config/contentSecurityPolicy.js
CSP_MODEoffContent security policy: off (no policy header, the default), report (Content-Security-Policy-Report-Only: nothing is blocked, and what would be is counted under Instance > Enforcement) or enforce (Content-Security-Policy: the browser blocks it). The policy is built on the first request from the storage endpoint and whether push is configured (restart to change them), and follows WEBURL and APIURL when they change; the public share and form pages keep their own stricter policy, and the Google Drive picker runs in its own popup page (/pickers/google-drive) whose policy allows eval for Google's picker script only, so installs using Drive may run enforce. Violation reports go to POST /api/v2/csp-report, which keeps the day, directive, blocked host and page path shape for 30 days and nothing else. Run report until the list is quiet, then enforce. An unknown value stops the server at boot. Read at boot.Config/contentSecurityPolicy.js
DISABLE_PERMISSION_ENFORCEMENTEmergency switch, never for normal running. Set to true to skip the per-key permission checks for API tokens and project edits and the task list rule on task reads, and to judge a workspace set to enforce in report mode: recorded, never refused. That covers browser sessions and API tokens on the task actions mapped after the first nine (Config/taskWritePermissions.js). Role guards stay on for API tokens, and so does the refusal of a task write body that names another company or an id that cannot be read.Config/permissionEnforcement.js, Config/permissionGuard.js
ENDPOINT_KEY (secret)Shared key expected by the internal service endpoints.Config/config.js
GLOBAL_RATE_LIMIT_PER_MINRequests per minute allowed per IP by the global rate limiter.Config/globalRateLimit.js
HELMET_ENABLEDSend the helmet security headers (default true).Config/securityHeaders.js
INSTANCE_ADMIN_KEY (secret)Shared key that grants the instance-admin API without a product-owner session.Modules/Instance/guard.js
MCP_CURSOR_SECRETKey that signs the MCP list cursors (HMAC-SHA256) while MCP_TOOLS_V2 is on. A cursor is bound to the company, the caller, the token and the query, and expires after an hour. Unset derives a key from JWT_SECRET; changing it ends the cursors in flight. With MCP_TOOLS_V2 on the server refuses to start when it is set but shorter than 32 characters or equal to JWT_SECRET.Modules/Mcp/cursor.js
MCP_OAUTHbothSet to both (on, true, 1 and yes mean the same) or only to run the OAuth 2.1 authorization server for the MCP endpoint (MCP authorization spec 2025-11-25): GET /.well-known/oauth-authorization-server, /oauth/authorize (authorization code with PKCE S256 only, resource required and equal to <issuer>/mcp, exact redirect URI match except that an http loopback redirect may use any port, https or loopback redirects; scope may be left out for read-only access), /oauth/token (single-use codes, rotating refresh tokens, a reused code or refresh token revokes its whole grant), /oauth/revoke (RFC 7009), and /api/v2/oauth-clients for workspace owners and admins to pre-register clients. Clients may also identify themselves with a client ID metadata document, fetched through the agents' SSRF-safe fetch (5 KB, 5 s, no redirects, one fetch per URL at a time, a failed URL retried after 5 s). Codes, tokens and client secrets are stored only as keyed hashes; every token is bound to one workspace, one person and the /mcp resource. A person answers each authorization on the consent screen, choosing one workspace in which an owner or admin has approved the client (Settings, Agent clients); revoking that approval, or the person revoking the grant under Accounts, Connected apps, ends it on the next call. On the resource side, GET /.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp (RFC 9728) name the /mcp resource, this authorization server and the scopes tasks:read tasks:write projects:read docs:read time:read time:write, with tasks:manage and docs:manage added while MCP_TOOLS_MANAGE is on; a 401 from /mcp carries resource_metadata and the scopes the call needs; a call whose token lacks a scope answers 403 insufficient_scope naming it; access tokens in the query string are refused; initialize also negotiates protocol version 2025-11-25. /mcp accepts these access tokens in the Authorization header only: the token must be live, bound to the <issuer>/mcp resource, its grant unrevoked and within its cap, its client unrevoked; the token's workspace is the only workspace, so a companyId in the query or header naming another is refused with 403; its scopes decide every tool; a session cookie or Mcp-Session-Id is never authorization; the token is never forwarded to any outbound call; actions are audited as the outside client acting for the person who granted it (viaAccount external, clientId, grantId, delegatedBy), the API activity log names the client, and they count as tainted, so under AGENT_TAINT_ROUTING a risky write is refused with a reason saying it needs a person's approval, or, when the token holds the manage scope the tool needs, filed as a proposal that a person approves only while the grant is live and still holds that scope, the client is still approved for it, and the person still has a seat and can open the target; the manage scopes are never implied: a token holds tasks:manage or docs:manage only when the client asked for it, the person ticked it on the consent screen, where it starts unticked, and an owner or admin approved it for that client by name; an approval that names no scopes never includes them, a pre-registered client that named no scopes cannot be given them, and taking the scope out of the approval or the person withdrawing it takes effect on the next call; an unsupported MCP-Protocol-Version header is answered 400. With both, personal access tokens keep working (read grants every *:read scope, write every *:write, neither a manage scope); with only, a personal access token gets 401 with the challenge. Unset means both on a new install, unless the issuer (MCP_OAUTH_ISSUER or APIURL) is not one the server could boot with: then it stays off and the server says why in one startup log line. Set to off (false, 0 or no) to register none of these routes, and /mcp answers exactly as before. Dynamic client registration stays off unless MCP_OAUTH_DCR is on. An install that already had a workspace before this default changed keeps it off: migration 074 records off in the instance settings (Settings, Instance, AI), where an owner can turn it on.Modules/OAuthServer/config.js
MCP_OAUTH_ACCESS_TOKEN_MINUTES15Lifetime of an MCP OAuth access token, in minutes. Never longer than the grant it belongs to.Modules/OAuthServer/config.js
MCP_OAUTH_CLIENT_METADATA_CACHE_SECONDS300How long a fetched client ID metadata document is cached when it sends no Cache-Control max-age (a max-age is followed up to a day; no-store is never cached).Modules/OAuthServer/config.js
MCP_OAUTH_DCRoffWith MCP_OAUTH on, set to on to open RFC 7591 dynamic client registration at /oauth/register and advertise it in the server metadata. Off by default: clients use a client ID metadata document or are pre-registered by a workspace owner or admin.Modules/OAuthServer/config.js
MCP_OAUTH_GRANT_MAX_DAYS90Longest an MCP OAuth grant lives, in days, however often its refresh token is rotated; after it the person authorizes the client again.Modules/OAuthServer/config.js
MCP_OAUTH_ISSUERIssuer URL of the MCP authorization server, and the base of its endpoints and of the /mcp resource tokens are bound to. Defaults to APIURL. It must be an origin (no path, query or fragment) and https; plain http is accepted only on localhost, 127.0.0.1 or [::1] while NODE_ENV is unset, development or test. With MCP_OAUTH on, any other value stops the server at boot.Modules/OAuthServer/config.js
MCP_OAUTH_RATE_LIMIT_PER_MIN30Requests per minute per client address to /oauth/authorize, and separately to /oauth/token with /oauth/revoke, before the MCP authorization server answers 429.Modules/OAuthServer/config.js
MCP_OAUTH_REFRESH_TOKEN_DAYS30Lifetime of an MCP OAuth refresh token, in days. Each use spends it and issues a new one; never longer than the grant it belongs to.Modules/OAuthServer/config.js
MCP_OAUTH_TOKEN_SECRET (secret)Key for the keyed hashes under which MCP OAuth codes, tokens and client secrets are stored, separate from JWT_SECRET, which is used when this is unset. Changing it invalidates every issued token and every pre-registered client secret.Modules/OAuthServer/tokenHash.js
MEMBERSHIP_CACHE_TTL_SECONDS60Seconds a user-company membership check is cached before MongoDB is asked again.Config/jwt.js
PERMISSION_ENFORCEMENT_CACHE_TTL_SECONDS30Seconds a workspace's own permissionEnforcement.mode is cached before the company row is read again; 0 reads it on every guarded request. A failed read inherits the instance default for 5 seconds whatever this is set to.Config/permissionEnforcement.js
PERMISSION_ENFORCEMENT_MODEoffServer-side permission checks for browser sessions: off (they pass as before), report (they pass, and each would-be denial is recorded in permission_decisions) or enforce (they are refused, and a check that cannot complete fails closed). The default for every workspace; a company row's permissionEnforcement.mode overrides it. Anything unrecognised reads as off. API tokens are always enforced on the keys they were refused on before, including nine task actions; on every other task write in Config/taskWritePermissions.js they follow this mode like browser sessions. A task write body that names another company or an id that cannot be read is refused for API tokens in every mode, and follows this mode for browser sessions. Also editable under Instance > Settings > Security.Config/permissionEnforcement.js
PLATFORM_API_KEY (secret)With HOSTING_MODE=managed, the key the platform presents to this instance; at least 32 characters.Config/hostingMode.js, Modules/Platform/entitlements.js and others
PLATFORM_PUBLIC_KEYWith HOSTING_MODE=managed, the Ed25519 public key that verifies what the platform signs: PEM, or base64 of the raw 32-byte key or of its SPKI DER.Config/hostingMode.js, Modules/Platform/entitlementsToken.js
PRECOMPANYKEY (secret)yesyour_preset_keyRandom key for the preset company routes, presented in the x-preset-key header; generated at setup.Config/config.js
SECRETS_KEY (secret)Key of at least 32 characters that seals the secrets store, separate from JWT_SECRET. Each stored secret records the id of the key that sealed it (derived from the stretched key, so it is no shortcut for guessing the key); keep the key out of the database and its backups. To rotate, move the old value to SECRETS_KEY_PREVIOUS, set the new one here, restart, and run npm run secrets:reencrypt.Config/secrets.js
SECRETS_KEY_PREVIOUS (secret)The previous SECRETS_KEY during a key rotation: secrets sealed under it still resolve, and npm run secrets:reencrypt reseals every company under SECRETS_KEY, writes an audit row per company and prints how many live secrets are still on this key. Unset it only when that command says it is safe to.Config/secrets.js
SECRETS_STOREfalseSet to true to keep integration and webhook secrets in a per-company secrets store, referenced by handle: a new or updated secret is sealed with AES-256-GCM under SECRETS_KEY and bound to its company and handle, the integration or webhook document keeps only the handle, every create, rotation, revocation and failed lookup is audited, and owners and admins see each secret's name, kind, key id and dates under Settings → Integrations, where they can rotate or revoke it. Migration 031 moves existing secrets in when this is on while it runs. Needs SECRETS_KEY; with a missing or short key the store refuses every read and write, connecting an integration or adding a webhook is refused rather than saved outside the store, and an error is logged. Unset or false keeps secrets sealed on their documents as before; a handle already written still resolves while a key is set, and a secret saved while this is off replaces the handle on its document, so the newest value is always the one in use. Turning this off again: the old store copy is revoked when SECRETS_KEY is still set, and otherwise marked, then revoked by npm run secrets:reencrypt (or by migration 031 when it runs again) once a key is set.Config/secrets.js
SKILL_EXTERNAL_READSoffDeclared external reads in data skills: "on" offers the url and api readers in the Skill Library, each naming one exact host, a path template, GET only, at most 512 KB, 10 s and 3 redirects, and optionally the handle of a skill_read workspace secret. A skill is saved only when every declared host is on the workspace egress allowlist (an empty list refuses them all, and private or reserved hosts are refused even when listed), and a raw secret anywhere in the skill body is refused. The built-in pr.summary skill (alias risk.flags) reads its pull request this way from github.com, patch-diff.githubusercontent.com or gitlab.com, each of which must be on the workspace egress allowlist. Default "off" hides the readers and refuses them on save; a skill that declares one, pr.summary included, is listed as unavailable with the reason, and running it fails with external_reads_not_available before anything is fetched. It also gates Research this on the task panel, which additionally needs a web-search tool; none ships yet, so Research stays hidden and its endpoint answers research_not_available.Modules/AI/taskResearch.js, Modules/Agents/skills/externalReads.js
SOCKETIO_ADMIN_PASSWORD_HASH (secret)bcrypt hash of the Socket.IO admin UI password; the UI stays off while unset. .env.example ships a sample value: blank it or set your own.socket/socketinit.js
SOCKETIO_ADMIN_USERNAMEalianUsername for the Socket.IO admin UI; the UI stays off while unset.socket/socketinit.js
STEP_CREDENTIALSoffSet to on to give the platform its own service identities and mint a step-scoped credential per workflow step: the workflow engine, its worker, the knowledge indexer and the model router record their rows as service:<name> instead of a borrowed user, and when the engine claims a step it signs a short-lived credential carrying the company, run, step, fencing token, actor context and allowed actions that expires one lease after it was minted, is re-minted whenever a heartbeat extends the lease, and is checked against the live step row whenever an agent step's actor presents it to perform an action. The tokens screen lists live credentials as step-scoped. Unset or off keeps today's behaviour: no credential is minted and rows are attributed as before.Modules/Agents/serviceIdentity.js
STEP_CREDENTIAL_SECRET (secret)Key material for signing step-scoped credentials (STEP_CREDENTIALS). Derived with scrypt, never used as-is; falls back to JWT_SECRET, so set a dedicated secret in production so rotating JWT_SECRET does not invalidate credentials of steps in flight, and keep it out of the database and its backups.Modules/Workflows/stepCredential.js
WEBHOOK_ALLOWED_PRIVATE_HOSTSPrivate hosts webhooks may post to: exact hostnames or CIDR ranges, comma-separated (empty by default, so every private host is refused). Also editable under Instance > Settings > Security. Link-local and cloud metadata addresses stay blocked, and a range can be no wider than /8 (IPv4) or /32 (IPv6).Modules/Webhooks/helpers/privateHostAllowlist.js

Logging ​

VariableRequiredDefaultDescriptionRead by
FATAL_FLUSH_TIMEOUT_MS3000Upper bound, in milliseconds, that a fatal crash waits for flushers (log files, telemetry exporter, crash report mail) before the process exits with 1.Config/processGuards.js
LOG_DATE_PATTERNYYYY-MM-DDDate pattern appended to rotated log file names.Config/loggerConfig.js
LOG_DIRlogDirectory that receives the rotated log files.Config/loggerConfig.js, Modules/Instance/logsPath.js
LOG_FORMATtextLog file record format: text (one formatted line) or json (one { ts, level, label, requestId, traceId, message } record per line).Config/logFormat.js, Config/loggerConfig.js
LOG_MAX_FILES14dRetention window for rotated log files (winston-daily-rotate-file).Config/loggerConfig.js
LOG_MAX_SIZE20mSize at which a log file rotates.Config/loggerConfig.js
OTEL_EXPORTER_OTLP_ENDPOINTOTLP/HTTP collector base URL (traces go to <endpoint>/v1/traces). Unset keeps the OpenTelemetry SDK and exporter off; trace ids are still recorded.Config/telemetry.js
OTEL_SERVICE_NAMEalianhubservice.name reported on exported spans.Config/telemetry.js

Limits ​

VariableRequiredDefaultDescriptionRead by
BODY_LIMIT2mbMaximum JSON/urlencoded/raw request body (body-parser size string).index.js
CLOUD_STORAGE_MAX_IMPORT_BYTESLargest file the cloud-storage importer will copy into a project.Modules/CloudStorage/controller.js
FORM_MAX_FILES3Maximum attachments on one public form submission.Modules/Forms/helpers/formUpload.js
FORM_MAX_FILE_BYTESMaximum size of one public form attachment.Modules/Forms/helpers/formUpload.js
MAX_IMAGE_FILE_BYTES26214400Largest image accepted for processing.utils/imageGuard.js
MAX_IMAGE_PIXELS50000000Largest image, in pixels, accepted for processing (decompression-bomb guard).utils/imageGuard.js
UPLOAD_MAX_FILES20Maximum files per upload request.utils/uploadConfig.js
UPLOAD_MAX_FILE_BYTESMaximum size of one uploaded file.utils/uploadConfig.js

Jobs ​

VariableRequiredDefaultDescriptionRead by
AGENT_ALERTS_INTERVAL_MINUTES15Minutes between runs of the agent rate-alert evaluator (error rate, approval rate, cost forecast, queue age).Modules/Agents/alerts.js
AGENT_PROPOSAL_STUCK_MINUTES10Minutes an agent proposal may sit in 'applying' before the reaper fails it.Modules/Agents/proposals.js
AGENT_SCHEDULE_TICK_SECONDS60Seconds between ticks of the agent schedule runner, which starts the scheduled report runs of L3 agents. Minimum 15; a slot missed by more than an hour is skipped, not caught up.Modules/Agents/schedules/scheduler.js
AUTOMATION_CONCURRENCY5Automation runs processed in parallel.Modules/Automations/engine/index.js
AUTOMATION_DUE_DATE_TICK_SECONDS300Seconds between checks for tasks whose due date has passed, which fire the "Task due date passes" automation trigger. Minimum 60. Each check fires at most 200 tasks per workspace, oldest first, and looks back 24 hours, so a due date missed by more than a day is skipped, not caught up.Modules/Automations/engine/dueDateTrigger.js
AUTOMATION_ENGINEtrueSet to false to stop the automation engine from starting.Modules/Automations/engine/index.js
AUTOMATION_EVENT_LOGSet to true to record every domain event in automation_event_log for debugging automations.event/domainEventBus.js
AUTOMATION_QUEUE_DRIVERagendaQueue driver for automation runs (agenda or memory).Modules/Automations/engine/index.js, Modules/Instance/controller.js and others
CRON_TZUTCIANA time zone for scheduled jobs (reminders, digests).Modules/GeneralReminders/scheduler.js, Modules/Instance/controller.js and others
GENERAL_REMINDERS_CRONCron expression for the general reminders scheduler.Modules/GeneralReminders/scheduler.js
NOTIFICATIONACTIVETIMEMinutes a user counts as active for notification batching.Modules/notification/prepare-notification-data/user-controllerV2.js
WORKFLOW_APPROVAL_DEADLINE_MS259200000Deadline given to an approval step whose definition names none, so a run cannot wait forever on a decision nobody will make.Modules/Workflows/flag.js
WORKFLOW_APPROVAL_POLL_MS300000How often a workflow step waiting on a human approval is looked at again. A decision wakes the step immediately; this is the floor that notices a deadline nobody answered.Modules/Workflows/flag.js
WORKFLOW_BACKOFF_MS30000,120000,600000Comma-separated backoff ladder, in milliseconds, a transient workflow step failure waits between attempts.Modules/Workflows/flag.js
WORKFLOW_ENGINEoffDurable workflow runs for automation rules: "off" runs a rule through the automation runner exactly as before, "on" makes it a one-node workflow run with a lease, a claim and engine-scheduled retries.Modules/Workflows/flag.js
WORKFLOW_HEARTBEAT_MSHow often a running workflow step renews its lease. Defaults to a third of WORKFLOW_LEASE_MS.Modules/Workflows/flag.js
WORKFLOW_JOIN_POLL_MS2000How often a fan-in join or a loop checks whether the steps it is waiting on have finished.Modules/Workflows/flag.js
WORKFLOW_LEASE_MSHow long a claimed workflow step run keeps its lease before another worker may reclaim it. Defaults to the agent run lock, so the two cannot drift.Modules/Workflows/flag.js
WORKFLOW_MAX_ATTEMPTS3Attempts a workflow step gets before a transient failure is given up on.Modules/Workflows/flag.js
WORKFLOW_MAX_FAN_OUT50Children one fan-out step may expand into. A workflow definition may ask for fewer, never more; over the bound the step fails instead of doing part of the work.Modules/Workflows/flag.js
WORKFLOW_MAX_LOOP_ITERATIONS25Iterations a loop step may re-enter its body for. The ceiling a definition's own maxIterations is capped to.Modules/Workflows/flag.js
WORKFLOW_MAX_RUNS_PER_HOUR0Agent runs an hour, per company and per agent, that a workflow loop may admit an iteration for. The ceiling a loop's own maxRunsPerHour and a rule's stored limits.maxRunsPerHour are capped to; 0 means no ceiling for the installation.Modules/Workflows/flag.js
WORKFLOW_RUN_BUDGET_USDCeiling on what a whole workflow run may spend, in dollars. Each step's cost is taken off the run's remaining budget; a step asking for more than remains is refused and the run is marked blocked. Unset means no ceiling.Modules/Workflows/flag.js
WORKFLOW_RUN_DEADLINE_MSCeiling on how long a whole workflow run may take, in milliseconds. Each step takes its share and hands on what is left; a step asking for more than remains is refused and the run is marked blocked. Unset means no ceiling.Modules/Workflows/flag.js
WORKFLOW_RUN_LIMIT_CACHE_MS60000How long an hour of an agent's run start times is trusted before it is read again. The hour rolls in memory between reads; this is only how quickly a run another process started is noticed.Modules/Workflows/flag.js
WORKFLOW_TENANT_CONCURRENCY5Workflow step runs one tenant may have claimed at once, counted across every worker.Modules/Workflows/flag.js

Frontend ​

VariableRequiredDefaultDescriptionRead by
VUE_APP_AFFILIATEONShow the affiliate programme entry points.frontend/src/App.vue, frontend/src/config/env.js
VUE_APP_APIKEY (secret)Firebase web API key (push notifications).frontend/src/config/firebaseConfig.js
VUE_APP_APPIDFirebase app id.frontend/src/config/firebaseConfig.js
VUE_APP_AUTHDOMAINFirebase auth domain.frontend/src/config/firebaseConfig.js
VUE_APP_CUSTOMFIELDIDCustom field id used by the support product link.frontend/src/components/organisms/TaskDetailOverlay/TaskDetailPanel.vue
VUE_APP_CUSTOMFIELDPRODUCTIDCustom field id that stores the product on support tickets.frontend/src/components/organisms/TaskDetailOverlay/TaskDetailPanel.vue
VUE_APP_GITHUB_BASE_OAUTH_URLGitHub OAuth authorize URL.frontend/src/config/publicConfig.js
VUE_APP_GITHUB_CLIENT_IDGitHub OAuth client id for the login button.frontend/src/config/publicConfig.js
VUE_APP_GITLAB_BASE_OAUTH_URLGitLab OAuth authorize URL.frontend/src/config/publicConfig.js
VUE_APP_GITLAB_CLIENT_IDGitLab OAuth client id for the login button.frontend/src/config/publicConfig.js
VUE_APP_GOOGLE_CLIENT_IDGoogle OAuth client id for the login button.frontend/src/config/publicConfig.js
VUE_APP_IS_GITHUB_LOGINShow the GitHub login button.frontend/src/config/publicConfig.js
VUE_APP_IS_GITLAB_LOGINShow the GitLab login button.frontend/src/config/publicConfig.js
VUE_APP_IS_GOOGLE_LOGINShow the Google login button.frontend/src/config/publicConfig.js
VUE_APP_MEASUREMENTIDFirebase analytics measurement id.frontend/src/config/firebaseConfig.js
VUE_APP_MESSAGINGSENDERIDFirebase messaging sender id.frontend/src/config/firebaseConfig.js
VUE_APP_PROJECTIDFirebase project id.frontend/src/config/firebaseConfig.js
VUE_APP_STORAGEBUCKETFirebase storage bucket.frontend/src/config/firebaseConfig.js
VUE_APP_STORAGE_TYPEStorage backend the web app assumes (server or wasabi); mirror STORAGE_TYPE.frontend/src/config/env.js, frontend/src/config/publicConfig.js
VUE_APP_SUPPORT_PROJECTIDProject id that receives support tickets raised from the app.frontend/src/components/organisms/TaskDetailOverlay/TaskDetailPanel.vue
VUE_APP_USEREMAILDefault account email pre-filled on demo builds.frontend/src/views/Projects/Comments/Comments.vue

General ​

VariableRequiredDefaultDescriptionRead by
ALIANHUB_DOCKERSet by the Docker image so defaults suit a container (storage under /app/storage).Modules/Instance/controller.js
ATLAS_TOKEN (secret)Session token the screenshot atlas (npm run atlas) signs in with; set it on the command line for one run, not in .env.scripts/atlas/session.js
ATLAS_TOKEN_FILEPath of a file holding the session token for npm run atlas, read when ATLAS_TOKEN is unset.scripts/atlas/session.js
BACKUP_DIRbackupsDirectory where in-app backups are written (default backups/).Modules/Instance/backups.js
CRON_ENABLEDRun the scheduled jobs in this process (default true; set false on extra replicas).Modules/Instance/controller.js, index.js
DEMO_EMAILEmail of the demo account, shown in the demo banner and filled into the login form when DEMO_MODE is on.Modules/Admin/common/controller.js, scripts/demo/industries/seedIndustries.js
DEMO_MODEfalseSet to true to show the demo banner and credentials, fill them into the login form, and offer a one-click "Try the demo" button when DEMO_EMAIL and DEMO_PASSWORD are both set.Modules/Admin/common/controller.js, scripts/demo/industries/lib/guard.js
DEMO_PASSWORD (secret)Password of the demo account, shown in the demo banner and filled into the login form when DEMO_MODE is on. Keep it in step with the account: "Try the demo" signs in with it, and repeated failures lock the shared account.Modules/Admin/common/controller.js, scripts/demo/industries/seedIndustries.js
ERRORRECIVEREMAILadmin@yourdomain.comEmail to receive error alertsConfig/config.js
HEALTH_DB_TIMEOUT_MSHow long /health waits for the database ping before reporting it down (default 2000).Modules/Instance/health.js
HOSTING_MODEselfself (the default) or managed. managed turns this into an AlianHub Cloud instance: the platform sets URLs, mail, storage, proxy and security values, which the Instance console then shows locked; the setup wizard, in-app billing and the backup, upgrade and log screens close. The server refuses to start in managed mode when INSTANCE_ID, PLATFORM_API_KEY, PLATFORM_PUBLIC_KEY, PLATFORM_URL, WEBURL or JWT_SECRET is missing or malformed, and on any other value here.Config/hostingMode.js
INSTANCE_IDWith HOSTING_MODE=managed, this instance's id on the AlianHub Cloud platform: ins_ followed by 20 to 32 lowercase letters or digits.Config/hostingMode.js, Modules/Platform/entitlements.js and others
MIGRATIONS_AUTOApply pending migrations at boot (default true; false only reports them).Modules/Instance/controller.js
MONGODB_URLyesmongo connection string (no trailing slash)Config/config.js, Modules/AICore/persistence.js and others
NODEMAILER_EMAIL_PASSWORD (secret)Use app password for GmailConfig/config.js
NODEMAILER_PORT587587 (STARTTLS) or 465 (SSL)Config/config.js
NOOFPRESETCOMPANY10Number of preset companies seeded at first run.Config/config.js
PLATFORM_URLWith HOSTING_MODE=managed, the https:// address of the AlianHub Cloud platform; also the account portal the app links to until entitlements name one.Config/hostingMode.js, Modules/Platform/entitlements.js
RESEND_API_KEY (secret)Get from resend.comConfig/config.js
RESEND_FROM_EMAILonboarding@resend.devUse verified domain in productionConfig/config.js
TRACKER_MIN_VERSION6.0.0Oldest desktop tracker version this server accepts, sent in the tracker handshake; an older tracker asks its user to update before it connects. Raise it only once the matching tracker release is on the stable channel.Modules/DesktopTracker/handshake.js
TRACKER_UPDATE_URLhttps://downloads.alianhub.com/trackerAddress of the desktop tracker update feed, holding a stable and a beta folder with the latest.yml files the tracker build writes. Settings > Time tracking offers the newest stable download from it. Set to off on an instance without internet access; the downloads uploaded under Admin > Tracker are offered instead.Modules/DesktopTracker/feed.js
TRANSLATE_API_KEY (secret)Machine-translation key used by scripts/i18n-backfill.js; unset means English copies are written for review.scripts/i18n-backfill.js

Released under the AGPL-3.0 licence.