Files
nick-doc/04 - Flows/Tenant Storefront Flow.md
Siavash Sameni e52ffce48a docs: sync vault with codebase state (2026-06-12)
- Update backend, frontend, scanner, deployment, amanat-assist service docs
- Update System Overview, Scanner Architecture, Telegram Mini App flow
- Update 10 - Services/README.md
- Add Tenant data model, Tenant API reference, Tenant Storefront Flow
- Add Multi-Shop Branch Project Scan (2026-06-10)
- Add tenant.md service doc
- Append activity log entry
- Reflects archived/search/stats route fix and new E2E test suite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:42:18 +04:00

8.5 KiB

title, tags
title tags
Tenant Storefront Flow
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)

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 <slug>.amn.gg automatically once active. Custom domains are now implemented through DNS verification plus dynamic Caddy Admin API routes in the multi-stack.

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.

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
<slug>.amn.gg myshop.amn.gg Slug extracted from subdomain label → findBySlug
Custom CNAME shop.example.com findByHostnamefindById
Preview (platform only) amn.gg/t/:slug/bootstrap Slug from URL param, host must be amn.gg / localhost
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

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 <claimToken>
    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.

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

<TenantProvider>           ← fetches bootstrap, provides useTenant()
  <ThemeProvider>          ← existing MUI theme
    <App>
      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.