Skip to content

Environment variables

A developer or operator who needs to know what a variable does, what happens when it is missing, and which process must be restarted after changing it.

  • Values are never documented here. This page lists names, purposes and the defaults written in code. Real values live only in bot/.env on each machine and in the hosting provider’s settings for the landing page.
  • Product configuration is read once, at import of bot/config.py, from bot/.env. Every change needs a restart of the bot, the API, or both.
  • APP_ENV defaults to prod. In prod a missing token, key, alert chat or admin password stops start-up.
  • Typos in enumerated values (STICKER_RENDER_MODE, ART_STYLE, …) stop start-up in every mode.
  • The landing page has five variables; four are server-only and must never get the NEXT_PUBLIC_ prefix.
  • .env.example in the product repository carries production values, which differ from the code defaults in a few places listed at the end.

With APP_ENV=prod (the default) the services refuse to start without these. Everything else has a working default.

Variable Needed by Rule
TELEGRAM_BOT_TOKEN bot Always, even in dev
TELEGRAM_BOT_USERNAME bot, API Without @; used in the QR deep link and pack names
GEMINI_API_KEY or OPENAI_API_KEY bot At least one; the production key must differ from TEST_GEMINI_API_KEY
ADMIN_CHAT_ID bot Numeric chat id for alerts
ADMIN_PANEL_PASSWORD API Non-empty; 12+ characters recommended
LEADS_SECRET + LEADS_IP_SALT API Only when the leads module is used: the secret at least 24 characters, the salt non-empty
LEADS_CHAT_ID, LEADS_ADMIN_IDS bot Only with the leads module; missing values are warnings, not start-up errors

Strongly recommended for an event, though not enforced: ADMIN_USER_IDS (who may use /stats, /pause, /resume), STICKER_RENDER_MODE=figure, the PRICE_* values, BUDGET_USD, BASE_URL. A failed start shows the reason in the journal as Конфигурация некорректна: … (“configuration is invalid”).

  • File: bot/.env, next to config.py. Encoding utf-8-sig, so a byte-order mark is tolerated.
  • Precedence: a variable already present in the process environment wins over the file. On the server systemd loads the same file as EnvironmentFile, so both sources agree.
  • Format: KEY=value, comments only on their own lines.
  • Relative paths (DATA_DIR, DB_PATH, TEMPLATES_DIR, LOG_DIR) resolve from the bot/ folder, not from the current directory, so both processes see the same database.
  • Booleans: 1, true, yes, on (any case) mean true; any other non-empty value means false; empty means the default.
  • Integers are parsed with int(). A non-numeric value raises an exception at import, before validation runs.
  • Validation: Config.validate(component) returns errors and warnings. Errors stop the process with exit code 1 and a log line Конфигурация некорректна: … (“configuration is invalid”). See Local setup: product for what dev relaxes.

Legend for the Required column: yes = start-up error when missing; warn = warning only; no = optional. The value before the slash is for APP_ENV=prod, after it for dev.

“Restart” in the notes means: bot = main.py (bot + worker), API = uvicorn, both = both processes.

