Skip to content

Planned: Database-per-Tenant Migration ​

A planned architectural change to essence's multi-tenancy model. This is a feature initiative, not part of the public-launch roadmap — 0.12.0 shipped without touching tenancy at all. It has its own branch, timeline, and version-bump wave, and is picked up independently of the launch work.

Decision (2026-08-25): adopt real database-per-tenant ​

The original open question was whether stancl/tenancy was earning its weight as a hard dependency, given essence only used row-scoping (BelongsToTenant + bot_id) and every other Stancl feature — DB switching, domain/subdomain resolution, tenant lifecycle jobs/events — was either explicitly disabled or configured but never wired up (config/tenancy.php's bootstrappers array and TenancyServiceProvider::bootEvents() are both entirely commented out).

Decision: go the other way — adopt real database-per-tenant multi-tenancy, the thing Stancl was actually built for, instead of dropping it for a lighter row-scoping shim. Motivation: row scoping means every bot-owned table across every companion package carries a bot_id column and a global scope; database-per-tenant removes that column entirely and gets real data isolation between bots as a side effect. This is a bigger commitment to Stancl, not a smaller one.

Should stancl/tenancy stay a hard dependency? Yes, more than before.

Scope, audited 2026-08-25 ​

BelongsToTenant is imported directly by 14 model files across 7 companions (affiliates, announcements, billing, settings, user-management, user-wallet, plus essence's own BotUser) — every one of them needs to drop the trait and its bot_id column once their tables move to per-tenant databases. Everything else Stancl-related in essence is either unused (TenantScope imported but never referenced in tbe-user-wallet/BotUserWallet.php) or thin/doc-only (TenantCouldNotBeIdentifiedById only appears in @throws docblocks, never caught).

Spans essence and all 7 companion packages that have models, plus a real production data migration (vshop currently has 6 live bots on the shared-DB-with-bot_id model).

Central vs. per-tenant split, decided ​

telegram_users stays central — one person can talk to multiple bots, so it's one row shared across all of them, not bot-scoped. bots itself also stays central, by construction: Stancl's tenant registry (the thing that resolves which database to switch to) has to live somewhere every request can reach before tenancy initializes. Everything else — bot_users and every table currently BelongsToTenant across every companion — moves into each bot's own database. The bot_users.telegram_user_peer_id → telegram_users foreign key becomes a cross-database reference with no DB-level constraint once bot_users moves to a tenant database; integrity has to be enforced at the application layer instead.

Cutover ​

A maintenance-window migration, not zero-downtime. Telegram retries webhook deliveries on non-2xx responses, so a few minutes down (per bot, or all 6 at once) loses nothing.

Work items ​

  • [ ] Design the per-tenant database naming/provisioning convention (Stancl's default: prefix + tenant_id + suffix) and confirm a single MySQL server hosting all tenant databases is sufficient at this scale
  • [ ] Wire up the tenant-creation pipeline that's currently scaffolded but dark: TenancyServiceProvider::events()'s TenantCreated → CreateDatabase/MigrateDatabase jobs are commented out, and bootEvents() never actually calls Event::listen() at all
  • [ ] Enable DatabaseTenancyBootstrapper in config/tenancy.php (currently commented out, "dark since introduction" per that file's own comment)
  • [ ] Split essence's own migrations: bots/telegram_users stay in the central migration path, bot_users/state_data/inline_confirmations/message_metas move to a tenant migration path
  • [ ] Same split for every companion package with models — in practice this is "move everything," since nothing else needs to stay central
  • [ ] Write and rehearse the production data migration against a clone before touching the real thing: create each bot's database, copy that bot's scoped rows out of the shared tables, verify row counts/spot-check data, only then cut the app over
  • [ ] Version-bump wave: every affected companion's own essence constraint moves again — a second breaking-change wave after the 0.12.0 locale-lazy one, so give it its own version bump per repo rather than folding it into an unrelated release
  • [ ] Document the migration path for vshop, the one existing consumer