Multi-tenant and white label
Who is this for
Section titled “Who is this for”The owner planning to sell through several agencies, and the developer who will have to make it possible.
- Today one installation = one bot, one event look, one sales group, one budget. Nothing in the database says which event or agency a guest belongs to.
- Route A — one installation per tenant. Works with the existing deploy scripts, needs about a day of small code clean-ups, and scales to a handful of agencies.
- Route B — true multi-tenancy. An
event_id/tenant_idthrough the whole data model, per-tenant branding, budgets, bots and admin access. A large change; only worth it past roughly ten active tenants or when self-service is needed. - Start with Route A. Its clean-ups are also the first steps of Route B.
What is single-tenant today
Section titled “What is single-tenant today”Verified in the code at the commit below.
| Area | Where | What is fixed |
|---|---|---|
| Kiosk look | bot/kiosk/brand/, bot/kiosk/index.html |
The palette, background art, fonts and the header logo of one specific event’s brand book are committed files; the logo mark-up is in index.html. Re-branding is a code change and a deploy. |
| Footer logos | KIOSK_LOGOS in bot/api.py |
Two fixed file slots in bot/kiosk/static/; one slot’s file name and alt text carry a partner’s name. Files must be committed to be deployed. |
| Kiosk theme | KIOSK_THEME |
One of two themes of the same brand book. |
| Bot identity | TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_USERNAME |
One bot per installation. Its username is part of every pack name (…_by_<bot_username>), so guests see it. |
| Pack title | pack_title in bot/services/telegram_stickers.py |
The Russian phrase for “AI sticker pack” plus the guest’s username; not configurable. |
| Guest texts | bot/texts.py, TEXTS in bot/kiosk/kiosk.js |
Russian only. The kiosk has a ?lang= hook, but only ru exists; the bot has no language switch. |
| Outfit catalogue and art style | bot/catalog.py, ART_STYLE |
Global to the installation. |
| Event label | EVENT_NAME |
Used only as a prefix in admin alerts. Sessions and jobs carry no event identifier. |
| Budget and quota | BUDGET_USD, IMAGE_RPD_LIMIT, one provider key |
One counter for everything. |
| Pack limit | users table keyed by Telegram user id |
“One pack per account” is global: a guest who got a pack at one event is refused at the next one on the same installation, until an admin grants an attempt or the account is reset. |
| Admin panel | ADMIN_PANEL_USER / ADMIN_PANEL_PASSWORD |
A single shared login that sees every guest. |
| Alerts | ADMIN_CHAT_ID |
One chat. |
| Leads | LEADS_CHAT_ID, the stickers_ prefix, card title |
One sales group; the product name and prefix are constants in code. |
| Landing page | separate repository | One brand, one footer credit switch (SITE.partner.enabled), one leads endpoint. |
| Domain | deploy/caddy/stickers.caddy, BASE_URL |
The API domain is written in the Caddy site file. |
Route A — one installation per tenant
Section titled “Route A — one installation per tenant”Each agency (or each large event) gets its own copy: a server or a directory with its own port, its own bot, its own
.env, its own database and its own kiosk branding.
flowchart LR
subgraph T1["Installation: agency A"]
B1["Bot A"] --- D1[("DB A")]
K1["Kiosk in brand A"]
end
subgraph T2["Installation: agency B"]
B2["Bot B"] --- D2[("DB B")]
K2["Kiosk in brand B"]
end
REPO["One code repository"] --> T1
REPO --> T2
P1["Provider project A"] --> T1
P2["Provider project B"] --> T2
What already works: the deploy scripts set up a fresh server end to end; every secret and switch is an environment variable; data, backups and logs are per installation; isolation between tenants is total.
What to clean up first (small changes):
| # | Change | Why |
|---|---|---|
| 1 | Move the kiosk brand out of the repository tree: load palette, logo and background from a per-installation folder outside bot/kiosk/ |
Re-branding without a commit and without mixing tenants’ art in one repository |
| 2 | Rename the logo slots to neutral names and read alt texts from configuration | A partner’s name is in a file name today |
| 3 | Make the pack title and the product name on lead cards configurable | White label |
| 4 | Parameterise the domain in the Caddy site file and the deploy scripts | Today it is a literal |
| 5 | Decide the multi-event rule for the pack limit: reset per event, or keep it global | Otherwise returning guests are refused |
| 6 | Add the missing languages to the bot and the kiosk | The landing page already sells in three |
| 7 | Document a “new tenant” checklist: bot, provider project and key, domain record, .env, branding, rehearsal |
Repeatability |
Costs of this route: a server (or a slice of one) per tenant; a bot and a provider project per tenant; upgrades are rolled out installation by installation; no combined statistics; each tenant’s daily image quota is separate — which is an advantage.
When it stops scaling: roughly ten installations, or the first agency that wants to create events by itself.
Route B — true multi-tenancy
Section titled “Route B — true multi-tenancy”One system serving many tenants and events.
| Layer | Change |
|---|---|
| Data model | tenants and events tables; event_id on sessions, jobs, api_usage, audit_log; the pack limit scoped per event; tenant_id on leads |
| Kiosk | Event resolved from the address (/kiosk/<event>/); branding, texts, outfit list, theme and timeouts loaded per event |
| Bot | Either one bot per tenant (several tokens, webhooks) or one shared bot with the event carried in the deep-link token. Pack names expose the bot username, so a white-label tenant will want its own bot. |
| Generation | Per-event art style, outfit catalogue, mode flags and cost cap; per-tenant provider keys and quota counters |
| Money | Budget, spend and alerts per event; a per-event money ceiling (missing today even for one tenant) |
| Admin panel | User accounts with roles and tenant scoping instead of one shared password; per-event dashboards |
| Leads | A leads service that knows products and tenants — see Playbook: new product |
| Privacy | Per-tenant retention settings, export and deletion; a clear statement of who is the data controller for each event |
| Infrastructure | The same prerequisites as the largest scaling scenario: PostgreSQL, separate workers, shared file storage — see Scaling scenarios |
This is a rewrite of the data layer and the admin panel. It should not start before there is demand that Route A cannot serve.
What to give an agency partner, in either route
Section titled “What to give an agency partner, in either route”- A station in their client’s colours and logos (kiosk screen only — outfits stay from the catalogue).
- A bot under a name agreed with them.
- Access to the admin panel for the event’s operator.
- The privacy statement: consent in the bot, photos deleted within 72 hours, one pack per account.
- Not included without further work: their own landing page and their own requests group.
Legal and commercial terms between the studio and an agency are outside the repositories.
Where to go next
Section titled “Where to go next”- Scaling scenarios — the infrastructure Route B depends on.
- Venue requirements and branding — what branding means for a client today.
- Roadmap.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.