Name Required (prod / dev) Purpose Default Notes
APP_ENV no / no prod = strict checks; dev = missing keys become warnings. prod Returned by GET /health as app_env. Restart both.
Name Required (prod / dev) Purpose Default Notes
TELEGRAM_BOT_TOKEN yes / yes for the bot, warn for the API Bot API token. empty Secret. Exactly one main.py may poll a token. The API uses it only to send single messages (“generating…”, pack-limit notices) and lead flood alerts. Restart both.
TELEGRAM_BOT_USERNAME yes / warn Bot username without @: builds the QR deep link https://t.me/<bot_username>?start=<token>; fallback for the pack name suffix. empty Restart both.
Name Required (prod / dev) Purpose Default Notes
ADMIN_CHAT_ID yes / warn (bot) Chat that receives alerts (failed job, provider fallback, out of credit). empty Must be an integer; group ids are negative. Not a number = error at import. Empty = alerts only in the log. Restart bot (and API for lead flood alerts).
ADMIN_USER_IDS warn / warn Comma-separated Telegram user ids allowed to call /stats, /admin_stats, /pause, /resume. Also excluded from the pack counter in /stickers_stats. empty set Non-numeric entry = error at import. Empty = nobody can call these commands. Restart bot.
LOG_DIR no / no Folder for app.log (bot) and api.log (API). ../logs (relative to bot/) Rotation 5 MB × 3 backups. Restart both.
Name Required (prod / dev) Purpose Default Notes
LEADS_ENABLED no / no Master switch of the leads module. 1 0 = POST /leads answers 404, the bot has no leads router and no publishing loop. Restart both.
LEADS_SECRET no / no Shared secret with the landing page, sent in the X-Stickers-Leads-Secret header. empty Secret. Empty = POST /leads answers 404. When set: shorter than 24 characters is an error in prod, a warning in dev. Must equal the landing’s LEADS_SECRET. Restart API.
LEADS_IP_SALT yes / warn when LEADS_SECRET is set HMAC salt for the visitor IP hash; the raw IP is never stored. empty Secret. Changing it makes new hashes differ from the stored ones, so matching by visitor starts from zero. Restart API.
LEADS_CHAT_ID warn / warn Group where lead cards are published and /stickers_* commands work. empty Integer, negative for groups. Empty = leads accumulate in the database unpublished, and the bot side of the module is off. Restart bot.
LEADS_ADMIN_IDS warn / warn Comma-separated Telegram user ids allowed to press status buttons and call /stickers_*. empty set Empty = nobody can. Restart bot.
LEADS_CARD_PII no / no 1 = cards show name, Telegram handle, phone, comment; 0 = masked cards. 1 Restart bot.
LEADS_IP_LIMIT no / no Leads accepted per visitor IP per window. 5 Must be ≥ 1. Restart API.
LEADS_IP_WINDOW_SEC no / no Length of that window, seconds. 600 Must be ≥ 1. Restart API.
LEADS_HOURLY_LIMIT no / no Leads accepted per hour in total; protects the group from a flood. 60 Must be ≥ 1. Exceeding it sends an alert at most once an hour. Restart API.

The bot publishes leads only when LEADS_ENABLED is on and LEADS_CHAT_ID is set. The API accepts leads only when LEADS_ENABLED is on and LEADS_SECRET is set. The two conditions are independent.

Name Required (prod / dev) Purpose Default Notes
ADMIN_PANEL_USER no / no HTTP Basic user name for /admin. admin Restart API.
ADMIN_PANEL_PASSWORD yes / warn (API) HTTP Basic password. empty Secret. Empty in dev = /admin answers 503. Shorter than 12 characters = warning. Restart API.
ADMIN_SHOW_ANCHOR no / no Show the anchor portrait and the consistency summary of a job in the panel. 1 0 = both endpoints answer 404. Restart API.
Name Required (prod / dev) Purpose Default Notes
GEMINI_API_KEY at least one of the two keys: yes / warn Key of the Google provider. empty Secret. Must differ from TEST_GEMINI_API_KEY (error in prod). Restart bot.
OPENAI_API_KEY see above Key of the OpenAI provider. empty Secret. Only one key set = warning: fallback cannot work. Restart bot.
PRIMARY_AI_PROVIDER no / no Provider tried first. gemini Must be gemini or openai; anything else, including an empty value, is an error. Restart bot.
FALLBACK_AI_PROVIDER no / no Provider tried when the primary fails. openai Same rule. Equal to the primary = warning (fallback effectively off). Restart bot.
AI_TIMEOUT_SEC no / no Timeout of one sheet generation request, seconds. 180 Restart bot.
GEMINI_IMAGE_MODEL no / no Image model id for Google. gemini-3.1-flash-image Read in services/ai_pipeline.py. Empty = default. Restart bot.
OPENAI_IMAGE_MODEL no / no Image model id for OpenAI. gpt-image-2 Same.
GEMINI_VISION_MODEL no / no Cheap vision model: appearance card, anchor check, identity QA. gemini-3.5-flash-lite Same.
OPENAI_VISION_MODEL no / no Vision model for OpenAI. gpt-5-mini Same.
QA_IDENTITY_ENABLED no / no Identity QA of each sheet: one extra vision call per sheet and a retry on mismatch. true Restart bot.
AI_MAX_PARALLEL_IMAGES no / no Maximum simultaneous image requests per process, across all worker slots. 9 Must be ≥ 1. Restart bot.

