Configuration Reference

Every environment variable AVA reads, grouped by area, with its type, default,
whether it is required, and what it changes. The authoritative template is
.env.example in the repo root — this document explains
it.

How config is loaded. In development the custom server loads .env.local
from the repo root (with override: true, so .env.local wins over shell
vars). In the container, your env file is mounted read-only at /app/.env.local
and docker-entrypoint.sh validates the required set before the server starts.
Almost every non-NEXT_PUBLIC_* value is read at call time, so you can
change it and restart the container with no rebuild.

Two special cases:

  • NEXT_PUBLIC_* variables are compiled into the browser bundle at build
    time
    (Dockerfile build args). Changing them requires a rebuild, not just a
    restart.
  • AUTOTASK_API_SECRET is read raw from the env file (bypassing dotenv
    variable-expansion) because it commonly contains $. Wrap it in double
    quotes.

Legend: Required = the container refuses to start without it (or it is
required for the configured AI provider). Recommended = boots without it but
you should set it for a correct deployment. Optional = safe default.


Required to boot

These are validated by docker-entrypoint.sh; a missing one aborts startup
with a clear error naming the variable.

VariableTypeEffect
NEXTAUTH_SECRETstring (32+ chars)Signs the techchat-session JWT (HS256, 24 h). Generate with openssl rand -base64 48.
AUTOTASK_API_USERstring (email)AutoTask API-only user.
AUTOTASK_API_SECRETstringAutoTask API secret. Quote it (may contain $).
AUTOTASK_API_INTEGRATION_CODEstringAutoTask API tracking integration code.
AUTOTASK_API_ZONE_URLURLYour AutoTask REST zone base. Trailing slash optional; /V1.0 appended if absent.
AZURE_AD_CLIENT_IDGUIDEntra app registration client ID.
AZURE_AD_CLIENT_SECRETstringEntra app registration client secret.
AZURE_AD_TENANT_IDGUIDEntra directory (tenant) ID.
ENGINEERING_GROUP_IDGUIDEntra security-group object ID; only members may sign in (checked against the groups claim, fail-closed).
RECAPTCHA_SECRET_KEYstringreCAPTCHA v3 server-side secret.
SMTP2GO_USERstringSMTP2GO username.
SMTP2GO_PASSWORDstringSMTP2GO password.
SUPPORT_FALLBACK_EMAILemailRecipient for the after-hours "email a ticket" fallback form. No vendor default — AVA never falls back to a built-in address, so the container refuses to start without it.
AI provider keystringThe key for whichever provider LLM_FAST / LLM_QUALITY use — see AI / LLM providers. With defaults, this is ANTHROPIC_API_KEY.

NEXTAUTH_SECRET strength checks

Beyond mere presence, the entrypoint enforces secret quality at boot:

  • Shorter than 32 characters → startup aborts. This secret signs every
    engineer JWT and client session token; a short value is a brute-forceable
    auth bypass.
  • Placeholder-looking values are refused (anything containing changeme,
    secret, password, placeholder, and similar, case-insensitive) —
    a value copy-pasted from documentation cannot reach production.

Generate a real one:

openssl rand -base64 48

The entrypoint also warns (without failing) if the mounted env file is group-
or world-readable — it holds live secrets, so keep it chmod 600.


AutoTask — default ticket values

Picklist IDs are specific to your AutoTask instance; discover them with
npx tsx scripts/discover-picklists.ts. Do not copy another instance's numbers.

VariableTypeDefaultReq.Effect
AUTOTASK_DEFAULT_TICKET_STATUSint1RecommendedStatus ID for new chat tickets ("New").
AUTOTASK_DEFAULT_TICKET_PRIORITYint3RecommendedFallback priority ID ("Normal").
AUTOTASK_DEFAULT_TICKET_QUEUE_IDintRecommendedHelpdesk queue the ticket lands in.
AUTOTASK_DEFAULT_TICKET_SOURCEintRecommendedTicket "source" ID (create a "Chat" source in AT if needed).
AUTOTASK_DEFAULT_TICKET_TYPEintRecommendedTicket type for chat-originated tickets.
AUTOTASK_PRIORITY_P1int(falls back to AUTOTASK_DEFAULT_TICKET_PRIORITY, then 3)RecommendedAT priority ID mapped from triage P1 (critical).
AUTOTASK_PRIORITY_P2int(falls back to AUTOTASK_DEFAULT_TICKET_PRIORITY, then 3)RecommendedAT priority ID mapped from triage P2 (high).
AUTOTASK_PRIORITY_P3int(falls back to AUTOTASK_DEFAULT_TICKET_PRIORITY, then 3)RecommendedAT priority ID mapped from triage P3 (normal).
AUTOTASK_PRIORITY_P4int(falls back to AUTOTASK_DEFAULT_TICKET_PRIORITY, then 3)RecommendedAT priority ID mapped from triage P4 (low).
AUTOTASK_NOTE_PUBLISH_EXTERNALint1RecommendedThe publish value for the client-facing close note. Per-instance picklist1 = "Internal Only" is universally valid; only change to a value your instance defines, or the note post fails with a 500.

