Skip to content

Code rules

Anyone, human or AI agent, about to change code in ai-sticker-pack or stickers-landing. Read this page before the first commit. Several rules are legal or money constraints, not style preferences.

  • Product: two AI providers, photo auto-deletion within 72 hours, consent before the photo, one pack per account, the kiosk never waits, image assembly by code, configuration only through environment variables.
  • Money: no paid call without a budget stated for the task; tests use mocks only.
  • Privacy: logs carry numbers and ids of records, never names, phones, handles or faces.
  • No Docker in either repository.
  • Git: the author comes from the repository’s local git configuration; commit and push only when asked.
  • Landing: fixed palette, one amber call-to-action, a list of forbidden patterns, no hard-coded strings, animate only transform and opacity, and a strict list of what may be promised.
  • A change to behaviour, API, environment variables or deployment updates the matching page of this documentation in the same pull request.

These come from the repository’s CLAUDE.md. “Enforced by” names the code that makes the rule hard to break.

# Rule Reason Enforced by
1 Two AI providers from day one. Never hard-code one provider; the pipeline takes the provider from configuration. A provider outage, quota or billing stop during an event must not stop the station. PRIMARY_AI_PROVIDER / FALLBACK_AI_PROVIDER, validated against KNOWN_PROVIDERS. Start-up warns when only one key is set or both variables name the same provider.
2 Photos are deleted automatically within 72 hours. The database holds metadata only (user id, timestamps, status, pack link), never images. Legal requirement for face photos; it is also promised to guests in the consent text and to clients on the landing page. PHOTO_TTL_HOURS must be 1..72 or the process does not start. Hourly cleanup in main.py over sessions/ and debug_sheets/. The upload is re-encoded, which drops EXIF.
3 Consent before the photo. Without an explicit “I agree” the session does not continue. Processing a face needs consent; the fact is recorded (users.consented_at) and logged. Only the bot’s consent callback moves a session to consented. POST /session/{token}/template answers 409 in any earlier state; the photo endpoint accepts only template_chosen.
4 One pack per Telegram account, checked before the session starts. Every pack is a paid generation and takes a worker slot. db.pack_quota: at most 1 + users.extra_packs_allowed non-failed jobs. Checked in /start, on outfit choice, and atomically when the job is created. An admin can grant one more. ALLOW_MULTIPLE_PACKS=1 is for tests and demos only.
5 The kiosk never waits for generation. Photo taken → the tablet is free for the next guest. Delivery is asynchronous through the bot. One station serves the whole event, and generation takes around a minute per pack (see Capacity). POST /session/{token}/photo only stores the file and queues a job; the “generating…” message is sent in a background task after the response.
6 Image assembly is done by code, not by the model. In composite mode the model draws only heads and code places them on the body. In figure mode the model draws the figures, and code does cutting, outlining, colour normalisation and export. Deterministic geometry, sticker size and file limits; no second paid call for something code does exactly. services/sticker_builder.py.
7 A style or brand reference is prompt content, not architecture. Do not block code on a missing reference. References arrive late and change; the pipeline must run without them. Style presets and outfit texts are plain data (Outfits, templates and art styles).

Related decisions that are treated as fixed:

Rule Reason
The outfit is chosen on the tablet, not in the bot. There is no template handler in the bot. The guest’s attention is on the tablet; the bot is only for consent and delivery. Do not change the guest flow without an explicit decision by the owner.
Packs are never deleted and never recreated under the same name. A retry takes the next free name. Telegram clients cache a pack by name; a recreated name shows “sticker set not found”. Tests assert that deleteStickerSet is never called.
The pack link is sent only after it is confirmed. getStickerSet must report 12 stickers and the public pack page must confirm twice. A link sent too early opens an empty or missing pack on the guest’s phone.
Guest texts live in one file, bot/texts.py. The bot, the API and the worker must not drift apart in wording.
Only one main.py per data directory. Two workers on one queue render with different configurations. Enforced by bot/instance_lock.py.

Product: configuration only through the environment

Section titled “Product: configuration only through the environment”
Rule Reason
Every setting is an environment variable read in bot/config.py. No token, key, id, price or address in code. The same commit runs locally, in tests and on the server; secrets never enter git.
bot/.env is never committed, printed, logged or pasted into a chat. It holds every secret of the installation.
A typo in an enumerated value stops start-up. An empty STICKER_RENDER_MODE= used to fall back silently to composite; a wrong mode is only noticed when a guest’s pack looks different.
Prices are configuration too; an unset price counts as zero and the panel warns. Provider prices change; a hard-coded number becomes a wrong cost report.
A new variable gets a default, a validation rule, a line in .env.example, and a row on Environment variables. Otherwise the next deploy fails or silently uses a default.

