Multi-tenant registry and schema
tenants table + registry + dispatch gate + llm_overrides/storage_prefix; handler packs read per update by cb_gateway/packs.py, first family legacy_custom
- v1 scenarios
- —
- Ported
- —
- Green
- —
- Failing
- —
tenants table + registry + dispatch gate + llm_overrides/storage_prefix; handler packs read per update by cb_gateway/packs.py, first family legacy_custom
What it does
Nothing, from a group's side — and that is the point. One deployment serves several bot brands (Cookiebot, Bombot, Pawsy, Tarinbot, Connectbot), each with its own token, owners, language, command set, LLM budget and storage prefix, out of one process and one database. v1 did this with five copies of the bot running five divergent caches.
Behaviour that must not change
A brand's command set is enforced in two places, and both matter. /commands
lists only what that brand advertises, and dispatch refuses what it has
turned off — before this, a "disabled" command was merely hidden and still ran
for anyone who typed it. Every lookup fails open: if the tenant registry or the
command catalog cannot be reached, the command runs. Dropping commands during a
database blip would be a far worse failure than briefly honouring one a brand
disabled.
handler_pack is the other half, and it is read now: a pack names the command
families a brand receives on top of the core ones, and v1's Custom/ picture
pools (x_custom_commands) are the first.
A brand that wants none of them sets handler_pack = 'minimal' rather than
listing 53 command names.
The shard key stays group_id. Tenancy is a logical boundary on top of the
physical one, never a second distribution column.
How to verify it
qa/features/x_custom_commands.feature's last scenario drives a brand on the
minimal pack; packages/cb-gateway/tests/test_custom_command.py covers the
pack registry, including the fail-open path and an unknown pack name.
qa/integration/ exercises the registry against a real database. Read
docs/multi-tenant for the rollout and the isolation table.
String catalog ported from v1 locales
strings must match v1 verbatim - port the files, do not rewrite the copy
Mongo -> Citus backfill
all 8 collections. 7 through the pure ETL mappers, idempotent upserts; randomdatabase through cb.py backfill-random, which downloads what each v1 pointer references and writes a real media_objects row - resumable by telegram_file_id, so a second run costs a query per pointer, not a download