⚠️ Warning — the four priority vars are not a graduated default. There is
no built-in per-priority-level mapping. Each of AUTOTASK_PRIORITY_P1..P4
resolves independently: if a given Pn is unset, it falls through to the
single shared AUTOTASK_DEFAULT_TICKET_PRIORITY, and if that is also unset,
to a hardcoded 3. If you do not set these four vars, ALL AI-triaged chats
file at the same AT priority
, regardless of what the AI determined —
P1/critical and P4/low tickets become indistinguishable in AutoTask.
Discover your instance's priority picklist IDs with
npx tsx scripts/discover-picklists.ts and set all four.

Resource role ID — AUTOTASK_DEFAULT_ROLE_ID

VariableTypeDefaultReq.Effect
AUTOTASK_DEFAULT_ROLE_IDpositive int(built-in fallback — reference-instance value)Set itResource role ID used when assigning a ticket to the accepting engineer and when posting the billed time entry on close.

Resolution is warn-not-fail: if unset — or set to something that isn't a
positive integer — AVA logs a one-time warning and falls back to a built-in
role ID that is only correct for the reference AutoTask instance. Your
instance's role IDs are different, so always set your own (find them with
GET /Roles or via your AT admin's Roles list). An invalid value never blocks
startup; it just degrades to the fallback — which would post time entries under
a role your instance doesn't recognize.

Catchall company — AUTOTASK_CATCHALL_COMPANY_ID

VariableTypeDefaultReq.Effect
AUTOTASK_CATCHALL_COMPANY_IDint (AT company ID)(unset)RecommendedCompany that receives tickets from email domains not tied to any AT company (personal Gmail, etc.).

If unset, unknown-domain chats start without an AutoTask ticket and fall
into a degraded recovery path (late ticket creation at close, or an
engineer-confirmed close-without-sync). Set it to avoid that path entirely. Note
0 is a valid AutoTask company ID — AVA treats "unset" and "0"
differently, so an explicit 0 does enable the catchall. Find a company's ID
with npx tsx scripts/test-autotask.ts <domain>.


Microsoft Entra ID

VariableTypeDefaultReq.Effect
AZURE_AD_CLIENT_IDGUIDRequiredApp registration client ID.
AZURE_AD_CLIENT_SECRETstringRequiredApp registration client secret.
AZURE_AD_TENANT_IDGUIDRequiredTenant ID (forms the MSAL authority URL).
AZURE_AD_REDIRECT_URIURLhttp://localhost:3000/api/auth/callbackRecommendedOAuth callback; must match the app registration exactly. In production set it to https://<your-chat-domain>/api/auth/callback.
ENGINEERING_GROUP_IDGUIDRequiredSecurity-group object ID; sign-in requires membership (fail-closed if the groups claim is missing or malformed).

AI / LLM

AVA's AI layer (triage, similar-resolution suggestions, trend summaries, field
summarization, and the close-flow ticket summary) is provider-agnostic: it
runs on the Vercel AI SDK behind two role-based model slots, LLM_FAST and
LLM_QUALITY, supporting Anthropic (default), OpenAI, Google Gemini, and
OpenRouter.

The full reference — provider matrix, provider/model string format,
conditional key requirements, which features use which tier, and the timeout
caveat — lives in ai-providers.md. Quick summary:

VariableDefaultNotes
LLM_FASTanthropic/claude-haiku-4-5-20251001Triage, suggestions, trends, field summarization.
LLM_QUALITYanthropic/claude-sonnet-4-6Final ticket summary.
ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENROUTER_API_KEYOnly the key(s) for the provider(s) you actually reference are required.
ANTHROPIC_TIMEOUT_MS15000Per-call AI timeout — applies to all providers despite the name.

