Skip to content

Privacy and personal data

Anyone who must explain the product’s data handling to a client, a venue or a lawyer, and developers who add a field, a log line or a storage location.

  • Guests: the selfie is a file, never a database value. It is deleted right after the pack is created. Every other guest image is deleted by a file TTL of at most 72 hours; the config rejects any PHOTO_TTL_HOURS outside 1–72.
  • Guest database rows hold the Telegram id, first name, last name, username, consent time and pack link. They have no automatic expiry; an admin removes them on request.
  • Leads: name, Telegram username, phone and free-text fields are stored in SQLite and shown on a card in a Telegram group. Spam is anonymised after 30 days, everything else after 365 days, or at once on request.
  • Never stored: the raw IP address of a landing visitor, photo EXIF metadata, the photo itself inside the database.
  • Consent is collected twice: a button in the bot before any photo, and a notice under the landing form’s submit button.
  • Open legal question: whether personal data of Uzbekistan citizens may be stored on servers outside the country. It is tracked in Known gaps.
flowchart TD
    S["/start in the bot"] --> U["users row: Telegram id, name, username"]
    U --> C{"Consent button pressed?"}
    C -- no --> X["Nothing else is collected"]
    C -- yes --> CT["users.consented_at"]
    CT --> PH["Photo upload: EXIF stripped, JPEG file in the session folder"]
    PH --> AI["Photo sent to the AI provider"]
    AI --> PP["Appearance passport cached on the job row"]
    AI --> DR["Derived files: face crop, anchor, sheets, stickers"]
    DR --> TG["12 stickers uploaded to Telegram as the guest's pack"]
    TG --> DEL1["Selfie file deleted at once"]
    TG --> DEL2["Passport set to NULL when the link is sent"]
    DR --> TTL["Hourly cleanup deletes files older than PHOTO_TTL_HOURS"]
    U --> KEEP["users, sessions, jobs rows stay until an admin reset"]
    KEEP --> BK["Daily database backup, 14 copies"]
Data Where Lifetime Who can see it
Selfie File bot/data/sessions/<token>/selfie_*.jpg Until the pack is created. If the job failed: until the TTL Server root and the service user
Face crop, anchor portrait, generated sheets, single-figure regenerations, final sticker files Same session folder File TTL Server root and the service user
Debug copies of sheets, anchor copy, JSON diagnostics bot/data/debug_sheets File TTL Server root and the service user. The anchor image and a numeric QA summary are also visible in the admin panel while the file exists (ADMIN_SHOW_ANCHOR)
Appearance passport: skin tone hex, hair, facial hair, age band, glasses, face shape jobs.appearance_json Until the job is done or failed; an hourly pass clears leftovers Server root
Telegram user id users, sessions, jobs, audit_log; pack name Until an admin reset; audit_log and the pack name persist Admin panel users; server root
First name, last name, username users Until an admin reset. Refreshed at /start and at consent Admin panel users; server root
Consent timestamp users.consented_at Until an admin reset Server root
Pack name and link users, jobs Until an admin reset Admin panel users. The link itself is public for anyone who has it
The sticker pack Telegram Not managed by the product Anyone with the link
  • The kiosk captures the photo in the browser and uploads it over HTTPS. The camera is switched on only after the guest consented and off again after the upload (kiosk.js).
  • The API re-encodes the upload, which removes EXIF metadata such as device model (api.py _normalize_photo).
  • The worker sends the photo, and images derived from it, to the configured AI provider for the passport, the portrait, the sheets and the quality checks (services/ai_pipeline.py). With a fallback provider configured, the same images may go to the second provider. What a provider retains is governed by its own terms, not by this code.
  • The final stickers are uploaded to Telegram as a sticker set owned by the guest’s account.

PHOTO_TTL_HOURS defaults to 72. Config.validate in config.py makes any value outside 1–72 a start-up error for both processes. The hourly pass in services/storage.py deletes by the age of each file, and deletes one hour early so that no file outlives the configured limit. The pass runs only while the bot process runs.

flowchart TD
    F["Landing form: consent notice under the button"] --> SA["Server action: honeypot, validation, rate limit"]
    SA --> PL["POST /leads with visitor IP in the body"]
    PL --> H["IP replaced by a salted HMAC hash"]
    H --> DB["leads row in SQLite"]
    DB --> CARD["Card in the Telegram leads group"]
    DB --> BK["Daily database backup, 14 copies"]
    DB --> CSV["CSV export to an admin's private chat"]
    SA -- "server unreachable" --> FB["Fallback: plain message straight to the group, not in the database"]
    DB --> E1["Spam: anonymised after 30 days"]
    DB --> E2["Other statuses: anonymised after 365 days"]
    DB --> E3["On request: /stickers_forget, at once"]
    E1 --> AN["name, telegram, phone, comment, ip_hash set to NULL, card rewritten"]
    E2 --> AN
    E3 --> AN