Prices are never hard-coded. An empty price contributes 0 to the estimate and the admin panel shows a “prices not set” warning. .env.example ships the public list prices that were current when it was written; verify them against the provider’s price page before an event.

Name Required (prod / dev) Purpose Default Notes
PRICE_GEMINI_IMAGE_INPUT_PER_MTOK_USD no / no Image model, input, USD per 1M tokens. not set Decimal comma is accepted. Not a number = error at import. Restart bot (and API, which shows the totals).
PRICE_GEMINI_IMAGE_OUTPUT_IMAGE_PER_MTOK_USD no / no Image model, image output tokens. not set
PRICE_GEMINI_IMAGE_OUTPUT_TEXT_PER_MTOK_USD no / no Image model, text and reasoning output tokens. not set
PRICE_GEMINI_IMAGE_1K_USD, …_2K_USD, …_4K_USD no / no Per-image price by resolution. not set Used only when a response carries no token counts.
PRICE_GEMINI_VISION_INPUT_PER_MTOK_USD no / no Vision model, input. not set
PRICE_GEMINI_VISION_OUTPUT_PER_MTOK_USD no / no Vision model, output including reasoning. not set
PRICE_OPENAI_IMAGE_PER_IMAGE_USD no / no OpenAI price per generated image. not set
PRICE_OPENAI_VISION_INPUT_PER_MTOK_USD, PRICE_OPENAI_VISION_OUTPUT_PER_MTOK_USD no / no OpenAI vision prices. not set
PRICE_IMAGE_PER_IMAGE_USD, PRICE_IMAGE_INPUT_PER_MTOK_USD, PRICE_IMAGE_OUTPUT_PER_MTOK_USD, PRICE_VISION_INPUT_PER_MTOK_USD, PRICE_VISION_OUTPUT_PER_MTOK_USD no / no Legacy names without a model in them. not set Fallback for the Google prices when the new names are empty. Do not use in new installations.
BUDGET_USD no / no Starting budget; the panel shows budget minus estimated spend. not set A real balance typed into the admin panel overrides it.
JOB_MAX_COST_USD no / no Ceiling for one job across all attempts. Exceeded = job fails, alert. 1.5 Must be > 0. Restart bot.
IMAGE_RPD_LIMIT no / no Daily image-request quota of the provider account; the day follows Pacific time. 1000 Must be ≥ 1.
IMAGE_RPD_WARN_FRAC no / no Fraction of the quota at which the panel warns and an alert is sent. 0.7 Must be in (0, 1].
DISPLAY_UTC_OFFSET_HOURS no / no UTC offset used for “today” in the admin panel. 5 A value of 0 is treated as “not set” and becomes 5.
Name Required (prod / dev) Purpose Default Notes
STICKER_RENDER_MODE no / no composite = AI draws heads, code puts them on a body template; figure = AI draws whole chest-up figures, code cuts and outlines them. composite when the key is absent Key present but empty or misspelt = error. Returned by GET /health. Restart both.
ART_STYLE no / no Art style of figure sheets, stored in jobs.art_style for new jobs. cute_cartoon One of realistic_illustration, comic_popart, stylized_3d, cute_cartoon. The API writes it when the job is created. Restart both.
STICKER_OUTLINE_MODE no / no White outline of the head in composite mode. partial partial or none. Restart bot.
FIGURE_SHEET_ATTEMPTS no / no Attempts per figure sheet before giving up. 3 Must be ≥ 1.
FIGURE_FALLBACK_ENABLED no / no When a figure sheet never becomes valid, build the same job in composite mode. 1
FIGURE_SHEET_ASPECT no / no Aspect of a figure sheet in the 2x6 layout. 2:3 2:3 or 3:2.
SHEET_LAYOUT no / no 3x4 = three sheets of four figures; 2x6 = two sheets of six. 3x4 Must match a key of catalog.SHEET_LAYOUTS.

Product: consistency and likeness (figure mode)

