14 feature gaps mapped against existing implementation on feature/white-label-shops: upgrade/billing, theming, seller dashboard, storefront, user base, custom email, invoicing, bot admin commands, multi-admin, monitoring, DB isolation, backup, external payment, CRM. Phase 0 (schema + services + admin UI) marked complete. Phases 1–4 planned with estimates and dependencies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
31 KiB
title, tags, created, status, branch
| title | tags | created | status | branch | |||||
|---|---|---|---|---|---|---|---|---|---|
| White-Label Shops — Product Requirements Document |
|
2026-06-10 | draft | feature/white-label-shops |
White-Label Shops — PRD
1. Context & Vision
Amanat is an escrow marketplace. Today every seller already has a self-contained shop: they can list items, track orders, manage inventory, and handle delivery/dispute flows.
This PRD defines the White-Label Shops tier: an upgrade path that lets a seller spin up one or more fully branded, independently-operated shops — each with their own domain, their own Telegram bot, their own user base, their own theming, and their own billing identity — all running on Amanat infrastructure.
The goal is that a white-label shop feels like a completely separate product to the end buyer. The platform's presence is invisible unless the shop owner explicitly acknowledges it.
2. What Is Already Built (Baseline)
The following is in production or merged on feature/white-label-shops. The PRD
does not re-specify these; it specifies the gaps.
| Component | Status |
|---|---|
DB schema: tenants, tenant_domains, tenant_bots, tenant_user_roles, tenant_payment_policies, tenant_integrations |
✅ Migrated |
Tenant CRUD backend (tenantService.ts) |
✅ Built |
Tenant role-based auth middleware (tenantAuthService.ts) |
✅ Built |
Custom domain provisioning + Caddy route injection (domainProvisioningService.ts) |
✅ Built |
Bot registration + AES-256-GCM token encryption + claim flow (tenantBotService.ts) |
✅ Built |
| Drizzle repos for all 6 tenant tables | ✅ Built |
| Platform admin UI: tenant list + detail pages | ✅ Built |
escrow-multi deployment stack (separate DB, separate containers) |
✅ Running on multi.amn.gg |
Storefront bootstrap endpoint (GET /api/storefront/bootstrap) |
✅ Built |
TenantContext on frontend (resolves shop branding from Host header) |
✅ Built |
3. Gap Map (What This PRD Specifies)
| # | Feature Area | Gap |
|---|---|---|
| G1 | Upgrade & Billing | No seller-facing upgrade flow from normal → multi-shop |
| G2 | Shop Theming | brand JSONB stored but no theming pipeline applied to UI |
| G3 | Seller Shop Dashboard | No tenant-owner-facing admin UI (only platform-admin UI exists) |
| G4 | Storefront (buyer-facing) | Catalog / checkout / order routes are 501 stubs |
| G5 | Tenant User Base | No per-tenant user management (register, login, Telegram link, password) |
| G6 | Email with Custom Domain | No per-tenant SMTP / transactional email |
| G7 | Invoicing | No per-tenant branded invoice generation |
| G8 | Telegram Bot Admin Commands | Bot claim works; admin commands not designed |
| G9 | Multi-Admin Management | Role model exists; no seller-facing UI to grant/revoke shop admins |
| G10 | Platform Monitoring | No per-shop observability, alerting, or ops dashboard |
| G11 | DB Isolation | isolation_mode column exists; per-tenant schema isolation not enforced |
| G12 | Backup & Recovery | No automated backup job or restore procedure per tenant |
| G13 | External Payment Provider | external_provider rail defined; integration not implemented |
| G14 | CRM Light | No customer/buyer management view for shop owners |
4. Detailed Requirements
G1 — Upgrade & Billing
Story: A normal seller sees an "Upgrade to Multi-Shop" banner in their dashboard.
They click, pick a plan, pay (via Amanat escrow or Stripe), and their account is
immediately provisioned with a white_label tenant in pending status. They land on
a setup wizard.
Requirements:
| ID | Requirement |
|---|---|
| G1-1 | A seller_plans table (or config) defines plan tiers: starter (1 shop), growth (3 shops), business (unlimited). Each tier has a monthly price and a max_tenants allowance. |
| G1-2 | The upgrade purchase flow must go through Amanat's existing escrow payment rail (not a separate billing system). On confirmation, the backend grants the seller a tenant_allowance record. |
| G1-3 | On successful purchase, the system auto-creates one tenant row for the buyer (status=pending) and sends a welcome notification (Telegram + email). |
| G1-4 | The seller dashboard must show remaining shop allowance and a "Create New Shop" button guarded by allowance. |
| G1-5 | Downgrade: if a seller's subscription lapses, shops go to suspended (not deleted). Data retained for 90 days; shops reactivate if subscription resumes. |
| G1-6 | Platform admin can manually override a seller's plan tier (for pilots, enterprise deals). |
Out of scope: Recurring billing automation. First version uses manual renewal via Amanat purchase; Stripe subscription integration is a separate initiative.
G2 — Shop Theming
Story: In the shop setup wizard, the owner uploads a logo, picks a primary/accent colour, sets a display name and tagline. These are immediately reflected on the storefront and in Telegram Mini App headers.
Requirements:
| ID | Requirement |
|---|---|
| G2-1 | The brand JSONB column in tenants must be validated against a schema: { logo_url, favicon_url, primary_color (hex), accent_color (hex), display_name, tagline, font_family? }. |
| G2-2 | GET /api/storefront/bootstrap already returns brand. The frontend must apply primary/accent colours as CSS variables (--brand-primary, --brand-accent) on the root element when TenantContext is not the Amanat default. |
| G2-3 | Logo and favicon must be stored in a tenant-scoped path in object storage (prefix: tenants/{tenantId}/). The file ownership check from H2 (audit) must apply here. |
| G2-4 | A "Brand Preview" live preview panel in the seller dashboard must show how the storefront looks with current brand settings before saving. |
| G2-5 | Email templates and invoice PDFs must inject logo_url, primary_color, and display_name from brand when sent on behalf of the shop. |
| G2-6 | Telegram Mini App headerColor and backgroundColor must be set via WebApp.setHeaderColor / WebApp.setBackgroundColor using primary_color from bootstrap when running inside a tenant bot. |
G3 — Seller Shop Dashboard
Story: After setting up a shop, the owner navigates to a separate "Shop Admin" section (distinct from the normal seller dashboard). This section feels like owning a real admin panel for their own platform.
The seller dashboard already exists for normal orders. The shop dashboard is an additional, separate surface.
Requirements:
| ID | Requirement |
|---|---|
| G3-1 | Route: /dashboard/shop/[tenantSlug] (or subdomain-aware: /dashboard/shop when browsing on the tenant's custom domain). The shop admin shell must load tenant brand from TenantContext. |
| G3-2 | Navigation sections in shop admin: Overview, Catalog, Orders, Customers, Bots, Domains, Admins, Settings, Billing. |
| G3-3 | Overview: total orders (7d/30d), GMV, new customers, active offers, revenue chart. Data must be scoped strictly to this tenant. |
| G3-4 | Catalog: CRUD for the shop's own product catalog (separate from the global Amanat marketplace catalog). Each item: title, description, price, category, images, stock, active toggle. |
| G3-5 | Orders: list of orders placed through this shop (buyer, item, amount, status, payment rail, escrow state). Seller can trigger delivery confirmation, raise dispute, release escrow from here. |
| G3-6 | Customers: list of users who have placed at least one order in this shop. Fields: display name, joined date, order count, total spent. No cross-tenant data visible. |
| G3-7 | Bots: mirrors the existing platform-admin bot panel but scoped to this tenant (register bot, get claim link, set Mini App URL, revoke). |
| G3-8 | Domains: mirrors the existing platform-admin domain panel. Add CNAME, trigger verification, check TLS, remove. |
| G3-9 | Admins: list of users with roles in this tenant. Owner can grant manager, finance, support, developer roles to any Amanat user by searching their username/email. Owner cannot revoke their own owner role. |
| G3-10 | Settings: brand editor (G2), locale, timezone, currency, shop URL slug (rename, slug-rename triggers Caddy route update). |
| G3-11 | Billing: current plan, renewal date, shop count vs. allowance, upgrade / downgrade button. |
| G3-12 | All shop-admin API calls must go through tenantAuthService.requireTenantRole() middleware — not the platform-admin guard. |
G4 — Storefront (Buyer-Facing)
Story: A buyer visits myshop.amn.gg (or shop.example.com). They see the shop's
branded catalog, can search/filter items, add to cart, and check out using escrow.
They don't see "Amanat" unless the shop has disclosed it.
The storefront routes currently return HTTP 501. This gap is the largest one.
Requirements:
| ID | Requirement |
|---|---|
| G4-1 | GET /api/storefront/catalog?page&limit&search&category — Return paginated items for the resolved tenant. Items sourced from the shop's own catalog (G3-4). |
| G4-2 | GET /api/storefront/items/:itemId — Item detail: title, description, images, price, seller info (display name, rating), stock status. |
| G4-3 | POST /api/storefront/checkout — Initiate an order for a tenant item. Must create a payment record scoped to this tenant's payment_policy. Enforce allowed_rails from tenant_payment_policies. |
| G4-4 | GET /api/storefront/orders/:orderId — Buyer can look up their own order by ID. Must verify buyer identity matches. |
| G4-5 | Frontend storefront pages (path-routed when on Amanat base domain, full-page when on custom domain): / (catalog), /items/[id] (item detail), /checkout, /orders/[id] (order status). |
| G4-6 | Cart is client-side (localStorage or in-memory). No server-side cart required for v1. |
| G4-7 | The storefront must be responsive / mobile-first and function as a Telegram Mini App for the tenant's registered bots. |
| G4-8 | SEO: <title> and <meta og:*> must use brand.display_name and brand.tagline. |
| G4-9 | Buyers who are not logged into Amanat must be able to browse the catalog. Login is required only at checkout. Login on custom domain must still authenticate against the Amanat identity provider (federated SSO), or against the tenant's own user base if G5 is enabled. |
G5 — Tenant User Base
Story: A shop owner wants their buyers to experience a self-contained login flow.
Buyers register/log in at myshop.amn.gg/login, see the shop's branding, and never
encounter "Amanat" in the auth flow. Behind the scenes, Amanat can use federated
identity or maintain a per-tenant user namespace.
Requirements:
| ID | Requirement |
|---|---|
| G5-1 | Tenant auth mode (configurable in features JSONB): federated (default — buyers are Amanat users, no separate accounts) or isolated (per-tenant user namespace). V1 only ships federated; isolated is designed but not built. |
| G5-2 | In federated mode: login at a custom domain proxies through the Amanat JWT flow. The JWT issued must carry a tenantId claim so backend middleware knows the tenant context. Session cookies must be Domain-scoped to the custom domain, not .amn.gg. |
| G5-3 | Login options visible to buyers at a tenant storefront are controlled by the shop's features.login_methods config: email_password, google_oauth, telegram_widget. Default: all three. The shop owner can disable any method (e.g., Telegram-only). |
| G5-4 | Password reset emails sent to buyers from a tenant shop must use the tenant's SMTP config (G6) and must brand the email with the shop name, not "Amanat". |
| G5-5 | A buyer's Telegram account can be linked at the tenant shop login screen using the Telegram Login Widget or the tenant's own bot (if registered). |
| G5-6 | The Customers panel (G3-6) is the shop admin's view into the tenant user base. It must show login method, linked Telegram, last active, and order count. |
| G5-7 | A shop admin can manually disable a buyer's access to their shop (without affecting the buyer's Amanat account on other shops). |
| G5-8 | isolated user namespace (future): separate tenant_users table, bcrypt passwords, no Amanat JWT dependency. Migrations must be designed before any isolated tenant goes live. |
G6 — Email with Custom Domain
Story: When a buyer on shop.example.com gets an order confirmation, the "From"
address is orders@example.com — not noreply@amn.gg. The shop owner provides their
own SMTP credentials (or Mailgun/SendGrid API key) and we deliver on their behalf.
Requirements:
| ID | Requirement |
|---|---|
| G6-1 | A tenant_integrations row with kind='notification', provider='smtp' stores: host, port, user, from_address in config JSONB, and password AES-256-GCM encrypted in encrypted_config. |
| G6-2 | Backend email service must resolve the active notification integration for the tenant before sending. Fallback to Amanat SMTP if none configured. |
| G6-3 | Supported providers for v1: smtp (generic), sendgrid (API key). mailgun is phase 2. |
| G6-4 | DKIM/SPF setup guidance: after the shop owner adds their SMTP details, the UI must display the DKIM selector and SPF record they need to add to their DNS, with a "Verify SPF/DKIM" button that probes the DNS records. |
| G6-5 | Email templates for order confirmation, payment receipt, delivery notice, and dispute opened must exist in both English and Persian (Farsi) and use the tenant's brand (logo, primary colour, display name). |
| G6-6 | POST /api/tenants/:id/integrations — Create/update integration. Credentials must never be logged or returned in plain text. |
| G6-7 | Test email: POST /api/tenants/:id/integrations/:integrationId/test — Send a test email to the tenant owner's registered email address. |
G7 — Invoicing
Story: When a buyer completes checkout and an order is confirmed, they can download a PDF invoice. The invoice header shows the shop's logo and name, not Amanat's. The shop admin can also download invoices from the Orders panel.
Requirements:
| ID | Requirement |
|---|---|
| G7-1 | Invoice PDF generation: triggered on order status transition to confirmed or delivered. Libraries: pdfkit or puppeteer (existing dependency evaluation needed). |
| G7-2 | Invoice fields: invoice number ({tenantSlug}-{YYYY}-{sequence}), date, buyer name, seller name (brand.display_name), logo, line items (description, qty, unit price, total), subtotal, platform fee if disclosed, total, payment rail used. |
| G7-3 | Invoice storage: tenants/{tenantId}/invoices/{invoiceId}.pdf in object storage, not in DB. |
| G7-4 | GET /api/storefront/orders/:orderId/invoice — Buyer downloads their invoice. Auth required; buyer must own the order. |
| G7-5 | GET /api/tenants/:id/orders/:orderId/invoice — Shop admin download. |
| G7-6 | The invoice must note the payment method (escrow, direct, external) but must NOT disclose blockchain transaction hashes unless the shop explicitly enables it in settings. |
| G7-7 | If the shop has not configured a logo, the Amanat wordmark is used as fallback (not absent). |
G8 — Telegram Bot Admin Commands
Story: The shop owner has claimed their bot (via /start <claimToken>). Now they
want to use the bot to get order alerts, approve/reject high-value orders, and answer
buyer queries. The bot must distinguish between the shop owner/admin and regular buyers.
Requirements:
| ID | Requirement |
|---|---|
| G8-1 | After claim, the bot must respond to /start for a non-admin buyer with a welcome message and a "Browse Shop" Mini App button. |
| G8-2 | Admin commands (only for users in tenant_user_roles with role ≥ manager): /orders — last 10 open orders; /order <id> — order detail + action buttons (confirm delivery, raise dispute); /stats — 7-day GMV and order count; /pause — pause new orders (sets shop_settings.accepting_orders = false); /resume — unpause. |
| G8-3 | Order notification to admin: when a new order is placed on the shop, send a Telegram message to all users with manager or owner role (who have a linked Telegram account) via the tenant's bot (not via the platform @amnescrow_Bot). |
| G8-4 | Dispute alert: when a buyer opens a dispute, immediately notify the admin(s) via the tenant bot with the dispute ID and a "View Dispute" deep link. |
| G8-5 | The tenant bot webhook handler (POST /api/telegram/tenant-webhook/:botId) already exists. Extend it to route commands to the above handlers after verifying admin_telegram_user_id or cross-checking tenant_user_roles. |
| G8-6 | Rate limit: 20 bot command requests / minute per user to prevent abuse. |
| G8-7 | Bot errors (expired token, revoked bot) must set the bot status to suspended and notify the shop owner via Amanat's own Telegram notification (not the tenant bot). |
G9 — Multi-Admin Management
Story: The shop owner wants to add their business partner as a manager. They go to the Admins section in the shop dashboard, search for the partner's Amanat username, pick a role, and send an invite. The partner gets a Telegram/email notification.
The backend role model is already built. The gap is the seller-facing UI and the invite flow.
Requirements:
| ID | Requirement |
|---|---|
| G9-1 | POST /api/tenants/:id/roles (existing) — Seller-accessible when requester has owner role. Add validation: a tenant may have at most 1 owner, up to 5 manager, up to 10 combined finance/support/developer roles. |
| G9-2 | Invite flow: instead of granting the role immediately, create a tenant_invitations record (id, tenant_id, email_or_username, role, token, expires_at). Send invite notification. Invitee must accept before role is active. |
| G9-3 | GET /api/tenants/:id/invitations — List pending invitations (owner/manager only). |
| G9-4 | POST /api/tenant-invitations/:token/accept — Invitee accepts. Validates token not expired, creates tenant_user_roles row, deletes invitation. |
| G9-5 | DELETE /api/tenants/:id/roles (existing) — Seller can revoke any role except their own owner. |
| G9-6 | Role change notification: when a role is granted or revoked, notify the affected user via Telegram and email. |
| G9-7 | Frontend: Admins panel in shop dashboard shows current roles + pending invitations. "Invite Admin" button opens a dialog: search field (username/email), role picker, send invite. |
G10 — Platform Monitoring
Story: The platform ops team (Amanat) must see at a glance that all white-label shops are healthy: uptime, error rates, queue depths, domain/TLS status, bot health. Shop owners get alerts when something affects their shop.
Requirements:
| ID | Requirement |
|---|---|
| G10-1 | A tenant_health view (or materialized cache) per tenant: domain TLS status, bot webhook last received (staleness), last order timestamp, error count in last 24h. |
| G10-2 | Platform admin dashboard (existing tenant detail page) must show this health summary per tenant with colour-coded status (green/yellow/red). |
| G10-3 | Alert conditions and notification targets: |
| • TLS cert expired or near expiry (< 7 days) → notify shop owner | |
| • Bot webhook not received for > 4 hours → notify shop owner | |
| • Domain DNS verification failed → notify shop owner | |
| • Error rate > 5% of requests in 1h window → notify platform ops | |
| • No orders in 72h for an active shop → no alert (normal) | |
| G10-4 | Alerts are delivered via Amanat's platform bot (@amnescrow_Bot) to the shop owner's linked Telegram, and by email if they have one configured. Not via the tenant bot (which may be broken). |
| G10-5 | Metrics endpoint: GET /api/internal/tenants/health-summary (platform-admin only) — returns JSON array of tenant health records for all active tenants. Used by a future Grafana panel. |
| G10-6 | Log isolation: backend logs must include a tenantId field in every log line emitted during a tenant-scoped request. This enables per-tenant log filtering in Loki/Grafana. |
| G10-7 | The domain provisioning poller (already runs every 60s) must emit a structured log on each poll result so it can be scraped. |
G11 — Database Isolation
Story: By default all tenants share the escrow_multi Postgres database, with
row-level ownership enforced by tenant_id FKs. For enterprise shops that require
stronger isolation (regulatory, contractual), the platform must support a separate
Postgres schema or separate Postgres database.
Requirements:
| ID | Requirement |
|---|---|
| G11-1 | The isolation_mode column already exists on tenants with values shared | schema | database. V1 only provisions shared mode tenants. |
| G11-2 | All queries that read tenant-scoped data (catalog, orders, customers) must include an explicit WHERE tenant_id = $1 clause. No implicit context. The code review checklist must include this check. |
| G11-3 | schema mode (Phase 2): on tenant creation with isolation_mode='schema', run CREATE SCHEMA tenant_{slug} and apply migrations scoped to that schema. Connection pool must select the schema via SET search_path. |
| G11-4 | database mode (Phase 3): separate Postgres database + separate connection pool. Only viable for enterprise tier. Migration tooling must support it. |
| G11-5 | Regardless of isolation mode, the tenant_user_roles table in the platform schema always governs access. There is no separate auth DB per tenant in any mode. |
| G11-6 | The existing DB privilege isolation migration (0018_db_privilege_isolation.sql) that creates escrow_vital_user and escrow_nonvital_user Postgres roles must be extended so that vital tables (orders, payments, balances) are inaccessible to the nonvital role even across schemas. |
G12 — Backup & Recovery
Story: The platform runs nightly backups of each tenant's data and notifies shop owners that their data is safe. In a recovery scenario, a shop's data can be restored to a point-in-time without affecting other tenants.
Requirements:
| ID | Requirement |
|---|---|
| G12-1 | A backup job (cron, daily 02:00 UTC) runs pg_dump --schema=public --table='tenant_*' --where="tenant_id='...'" for each active tenant and uploads a .dump file to object storage at backups/{tenantId}/{date}.dump. |
| G12-2 | Object storage retention: 30 daily backups, then weekly for 6 months, then monthly for 3 years. Lifecycle policy must be set on the bucket. |
| G12-3 | Backup success/failure is recorded in a backup_logs table: tenant_id, job_run_at, status, size_bytes, storage_path, error. |
| G12-4 | Platform alert: if any tenant's backup job fails 2 consecutive times, notify platform ops via @amnescrow_Bot and email. |
| G12-5 | Shop owner notification: weekly digest email ("Your shop data was backed up successfully on [dates]. Backup size: X MB."). No alert on success for daily — only on failure. |
| G12-6 | Restore procedure: platform admin can trigger a restore for a specific tenant from the admin panel. Restore runs in a transaction; original data is renamed (not deleted) before restore so rollback is possible. This is an operator action, not self-service for V1. |
| G12-7 | The backup job must not hold locks on the tenant tables for > 5 seconds. Use --snapshot / REPEATABLE READ isolation. |
G13 — External Payment Provider
Story: A large merchant doesn't want to use Amanat's escrow rails for all transactions. They want to offer credit-card checkout powered by Stripe (or their existing payment gateway) alongside — or instead of — Amanat escrow.
The tenant_payment_policies.allowed_rails enum already includes external_provider.
The gap is the integration layer.
Requirements:
| ID | Requirement |
|---|---|
| G13-1 | A tenant_integrations row with kind='payment', provider='stripe' (or paypal, zarinpal for IR market) stores the provider API credentials (AES-256-GCM encrypted). |
| G13-2 | POST /api/storefront/checkout must check tenant_payment_policies.allowed_rails. If external_provider is in allowed_rails and buyer selects it, the backend creates a checkout session on the external provider and returns a redirect_url. |
| G13-3 | Webhook handler: POST /api/tenants/:id/payment-webhook/:provider — receives payment confirmation from the external provider, verifies signature, transitions order status. |
| G13-4 | V1 providers: stripe only. paypal and zarinpal are Phase 2. |
| G13-5 | If the tenant's external provider integration is not configured, external_provider must not appear as a buyer-facing option even if it's in allowed_rails. |
| G13-6 | Amanat's escrow guarantee does not apply to external_provider transactions. The buyer UI must show a clear disclosure: "Payment processed by [Provider Name]. Amanat escrow does not apply to this transaction." |
| G13-7 | Revenue split: Amanat charges a platform fee (configurable per tenant) even on external provider transactions. The fee is invoiced monthly to the shop owner, not deducted at checkout. |
G14 — CRM Light
Story: A shop owner wants to see who their repeat buyers are, filter by order count or total spent, and send a bulk announcement to buyers who opted in to notifications.
Requirements:
| ID | Requirement |
|---|---|
| G14-1 | The Customers panel (G3-6) is the entry point. It must support sorting and filtering: by join date, total orders, total spent, last active, login method, Telegram linked (yes/no). |
| G14-2 | Buyer profile detail page (within shop admin, not exposed to other tenants): name, email (if federated, masked as j***@gmail.com), Telegram username (if linked), order history within this shop, total lifetime spend in this shop. |
| G14-3 | Bulk notification: "Announce to Customers" — compose a message (plain text, max 1000 chars), target audience (all opted-in, filtered list), delivery channel (Telegram only for V1; email in Phase 2). |
| G14-4 | Opt-in/opt-out: buyers can opt out of marketing notifications from the shop at any time (unsubscribe link in email, /unsubscribe command in tenant bot). Opt-out is per-tenant, not global. |
| G14-5 | Announcement rate limit: max 2 bulk announcements per shop per 24 hours to prevent spam. Platform can override. |
| G14-6 | Announcement log: every announcement is recorded (content, targets count, sent at, delivery count, opt-out count). Retained for 12 months. |
| G14-7 | No PII export API for V1. Shop owners can view customer data in the UI but cannot bulk-export email lists. |
5. User Stories Summary
| Story | Actor | When | Then |
|---|---|---|---|
| Upgrade to multi-shop | Seller | Clicks "Upgrade" in dashboard | Lands on plan picker, pays, shop provisioned |
| Create a shop | Shop owner | After upgrade | Setup wizard: name → brand → domain → bot |
| Apply theming | Shop owner | In Settings / Brand | Storefront and Mini App reflect new colours / logo |
| Manage catalog | Shop owner | In Shop Admin → Catalog | Add, edit, archive items; set price, stock, images |
| Invite shop manager | Shop owner | In Shop Admin → Admins | Send invite by username; manager gets notification |
| Buyer browses shop | Buyer | Opens custom domain | Sees branded storefront, searches catalog |
| Buyer checks out | Buyer | Adds item to cart | Selects payment rail; escrow or external provider |
| Order notification | Shop admin | Order placed | Telegram message via tenant bot with order summary |
| Domain setup | Shop owner | In Shop Admin → Domains | Adds CNAME, verifies DNS, TLS cert auto-issued |
| Bot claim | Shop owner | After bot registration | Sends /start <token> to their bot; becomes bot admin |
| Send announcement | Shop owner | In CRM → Announce | Sends Telegram message to opted-in buyers |
| Download invoice | Buyer | On order confirmation | Downloads branded PDF invoice |
| Monitor shop health | Platform ops | Admin dashboard | Sees per-shop TLS, bot, and error status |
| Receive backup alert | Shop owner | Weekly digest | Email confirming backup size and dates |
| Restore shop data | Platform ops | After incident | Admin UI triggers restore; owner notified |
6. Technical Dependencies & Constraints
| Constraint | Detail |
|---|---|
| Multi-stack isolation | Work on feature/white-label-shops targets escrow-multi only. Never touch escrow-dev. |
| Separate bot tokens | escrow-multi TELEGRAM_BOT_TOKEN must differ from escrow-dev. Each tenant bot is a third token. |
| Caddy Admin API | Domain provisioning calls infra-caddy:2019. The Caddy container must have Admin API enabled and the backend must be on shared-web network. |
TENANT_SECRET_KEY |
Must be 32 bytes of cryptographically random data, set before any bot is registered. Key rotation requires re-encrypting all encrypted_token fields. |
| PDF generation | No PDF library in backend yet. pdfkit is lightweight; puppeteer gives richer templates but adds ~200MB to the Docker image. Recommend pdfkit for V1. |
| Per-tenant SMTP | Backend email service must be refactored to accept a transport config per tenant rather than reading global env vars. |
| Object storage | No S3/Minio integration exists yet. Required for logo uploads (G2), invoice storage (G7), and backups (G12). minio sidecar in escrow-multi stack is the simplest path. |
| DB backup job | Needs a separate escrow-multi-backup container or cron job on the host. Cannot run inside the backend container. |
| Tenant catalog | Requires a tenant_catalog_items table (not yet in schema). Must be added in a new migration before G4 can be built. |
| Invitation table | tenant_invitations table not yet in schema. Add before G9. |
backup_logs table |
Not yet in schema. Add before G12. |
7. Phase Plan
| Phase | Features | Estimate | Depends On |
|---|---|---|---|
| Phase 0 (done) | Schema, services, admin UI, domain provisioning, bot lifecycle | — | — |
| Phase 1 | G1 (upgrade/billing), G2 (theming), G3 (seller shop dashboard), G9 (multi-admin) | 3–4 weeks | Phase 0 |
| Phase 2 | G4 (storefront), G5 (user base / federated login), G8 (bot admin commands), G14 (CRM light) | 4–6 weeks | Phase 1 |
| Phase 3 | G6 (custom email), G7 (invoicing), G10 (monitoring), G12 (backup) | 3–4 weeks | Phase 2 |
| Phase 4 | G11 (schema isolation), G13 (external payment provider) | 4–6 weeks | Phase 3 |
8. Out of Scope (This PRD)
- Recurring billing / Stripe subscription management
isolateduser namespace (per-tenant user DB)database-mode isolation (separate Postgres per tenant)- Multi-language catalog items (seller-managed translation)
- Advanced analytics / BI dashboards for shop owners
- Shop-to-shop referral / affiliate programs
- White-label mobile apps (iOS/Android native)
- Automated A/B testing on storefronts
9. Open Questions
| # | Question | Owner | Due |
|---|---|---|---|
| OQ1 | PDF library choice: pdfkit vs puppeteer? |
Engineering | Phase 3 kickoff |
| OQ2 | Object storage provider: Minio on-premise vs. external S3? | Ops | Phase 1 kickoff |
| OQ3 | First-version upgrade price points for plan tiers? | Product | Phase 1 kickoff |
| OQ4 | Should federated login show "Powered by Amanat" or hide it entirely? |
Legal / Product | Phase 2 kickoff |
| OQ5 | Stripe vs. Zarinpal priority for external payment provider? | Business | Phase 4 kickoff |
| OQ6 | Max allowed shops per business tier — unlimited or a number (e.g., 20)? |
Product | Phase 1 kickoff |
| OQ7 | Should backup notification be opt-out or opt-in for shop owners? | Product | Phase 3 kickoff |