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. |
| 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.
.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.