Section titled “Product: consistency and likeness (figure mode)”
Name Required (prod / dev) Purpose Default Notes
CONSISTENCY_MODE no / no Anchor portrait → sheets in parallel with the anchor as main reference → skin normalisation → pack QA → targeted regeneration. 1 0 = sequential sheets with per-sheet QA.
LIKENESS_MODE no / no Empty = CONSISTENCY_MODE decides. classic = the selfie and its face crop are the only face source, no anchor. hybrid = face as classic, sameness enforced in code (colour targets, despill, QA against the selfie). empty Overrides CONSISTENCY_MODE. Must be empty, classic or hybrid.
HYBRID_SELFIE_PULL no / no hybrid: share by which the colour target (pack median) moves towards the selfie measurement. 0.15 Must be 0..1. 0 = selfie not used.
ANCHOR_VERIFY no / no Check the anchor against the selfie (hair length and colour, facial hair, glasses); one retry on mismatch. 1
ANCHOR_VERIFY_TIMEOUT_SEC no / no Timeout of that check. 10 Must be > 0.
CONSISTENCY_SKIN_DE_MAX no / no Skin colour distance (ΔE, CIE76) from the anchor above which a sticker is corrected. 6 Must be > 0.
CONSISTENCY_SKIN_STRENGTH no / no Share of the correction towards the anchor colour. 0.85 Must be 0..1.
ANCHOR_SKIN_STRENGTH no / no Share by which the anchor’s skin is moved towards the selfie estimate. 0.6 Must be 0..1. 0 = leave as is.
SKIN_NORM_MAX_DE no / no Cap on the length of any skin shift (ΔE), in all normalisations. 4 Must be ≥ 0. 0 = no cap.
CONSISTENCY_REGEN_BUDGET_SEC no / no Extra seconds allowed for regenerating QA outliers. 20 Must be ≥ 0.
CONSISTENCY_MAX_REGEN no / no Maximum single-figure regenerations per pack. 4 Must be ≥ 0. 0 = QA is logged only.
EDGE_ALLOW_TORSO_CROP no / no A torso cut by the sheet edge below the chin, with no skin at the edge, is accepted. 1 0 = any edge contact is a defect.
EDGE_FLAT_CUT_CHECK no / no Detect an arm cut by a straight vertical line away from the sheet edge and regenerate. 1
EDGE_REGEN_TIMEOUT_SEC no / no Timeout for regenerating a figure that was cut at the edge. 35 Must be ≥ 0.

All of these are read by the worker. Restart the bot.

Name Required (prod / dev) Purpose Default Notes
WORKER_SLOTS no / no Jobs processed in parallel. 2 Must be ≥ 1. Restart bot.
WORKER_CONCURRENCY no / no Legacy name of WORKER_SLOTS. — Read only when WORKER_SLOTS is empty.
CUT_MAX_PARALLEL no / no Simultaneous sheet cuts per process (CPU and memory bound). number of CPUs, else 2 Must be ≥ 1.
Name Required (prod / dev) Purpose Default Notes
ALLOW_MULTIPLE_PACKS no / no 0 = one pack per Telegram account plus what an admin grants; failed jobs do not count. 1 = no limit. 0 1 is for tests and demos only. Restart both.
Name Required (prod / dev) Purpose Default Notes
SHOW_PLACEHOLDER_TEMPLATES no / no Show catalogue entries with ready=False to guests. 0 Restart API.
KIOSK_IDLE_SEC no / no Seconds without a touch on the outfit screen before the “are you there?” prompt. 90 Values below 5 become 5. Served by GET /kiosk-config. Restart API.
KIOSK_IDLE_PROMPT_SEC no / no Countdown of that prompt before the kiosk resets. 15 Values below 3 become 3. Restart API.
KIOSK_THEME no / no Default kiosk theme. dark light or dark. /kiosk/light and ?theme= override it. Restart API.
BASE_URL no / no Public address, used in start-up logs and in the panel link of /admin_stats. empty Must start with https:// or http://. The kiosk itself uses relative paths.
Name Required (prod / dev) Purpose Default Notes
DATA_DIR no / no Sessions, debug sheets, instance lock. ./data (= bot/data) Restart both.
DB_PATH no / no SQLite database file. ./data/app.db Both processes must point to the same file.
TEMPLATES_DIR no / no Body templates and outfit preview cards. ../templates
PHOTO_TTL_HOURS no / no Hours after which selfies, sheets and debug files are deleted. 72 Must be 1..72. This is a legal requirement, not a tuning knob. Restart bot.

LOG_DIR is listed under monitoring.