Docker is not used in either repository: not for development, not for tests, not “just to check something”. The product runs in a Python virtual environment locally and under systemd on the server; the landing page runs on Node.js locally and on a managed host.

Reason: both repositories state this as an absolute rule and give no rationale. What can be verified is that nothing needs a container: the server runs two systemd units with memory limits on a small shared virtual machine, tests need no external service, and local development is on Windows. If a tool seems to need a container, stop and ask the owner.

Rule Reason
No live AI call without a budget in dollars stated for that task, with a hard stop at it. Image generation is billed per call; an unattended script can spend an event’s budget.
Tests and code checks use mocks only. A live call exists only so a human can judge the picture. Correctness of code does not depend on what the model draws; likeness and style do, and only eyes judge them.
One run per check: one face first, more only if the first is acceptable. A failed approach should cost one run.
No A/B, stress or load tests with live generation before an event. They consume the provider’s daily image quota that guests need.
Paid tools run on TEST_GEMINI_API_KEY when it is set; with EVENT_MODE=1 and no test key they do not start. In prod the bot refuses to start if the production key equals the test key. Test spending and guest spending must be separable, and tools must not eat the event-day quota. Enforced by services/event_guard.py and config.validate.
Every paid tool prints an estimate and asks before the first call; most carry a hard cap in code. See the table on Local setup: product.
One job has a cost ceiling (JOB_MAX_COST_USD). A retry loop on one guest must not be unbounded.
Rule Reason
Logs contain no personal data. Lead logs carry the lead number, request_id, locale and utm_source — never the name, handle or phone. Invalid-lead logs list field names, not values. Stage timing lines carry numbers only. Logs are rotated, copied and read during incidents; they must be safe to share.
The visitor IP of a lead is stored only as an HMAC with a server-side salt. Rate limiting and repeat detection do not need the address itself.
The admin panel never returns photos, file paths, the appearance card or debug sheets. The one exception is the stylised anchor portrait of a job, behind login. Faces stay on disk until the TTL removes them and nowhere else.
Preview images made by tools (preview_*.png, everything under out/) are never committed. They contain guest faces.
Lead data is anonymised on request (/stickers_forget) and by retention schedule. See Privacy and Data erasure.
Rule Reason
The commit author comes from the repository’s local git configuration. Never pass -c user.name or -c user.email on a git command. The landing host matches the commit email to an account; a different email makes the deployment fail. If the author is wrong, fix the configuration and make a new commit.
Do not rewrite history to fix an author. Same deployment pipeline; a force-push breaks more than it fixes.
Commit and push only when asked. The owner decides what is shipped; the product server is updated from committed state only.
Keep * text=auto eol=lf. Shell scripts, systemd units and the proxy configuration break on CRLF. A test checks line endings in deploy/.
Never commit bot/.env, bot/data/, databases, database backups, logs/, out/. Secrets and guest data.

Source: the landing repository’s CLAUDE.md. The landing page is a separate site. It shares no code, database or keys with the product; every demo on the page is a mock in code.

Rule Reason
Next.js App Router, TypeScript strict, Tailwind CSS v4, zod for the form, Node.js 22. No UI kits, no component libraries, no 3D, no animation runtime. Page weight and first-paint time; every dependency on the first screen costs measurable load time.
All movement is CSS, the Web Animations API and View Transitions. No animation library is installed; adding one requires measuring first. Same.
Server Components by default. Only the hero scene, pack showcase, “how it works”, language switch, header, sticker ribbon and form are client components. Client components are serialised into the HTML and hydrated.
Client components receive only their slice of the dictionary. The full dictionary would be duplicated into every client component’s payload.
No Docker. Nothing from the product repository is ever executed; its .env is never read. No paid API is called and no image is generated. The landing page must be buildable with no secrets and no cost. Assets are copied as finished files.
The site address comes only from NEXT_PUBLIC_SITE_URL. Canonical links and the sitemap must match the deployed domain.
Token Dark value Use
bg #0E1113 Base. Not pure black.
surface #171B1E Panels, cards, device bodies.
raised #20262A Active elements, message bubbles.
line #2B3236 Decorative borders.
line-strong #626B72 Field and outline-button borders (contrast ≥ 3:1).
ink #F5F2EC Text.
muted #A8A29A Secondary text.
primary #FFB43A Amber: only the main call-to-action and the key event of the scene.
on-primary #14110E Text on amber, coral and success.
accent #FF7A59 Coral: “pack ready”, active state, selected outfit.
success #37B26C Success only.
danger #FF5C5C Form errors only.
  • Proportions: roughly 70 % graphite, 20 % text and surfaces, 7 % amber, 3 % coral and states.
  • The page is dark by default and ignores the system colour scheme. A light theme exists only behind the header toggle, with its own token values; coral as text or outline uses the separate accent-ink token there.
  • State is never conveyed by colour alone.
  • Forbidden colours: messenger blue as an accent, purple “AI” gradients, neon, pure #000. The only exception is inside the mock Telegram screens, which imitate another application.
  • Reason: a consistent, accessible identity. All pairs pass WCAG AA; npm run contrast prints the table and npm run audit checks the live page, including “saturated blue outside the mock” as an error.

