- 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>
216 lines
8.5 KiB
Markdown
216 lines
8.5 KiB
Markdown
---
|
|
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 `<slug>.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 |
|
|
| --- | --- | --- |
|
|
| `<slug>.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 <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`.
|
|
|
|
```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
|
|
|
|
```
|
|
<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]].
|