Data Where Lifetime Who can see it
Name, Telegram username, phone leads table; card in the group Until anonymisation All members of the leads group, including any other bot that can read it; server root. With LEADS_CARD_PII=0 the card hides them
Comment (free text) leads; card Until anonymisation Same
Event date, guest count (free text) leads; card Not removed by anonymisation Same
Changed contacts of a repeat request lead_repeats; card Cleared together with the lead Same
Language, UTM values, timestamps leads Kept for statistics Group members (on the card); server root
ip_hash leads, lead_repeats Until anonymisation Server root
Status history: admin’s Telegram id and display name, time lead_status_history; card shows the last 3 Kept; removed only when a test lead is deleted completely Group members; server root
request_id leads; landing platform logs Kept Server root; whoever reads the landing logs
  • The visitor’s raw IP address. The landing sends it in the request body; the server keeps only HMAC-SHA256(LEADS_IP_SALT, ip) (services/leads.py ip_hash). The landing’s own limiter keeps IPs in process memory for 10 minutes.
  • User agent, cookies and location. The landing sets one cookie, NEXT_LOCALE, which records only the chosen language, and one localStorage flag with the time of the last submission.
Where Mechanism Text shown Recorded
Bot, before any photo Inline button ✅ Согласен (“I agree”) under a message that states the photo is used only to generate stickers and is deleted automatically within 72 hours handlers/start.py users.consented_at, a log line, and the session state consented. Without it the kiosk never shows the outfit screen and the API refuses the photo
Landing form A sentence under the submit button, no checkbox: “By sending this request, you agree to the processing of your contact details so we can reply to it.” (dictionary key form.consent, in three languages) en.ts Not recorded separately; a stored lead implies the form was submitted

The bot’s consent text always says “72 hours”. The configured TTL can be shorter but never longer.

Application logs (logs/app.log, logs/api.log, journald) are kept for 14 days by logrotate. Caddy keeps its access log in 14 files of 20 MiB.

In the logs Not in the logs
Telegram user ids of guests and admins Photos, file contents
Session tokens and job ids Lead name, username, phone, comment. The leads code logs the lead number, request_id, language and utm_source only
A short appearance summary per job (APPEARANCE_CARD, HARD_LOCKS lines): skin tone hex, hair description, facial hair, age range, glasses, face shape Guest names
Pack names, which embed the Telegram user id Secrets. Start-up lines print mode names, not key values
The first 8 characters of a visitor hash on a rate-limit line Raw visitor IP addresses
Caddy access log: caller addresses and request paths, including session tokens Request bodies

Two points deserve attention:

  • The appearance summary outlives the passport. The passport is cleared from the database when the job ends, but the log line that describes the same person stays for up to 14 days, next to a line that ties the job to a Telegram user id.
  • Stage-timing lines (STAGE_TIMING, STAGE_SUMMARY) are designed to carry no personal data at all: job number, seconds and counters only.
Copy Contains Retention
Daily backup in /var/backups/sticker-pack The whole database: guests, leads, history 14 files
app.db.bak-<timestamp> next to the database Made before a schema migration and before every guest reset Not rotated by any code
Provider-level server snapshots, if enabled Everything on the disk Provider policy
Telegram Lead cards, fallback messages, admin alerts with guest Telegram ids, CSV exports in private chats Until deleted by hand
AI provider Images sent for generation Provider policy
Landing platform logs [request] <request_id> → <route> lines; in a setup without any delivery target, the full request text Platform policy

Photos are never part of a database backup.

  • Data localisation. The in-repo leads specification records an unresolved legal question: whether the law of Uzbekistan requires personal data of its citizens to be stored on servers located in the country and in a registered database. The product server and Telegram are outside the country. The specification marks this as a question for a lawyer and notes that the product was launched without waiting for the answer. LEADS_CARD_PII=0 exists so that cards can be published without contacts if the answer requires it. Status and owner are tracked in Known gaps and Open decisions.
  • Guest self-service erasure. The bot has no command for a guest to delete their data. Requests go through an admin (Data erasure).
  • Guest rows without expiry. Names and usernames in users stay until someone resets the guest.
  • Appearance summary in logs (see above).
  • Free-text lead fields (event_date, guests) survive anonymisation.
  • Un-rotated app.db.bak-* files keep rows that were deleted on request.

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