Skip to content

Containers

A developer or operator who needs to know what processes exist, what listens where, and in which direction traffic flows — for example before changing a firewall rule, moving the server or debugging “who is supposed to call whom”.

  • The server runs two application processes: sticker-api (uvicorn + FastAPI, loopback port 8000) and sticker-bot (one Python process that long-polls Telegram and also runs the queue worker, the delivery sweeper, the TTL cleanup and the leads loop). They share one SQLite file and one data directory.
  • Only Caddy listens on public ports (80 and 443). It terminates TLS for stickers.eventtalks.uz and proxies everything to sticker-api.
  • The bot has no listening port. All Telegram and AI traffic is outbound. There is no webhook.
  • The two processes never call each other. They coordinate only through the database: the API inserts rows, the bot picks them up by polling.
  • The landing runs on Vercel and reaches the server with one call: POST /leads. If that fails, it posts the request straight to Telegram.
  • The kiosk is a static page served by sticker-api; it polls the API once per second while a session is open.
flowchart LR
    visitor["Site visitor browser"]
    tablet["Kiosk page in the tablet browser"]
    opbrowser["Operator browser"]
    phone["Guest and admin Telegram apps"]

    subgraph vercel["Vercel"]
        landing["Landing - Next.js 16, server action"]
    end

    subgraph droplet["DigitalOcean droplet - shared with a second, unrelated project"]
        caddy["Caddy - TLS and reverse proxy, ports 80 and 443"]
        api["sticker-api - uvicorn and FastAPI, loopback port 8000"]
        bot["sticker-bot - polling, worker, sweeper, cleanup, leads loop"]
        db[("SQLite app.db in WAL mode")]
        files[("File storage with TTL - sessions and debug_sheets")]
        backup["sticker-backup timer - daily sqlite3 backup"]
    end

    tg["Telegram Bot API and t.me"]
    ai["AI providers - Gemini primary, OpenAI fallback"]

    visitor -- "HTTPS" --> landing
    landing -- "HTTPS POST /leads with shared secret" --> caddy
    landing -. "fallback sendMessage" .-> tg
    tablet -- "HTTPS, polls every second" --> caddy
    opbrowser -- "HTTPS /admin, Basic auth" --> caddy
    caddy -- "HTTP reverse proxy" --> api
    api -- "SQL" --> db
    api -- "writes selfie" --> files
    api -- "one-off sendMessage" --> tg
    bot -- "SQL, polls the jobs queue and the leads outbox" --> db
    bot -- "reads selfie, writes sheets and stickers, deletes by TTL" --> files
    bot -- "long polling getUpdates, send, createNewStickerSet" --> tg
    bot -- "HTTPS via provider SDKs" --> ai
    backup -- "sqlite3 .backup" --> db
    phone -- "Telegram protocol" --> tg

Arrows point from the side that opens the connection.

Container Technology Responsibility Listens on Source
Landing Next.js 16 (App Router) on Vercel, React 19, zod Marketing page in ru / uz / en; request form; server action that validates, rate-limits and delivers the request HTTPS on Vercel’s edge (domain set in Vercel) src/app/actions/submitRequest.ts
Kiosk Static HTML + vanilla JavaScript, no build step, runs in the tablet browser in kiosk mode Start screen, QR, outfit cards, camera capture, upload; resets itself on timeouts — (a browser page) bot/kiosk/
sticker-api Python, FastAPI under uvicorn, one process without --reload Kiosk endpoints (session, QR, templates, photo), the web admin panel, POST /leads, /health; serves the kiosk files TCP 8000 on the loopback interface only bot/api.py, sticker-api.service
sticker-bot Python, aiogram, asyncio; exactly one instance Telegram updates (guest flow, admin commands, leads group), generation worker, delivery sweeper, TTL cleanup, leads outbox and housekeeping Nothing — outbound connections only bot/main.py, sticker-bot.service
SQLite database One file app.db, journal mode WAL, busy_timeout 5000 ms per connection Users, sessions, the jobs queue, AI usage records, audit log, settings, leads — (a file opened by both processes) bot/db/db.py, bot/db/models.py
File storage with TTL Plain directories under DATA_DIR (sessions/, debug_sheets/) Selfie, generated sheets, sticker PNGs, debug copies; every file is removed once it is older than PHOTO_TTL_HOURS — (a directory) bot/services/storage.py
Caddy Caddy web server from the official package TLS for stickers.eventtalks.uz (automatic Let’s Encrypt), security headers, 20 MB request body cap, reverse proxy to sticker-api. One Caddy serves both projects on the droplet; this project owns only its own site file TCP 80 and 443 on all interfaces deploy/caddy/stickers.caddy, deploy/caddy.sh
Backup timer systemd timer + oneshot service running a shell script as root Daily consistent copy of app.db (sqlite3 .backup), integrity check, gzip, keep the last 14 — deploy/backup.sh, sticker-backup.timer
Telegram Bot API (external) HTTPS API Updates, messages, sticker sets — —
AI providers (external) Gemini via google-genai, OpenAI via openai SDK Image generation and JSON-returning vision calls — bot/services/ai_pipeline.py