There is one kind of amber button on the page: the main call-to-action. The hero button and the form’s submit button are the same action. Every other button is outlined or text-only, in the header too. npm run audit checks that at most one amber button is in view.

Reason: an accent that appears everywhere stops directing attention. The accent is an event, not a section fill.

The repository lists these without individual justifications. The right-hand column is this documentation’s reading of them.

Forbidden Reason
Glassmorphism; a grid of three identical cards with an icon; blobs, 3D spheres, neon; badges above headings; letter-by-letter text. Generic “AI landing page” look; the page should show the product instead.
Emoji instead of graphics, including inside the bot mocks (reactions are drawn as SVG). Emoji render differently per platform and break the mock’s fidelity.
Confetti. One exception: stickers flying out after a successful form submit. Decoration without meaning.
Invented testimonials, client logos, numbers, counters; placeholder text. Honesty: nothing on the page may be fabricated.
Inflated wording (“revolution”, “next level”, “all-in-one”, “AI magic”). Tone: short sentences, active voice, concrete statements.
Stickers showing the faces of real people other than the owner. Demo people on the page are fictional. Consent and privacy.

Not a single string of page text may be written in a component. All text lives in src/i18n/dictionaries/{en,ru,uz}.ts; en defines the Dictionary type and the other two must match it. Each language is written as an original, not as a translation.

Reason: three languages must stay complete and in step. The dictionary test compares keys and array lengths and scans every string for forbidden promises. Details: i18n and copy.

Related: Uzbek is published but never the default; the browser language never routes to /uz.

Exceptions, each marked with a comment in code: the FAQ disclosure, the height of the form section when it swaps to the “sent” screen, and the drawing of the check mark.

Also fixed:

  • Durations come only from the --dur-* tokens; there is one spring easing.
  • Hover never moves neighbouring elements.
  • Only a short list of things may move endlessly (hero background, hero scene, CTA breathing, typing dots, sticker ribbon, submit shimmer), and nothing moves off-screen or in a hidden tab.
  • With prefers-reduced-motion everything is visible at once and the scene rests on its final frame.

Reason: these two properties are composited without layout or paint, so the page stays smooth on phones and the layout shift stays at zero. Details: Animation rules.

The page may state only what the product really does. The full list, with wording for sales conversations, is on How to sell. In short:

May be promised Must not be promised or mentioned
QR without installing an app, entry through Telegram Prices
Guest consent before the photo Any number except “12 stickers”, “a couple of minutes” and “72 hours”
Outfit chosen on the tablet Testimonials and client logos
12 chunky cartoon stickers with the guest’s face Other messengers; printing stickers
The pack arrives in Telegram within a couple of minutes “Works without internet”; “100 % likeness”
The station does not wait for generation Names of AI models and providers; a backup provider or fallback
One pack per Telegram account Branded outfits or props (only the station is branded)
Photos deleted automatically within 72 hours; only service data in the database Brands of past events
Monitoring and support during the event; station in the client’s colours and logos; setup with a rehearsal The internal name of the art style

Reason: the page is a sales promise to an event organiser. A promise the station cannot keep on the day costs more than a missing feature. src/i18n/dictionaries.test.ts enforces the forbidden list and the allowed numbers on every string of all three languages.

npx tsc --noEmit, npm run lint, npm test, npm run build, and for every visible change npm run shots and npm run audit, looked at with your own eyes. A check that hangs must fail within a minute; see Testing.

A change to behaviour, API, environment variables or deployment in ai-sticker-pack or stickers-landing updates the matching page in ai-sticker-pack-docs in the same pull request, and bumps the Last verified line of every page touched.

You changed Update
An endpoint, status code, limit or header API reference
A bot command, callback or guest text Bot commands
A variable, default or validation rule Environment variables
The catalogue, styles, poses Outfits, templates and art styles
Deploy scripts, units, proxy Deploy & rollback
A state or transition State machines

Reason: the code is the source of truth and this site is its only description for people without access to the author. Where the in-repo notes and the code disagree, the documentation follows the code and the difference is recorded on Known gaps.


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