--- title: Tenant Storefront Flow tags: [flow, tenant, white-label, storefront, multi-tenant] --- # Tenant Storefront Flow > **Last updated:** 2026-06-10 — current `feature/white-label-shops` scan. > Related: [[Tenant]], [[Tenant API]], [[PRD - Seller-Owned White-Label Shops and Bots]] Describes how a merchant tenant is created, approved, and how buyers land on a tenant storefront. --- ## 1. Tenant onboarding (operator-assisted, Phase 1) ```mermaid sequenceDiagram actor Seller actor Operator participant API as Backend /api/tenants participant DB as PostgreSQL Seller->>API: POST /api/tenants { slug, displayName, brand } API->>DB: INSERT tenants (status=pending) API->>DB: INSERT tenant_user_roles (role=owner) API->>DB: INSERT tenant_payment_policies (default amn_escrow) API-->>Seller: 201 { tenant, status: "pending" } Note over Seller,Operator: Operator reviews in admin panel Operator->>API: POST /api/tenants/:id/activate API->>DB: UPDATE tenants SET status='active' API-->>Operator: 200 { tenant, status: "active" } ``` Tenants start as `pending` and are not publicly accessible until a platform admin activates them. This prevents self-provisioning of white-label storefronts. --- ## 2. Domain registration and provisioning Tenants are accessible at `.amn.gg` automatically once active. Custom domains are now implemented through DNS verification plus dynamic Caddy Admin API routes in the multi-stack. ```mermaid sequenceDiagram actor Seller participant API as Backend participant DNS as Seller DNS participant Caddy as infra-caddy participant DB as PostgreSQL Seller->>API: POST /api/tenants/:id/domains { hostname: "shop.example.com" } API->>DB: INSERT tenant_domains status=pending tlsStatus=pending API-->>Seller: 201 { domain, status: "pending", verificationToken } Seller->>DNS: Add CNAME shop.example.com -> multi.amn.gg Seller->>API: POST /api/tenants/:id/domains/:domainId/verify API->>DNS: resolve A/CNAME DNS-->>API: hostname points to configured ingress API->>Caddy: add route for hostname API->>DB: UPDATE status=active, tlsStatus=pending API-->>Seller: 200 { dnsVerified: true } Seller->>API: POST /api/tenants/:id/domains/:domainId/tls-check API->>Caddy: HTTPS probe API->>DB: UPDATE tlsStatus=issued | pending | failed ``` The background poller also runs `verifyAndProvision()` for pending domains and re-checks active domains whose TLS status is still pending. On backend startup, `syncActiveDomains()` replays active domain routes into Caddy because API-injected routes are not the source of truth. --- ## 3. Buyer landing — storefront bootstrap The frontend fetches `/api/storefront/bootstrap` on every page load. The tenant is resolved entirely server-side from the `Host` header — the browser supplies no tenant hint. ```mermaid sequenceDiagram actor Buyer participant FE as Frontend (TenantProvider) participant API as GET /api/storefront/bootstrap participant MW as tenantResolutionMiddleware participant DB as PostgreSQL Buyer->>FE: Opens shop.example.com (or seller.amn.gg) FE->>API: GET /api/storefront/bootstrap Note right of API: Host: shop.example.com API->>MW: tenantResolutionMiddleware MW->>DB: SELECT * FROM tenant_domains WHERE hostname='shop.example.com' AND status='active' DB-->>MW: domain row MW->>DB: SELECT * FROM tenants WHERE id=domain.tenantId AND status='active' DB-->>MW: tenant row MW-->>API: req.tenant = tenant API->>DB: SELECT * FROM tenant_payment_policies WHERE tenant_id=... DB-->>API: policy row API-->>FE: 200 { tenantId, slug, brand, features, paymentRails, localeDefaults } FE->>FE: TenantProvider stores bootstrap FE->>FE: useTenantTheme() derives CSS vars from brand.primaryColor FE-->>Buyer: Branded storefront renders ``` **Fallback:** If `GET /api/storefront/bootstrap` returns 404 (no tenant for this host), `TenantProvider` uses `AMANAT_DEFAULTS` with `isAmanatDefault: true`. The frontend renders unchanged Amanat branding. --- ## 4. Tenant resolution paths Three resolution paths are supported simultaneously: | Host pattern | Example | Resolution method | | --- | --- | --- | | `.amn.gg` | `myshop.amn.gg` | Slug extracted from subdomain label → `findBySlug` | | Custom CNAME | `shop.example.com` | `findByHostname` → `findById` | | Preview (platform only) | `amn.gg/t/:slug/bootstrap` | Slug from URL param, host must be `amn.gg` / `localhost` | ```mermaid flowchart TD A[HTTP Request] --> B{Is host platform base?\namn.gg / localhost} B -- yes + slug param --> C[resolveTenantBySlug\npreviewOnly=true] B -- yes, no slug --> D[req.tenant = undefined\nAmanat default] B -- no --> E{Ends with .amn.gg?} E -- yes, single label --> F[resolveTenantByHost\nfindBySlug] E -- no --> G[resolveTenantByHost\nfindByHostname] C --> H{Found?} F --> H G --> H H -- yes --> I[req.tenant = TenantRecord] H -- no --> D I --> J[Route handler] D --> J ``` --- ## 5. Telegram bot registration and claim ```mermaid sequenceDiagram actor Developer participant API as POST /api/tenants/:id/telegram/bot participant BotSvc as tenantBotService participant TG as Telegram Bot API participant DB as PostgreSQL Developer->>API: { botToken, username?, miniAppUrl? } Note right of Developer: botToken is write-only API->>BotSvc: registerBot(tenantId, { botToken, username?, miniAppUrl? }) BotSvc->>TG: getMe when username omitted BotSvc->>BotSvc: AES-256-GCM encrypt(botToken, TENANT_SECRET_KEY) BotSvc->>BotSvc: generate webhookSecret + claimToken BotSvc->>DB: INSERT tenant_bots (status=pending, encryptedToken, webhookSecret, claimToken) BotSvc->>TG: setWebhook /api/telegram/tenant-webhook/:botId API->>BotSvc: configureBotMenu(bot.id, shopUrl) BotSvc->>TG: setChatMenuButton -> shopUrl/telegram/ BotSvc-->>API: public bot record with claimUrl API-->>Developer: 201 { id, telegramBotId, username, status: "pending", claimUrl } Developer->>TG: Open claimUrl and send /start TG->>API: POST /api/telegram/tenant-webhook/:botId with secret header API->>BotSvc: claimAdmin(botId, claimToken, telegramUserId) BotSvc->>DB: UPDATE status=active, adminTelegramUserId BotSvc->>TG: send confirmation message ``` --- ## 6. Payment policy Payment rails available to a tenant's buyers are controlled by `tenant_payment_policies`. ```mermaid flowchart LR PP[tenant_payment_policies] -->|allowedRails| R{Buyer checkout} R -->|amn_escrow| E[Amanat escrow — full protection] R -->|amn_direct| D[Amanat scanner — no escrow hold\nstrict buyer disclosure required] R -->|external_provider| X[External processor — Amanat records evidence only] R -->|manual_invoice| M[Operator / merchant confirms payment] ``` `buyerDisclosureMode = 'strict'` (default) mandates a prominent "not escrow protected" notice when `amn_direct` or external rails are used. The frontend reads `features.escrowCheckout` / `features.directCheckout` from the bootstrap payload to decide which checkout paths to expose. --- ## 7. Frontend context tree ``` ← fetches bootstrap, provides useTenant() ← existing MUI theme useTenant() ← brand, features, paymentRails useTenantTheme() ← primaryColor, cssVars (--tenant-primary) ``` `TenantProvider` wraps the application shell. All downstream components read tenant context via `useTenant()`. No tenant-specific props need to be threaded through the component tree. --- ## Phase roadmap | Phase | What ships | Status | | --- | --- | --- | | 0 | Drizzle schema (6 tables), enums, repositories, tenant auth roles | ✅ `feature/white-label-shops` | | 1 | Hosted subdomain (`seller.amn.gg`), tenant bootstrap endpoint, `TenantProvider`, admin tenant UI | ✅ `feature/white-label-shops` | | 2 | Custom domain + DNS verification + Caddy route + TLS status checks | ✅ `feature/white-label-shops` | | 3 | Tenant Telegram bot token storage, webhook registration, menu button, admin claim link | Partial — implemented for claim activation; multi-bot notification routing still planned | | 4 | `amn_direct` payment rail + buyer disclosure | ⬜ Planned | | 5 | Catalog / delivery / external payment adapters, billing events, stronger isolation | ⬜ Planned | Related: [[Tenant]], [[Tenant API]], [[PRD - Seller-Owned White-Label Shops and Bots]], [[Escrow Flow]], [[Telegram Mini App]].