The firewall (ufw, configured by deploy/setup.sh) allows inbound TCP 22, 80 and 443 only. Port 8000 is not reachable from outside: uvicorn binds to the loopback interface and trusts forwarded headers only from it (--proxy-headers --forwarded-allow-ips set to the loopback address in the unit file).

# From To Protocol When Verified in
1 Tablet browser Caddy → sticker-api HTTPS, then HTTP on loopback Page load; POST /session on Start; GET /session/{token} every 1000 ms while a session is open; template choice; photo upload; POST /session/{token}/expire on any reset CFG.pollMs in kiosk.js
2 Guest’s phone Telegram Telegram client protocol Scanning the QR opens https://t.me/<bot_username>?start=<session_token> _deep_link in api.py
3 sticker-bot Telegram Bot API HTTPS long polling Always. dp.start_polling(...); no webhook is registered anywhere main.py
4 sticker-bot Telegram Bot API HTTPS Replies to guests, alerts to ADMIN_CHAT_ID, createNewStickerSet, getStickerSet, lead cards and pings to LEADS_CHAT_ID worker.py, delivery.py, leads_bot.py
5 sticker-bot t.me HTTPS GET Pack readiness check: the page https://t.me/addstickers/<pack_name> must say “the set exists” twice in a row telegram_stickers.py
6 sticker-bot AI providers HTTPS through the SDKs, in daemon threads Every generation and vision call of a job ai_pipeline.py
7 sticker-api Telegram Bot API HTTPS, one-off sendMessage After a photo is accepted (Генерирую… — “Generating…”); when the pack limit blocks a session; a flood alert from POST /leads. The API never calls getUpdates, so it does not conflict with the bot’s polling get_bot, notify_generating, notify_pack_limit in api.py
8 Landing server action (Vercel) Caddy → sticker-api HTTPS POST /leads, header X-Stickers-Leads-Secret, 5 s timeout Each valid form submission, when LEADS_ENDPOINT and LEADS_SECRET are set deliverRequest.ts, leads_api.py
9 Landing server action (Vercel) Telegram Bot API HTTPS sendMessage Fallback when call 8 times out, fails on the network, or returns 5xx / 401 / 404; also the only route when the leads variables are not set deliverRequest.ts
10 Operator browser Caddy → sticker-api HTTPS, HTTP Basic /admin page and /admin/api/*; the page refreshes itself every 10 s while visible admin_panel.py, admin/index.html
11 sticker-api and sticker-bot SQLite file File I/O A new connection per database call _connect in db.py
12 Backup timer SQLite file sqlite3 .backup Daily at 03:30 UTC sticker-backup.timer
13 Developer workstation Droplet SSH / SCP on port 22 Deploy: upload an archive of the committed tree, then run the update script deploy/push.ps1, deploy/update.sh
14 Caddy Let’s Encrypt ACME Certificate issue and renewal, automatic stickers.caddy

How the two processes cooperate without talking

Section titled “How the two processes cooperate without talking”

There is no queue broker and no RPC between sticker-api and sticker-bot. The database is the only channel:

What Written by Picked up by How
A generation job sticker-api inserts a jobs row with status queued in the same transaction that moves the session to photo_taken A worker slot in sticker-bot Each slot runs claim_next_job() (one conditional UPDATE … RETURNING); an empty queue is re-checked every second
A session status sticker-bot (link, consent) and sticker-api (template, photo, expire) The kiosk The kiosk polls GET /session/{token}
A lead sticker-api inserts a leads row with published_at empty The leads loop in sticker-bot Outbox polling every 2 s
Maintenance pause Either process writes the maintenance key in settings Both Read before every guarded action; no restart needed

Why this shape: both processes start and restart independently, and a lead or a photo is never lost when the bot is down — it waits in the database. Every status change is a conditional UPDATE … WHERE status = …, and multi-step changes run inside BEGIN IMMEDIATE, so a double tap on the tablet or two worker slots cannot create a duplicate.

Both units carry the same hardening and CPU weight; memory limits differ because the bot holds the image work.

Unit MemoryHigh MemoryMax CPUWeight Stop behaviour
sticker-api 1.2G 1.6G 50 TimeoutStopSec=15
sticker-bot 1.45G 1.6G 50 KillSignal=SIGINT, TimeoutStopSec=30

Source: the unit files sticker-api.service and sticker-bot.service. The comments there give the reason: the droplet has 2 vCPU and 4 GB and is shared, so above MemoryHigh the kernel slows the process and reclaims its memory instead of letting it starve the neighbour; CPUWeight=50 against the default 100 gives the neighbour twice the CPU share under contention.


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