Name Required (prod / dev) Purpose Default Notes
EVENT_NAME no / no Label prefixed to every alert: [<EVENT_NAME>] …. event Does not affect pack names.
EVENT_MODE no / no 1 = paid tool scripts refuse to run on the production key. 0 The bot only logs the fact at start. See the guard table on Local setup: product.
MAINTENANCE_MODE no / no 1 = the bot answers guests with the maintenance text, the API answers 503 on session, outfit and photo, the worker takes no jobs. 0 Cannot be lifted by /resume; needs 0 and a restart of both. A pause stored in the database (automatic after an “out of credit” answer, or /pause) works without a restart.
TEST_GEMINI_API_KEY no / no Key of a separate provider project for paid tool scripts. empty Secret. Read only by tools/* through services/event_guard.py, plus one comparison in validate(). The bot and the API never use it for generation.
Name Read in Purpose Default
GEMINI_IMAGE_MODEL, OPENAI_IMAGE_MODEL, GEMINI_VISION_MODEL, OPENAI_VISION_MODEL bot/services/ai_pipeline.py Model ids (see the AI table). see above
TEST_GEMINI_API_KEY bot/services/event_guard.py Test key for tools. empty
GEMINI_API_KEY, OPENAI_API_KEY tests/quick_test.py The live mode of this script reads keys straight from the process environment, not from bot/.env. —
GEMINI_NB2_MODEL, GEMINI_PRO_MODEL, OPENAI_SUNBURST_MODEL, OPENAI_FLARE_MODEL, OPENAI_2_MODEL tests/quick_test.py Model ids of the comparison configurations in that script. Not used by the product. model ids hard-coded in the script

The tools kiosk_check.py and kiosk_screens.py start a child API with their own APP_ENV=dev, DATA_DIR, DB_PATH and LOG_DIR pointing at a temporary folder; they do not read your bot/.env values for those.

Product: where .env.example differs from code defaults

Section titled “Product: where .env.example differs from code defaults”

.env.example is a production template. These lines differ from what the code does when the key is absent.

Variable In .env.example Code default
STICKER_RENDER_MODE figure composite
FIGURE_SHEET_ASPECT 3:2 2:3
BASE_URL the production address empty
EVENT_NAME an event label event
PRICE_GEMINI_* public list prices not set (contribute 0)
CUT_MAX_PARALLEL empty number of CPUs

Source: .env.example and every process.env read in the repository. Locally copy .env.example to .env.local.

Name Visibility Required Purpose Default Notes
LEADS_ENDPOINT server-only no Full URL of the product’s leads endpoint, https://stickers.eventtalks.uz/leads in production. not set Used together with LEADS_SECRET. If either is empty the form skips the leads server.
LEADS_SECRET server-only, secret no Value of the X-Stickers-Leads-Secret header. not set Must equal the product’s LEADS_SECRET. Removing it is the rollback switch: requests go straight to Telegram.
TELEGRAM_BOT_TOKEN server-only, secret no Bot that posts a request straight into the requests group: the fallback when the leads server fails, and the only route without LEADS_*. not set The bot must be a member of the group.
TELEGRAM_CHAT_ID server-only no Id of the requests group for the fallback. not set Negative for groups.
NEXT_PUBLIC_SITE_URL public (inlined into the build) no Absolute site URL for canonical links, hreflang, sitemap, robots and Open Graph images. http://localhost:3000 In production https://pack.eventtalks.uz. Changing it needs a redeploy, not only saving the variable.

Rules enforced by code and tests:

  • The four server-only variables are read in exactly one place, the server action src/app/actions/submitRequest.ts, and passed into deliverRequest. A test in src/lib/deliverRequest.test.ts fails if a NEXT_PUBLIC_ leads variable appears or a client file reads them.
  • With no variables at all the form logs the request to the server console and still shows success. That is how the form is checked locally.

Variables of the quality scripts (not application configuration):

Name Read in Purpose Default
SHOTS_BASE scripts/shots.mjs, audit.mjs, scroll-film.mjs, lighthouse.mjs, js-weight.mjs Address of the already running site the script visits. http://localhost:3000
STEP_TIMEOUT scripts/lib/watchdog.mjs Milliseconds without progress before a script step is declared hung. 60000

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.