Application & networking

VariableTypeDefaultReq.Effect
NEXT_PUBLIC_APP_URLURL(build arg)RequiredPublic app URL. Pins the Socket.io CORS origin (server hard-fails in production if unset) and is baked into the client bundle. Build-time.
NEXT_PUBLIC_SOCKET_URLURL (wss://)(build arg)RequiredWebSocket URL the client connects to. Build-time.
NEXT_PUBLIC_RECAPTCHA_SITE_KEYstring(build arg)RequiredreCAPTCHA v3 site key (public by design). Build-time.
NODE_ENVproduction|developmentproduction (image)OptionalStandard Node environment. The image sets production.
PORTint3000OptionalListen port.
DATABASE_URLlibsql/SQLite URLfile:./techchat.db (code) / file:/app/data/techchat.db (image)OptionalSQLite database location. The image points it at the data volume; override only if you relocate the DB.

Trusted proxy — NGINX_TRUSTED_PROXY

VariableTypeDefaultReq.Effect
NGINX_TRUSTED_PROXY1(unset — never trust)Set 1 behind your proxyWhether AVA trusts X-Forwarded-For / X-Real-IP when determining the real client IP for per-IP rate limiting.

Trust is opt-in only. Set it to exactly 1 when — and only when — the app
always sits behind your own reverse proxy (as in the provided compose + nginx
templates, where nginx overwrites the header from the actual connection
address). Any other value — including unset, and even with
NODE_ENV=production
— means the header is never trusted: a naked,
un-proxied deployment must not let a client spoof its IP simply by sending the
header itself. When multiple comma-separated hops are present, only the
right-most (proxy-appended) entry is used — never the attacker-controlled
left-most one.

Operational consequence of forgetting it: without trust, every request's IP
resolves to "unknown", so all per-IP rate limits (chat starts, uploads)
collapse into one shared bucket for all visitors. Behind the template nginx,
always set NGINX_TRUSTED_PROXY=1.


Bot protection & email

VariableTypeDefaultReq.Effect
RECAPTCHA_SECRET_KEYstringRequiredreCAPTCHA v3 server-side secret (siteverify).
RECAPTCHA_DEV_BYPASStrue(unset — fail closed)Optional (dev only)See below. Refused in production.
SMTP2GO_HOSThostmail.smtp2go.comOptionalSMTP2GO host.
SMTP2GO_PORTint2525OptionalSMTP2GO port.
SMTP2GO_USERstringRequiredSMTP2GO username.
SMTP2GO_PASSWORDstringRequiredSMTP2GO password.
SMTP2GO_FROMemail(none — fail closed)Set itSender address on all outbound mail. No vendor default: when unset, every email-sending route fails closed — the send is skipped and the route returns an error rather than mailing from a placeholder address. Set it to an address on your own domain.
WEEKLY_REPORT_TOKENstring(unset)OptionalBearer token guarding the weekly-survey-report cron endpoint. Generate with openssl rand -hex 32; required only if you wire up that cron.

Recipient addresses

VariableTypeDefaultReq.Effect
SUPPORT_FALLBACK_EMAILemail(none)Required to bootRecipient for the after-hours "email a ticket" fallback (listed in Required to boot).
FEEDBACK_EMAILemail(none — fail closed)OptionalRecipient for engineer feedback. When unset, the feedback route fails closed: it skips the send and logs an error — it never falls back to a vendor address.
REPORTS_EMAILemail(none — fail closed)OptionalRecipient for the weekly survey report. Same fail-closed contract as FEEDBACK_EMAIL.

reCAPTCHA verification behavior

reCAPTCHA verification is fail-closed by default everywhere: a missing
secret, a missing token, a network error reaching Google, or a low score all
reject the request.

RECAPTCHA_DEV_BYPASS is the one narrow escape hatch, for development and test
environments only. It fails open (allows the request) when — and only
when — both hold:

  • NODE_ENV is not production, and
  • RECAPTCHA_DEV_BYPASS is set to exactly true (not 1, not yes).

In production it is refused outright — when NODE_ENV=production, no value
of this variable can re-enable the bypass; verification always fails closed.
Leave it unset everywhere except a local dev box that has no real reCAPTCHA
key. The siteverify call also enforces an outbound timeout
(RECAPTCHA_TIMEOUT_MS, below) so a hung Google endpoint cannot stall the
request.


File uploads

Chat participants (client and engineer) can attach files to a session. The
constraints, all enforced server-side:

  • Per-file size cap: 5 MB (fixed). Oversized bodies are rejected from the
    declared Content-Length before the request body is buffered (HTTP
    413), with the post-parse size check retained as defense-in-depth.
  • Type allowlist: PNG, JPEG, GIF, HEIC, PDF, XLSX, DOCX, EML, ZIP, TXT,
    CSV. Checked by MIME type, with an extension fallback for types browsers
    often mislabel (.eml, .heic, .zip, .txt, .csv).
  • Magic-byte content validation: for every type with a reliable signature,
    the actual file bytes must match the declared type — a file claiming to be a
    PNG must actually start like a PNG. Files are stored under
    collision-proof UUID-prefixed names, and served with X-Content-Type-Options: nosniff and a fixed content-type map; only true image/* types render
    inline (everything else downloads as an attachment).
  • Access control: clients can only read their own session's attachments
    (session-token auth); an active assigned session's attachments are
    owner-engineer-only, while closed sessions' attachments are readable by any
    authenticated engineer (archives semantics).

Upload budget, quota & retention

All optional; defaults shown. Invalid values warn once and fall back to the
default.

VariableTypeDefaultEffect
UPLOAD_MAX_PER_IP_PER_HOURpositive int30Per-IP upload budget per hour, enforced before auth/parsing (HTTP 429 when exceeded).
UPLOAD_TOTAL_MAX_BYTESpositive number (bytes)2147483648 (2 GB)Disk ceiling across all sessions' uploads combined. A new upload that would exceed it is rejected with HTTP 507.
UPLOAD_RETENTION_ENABLEDbooltrueScheduled retention job that deletes a closed session's upload directory once it ages past the window below — regardless of whether the files made it into AutoTask. Set exactly false to disable.
UPLOAD_RETENTION_DAYSpositive int30Retention window in days.
UPLOAD_RETENTION_INTERVAL_HOURSpositive number (may be fractional)6How often the retention job runs (in-process interval — no external cron needed).

Business hours

All optional; the defaults reproduce Mon–Fri 6 AM–6 PM Pacific. Invalid
values validate-and-fall-back (they never throw). The configured schedule also
drives the client-facing availability copy — the off-hours message quotes your
actual hours, not a hardcoded string.

VariableTypeDefaultEffect
BUSINESS_HOURS_BYPASSboolfalseExactly true ignores the schedule entirely (chat always "open"). Testing only — set false for real clients.
BUSINESS_HOURS_TZIANA TZAmerica/Los_AngelesTimezone the open/close hours are evaluated in. Invalid zone names warn once and fall back.
BUSINESS_HOURS_OPENint 0–236Opening hour (local to BUSINESS_HOURS_TZ).
BUSINESS_HOURS_CLOSEint 0–2318Closing hour.
BUSINESS_HOURS_DAYSCSV of 1–7 (ISO, 1=Mon)1,2,3,4,5Open days.

White-label / branding

AVA ships vendor-neutral: with none of these set, the app presents as a
generic "Your Company" deployment with no phone number and no client-portal
link. Every deploying MSP sets its identity here. All values are read at call
time — set them in your runtime env file and restart; no rebuild. See
white-label.md for the full walkthrough, worked example,
and verification checklist.

VariableTypeDefaultEffect
COMPANY_NAMEstringYour CompanyFull display name across the UI (page metadata, contact form, dashboard).
COMPANY_DESCRIPTORstringIT SupportDescriptor appended after the company name in prose — the page metadata description and the AI summary prompt (<COMPANY_NAME> <COMPANY_DESCRIPTOR>).
COMPANY_MONOGRAMstring (≤4 chars)AVADashboard rail tile monogram (silently truncated to 4 characters).
COMPANY_TAGLINEstringPowered by AVAFooter identity line on the client page and dashboard.
WELCOME_MESSAGEstring(neutral greeting — see below)System greeting posted at session start. Default: "Welcome to Tech Support! Your session has been created and an engineer will be with you shortly. …" including the recorded-conversation and no-passwords notices.
SUPPORT_PHONEstring(unset — hidden)Displayed support phone. When unset or empty, every phone element (banner, contact form, off-hours copy, timeout overlay) is hidden and prose adjusts. When set, a tel: href is derived automatically (bare 10-digit numbers get +1; +-prefixed international numbers pass through).
AT_CLIENT_PORTAL_URLURL(unset — plain ticket number)Base URL for client-portal ticket links. When unset or empty, the UI shows a plain ticket number instead of a link.
LOGO_PATHpublic path/ava-logo.svgPublic path of the brand logo shown on the client page and dashboard mobile bar. An empty value falls back to the default. The file must exist under public/ in the built image.
DISPLAY_TZIANA TZBUSINESS_HOURS_TZAmerica/Los_AngelesTimezone for all human-facing timestamps (message times, day dividers, transcript, wrap-up/billing draft). Invalid zones warn once and fall back.
COMPANY_PROFILEstringa managed service provider (MSP) supporting business clientsClientele description injected into the AI triage prompt to sharpen classification (e.g. an MSP serving medical and dental practices).

Recipient/sender addresses (SUPPORT_FALLBACK_EMAIL, FEEDBACK_EMAIL,
REPORTS_EMAIL, SMTP2GO_FROM) are part of the same identity surface but are
documented under Bot protection & email since they
carry required/fail-closed semantics.


Session limits

VariableTypeDefaultEffect
MAX_ACTIVE_SESSIONSint 1–102Per-engineer concurrent active-chat cap. Out-of-range/invalid values warn and fall back to 2.

External-call timeouts

All optional; defaults shown. Milliseconds.

VariableTypeDefaultEffect
AUTOTASK_TIMEOUT_MSint (ms)15000Per-request AutoTask REST timeout.
SMTP_TIMEOUT_MSint (ms)10000SMTP connect + socket timeout.
RECAPTCHA_TIMEOUT_MSint (ms)10000reCAPTCHA siteverify fetch timeout.
ANTHROPIC_TIMEOUT_MSint (ms)15000AI per-call timeout (all providers) — see ai-providers.md.

NinjaOne (optional, read-only device glance)

Off by default. With NINJAONE_BASE_URL / NINJAONE_CLIENT_ID /
NINJAONE_CLIENT_SECRET unset, the feature is completely dormant — the
engineer dashboard's client-context panel behaves byte-for-byte as if it
didn't exist (no Device section renders, no NinjaOne network calls are ever
made). Set all three to turn it on.

What it does. When a client starts a chat, AVA tries to identify their
machine and shows the engineer a read-only "device at a glance" card in the
chat's context panel — online/offline status, last boot, disk free,
pending/failed OS patches, antivirus state, public/private IP, make/model/RAM,
last logged-on user, and recent alerts — plus a one-click "Open in
NinjaOne"
deep link to the device's dashboard page. It never modifies
anything: the NinjaOne API credential is scoped to monitoring only, and the
NinjaOne client exposes GET requests exclusively. Two resolution paths work
independently of each other:

  • AutoTask-only baseline (no NinjaOne credentials needed). If your
    AutoTask instance already syncs NinjaOne data onto Configuration Items (the
    NinjaRMM-RemoteUrl / -BackgroundUrl / -Antivirus / -PrivateIpAddress
    / -DeviceUID UDFs), AVA matches the chat's AutoTask company + reported
    hostname to a Configuration Item and shows those UDF facts plus the deep
    link parsed out of the remote-URL UDF — even with NINJAONE_* fully unset.
  • Live NinjaOne API layer (optional, adds live telemetry). When the three
    vars above are set, AVA additionally queries the NinjaOne API directly
    for live device data and can fall back to a hostname/user search when no
    Configuration Item matched.

Create the API client (NinjaOne admin)

  1. In NinjaOne: Administration → Apps → API → Client App IDs → Add.
  2. Platform: API Services (machine-to-machine).
  3. Scope: Monitoring only — do not grant management or any write
    scope. This is a least-privilege design choice, not just a convention: the
    resulting credential is physically incapable of modifying a device, running
    a script, or triggering an action, no matter what a bug in AVA did.
  4. Grant type: Client Credentials. No redirect URI is needed (this is a
    server-to-server, not an interactive OAuth flow).
  5. Copy the generated Client ID and Client Secret into your env (below).

Env vars

VariableTypeDefaultReq.Effect
NINJAONE_BASE_URLURL(unset)Required to enableYour NinjaOne region's API base URL — see Region base URLs below. Trailing slash optional.
NINJAONE_CLIENT_IDstring(unset)Required to enableThe Client App ID from the step above.
NINJAONE_CLIENT_SECRETstring(unset)Required to enableThe Client App secret. Quote it in your env file — read via the same raw-file mechanism as AUTOTASK_API_SECRET, so a value containing $ or other shell-special characters is safe.
NINJAONE_TIMEOUT_MSint (ms)15000OptionalPer-request timeout for NinjaOne API calls.
NINJAONE_ENRICH_TICKETSboolfalse (unset)OptionalSole enable switch for AT ticket writes — attaches the resolved Configuration Item (configurationItemID) and, when available, the NinjaRMM-DeviceUID UDF, on any chat that resolves a device at high confidence. Read-only device glance itself is unaffected either way — this only gates the best-effort write-back onto your AutoTask ticket. Do not set this to true until you have run the UDF merge pre-flight against your own AutoTask instance — see below.

All three required vars must be set together — the feature is all-or-nothing.
NINJAONE_ENRICH_TICKETS is independent of those three and defaults to off
even when they're set.

UDF merge pre-flight (before enabling NINJAONE_ENRICH_TICKETS)

AT's PATCH /Tickets handling of the userDefinedFields array is
instance-dependent: some configurations merge the array, others could blank
UDFs the PATCH omits. Before flipping the flag on, verify your instance
merges:

  1. Create a throwaway scrap ticket in AutoTask.
  2. Seed two different UDFs on it with known values.
  3. PATCH the ticket updating only one of the two UDFs.
  4. Confirm the other UDF survives untouched.

Only enable NINJAONE_ENRICH_TICKETS=true after step 4 passes. Another
instance's pre-flight result does not transfer to yours.

Region base URLs

Use the same NinjaOne data-center region your admin console login URL shows:

RegionNINJAONE_BASE_URL
Americas (default/US)https://app.ninjarmm.com
Americas 2https://us2.ninjarmm.com
Canadahttps://ca.ninjarmm.com
Europehttps://eu.ninjarmm.com
Oceaniahttps://oc.ninjarmm.com

Systray URL setup — auto-identifying the client's machine

AVA resolves "which machine is this?" most reliably when the client reaches
the chat page via a link that carries systray-substituted hints. NinjaOne's
systray custom URL items support ${HOSTNAME} / $USERNAME / ${DOMAIN}
placeholders, substituted at click time. In your NinjaOne branding/systray
policy, set the chat link to:

https://<your-chat-domain>/?host=${HOSTNAME}&user=$USERNAME&domain=${DOMAIN}

AVA captures these once on page load, forwards them with the chat-start
request, and stores them on the session as display + lookup hints only —
never an authorization signal
. Omitting the query string entirely still
works; the device card simply has less to search on (falls back to a
user/email-based search when NinjaOne is configured, or shows a "no managed
device matched" empty state otherwise).

Validate your setup

npx tsx scripts/test-ninjaone.ts [hostname]

Confirms config presence, exercises the real OAuth2 client-credentials token
exchange, lists organizations and a few devices, and — with a hostname
argument — runs the same hostname search the device-resolution pipeline uses.

Honest limitations

  • The "Open in NinjaOne" deep link requires the engineer to already be
    logged into NinjaOne
    in their browser — it is a console URL, not a
    standalone remote-session launcher.
  • No remote-session launch, no reboot/script actions — this is a strictly
    read-only "glance" feature. Any action capability is a separate,
    not-yet-built tier.
  • Systray hints are client-reported, exactly like the browser user agent —
    useful for lookup, never trusted for identity/authorization.
  • Live telemetry values (disk, patches, AV, alerts) reflect NinjaOne's own
    last-check-in data for that device, not a real-time poll triggered by the
    chat.

Demo mode

.env.example also lists a DEMO_* block (DEMO_MODE plus lead-capture SMTP
settings). These exist solely for running a public, self-guided product demo
against in-memory fakes — never set any of them in a production
deployment
. With DEMO_MODE unset (the default), none of the demo code
paths can activate.


See also


Did this page help you?