Every environment variable, config.json, and the precedence rules
Configuration
Configuration comes from .env (see .env.example) plus config.json for feed URLs and the technology filter. Values in config.json support ${ENV_VAR} substitution, and a few keys (CRON_SCHEDULE, SLACK_ENABLED) can be overridden from the environment.
DATABASE_URL is the one variable Atalaia cannot start without. Everything else has a default or an off switch.
Optional integrations ship commented out, and should stay that way until you have real values. A variable set here beats the console, so a placeholder webhook or SMTP host is not harmless: Atalaia treats it as deliberate configuration, greys out the matching console section, and fails every Send test against it. ./scripts/atalaia.sh doctor flags any value in .env that is still the one from .env.example.
Core
| Variable | Default | Description |
|---|---|---|
API_KEY |
— | Required. Key for every /api/v1/* request (X-API-Key header). |
PORT |
3000 |
API port. |
HOST |
0.0.0.0 |
API bind address. |
NODE_ENV |
— | production disables the ngrok/Slack dev bootstrap. |
LOG_LEVEL |
info |
Pino level: trace…fatal. |
DATABASE_URL |
— | Required. Any Postgres 13+. Use the session connection on port 5432, not a 6543 transaction pooler: pgbouncer in transaction mode breaks prepared statements and LISTEN, and the queue needs both. Keep the host's address here (127.0.0.1 for a database on this machine) — the launcher translates it to host.docker.internal for the containers, so one value serves the host tools and the containers alike. |
DATABASE_POOL_MAX |
10 |
Connections per process. |
PGBOSS_SCHEMA |
pgboss |
Schema the queue keeps its own tables in. |
CRON_SCHEDULE |
0 * * * * |
Monitoring cycle; overrides config.json. Registered in the database by the worker, so a change takes effect when the worker restarts. |
CORS_ORIGINS |
http://localhost:3000 |
Comma-separated allowed origins. |
Console
| Variable | Default | Description |
|---|---|---|
UI_PORT |
3001 |
Console port. |
UI_HOST |
0.0.0.0 |
Console bind address. |
ATALAIA_API_URL |
http://localhost:3000 |
API base URL the console proxies to — and the one the terminal client uses. |
BFF_TIMEOUT_MS |
120000 |
Upstream timeout — a repository scan can take minutes. |
BFF_URL |
http://localhost:3001 |
Vite dev-server proxy target. Development only. |
UI_PASSWORD and UI_SESSION_SECRET are gone. The console signs people in with passkeys and signs nothing itself; UI_PASSWORD is still read, but only as a fallback name for SETUP_PASSWORD below.
Sign-in
Read by the API, and describing the console's address — the ceremony happens in the browser talking to the console, and the API verifies it. The service refuses to start on a value the browser would reject. See Authentication.
| Variable | Default | Description |
|---|---|---|
SETUP_PASSWORD |
— | Creates the first console account, then stops granting access. Generated by the launcher on first run. |
WEBAUTHN_RP_ID |
localhost |
The domain passkeys belong to. Bare domain: no scheme, no port. Changing it invalidates every passkey. |
WEBAUTHN_ORIGINS |
http://localhost:3001 |
Comma-separated console origins, in full. https unless loopback, and each must sit under WEBAUTHN_RP_ID. |
WEBAUTHN_RP_NAME |
Atalaia Console |
What the browser's prompt calls this service. |
WEBAUTHN_REQUIRE_UV |
false |
Require a PIN or fingerprint. Off, so authenticators that cannot do it still work. |
SESSION_TTL_HOURS |
720 |
Session lifetime. |
CHALLENGE_TTL_SECONDS |
120 |
How long a ceremony may take. |
AUTH_ALLOW_BREAKGLASS |
false |
Lets SETUP_PASSWORD enroll a passkey for an existing account. A recovery mechanism; leave it off in production. |
AUTH_SWEEP_CRON |
17 * * * * |
When spent challenges and week-old sessions are deleted. |
MCP_API_KEY |
— | A key that opens /mcp and nothing else. Set it, and the REST key stops opening /mcp. Unset, agents use API_KEY and can reach everything it can. |
TRUST_PROXY |
— | true, a hop count, or a subnet. Off by default: without a proxy in front, believing X-Forwarded-For lets any caller claim any address. |
Chat integrations
| Variable | Default | Description |
|---|---|---|
SLACK_ENABLED |
false |
Master switch; overrides config.json. |
SLACK_WEBHOOK_URL |
— | Incoming webhook for alerts. |
SLACK_SIGNING_SECRET |
— | Verifies interactive button callbacks. Required for Acknowledge/Resolve. |
SLACK_APP_TOKEN |
— | Dev only: lets Atalaia update the app's Request URL. |
SLACK_APP_ID |
— | Dev only: the app to update. |
TEAMS_WEBHOOK_URL |
— | Microsoft Teams Workflows webhook. Pins the integration to the environment. |
TEAMS_ENABLED |
— | Forces Teams delivery on or off wherever it is configured. |
TELEGRAM_BOT_TOKEN |
— | Bot token from @BotFather. Pins the integration to the environment. |
TELEGRAM_CHAT_ID |
— | Where alerts go: a group (-100…), a channel (@name) or a person's chat. |
TELEGRAM_ENABLED |
— | Forces Telegram delivery on or off wherever it is configured. |
Callbacks and tunnels
Slack and Telegram call back — a button pressed in a chat has to arrive somewhere. A deployment with a hostname sets PUBLIC_URL and needs nothing else; a laptop borrows one.
| Variable | Default | Description |
|---|---|---|
PUBLIC_URL |
— | Where this API answers from, as the internet sees it. Wins over any tunnel. |
TUNNEL_PROVIDER |
auto |
auto, ngrok, cloudflared or none. auto takes ngrok when it has a token, Cloudflare's quick tunnel otherwise. Outside production a tunnel is opened even when this is unset. |
NGROK_AUTH_TOKEN |
— | Required by ngrok; without it auto skips to Cloudflare. |
NGROK_REGION |
auto |
ngrok region. |
Cloudflare's quick tunnel needs no account and no token, and downloads its binary on first use — so the first start is slower, and a machine with no outbound network cannot use it.
Feeds and scanning
| Variable | Default | Description |
|---|---|---|
FEED_TIMEOUT_MS |
15000 |
Per-feed HTTP timeout. |
FEED_DELAY_MS |
2000 |
Pause between feeds in a cycle — keeps scraped sources happy. |
FEED_HEALTH_TTL_MS |
60000 |
Cache TTL for /api/v1/feeds/health. |
OPENCVE_API_URL |
— | OpenCVE instance for vendor/product lookup. |
OPENCVE_API_TOKEN |
— | OpenCVE token. |
GITHUB_TOKEN |
— | Fallback token for the GHSA feed and for repository scanning when an organization has none of its own. |
TOKEN_ENCRYPTION_KEY |
falls back to API_KEY |
Key used to encrypt organization tokens at rest. Change it and the stored tokens become unreadable. |
MITRE_MAX_RECORDS |
25 |
CVE records fetched per cycle from the MITRE delta — one request each. |
REDHAT_PAGE_SIZE |
100 |
CVEs per Red Hat Security Data page. |
USN_LIMIT |
10 |
Ubuntu notices per cycle. A single kernel notice can carry hundreds of CVEs. |
REGISTRY_CONCURRENCY |
6 |
Registry lookups at once during a freshness check. |
SCAN_CONCURRENCY |
10 |
Repositories scanned at once — by a fleet sweep and by one-off scan jobs alike. Parallelism does not reduce the number of GitHub requests, only the rate, and a token allows 5000 an hour: raise it for a fleet in the tens, lower it if GitHub starts refusing. |
LLM summaries
Normally configured from the console (Settings → Model). These still work and take precedence — setting LLM_PROVIDER turns the console section read-only.
| Variable | Default | Description |
|---|---|---|
LLM_PROVIDER |
— | ollama, lmstudio, openai, anthropic, gemini, openrouter, groq or custom. Unset leaves it to the console. |
OPENAI_API_KEY |
— | Key for whichever hosted provider is selected. |
OPENAI_MODEL |
per provider | Model name. |
OLLAMA_URL |
http://localhost:11434 |
Local Ollama endpoint. |
OLLAMA_MODEL |
llama3.1 |
Ollama model. |
Weekly email report
Delivery is normally configured from the console (Settings → Email), which stores the provider and its credential in the database. These variables still work and take precedence — set SMTP_HOST and the console section turns read-only, so a deployment that pins credentials in the environment keeps behaving exactly as before.
| Variable | Default | Description |
|---|---|---|
SMTP_HOST |
— | SMTP server. Setting it pins the whole email configuration to the environment. |
SMTP_PORT |
587 |
SMTP port. |
SMTP_USER / SMTP_PASS |
— | SMTP credentials. |
EMAIL_FROM |
atalaia@localhost |
Sender address. |
EMAIL_RECIPIENTS |
— | Comma-separated recipients. |
EMAIL_TEMPLATE |
professional |
professional or minimal. |
WEEKLY_REPORT_CRON |
0 9 * * 1 |
Digest schedule — Mondays at 09:00. |
config.json
| Key | Description |
|---|---|
cronSchedule |
Monitoring interval. CRON_SCHEDULE wins. |
slack.enabled / slack.webhookUrl |
Slack switch and webhook (env-substituted). |
feeds.* |
Source URLs for CISA, Snyk, VulDB. |
opencve.* |
OpenCVE API URL and token (env-substituted). |
providers[] |
Git providers (org key, type, token) pinned in configuration. Organizations registered in the console take precedence over an entry with the same key. |
repositories.autoScan / scanCron |
Scheduled dependency scanning. |
repositories.autoFilterFromDeps |
Extend the technology filter from scanned dependencies. |
filterSettings.enabled / technologies[] |
The stack filter applied to every feed item. |
Rules that hold everywhere
The same handful of decisions recur across every feature; knowing them explains most of the behaviour without reading the code.
Environment beats database beats file. Anything set as an environment variable wins and turns the matching console field read-only — a deployment can always pin a value, and a write that would have no effect is refused with 409 rather than silently stored. Next comes what the console wrote to the database, and last config.json, which is the committed default.
Secrets are encrypted at rest and never come back. GitHub tokens, SMTP passwords, Slack credentials and LLM keys are stored with AES-256-GCM keyed by TOKEN_ENCRYPTION_KEY (or API_KEY). The API returns whether one is held and its last four characters, never the value. Changing provider drops the stored key — a SendGrid key is not a Mailgun password.
Reads only, on everything external. GitHub, the vulnerability feeds and the package registries are read and never written to.
Long work runs detached. A fleet scan, a version check or a monitoring cycle answers 202 immediately, refuses a second concurrent run with 409, and reports progress on a GET at the same path. Nothing that outlives an HTTP timeout is run inside a request.
Deleting is soft, and stays. Repositories, organizations, owners and dependencies are marked deleted rather than removed, and an import will not resurrect one behind your back — only asking for it by name will.
Enabled is the operator's switch. Re-importing or re-scanning never flips it, and a disabled repository stops counting towards exposure and relevance.
Counts and filters share one definition. Where a header states a number — repositories exposed, CVEs affecting you, dependencies behind — the same SQL backs the number and the rows beneath it, so the two cannot disagree.
Nothing is claimed that was not verified. A source that answers with zero items is reported as EMPTY, not healthy; a version that cannot be compared answers unknown with the reason instead of guessing; a repository that was never scanned says so instead of showing as clean.