Configuration
Environment variables, deployment modes, and advanced configuration options.
Deployment Modes
Elmo supports several deployment modes, controlled by the DEPLOYMENT_MODE environment variable:
| Mode | Description |
|---|---|
local | Single-org, email/password auth. Best for individual use. (default) |
demo | Read-only mode for showcasing Elmo without allowing changes. |
cloud | Managed hosting. Sign up for Elmo Cloud. |
Environment Variables
When you run elmo init, the CLI generates a .env file in your config directory. You can edit this file directly to change settings.
Core Settings
| Variable | Description | Default |
|---|---|---|
DEPLOYMENT_MODE | Deployment mode | local |
DATABASE_URL | PostgreSQL connection string | Generated by CLI |
BETTER_AUTH_SECRET | Secret key for session encryption | Auto-generated |
ELMO_ENCRYPTION_KEY | Base64-encoded 32-byte key used to encrypt provider credentials stored in the database | Auto-generated |
ELMO_ENCRYPTION_KEY_OLD | Previous encryption keys, comma-separated, kept readable during a rotation | — |
DISABLE_TELEMETRY | Set to 1 to disable all telemetry. See Telemetry. | — |
DEFAULT_DELAY_HOURS | How often each enabled prompt is re-run against the AI models, in hours. | 24 |
RUNS_PER_PROMPT | How many times each run samples every tracked platform. Answers vary between identical calls, so several samples make a mention rate a measurement rather than an anecdote — but each one is a provider call you pay for. | 5 |
Back up ELMO_ENCRYPTION_KEY alongside your database. Credentials stored in the database are encrypted with it and nothing else — if you lose the key, or restore a database backup onto a deployment that generated a different one, those credentials cannot be recovered and have to be entered again. Elmo logs an error and falls back to the provider keys in your .env when a stored credential will not decrypt.
Rotating the encryption key
Every stored credential records which key encrypted it, so a rotation doesn't have to happen all at once:
- Move the current value of
ELMO_ENCRYPTION_KEYintoELMO_ENCRYPTION_KEY_OLD. - Put a freshly generated key in
ELMO_ENCRYPTION_KEY(openssl rand -base64 32), and restart.
New values are encrypted under the new key while anything saved under a retired one stays readable, so nothing has to be re-entered up front. Drop ELMO_ENCRYPTION_KEY_OLD once every stored credential has been saved again — until then, keep it, or those credentials become unreadable and Elmo logs which key it is missing.
RUNS_PER_PROMPT multiplies provider spend one-for-one: at the default, one firing of one prompt
against four platforms is twenty provider calls. Lowering it to 1 cuts the bill by five and makes
each result a single sample, so day-to-day movement gets noisier. It applies from the next firing —
no rescheduling needed. A value below 1, fractional, or unparseable is ignored in favour of the
default, so a typo cannot silently stop tracking or inflate the bill.
DEFAULT_DELAY_HOURS only takes effect for prompts scheduled after you change it. Prompts that are already scheduled keep the cadence they were created with, even after a restart. To apply a new cadence to an existing prompt, disable it and then re-enable it under Settings → Prompts — re-enabling reschedules it at the current DEFAULT_DELAY_HOURS.
AI Provider Keys
At least one provider is required for Elmo to track visibility. See Providers for a full cost/capability breakdown and recommended setups.
Brand onboarding and the Opportunities report also need one direct LLM API key — OPENAI_API_KEY, OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or MISTRAL_API_KEY — even if SCRAPE_TARGETS only uses scrapers. Elmo uses the first of those that's set; to pick one explicitly, set ONBOARDING_LLM_TARGET (e.g. claude:anthropic-api).
| Variable | Description |
|---|---|
BRIGHTDATA_API_TOKEN | BrightData token — recommended scraper for ChatGPT + Google AI Mode. Sign up |
OXYLABS_USERNAME | Oxylabs Web Scraper API username — alternative scraper for ChatGPT, Perplexity, Google AI Mode. Sign up |
OXYLABS_PASSWORD | Oxylabs Web Scraper API password |
CLORO_API_KEY | Cloro key — alternative scraper for ChatGPT, Perplexity, Copilot, Gemini, Google AI Mode, and Google AI Overview. Sign up |
SEARCHAPI_API_KEY | SearchApi.io key — alternative scraper for ChatGPT, Perplexity, Copilot, Gemini, Google AI Mode, and Google AI Overview, at one flat price per search. Sign up |
OLOSTEP_API_KEY | Olostep key — recommended scraper, powers most large-scale trackers. Sign up |
OPENAI_API_KEY | OpenAI API key — enables ChatGPT (via API, not the consumer UI) |
ANTHROPIC_API_KEY | Anthropic API key — enables Claude tracking |
OPENROUTER_API_KEY | OpenRouter key — one key for Claude + other hosted models |
MISTRAL_API_KEY | Mistral API key — enables Mistral tracking |
DATAFORSEO_LOGIN | DataForSEO login — optional, enables DataForSEO targets such as chatgpt:dataforseo:online, gemini:dataforseo:online, google-ai-mode:dataforseo:online, and perplexity:dataforseo:online |
DATAFORSEO_PASSWORD | DataForSEO password |
SCRAPE_TARGETS | Comma-separated model:provider[:version][:online] entries. See Providers. |
Mention Judging
Elmo uses Jev from TypeSafe to read each answer. Jev decides which tracked companies the answer really mentions, its sentiment toward each one, and the passage that shows it. The worker won't start without a key for Jev: either a TypeSafe key from console.typesafe.ai, or an OPENROUTER_API_KEY, which calls Jev through OpenRouter and bills your OpenRouter account.
| Variable | Description | Default |
|---|---|---|
TYPESAFE_API_KEY | TypeSafe API key for Jev. When set, Elmo calls TypeSafe directly, even if OPENROUTER_API_KEY is set too. | — |
OPENROUTER_API_KEY | Without TYPESAFE_API_KEY, Elmo calls Jev through OpenRouter with this key. | — |
REPROCESS_DAYS | How far back reprocessing (re-extracting and re-judging history) goes, in days, or all | 90 |
elmo init asks which one to use, offering the OpenRouter key you entered for onboarding if there is one. If you add an OpenRouter key just for Jev, it also pins ONBOARDING_LLM_TARGET to the provider you chose for onboarding, which OpenRouter would otherwise take over from Anthropic or Mistral. elmo upgrade asks the same before it stops anything when .env has neither key, or only an OpenRouter key on a deployment from before Jev; it can't see a key saved only in Elmo's settings.
Elmo makes one Jev call for each answer that names a tracked company. Answers that name none are never sent. Its cost grows with the answer's length and with how many tracked companies it names.
Very roughly, that's $0.15–0.35 per 1,000 answers. A month of answers is about prompts × platforms × RUNS_PER_PROMPT × 30 when prompts run daily:
| What you track (daily, 5 samples) | Answers a month | Jev, per month |
|---|---|---|
| 25 prompts × 3 platforms | ~11,000 | ~$2–4 |
| 100 prompts × 5 platforms | ~75,000 | ~$11–26 |
| 500 prompts × 5 platforms | ~375,000 | ~$55–130 |
These are estimates, not quotes; see TypeSafe's pricing, or OpenRouter's for Jev, for current rates.
The worker picks TypeSafe or OpenRouter when it starts, and never falls back from one to the other. Both serve the same Jev release, so switching keeps the judgements you already have; only a new Jev version in an Elmo release re-judges history. OpenRouter serves Jev 1.13 from a dated snapshot that it may update to a newer 1.13 one, and runs requests over about 32 KB in Jev's long-context mode, so long answers may be judged slightly differently there.
A large re-judge goes as fast as your TypeSafe or OpenRouter rate limit allows, with nothing to configure. Each worker finds the limit itself: it speeds up while calls succeed, slows down when Jev rate-limits it, and retries those calls after the wait Jev asks for. For example, with a 100,000 input tokens per second limit, one worker judges about a million answers in roughly 16 hours.
When the judge's version, or a brand's or competitor's names, change, the worker re-judges that brand's history in the background, starting with the last 30 days. A re-judge only goes back REPROCESS_DAYS, and only through enabled prompts of enabled brands. Older answers keep how they were judged when they were recorded. A prompt or brand that's enabled again has its answers within the window caught up at that point. Re-judging only asks about what changed. Adding a competitor, or editing one's names, aliases or domains, asks Jev about that company alone, and only in answers that name it. That costs a fraction of judging those answers from scratch. Removing a competitor asks nothing. Edits made within a minute of each other are handled in one pass. A new judge version re-judges everything in the window, so it costs about REPROCESS_DAYS worth of answers: at the default, roughly three months of the figures above.
Updating Configuration
To change environment variables after initial setup:
# Edit the .env file
nano ~/.elmo/.env
# Restart services to pick up changes
elmo compose down
elmo compose up -dServing Elmo from a Domain
elmo init asks how you'll access Elmo. Choose From a domain and enter it (for example elmo.example.com, which becomes https://elmo.example.com), then point your reverse proxy at the port you pick. Sign-in is only accepted from the configured URL.
To change it later, run elmo edit env, set both APP_URL and VITE_APP_URL to the new URL (for example https://elmo.example.com), then restart with elmo compose up -d. Elmo must be served from the root of its domain, not a subpath.
Using an External Database
By default, elmo init provisions a PostgreSQL container. To use an existing database instead, choose "Use existing Postgres" during elmo init and provide your connection string when prompted.
Your database must be PostgreSQL 15 or later.
Upgrading Postgres
elmo init provisions Postgres 18. elmo upgrade rolls the Elmo images but never touches the Postgres container, so a deployment created on an earlier major keeps running on it — supported, and nothing you have to act on. Postgres 16 gets fixes until November 2028.
If you do want to move, Postgres cannot read a data directory written by a different major version, so it takes a dump and restore.
Do not just change the image tag. From version 18 on, the official Postgres image stores its data in a version-specific subdirectory (/var/lib/postgresql/18/docker) and declares its volume one level up, at /var/lib/postgresql. An 18 image with the old /var/lib/postgresql/data mount does not fail — it writes a brand new, empty cluster to an anonymous volume that is discarded the next time the container is recreated. Your original data survives in elmo_postgres_data, but Elmo comes up empty and anything written in the meantime is lost.
Take a dump while the old container is still running:
elmo compose up -d postgres
docker compose -f ~/.elmo/elmo.yaml exec -T postgres pg_dump -U postgres -d elmo > elmo-backup.sql
elmo compose downCheck that elmo-backup.sql is non-empty and ends with -- PostgreSQL database dump complete before continuing. Then edit the Postgres service with elmo edit compose, changing both the image and the mount path:
postgres:
image: postgres:18-alpine
volumes:
- postgres_data:/var/lib/postgresqlNow restore. Postgres 18 finds no cluster at its new path, initializes an empty one alongside the old files, and the dump goes into that:
elmo compose up -d postgres
docker compose -f ~/.elmo/elmo.yaml exec -T postgres psql -U postgres -d elmo < elmo-backup.sql
elmo compose up -dBringing up postgres on its own first matters: it keeps db-migrate from initializing an empty schema ahead of the restore.
Nothing is destroyed along the way: the old cluster stays in the volume, unused, so you can roll back by putting the original image and /var/lib/postgresql/data mount back. Keep elmo-backup.sql either way.
Was this page helpful?