wm2.dev
appKitFeatured

App-Kit

Multi-tenant SaaS platform: build Telegram bots, mini-apps and sites from pluggable modules.

Lead engineer
backendpaymentsfrontend
31architecture decisions (ADR)
15 / 9modules / adapter families
102tagged releases

01 Problem

A single tenant owns projects, sites and modules. Every business (shop, booking, CMS, reviews) wanted its own data, roles and integrations without forking the codebase. The platform needed strict per-site isolation and a way to swap an integration for an in-house service without touching feature code.

02 What I built

A Django + DRF backend with a Project → Site → Modules tenancy model. Features ship as pluggable modules (shop/orders, booking, CMS, FAQ, reviews, pricing, loyalty, newsletter). Every external dependency sits behind an ABC + factory adapter (payments, telegram, storage, image, notifications, auth) chosen per site by config. Per-site RBAC lets an owner define custom roles and a granted/denied permission matrix. Auth supports both JWT for admins and Telegram HMAC for mini-app clients. Three Next.js frontends — client-admin, platform-admin and a public storefront — consume the same API.

  • Project → Site → Modules multi-tenant model with strict per-site data isolation
  • Features as pluggable modules; external services behind ABC + factory adapters, chosen per site
  • Per-site RBAC: owner-defined roles + granted/denied permission matrix, auto-seeded from modules
  • Dual auth — JWT for admins, Telegram HMAC for mini-app clients; three Next.js frontends on one API

03 Tenancy and isolation

Every request is resolved to a site before it reaches a view. A middleware maps the host to Site.domain and puts the site on the request. Every view then filters by that site or by the site ID in the URL. All tenants share one PostgreSQL database, and the ADR says so openly: there is no physical separation, so isolation is enforced in code on every request.

The same rule holds outside the database. Files are stored under a project and site prefix, and each site has its own storage quota. Keys in the shared Redis carry a prefix, so tenants and environments cannot collide.

04 Modules and adapters

Features are plugins with a registry and autodiscovery. There are 15 modules today: shop, orders, booking, schedule, CMS, FAQ, reviews, comments, pricing, loyalty, newsletter, bulletin, cities, portfolio and support. An owner switches them on per site, and the admin navigation and permissions follow what is enabled.

External services sit behind nine adapter families: payments, booking, auth, notifications, storage, cache, geo, image processing and Telegram. Payments alone have five providers (internal, Stripe, YooKassa, Lava and Telegram Payments), and booking can run on the built-in engine or YCLIENTS. The factory reads the provider from the site’s module config, so two sites on the same code can take payments through different providers. Heavy work such as image conversion runs as a separate service behind an adapter.

Site templates are separate Next.js apps with an access list. A private template, like the one behind this site, is offered only to the owners it was made for. An owner can register on their own, go through an onboarding wizard and land in the dashboard. Platform support can impersonate an owner to see a problem exactly as that owner does.

05 Roles, auth and security

RBAC was rebuilt after a real bug: one coarse permission covered every action on reviews, so anonymous storefront visitors got 401 on a simple read. The new model has platform roles and site roles (system, preset and owner-defined), and every module declares its own permissions, which are seeded when the module is enabled.

Admins sign in with JWT, while mini-app clients are verified by the HMAC-SHA256 signature of Telegram initData. Tokens moved from localStorage to httpOnly cookies, and the refresh cookie is scoped to the auth path only.

06 Infrastructure and releases

Development runs in Docker Compose and production in k3s. A version tag triggers Forgejo Actions, which builds the images and pushes them to a private registry. An OpenAPI snapshot test fails CI when the API schema changes unnoticed.

Custom domains used to need a manual nginx edit for every new tenant. Now an OpenResty Lua hook asks the backend for an SNI allowlist and caches it briefly. A new domain works as soon as the site has it. The API client and shared storefront code are published to a private npm registry with versions locked to the backend release.

07 Result

A new business stands up on shared infrastructure with its own isolated data, roles and modules. Swapping an integration is a config change, not a rewrite. Since March 2026 the repository has grown to almost 900 commits and 102 tagged releases. The backend has close to 3,000 test functions. The reasoning behind tenancy, adapters, auth and deployment is kept in 31 ADRs. Client storefronts run on the platform, and this site is built on its storefront packages.