From e52ffce48a96aa5579c8545cc7a704bfd86716ea Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 12 Jun 2026 11:42:18 +0400 Subject: [PATCH] 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 --- .obsidian/graph.json | 2 +- 00 - Overview/System Overview.md | 30 +- 01 - Architecture/Scanner Architecture.md | 38 +- 02 - Data Models/Tenant.md | 289 ++++++++ 03 - API Reference/Tenant API.md | 459 ++++++++++++ 04 - Flows/Telegram Mini App.md | 71 +- 04 - Flows/Tenant Storefront Flow.md | 215 ++++++ 09 - Audits/Activity Log.md | 8 + 09 - Audits/Audit Index - 2026-05-24.md | 2 + ...i-Shop Branch Project Scan - 2026-06-10.md | 64 ++ 10 - Services/README.md | 44 +- 10 - Services/amanat-assist.md | 9 +- 10 - Services/backend.md | 667 ++++++++---------- 10 - Services/deployment.md | 515 +++++++++----- 10 - Services/frontend.md | 572 +++++++-------- 10 - Services/scanner.md | 417 ++++++----- 10 - Services/tenant.md | 304 ++++++++ README.md | 15 +- 18 files changed, 2619 insertions(+), 1102 deletions(-) create mode 100644 02 - Data Models/Tenant.md create mode 100644 03 - API Reference/Tenant API.md create mode 100644 04 - Flows/Tenant Storefront Flow.md create mode 100644 09 - Audits/Multi-Shop Branch Project Scan - 2026-06-10.md create mode 100644 10 - Services/tenant.md diff --git a/.obsidian/graph.json b/.obsidian/graph.json index fa47a0d..fe5a668 100644 --- a/.obsidian/graph.json +++ b/.obsidian/graph.json @@ -17,6 +17,6 @@ "repelStrength": 10, "linkStrength": 1, "linkDistance": 250, - "scale": 0.1383377348281637, + "scale": 0.19993564150556878, "close": true } \ No newline at end of file diff --git a/00 - Overview/System Overview.md b/00 - Overview/System Overview.md index 71f993a..3d10ddf 100644 --- a/00 - Overview/System Overview.md +++ b/00 - Overview/System Overview.md @@ -11,12 +11,18 @@ created: 2026-05-23 ## The 10,000-foot view -Amn is a **two-repo system**: +Amn is a **multi-repo workspace**: -- **Frontend** (`/Users/mojtabaheidari/code/frontend`) — a Next.js 16 App Router application that serves the marketplace UI, the admin dashboard, the public blog, and the user-facing Web3 wallet flow. -- **Backend** (`/Users/mojtabaheidari/code/backend`) — an Express 5 + TypeScript API server that owns all business logic, persists live app state primarily to MongoDB, caches in Redis, and brokers all external integrations. The active integration backend also contains the Postgres/Drizzle migration layer, but it is not yet the broad runtime store. +- **Frontend** (`frontend/`) — a Next.js 16 App Router application that serves the marketplace UI, admin dashboard, public blog, Telegram Mini App shell, seller shop surfaces, and the white-label tenant admin UI. +- **Backend** (`backend/`) — an Express 5 + TypeScript API server that owns business logic, persists runtime state through PostgreSQL/Drizzle repositories, caches in Redis, brokers external integrations, and now hosts the tenant/storefront/custom-domain APIs. +- **Deployment** (`deployment/`) — Docker Compose, Caddy, migrations, and Gatus configuration for `dev.amn.gg` plus the `escrow-multi` / `multi.amn.gg` stack. +- **Scanner** (`scanner/`) — the Go AMN Pay Scanner that watches chains and delivers signed payment webhooks back to the backend. +- **Amanat Assist** (`amanat-assist/`) — the AI request-assistant Mini App and LLM proxy. +- **Documentation vault** (`nick-doc/`) — Obsidian/Taskmaster documentation and audit history. -The two repos are deployable independently. They communicate over **HTTPS (REST)** for stateful actions and over **WebSocket (Socket.IO)** for live updates. The frontend never talks directly to MongoDB, Redis, Request Network API keys, OpenAI, or admin custody secrets -- every sensitive external interaction is mediated by the backend so that secrets stay on the server. +The deployable repos are versioned independently, but frontend/backend are kept in lockstep for image tags. They communicate over **HTTPS (REST)** for stateful actions and over **WebSocket (Socket.IO)** for live updates. The frontend never talks directly to PostgreSQL, Redis, scanner API keys, OpenAI, Telegram BotFather tokens, or admin custody secrets -- every sensitive external interaction is mediated by server-side services. + +The active multi-shop branch is `feature/white-label-shops` in `frontend/` and `backend/`. It powers `multi.amn.gg`, tenant subdomains, custom domains routed dynamically through Caddy, and tenant-owned Telegram bots. See [[Tenant]], [[Tenant API]], and [[Tenant Storefront Flow]]. ## System map @@ -40,6 +46,7 @@ flowchart TB SocketS["Socket.IO server
rooms per user / chat / request"] Auth["Auth service
JWT + Passkey + Google + Telegram"] Market["Marketplace service
Requests, Offers, Templates"] + TenantSvc["Tenant service
host resolution + domain + bot"] ChatSvc["Chat service"] PaySvc["Payment service
Request Network + ledger + custody controls"] TelegramSvc["Telegram service
bot + Mini App + notifications"] @@ -52,8 +59,7 @@ flowchart TB end subgraph Data["Data tier"] - Mongo[("MongoDB
via Mongoose
primary runtime")] - PG[("PostgreSQL 18
Drizzle migration layer")] + PG[("PostgreSQL 18
Drizzle repositories")] RedisDB[("Redis
cache + locks")] Disk[("Local disk
/uploads")] end @@ -81,11 +87,10 @@ flowchart TB ClientJS --> REST SocketC <--> SocketS - REST --> Auth & Market & ChatSvc & PaySvc & TelegramSvc & Disp & Points & BlogSvc & AISvc & Notif & Files + REST --> Auth & Market & TenantSvc & ChatSvc & PaySvc & TelegramSvc & Disp & Points & BlogSvc & AISvc & Notif & Files SocketS --> ChatSvc & Notif & Market - Auth & Market & ChatSvc & PaySvc & Disp & Points & BlogSvc & TelegramSvc --> Mongo - PaySvc -.->|oracle payment_quotes when enabled| PG + Auth & Market & TenantSvc & ChatSvc & PaySvc & Disp & Points & BlogSvc & TelegramSvc --> PG Auth & PaySvc & Notif --> RedisDB Files --> Disk @@ -96,6 +101,7 @@ flowchart TB PaySvc -.tx fetch.-> Alchemy TelegramSvc <--> TelegramAPI + TenantSvc <--> TelegramAPI TelegramAPI -.webhook.-> TelegramSvc Auth --> TelegramAPI Notif --> SMTP @@ -151,7 +157,7 @@ Chat is built on Socket.IO rooms. Every entity that needs live updates gets its - `buyer-` / `seller-` — marketplace-wide updates - `sellers` / `buyers` — global broadcast pools -Messages persist to MongoDB through the `Chat` model and are rate-limited per chat (`chatRateLimiter.ts`). The frontend's `socket/` directory wraps `socket.io-client` and exposes typed event hooks to React components. +Messages persist through the backend chat repository layer and are rate-limited per chat (`chatRateLimiter.ts`). The frontend's `socket/` directory wraps `socket.io-client` and exposes typed event hooks to React components. ### Notifications — [[Notifications]] @@ -164,7 +170,7 @@ Push and SMS are tracked as **planned** in `backend/TODO.md`. ### Disputes — [[Dispute System]] -When a deal goes wrong (see [[Glossary#Dispute]]), either party can open a dispute. The backend creates a **three-way chat** between buyer, seller, and admin, opens a `Dispute` document with a structured `timeline[]` and `evidence[]`, and can assign the dispute to an admin via `assignAdmin()`. Resolution can be `refund | replacement | compensation | warning_seller | ban_seller | no_action` in the current Mongoose model. +When a deal goes wrong (see [[Glossary#Dispute]]), either party can open a dispute. The backend creates a **three-way chat** between buyer, seller, and admin, opens a dispute record with a structured timeline/evidence model, and can assign the dispute to an admin. Resolution can be `refund | replacement | compensation | warning_seller | ban_seller | no_action` in the current service surface. > [!note] State alignment gap > The dispute module exists now, but its model still uses the legacy `pending | in_progress | resolved | ...` enum. [[Funds Ledger and Escrow State Machine Specification]] defines the canonical future enum and financial side effects. @@ -226,6 +232,6 @@ OpenAI (model configurable per call) is exposed through `/api/ai/*`. The current - [[Roles & Personas]] — who does what in the system. - [[Glossary]] — a domain dictionary you will want open in another pane. - [[01 - Architecture]] — service boundaries, module layout, and deployment topology. -- [[02 - Data Models]] — MongoDB collections and field-by-field schemas. +- [[02 - Data Models]] — PostgreSQL/Drizzle tables plus legacy model references where still relevant. - [[03 - API Reference]] — every endpoint, its payload, and its auth requirements. - [[04 - Flows]] — diagrammed user journeys for every major use case. diff --git a/01 - Architecture/Scanner Architecture.md b/01 - Architecture/Scanner Architecture.md index 43417a8..7e88d4f 100644 --- a/01 - Architecture/Scanner Architecture.md +++ b/01 - Architecture/Scanner Architecture.md @@ -2,7 +2,7 @@ title: Scanner Architecture tags: [architecture, scanner, payment] created: 2026-05-30 -updated: 2026-06-08 +updated: 2026-06-12 --- # Scanner Architecture @@ -25,7 +25,7 @@ AMN Pay Scanner is a standalone Go microservice that watches on-chain payment ev - Retry failed webhook deliveries with exponential back-off - Expire stale pending intents on a configurable TTL - Read an EVM ERC-20 balance on demand (`POST /balances/check`) -- Watch an EVM address/token pair for balance changes with age-decayed polling cadence (`POST /balance-watches`) +- Watch an EVM address/token pair for balance changes with age-decayed polling cadence (`POST /balance-watches`); checks every 5 min for the first 24 h, then 10 → 20 → 40 min as the watch ages; watches expire after 7 days --- @@ -33,19 +33,21 @@ AMN Pay Scanner is a standalone Go microservice that watches on-chain payment ev Chains are defined in `supported-chains.json`. A worker is spawned only for chains with `"verified": true` (or listed in `SCANNER_ENABLED_CHAINS`). -| Chain | Chain ID | Type | Proxy address | Conf. threshold | Active by default | +| Chain | Chain ID | Type | Proxy / contract address | Conf. threshold | `verified` | |---|---|---|---|---|---| -| BNB Smart Chain | 56 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 200 blocks | Yes | -| Ethereum Mainnet | 1 | EVM | `0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C` | 50 blocks | Yes | -| BSC Testnet | 97 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 5 blocks | Yes (testnet) | -| Arbitrum One | 42161 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 2400 blocks | No | -| Polygon | 137 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 300 blocks | No | -| Base | 8453 | EVM | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` | 300 blocks | No | -| Tron Mainnet | 728126428 | Tron | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` (USDT TRC20) | 200 (API-confirmed) | No | -| TON Mainnet | 1100 | TON | `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` (USDT Jetton master) | 120 (API-finalized) | No | +| BNB Smart Chain | 56 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 200 blocks | **true** | +| Ethereum Mainnet | 1 | EVM | `0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C` | 50 blocks | **true** | +| BNB Smart Chain Testnet | 97 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 5 blocks | **true** (testnet) | +| Arbitrum One | 42161 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 2400 blocks | false | +| Polygon | 137 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 300 blocks | false | +| Base | 8453 | EVM | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` | 300 blocks | false | +| Tron Mainnet | 728126428 | Tron | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` (USDT TRC20 contract) | 200 (API-confirmed) | false | +| TON Mainnet | 1100 | TON | `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` (USDT Jetton master) | 120 (API-finalized) | false | > [!note] Proxy address variations -> Ethereum mainnet uses a v0.1.0 proxy (`0x370DE...`). Base uses a non-canonical CREATE2 address (`0x189219...`). All other EVM chains use the canonical v0.2.0 address (`0x0DfbEe...`). The memory note [[RN proxy addresses per chain]] has background on why CREATE2 canonical-address claims should not be trusted without verification. +> Ethereum mainnet uses the v0.1.0 proxy (`0x370DE...`); a v0.2.0 proxy is also deployed on ETH but checkout still uses the v0.1.0 ABI. Base uses a non-canonical CREATE2 address (`0x189219...`). All other EVM chains use the canonical v0.2.0 address (`0x0DfbEe...`). The memory note [[RN proxy addresses per chain]] has background on why CREATE2 canonical-address claims should not be trusted without verification. +> +> Tron and TON have no fee-proxy contract. The `proxyAddress` field for those chains holds the token contract address used to filter Transfer events (Tron) or Jetton transfers (TON). To enable a disabled chain without a rebuild: set `SCANNER_ENABLED_CHAINS=56,1,42161` (overrides the JSON `verified` flags). @@ -130,8 +132,9 @@ One worker goroutine is spawned per active chain. All three chain types implemen | Scanner → Backend | `POST ` | Payment confirmed; signed with `X-AMN-Signature` HMAC-SHA256 | | Backend → Scanner | `POST /balances/check` | Synchronous ERC-20 balance read (direct-address rail) | | Backend → Scanner | `POST /balance-watches` | Start async balance watch (direct-address rail) | -| Scanner → Backend | `POST ` | Balance changed; `X-AMN-Event-Type: balance_changed` | -| Backend → Scanner | `DELETE /balance-watches/{id}` | Stop watch after payment accepted or cancelled | +| Backend → Scanner | `GET /balance-watches/{id}` | Get balance-watch status | +| Scanner → Backend | `POST ` | Balance changed; `eventType: balance_changed` in body | +| Backend → Scanner | `DELETE /balance-watches/{id}` or `POST /balance-watches/{id}/stop` | Stop watch after payment accepted or cancelled | | Backend → Scanner | `GET /scanner/status` | Chain lag + pending counts (ops/monitoring) | | Backend → Scanner | `POST /admin/webhooks/retry` | Force re-delivery of `webhook_failed` intents | @@ -171,7 +174,8 @@ pending ──(tx seen)──► confirming ──(enough blocks)──► confi | Item | Detail | |---|---| -| TON O(n) API calls | Per-intent polling — one TonCenter call per pending TON intent per scan cycle. Fine at low volume; needs batching for scale. | +| TON O(n) API calls | Per-intent polling — one TonCenter v3 call per pending TON intent per scan cycle. Fine at low volume; needs batching for scale. | | Direct balance reads: EVM only | `POST /balances/check` and `POST /balance-watches` only support EVM ERC-20 (`eth_call` balanceOf). Tron/TON balance reads are future scope. | -| Arbitrum / Polygon / Base / Tron / TON disabled | `verified: false` in `supported-chains.json`. Enable via `SCANNER_ENABLED_CHAINS` env var without a code change. | -| Ethereum proxy version | Chain 1 uses the v0.1.0 proxy (`0x370DE...`). A v0.2.0 proxy is also deployed but checkout still uses the v0.1.0 ABI. | +| Arbitrum / Polygon / Base / Tron / TON disabled | `"verified": false` in `supported-chains.json`. Enable via `SCANNER_ENABLED_CHAINS` env var without a code change or rebuild. | +| Ethereum proxy version | Chain 1 uses the v0.1.0 proxy (`0x370DE...`). A v0.2.0 proxy is also deployed on ETH but checkout still uses the v0.1.0 ABI. Upgrading requires a coordinated frontend change. | +| BSC Testnet tokens | Test USDT on BSC Testnet: `0x109F54Dab34426D5477986b0460aE5dFBA65f022` (has public `mint()`). Faucet: `testnet.bnbchain.org/faucet-smart`. | diff --git a/02 - Data Models/Tenant.md b/02 - Data Models/Tenant.md new file mode 100644 index 0000000..8a01909 --- /dev/null +++ b/02 - Data Models/Tenant.md @@ -0,0 +1,289 @@ +--- +title: Tenant +tags: [data-model, postgres, drizzle, white-label, multi-tenant] +aliases: [TenantRecord, Merchant Tenant, White-Label Shop] +--- + +# Tenant + +> **Last updated:** 2026-06-10 — current `feature/white-label-shops` scan. + +Six Drizzle/PostgreSQL tables that form the multi-tenant layer of the Amanat marketplace operating system. Introduced by [[PRD - Seller-Owned White-Label Shops and Bots]]. + +> [!note] Source +> All six tables live in a single file: `backend/src/db/schema/tenant.ts` +> Repositories: `backend/src/db/repositories/drizzle/DrizzleTenant*.ts` +> Services: `backend/src/services/tenant/` + +--- + +## Table overview + +| Table | Purpose | Isolation key | +| --- | --- | --- | +| `tenants` | Top-level tenant entity (one per merchant) | `id` (PK) | +| `tenant_domains` | Custom / managed hostnames | `tenant_id` FK, unique on `hostname` | +| `tenant_bots` | Telegram bot token registrations (encrypted) | `tenant_id` FK, unique on `telegram_bot_id` | +| `tenant_integrations` | Catalog / delivery / payment adapter configs | `tenant_id` FK | +| `tenant_payment_policies` | Per-tenant payment rail configuration | `tenant_id` FK, 1:1 | +| `tenant_user_roles` | User ↔ tenant role grants | composite unique `(tenant_id, user_id, role)` | + +--- + +## `tenants` + +| Column | Type | Constraints | Default | Description | +| --- | --- | --- | --- | --- | +| `id` | `uuid` | PK | `gen_random_uuid()` | Tenant identifier. | +| `owner_user_id` | `uuid` | NOT NULL, FK → `users.id` RESTRICT | — | Owner [[User]] (`pgId`). RESTRICT prevents silent orphan on user delete. | +| `slug` | `text` | NOT NULL, UNIQUE | — | URL-safe label `[a-z0-9-]{3,40}` used for `seller.amn.gg` and `/t/:slug`. | +| `type` | `tenantType` enum | NOT NULL | `hosted_seller` | Tenant tier. | +| `status` | `tenantStatus` enum | NOT NULL | `pending` | Lifecycle state. | +| `display_name` | `text` | NOT NULL | — | Human name for the shop. | +| `billing_account_id` | `text` | nullable | — | External billing reference (no FK in Phase 0/1). | +| `isolation_mode` | `tenantIsolationMode` enum | NOT NULL | `shared` | Data isolation level. | +| `shop_settings_id` | `uuid` | nullable, FK → `shop_settings.id` SET NULL | — | Link to existing [[ShopSettings]] row. | +| `brand` | `jsonb` | nullable | — | `{ name?, logoUrl?, primaryColor?, supportEmail? }` — drives bootstrap payload. | +| `features` | `jsonb` | nullable | — | `{ escrowCheckout?, directCheckout?, externalPayments?, telegramMiniApp? }` — overrides policy-derived flags. | +| `locale_defaults` | `text[]` | nullable | — | e.g. `['en', 'fa']`. | +| `legacy_object_id` | `text` | nullable | — | Convention parity field; tenants are PG-native. | +| `created_at` | `timestamptz` | NOT NULL | `now()` | — | +| `updated_at` | `timestamptz` | NOT NULL | `now()` | — | + +### Indexes + +| Name | Columns | Type | +| --- | --- | --- | +| `tenants_slug_uq` | `slug` | UNIQUE | +| `tenants_owner_user_id_idx` | `owner_user_id` | B-tree | +| `tenants_status_idx` | `status` | B-tree | + +### Enums + +| Enum | Values | +| --- | --- | +| `tenantType` | `hosted_seller`, `white_label`, `isolated`, `enterprise` | +| `tenantStatus` | `pending`, `active`, `suspended`, `closed` | +| `tenantIsolationMode` | `shared`, `schema`, `database`, `stack` | + +--- + +## `tenant_domains` + +| Column | Type | Constraints | Default | Description | +| --- | --- | --- | --- | --- | +| `id` | `uuid` | PK | `gen_random_uuid()` | — | +| `tenant_id` | `uuid` | NOT NULL, FK → `tenants.id` CASCADE | — | Owning tenant. | +| `hostname` | `text` | NOT NULL, UNIQUE | — | Full hostname e.g. `shop.example.com`. Globally unique — the resolution key. | +| `mode` | `tenantDomainMode` enum | NOT NULL | `cname` | How DNS is managed. | +| `status` | `tenantDomainStatus` enum | NOT NULL | `pending` | Domain lifecycle state. | +| `verification_token` | `text` | NOT NULL | — | Random hex token for TXT/CNAME proof. | +| `tls_status` | `tenantTlsStatus` enum | NOT NULL | `pending` | TLS certificate state. | +| `last_checked_at` | `timestamptz` | nullable | — | Last validation probe. | +| `created_at` | `timestamptz` | NOT NULL | `now()` | — | +| `updated_at` | `timestamptz` | NOT NULL | `now()` | — | + +### Enums + +| Enum | Values | +| --- | --- | +| `tenantDomainMode` | `managed_ns`, `cname` | +| `tenantDomainStatus` | `pending`, `active`, `degraded`, `suspended`, `removed` | +| `tenantTlsStatus` | `pending`, `issued`, `failed`, `expired` | + +> [!warning] Hostname uniqueness is the security boundary +> A single hostname MUST map to at most one tenant. The unique index `tenant_domains_hostname_uq` enforces this. Code in `tenantResolutionMiddleware` relies on `findByHostname` returning at most one row. + +--- + +## `tenant_bots` + +| Column | Type | Constraints | Default | Description | +| --- | --- | --- | --- | --- | +| `id` | `uuid` | PK | `gen_random_uuid()` | — | +| `tenant_id` | `uuid` | NOT NULL, FK → `tenants.id` CASCADE | — | Owning tenant. | +| `telegram_bot_id` | `text` | NOT NULL, UNIQUE | — | Numeric Telegram bot id stored as text (exceeds JS safe int). | +| `username` | `text` | NOT NULL | — | Bot @username. | +| `encrypted_token` | `text` | NOT NULL | — | AES-256-GCM ciphertext of the BotFather token. | +| `encrypted_token_iv` | `text` | NOT NULL | — | GCM IV (base64). | +| `encrypted_token_tag` | `text` | NOT NULL | — | GCM auth tag (base64). | +| `webhook_secret` | `text` | NOT NULL | — | Per-bot random hex webhook path secret used by `/api/telegram/tenant-webhook/:botId`. | +| `status` | `tenantBotStatus` enum | NOT NULL | `pending` | Bot lifecycle. | +| `mini_app_url` | `text` | nullable | — | Telegram Mini App URL when configured. | +| `claim_token` | `text` | nullable | — | One-time Telegram `/start ` deep-link token for the first admin claim. | +| `admin_telegram_user_id` | `text` | nullable | — | Telegram user id that claimed the bot admin role. | +| `last_webhook_at` | `timestamptz` | nullable | — | Last received webhook update. | +| `created_at` | `timestamptz` | NOT NULL | `now()` | — | +| `updated_at` | `timestamptz` | NOT NULL | `now()` | — | + +### Enums + +| Enum | Values | +| --- | --- | +| `tenantBotStatus` | `pending`, `active`, `suspended`, `revoked` | + +> [!warning] Token fields +> `encrypted_token`, `encrypted_token_iv`, and `encrypted_token_tag` are AES-256-GCM fields. The repository layer **never decrypts** them. Decryption belongs exclusively to `tenantBotService`. Never include these columns or `webhook_secret` in API responses. + +> [!note] Claim flow +> New bots start as `pending` with a `claim_token`. The public service response exposes only a derived `claimUrl` while the bot is pending. When Telegram sends `/start ` to `/api/telegram/tenant-webhook/:botId` with the correct Telegram webhook secret header, `tenantBotService.claimAdmin()` stores `admin_telegram_user_id` and flips the bot to `active`. + +--- + +## `tenant_integrations` + +| Column | Type | Constraints | Default | Description | +| --- | --- | --- | --- | --- | +| `id` | `uuid` | PK | `gen_random_uuid()` | — | +| `tenant_id` | `uuid` | NOT NULL, FK → `tenants.id` CASCADE | — | — | +| `kind` | `tenantIntegrationKind` enum | NOT NULL | — | Integration category. | +| `provider` | `text` | NOT NULL | — | Free-form provider slug e.g. `shopify`, `http_json`. | +| `status` | `tenantIntegrationStatus` enum | NOT NULL | `draft` | Integration lifecycle. | +| `config` | `jsonb` | nullable | — | Non-secret config blob. | +| `encrypted_config` | `text` | nullable | — | AES-GCM ciphertext for provider keys/secrets. | +| `encrypted_config_iv` | `text` | nullable | — | GCM IV. | +| `encrypted_config_tag` | `text` | nullable | — | GCM auth tag. | +| `last_sync_at` | `timestamptz` | nullable | — | — | +| `last_error` | `text` | nullable | — | Last sync error message. | +| `created_at` | `timestamptz` | NOT NULL | `now()` | — | +| `updated_at` | `timestamptz` | NOT NULL | `now()` | — | + +Unique index: `tenant_integrations_tenant_kind_provider_uq` on `(tenant_id, kind, provider)`. + +### Enums + +| Enum | Values | +| --- | --- | +| `tenantIntegrationKind` | `catalog`, `delivery`, `payment`, `accounting`, `notification` | +| `tenantIntegrationStatus` | `draft`, `active`, `error`, `disabled` | + +--- + +## `tenant_payment_policies` + +1:1 with `tenants` (enforced by unique index on `tenant_id`). Created automatically with `amn_escrow` defaults when a tenant is created. + +| Column | Type | Constraints | Default | Description | +| --- | --- | --- | --- | --- | +| `id` | `uuid` | PK | `gen_random_uuid()` | — | +| `tenant_id` | `uuid` | NOT NULL, FK → `tenants.id` CASCADE, UNIQUE | — | Owning tenant (1:1). | +| `allowed_rails` | `tenantPaymentRail[]` | NOT NULL | `ARRAY['amn_escrow']` | PG enum array of permitted payment rails. | +| `default_rail` | `tenantPaymentRail` | NOT NULL | `amn_escrow` | Rail used when buyer doesn't specify. CHECK: must be in `allowed_rails`. | +| `escrow_required_above_amount` | `numeric(38,18)` | nullable | — | Orders above this amount force `amn_escrow`. Matches `payments.amount` precision. | +| `escrow_required_for_categories` | `text[]` | nullable | — | Category slugs that always require escrow. | +| `buyer_disclosure_mode` | `tenantBuyerDisclosureMode` | NOT NULL | `strict` | How prominently the non-escrow notice is shown to buyers. | +| `created_at` | `timestamptz` | NOT NULL | `now()` | — | +| `updated_at` | `timestamptz` | NOT NULL | `now()` | — | + +### Enums + +| Enum | Values | +| --- | --- | +| `tenantPaymentRail` | `amn_escrow`, `amn_direct`, `external_provider`, `manual_invoice` | +| `tenantBuyerDisclosureMode` | `plain`, `strict` | + +> [!note] CHECK constraint +> `tenant_payment_policies_default_in_allowed_ck` enforces `default_rail = ANY(allowed_rails)` at the DB level. Route validation mirrors this at the application level. + +--- + +## `tenant_user_roles` + +| Column | Type | Constraints | Default | Description | +| --- | --- | --- | --- | --- | +| `id` | `uuid` | PK | `gen_random_uuid()` | — | +| `tenant_id` | `uuid` | NOT NULL, FK → `tenants.id` CASCADE | — | — | +| `user_id` | `uuid` | NOT NULL, FK → `users.id` CASCADE | — | `users.id` (Postgres UUID, i.e. `pgId`). | +| `role` | `tenantUserRole` enum | NOT NULL | — | Role within the tenant. | +| `created_at` | `timestamptz` | NOT NULL | `now()` | — | +| `updated_at` | `timestamptz` | NOT NULL | `now()` | — | + +Unique index: `tenant_user_roles_tenant_user_role_uq` on `(tenant_id, user_id, role)` — a user may hold each role at most once per tenant. + +### Enum + +| Enum | Values | +| --- | --- | +| `tenantUserRole` | `owner`, `manager`, `finance`, `support`, `developer` | + +--- + +## State transitions + +### Tenant status + +```mermaid +stateDiagram-v2 + [*] --> pending : createTenant() + pending --> active : operator activateTenant() + active --> suspended : operator suspendTenant() + suspended --> active : operator activateTenant() + active --> closed : operator (Phase 2+) + pending --> closed : operator rejects +``` + +### Domain status + +```mermaid +stateDiagram-v2 + [*] --> pending : POST /domains + pending --> active : DNS verified + Caddy route added + active --> active : TLS pending/issued + pending --> degraded : Caddy provisioning fails + degraded --> active : probe recovers + active --> suspended : DELETE /domains/:domainId + suspended --> removed : future cleanup +``` + +--- + +## Key relationships + +```mermaid +erDiagram + users ||--o{ tenants : "owns (ownerUserId)" + shop_settings ||--o| tenants : "linked (shopSettingsId)" + tenants ||--o{ tenant_domains : "has" + tenants ||--o{ tenant_bots : "has" + tenants ||--o{ tenant_integrations : "has" + tenants ||--|| tenant_payment_policies : "has (1:1)" + tenants ||--o{ tenant_user_roles : "grants" + users ||--o{ tenant_user_roles : "receives" +``` + +--- + +## Common queries + +```ts +// Resolve tenant from HTTP Host header (via service) +const result = await tenantService.resolveTenantByHost(req.hostname); +// result?.tenant or null + +// Find active domain +const domain = await getTenantDomainRepo().findByHostname('shop.example.com'); +// domain.status must be 'active' before trusting + +// Build bootstrap payload for public storefront +const payload = await tenantService.buildBootstrapPayload(tenant); + +// Check user roles in tenant +const roles = await getTenantUserRoleRepo().findRolesForUserInTenant(tenantId, userId); + +// Upsert payment policy (idempotent) +await getTenantPaymentPolicyRepo().upsertForTenant(tenantId, { allowedRails, defaultRail }); +``` + +--- + +## Migration + +Tables are PG-native — no Mongo backfill path. Run: + +```bash +cd backend && npx drizzle-kit generate +# review the generated SQL +npx drizzle-kit migrate +``` + +Related: [[PRD - Seller-Owned White-Label Shops and Bots]], [[ShopSettings]], [[User]], [[Payment]]. diff --git a/03 - API Reference/Tenant API.md b/03 - API Reference/Tenant API.md new file mode 100644 index 0000000..56dc959 --- /dev/null +++ b/03 - API Reference/Tenant API.md @@ -0,0 +1,459 @@ +--- +title: Tenant API +tags: [api, tenant, white-label, storefront, reference] +aliases: [White-Label API, Storefront API, Merchant API] +--- + +# Tenant API + +> **Last updated:** 2026-06-10 — current `feature/white-label-shops` scan. +> Related: [[Tenant]], [[PRD - Seller-Owned White-Label Shops and Bots]], [[Authentication API]] + +Three route groups: + +| Mount | File | Auth | Description | +| --- | --- | --- | --- | +| `/api/tenants` | `backend/src/routes/tenantRoutes.ts` | Required (JWT) | Admin + tenant-owner management surface. | +| `/api/storefront` | `backend/src/routes/storefrontRoutes.ts` | None (public) | Public storefront surface — tenant resolved from `Host` header. | +| `/api/telegram` | `backend/src/routes/tenantWebhookRoutes.ts` | Telegram webhook secret header | Tenant-bot webhook receiver for claim activation. | + +--- + +## Authentication and authorization + +All `/api/tenants/*` routes require `Authorization: Bearer ` via `authenticateToken`. + +Three authorization tiers: + +| Tier | How enforced | Who | +| --- | --- | --- | +| Platform admin | `authorizeRoles('admin')` middleware | Users with `role = 'admin'` | +| Tenant role | `requireTenantRole(...roles)` middleware | Users with a matching row in `tenant_user_roles` for the target tenant | +| Self (tenant creation) | Inline check | Any authenticated user (creates for themselves) | + +`requireTenantRole` looks up `tenant_user_roles` by `(tenantId, req.user.id)`. It passes if the user's role is in the allowed list **or** if the user is a platform admin. + +`/api/storefront/*` routes are fully public — no `authenticateToken`. Tenant identity comes from the `Host` header only, never from the request body. + +--- + +## Storefront routes — `GET /api/storefront/...` + +### `GET /api/storefront/bootstrap` + +Resolves the tenant from the `Host` header and returns the bootstrap payload for the frontend `TenantProvider`. + +**Auth:** None. +**Middleware:** `tenantResolutionMiddleware` → `storefrontRateLimiter` (120 req/min per tenant+IP). + +**Response 200:** +```json +{ + "success": true, + "data": { + "tenantId": "uuid", + "slug": "myshop", + "shopId": "uuid", + "brand": { + "name": "My Shop", + "logoUrl": "https://cdn.example.com/logo.png", + "primaryColor": "#1F6FEB", + "supportEmail": "support@example.com" + }, + "features": { + "escrowCheckout": true, + "directCheckout": false, + "externalPayments": false, + "telegramMiniApp": false + }, + "paymentRails": ["amn_escrow"], + "localeDefaults": ["en", "fa"] + } +} +``` + +**Response 404:** Host does not match any active tenant or domain. + +> [!note] Amanat default +> The frontend `TenantProvider` treats a 404 as "no tenant for this host" and falls back to Amanat platform defaults — this is not an error condition for the frontend. + +--- + +### `GET /api/storefront/t/:slug/bootstrap` + +Preview-only bootstrap by tenant slug. Allowed only when the request arrives from the platform base domain (`amn.gg`, `localhost`). Owner can preview a `pending` tenant this way. + +**Auth:** None. +**Restrictions:** Returns `403 PREVIEW_FORBIDDEN` if the `Host` is not the platform base domain. + +--- + +### `GET /api/storefront/catalog` *(Phase 2 stub)* +### `POST /api/storefront/checkout` *(Phase 2 stub)* +### `GET /api/storefront/orders/:orderId` *(Phase 2 stub)* + +All return `501 NOT_IMPLEMENTED`. Namespace reserved. + +--- + +## Tenant management routes — `/api/tenants/...` + +### `POST /api/tenants` — create tenant + +Any authenticated user may create a tenant for themselves (`ownerUserId = req.user.id`). Only platform admins may supply a different `ownerUserId`. + +Created tenants start with `status = 'pending'`. A platform admin must call `POST /activate` before the tenant becomes publicly accessible. + +**Auth:** `authenticateToken`. + +**Request body:** +```json +{ + "slug": "myshop", + "displayName": "My Shop", + "type": "hosted_seller", + "brand": { "name": "My Shop", "primaryColor": "#1F6FEB" }, + "features": { "escrowCheckout": true }, + "localeDefaults": ["en"] +} +``` + +| Field | Required | Description | +| --- | --- | --- | +| `slug` | yes | URL-safe identifier `[a-z0-9-]{3,40}`. Lowercased automatically. | +| `displayName` | yes | Human-readable shop name. | +| `type` | no | One of `hosted_seller`, `white_label`, `isolated`, `enterprise`. Default `hosted_seller`. | +| `brand` | no | `{ name?, logoUrl?, primaryColor?, supportEmail? }` | +| `features` | no | Feature flag overrides. | +| `localeDefaults` | no | Default `['en']`. | +| `ownerUserId` | no | Admin-only override. | + +**Response 201:** Created tenant record. + +**Response 409 `TENANT_SLUG_TAKEN`:** Slug already in use. + +Side effects on create: +1. Auto-grants `owner` role to the creating user (`tenant_user_roles`). +2. Seeds a default `tenant_payment_policies` row with `allowedRails: ['amn_escrow']`. + +--- + +### `GET /api/tenants` — list tenants + +Platform admins only. + +**Auth:** `authenticateToken` + `authorizeRoles('admin')`. + +**Query params:** + +| Param | Description | +| --- | --- | +| `status` | Filter by `tenantStatus` enum value. | +| `type` | Filter by `tenantType` enum value. | +| `page` | Page number (default 1). | +| `limit` | Page size (default 20). | + +**Response 200:** `{ data: { tenants: Tenant[], total: number } }` + +--- + +### `GET /api/tenants/:tenantId` — get tenant + +**Auth:** `authenticateToken` + any tenant role. + +**Response 200:** Full tenant record (no secrets). + +**Response 404 `TENANT_NOT_FOUND`** + +--- + +### `GET /api/tenants/:tenantId/bootstrap` — authenticated bootstrap + +Same payload shape as the storefront route, but requires authentication and a tenant role. Used by the merchant admin dashboard. + +**Auth:** `authenticateToken` + any tenant role. + +--- + +### `PATCH /api/tenants/:tenantId` — update tenant + +**Auth:** `authenticateToken` + tenant role `owner`. + +**Request body** (all optional): +```json +{ + "displayName": "Updated Shop Name", + "brand": { "primaryColor": "#FF6B35" }, + "features": { "telegramMiniApp": true }, + "localeDefaults": ["en", "fa"] +} +``` + +**Response 200:** Updated tenant record. + +--- + +### `POST /api/tenants/:tenantId/suspend` — suspend tenant + +Platform admins only. Sets `status = 'suspended'`. + +**Auth:** `authenticateToken` + `authorizeRoles('admin')`. + +--- + +### `POST /api/tenants/:tenantId/activate` — activate tenant + +Platform admins only. Sets `status = 'active'`. A new tenant must be activated before it is publicly accessible. + +**Auth:** `authenticateToken` + `authorizeRoles('admin')`. + +--- + +### `POST /api/tenants/:tenantId/domains` — add custom domain + +**Auth:** `authenticateToken` + tenant role `owner`. + +**Request body:** +```json +{ + "hostname": "shop.example.com", + "mode": "cname" +} +``` + +| Field | Required | Description | +| --- | --- | --- | +| `hostname` | yes | Full hostname to register. Must be globally unique. | +| `mode` | no | `cname` (default) or `managed_ns`. | + +**Response 201:** Domain record including `verificationToken` (the merchant uses this for DNS proof). + +**Response 400:** `hostname` missing. + +Domain starts with `status = 'pending'`. The tenant admin can trigger verification manually, and the backend domain poller retries pending domains on an interval. DNS can point either directly at the configured server IP or by CNAME to the configured Caddy target. + +--- + +### `POST /api/tenants/:tenantId/domains/:domainId/verify` — verify DNS and provision route + +Checks whether the hostname resolves to the multi-stack ingress. If DNS passes, the backend adds an idempotent Caddy Admin API route for the hostname and marks the domain `active` with `tlsStatus = 'pending'`. + +**Auth:** `authenticateToken` + tenant role `owner` or `developer`. + +**Response 200:** +```json +{ + "success": true, + "data": { "...": "updated domain" }, + "meta": { "dnsVerified": true }, + "statusCode": 200 +} +``` + +If DNS is not ready yet, the response still succeeds with `dnsVerified: false` and the domain remains `pending`. + +--- + +### `POST /api/tenants/:tenantId/domains/:domainId/tls-check` — check TLS status + +Probes HTTPS for an active domain and updates `tlsStatus` to `issued`, `pending`, or `failed`. + +**Auth:** `authenticateToken` + tenant role `owner` or `developer`. + +**Response 400 `DOMAIN_NOT_ACTIVE`:** Domain must be `active` before TLS can be checked. + +--- + +### `DELETE /api/tenants/:tenantId/domains/:domainId` — remove domain + +Deprovisions the Caddy route and marks the domain `suspended` with `tlsStatus = 'expired'`. + +**Auth:** `authenticateToken` + tenant role `owner`. + +**Response 200:** `{ "data": { "removed": true } }` + +--- + +### `GET /api/tenants/:tenantId/domains` — list domains + +**Auth:** `authenticateToken` + any tenant role. + +**Response 200:** Array of `TenantDomain` records. + +--- + +### `POST /api/tenants/:tenantId/telegram/bot` — register Telegram bot + +Stores the encrypted bot token, derives `telegramBotId` from the token prefix, resolves the username via Telegram `getMe` when not supplied, registers the tenant webhook when `APP_URL` or `FRONTEND_URL` is configured, and attempts to set the bot chat menu button to the tenant Mini App URL. + +**Auth:** `authenticateToken` + tenant role `owner` or `developer`. + +**Request body:** +```json +{ + "botToken": "123456789:AAABB...", + "username": "MyShopBot", + "miniAppUrl": "https://myshop.amn.gg" +} +``` + +| Field | Required | Description | +| --- | --- | --- | +| `botToken` | yes | BotFather token. Must use `:` format. Stored AES-256-GCM encrypted — never returned in responses. | +| `username` | no | Bot @username without `@`. If omitted, backend calls Telegram `getMe` and stores the returned username when available. | +| `miniAppUrl` | no | Tenant storefront base URL. If omitted, backend derives `https://.`. The menu button opens `/telegram/`. | + +**Response 201:** Public bot record (no encrypted token fields and no webhook secret). Pending bots include `claimUrl`. + +> [!warning] Token handling +> `botToken` in the request body is write-only. The API never returns it. Keep it out of logs. + +--- + +### `GET /api/tenants/:tenantId/telegram/bot/:botId/claim-link` — get bot claim URL + +Returns the pending bot's Telegram deep link: + +```json +{ "success": true, "data": { "claimUrl": "https://t.me/MyShopBot?start=..." } } +``` + +**Auth:** `authenticateToken` + tenant role `owner` or `developer`. + +--- + +### `DELETE /api/tenants/:tenantId/telegram/bot/:botId` — remove bot + +Physically deletes the bot row after verifying it belongs to the tenant. + +**Auth:** `authenticateToken` + tenant role `owner` or `developer`. + +**Response 200:** `{ "data": { "removed": true } }` + +--- + +### `POST /api/telegram/tenant-webhook/:botId` — tenant Telegram webhook + +Unauthenticated public endpoint mounted before global auth/rate-limit middleware. Telegram must send `X-Telegram-Bot-Api-Secret-Token`; the route compares it to the stored `webhookSecret`. + +Current handled update: + +| Update | Behavior | +| --- | --- | +| `/start ` on a pending bot | Calls `tenantBotService.claimAdmin()`, stores `adminTelegramUserId`, flips bot status to `active`, and sends a confirmation message to the claimant. | +| Any other valid update | Acknowledged with `200 { ok: true }` and ignored for now. | + +--- + +### `GET /api/tenants/:tenantId/telegram/bots` — list bots + +**Auth:** `authenticateToken` + any tenant role. + +**Response 200:** Array of public bot records. Secret fields are excluded. Each pending bot may include `claimUrl`. + +| Public bot field | Description | +| --- | --- | +| `id` | Internal bot row id. | +| `tenantId` | Owning tenant id. | +| `telegramBotId` | Numeric Telegram bot id as text. | +| `username` | Bot username without `@`. | +| `status` | `pending`, `active`, `suspended`, or `revoked`. | +| `miniAppUrl` | Stored Mini App base URL, if supplied. | +| `claimUrl` | Derived Telegram deep link while pending; `null` after claim. | +| `adminTelegramUserId` | Telegram user id that claimed admin control, if any. | + +> [!warning] Bot token handling +> `encryptedToken`, `encryptedTokenIv`, `encryptedTokenTag`, and `webhookSecret` never appear in route responses. + +--- + +### `PUT /api/tenants/:tenantId/payment-policy` — upsert payment policy + +Idempotent — creates or replaces the single policy row for the tenant. + +**Auth:** `authenticateToken` + tenant role `owner` or `finance`. + +**Request body:** +```json +{ + "allowedRails": ["amn_escrow", "amn_direct"], + "defaultRail": "amn_escrow", + "buyerDisclosureMode": "strict", + "escrowRequiredAboveAmount": "500.000000000000000000", + "escrowRequiredForCategories": ["digital-goods"] +} +``` + +`defaultRail` must be a member of `allowedRails` — returns `400 VALIDATION_ERROR` if not. + +**Response 200:** Policy record. + +--- + +### `GET /api/tenants/:tenantId/payment-policy` — get payment policy + +**Auth:** `authenticateToken` + any tenant role. + +**Response 200:** Policy record or `null` if none exists yet. + +--- + +### `POST /api/tenants/:tenantId/roles` — grant role + +**Auth:** `authenticateToken` + tenant role `owner`. + +**Request body:** +```json +{ "userId": "uuid", "role": "manager" } +``` + +**Response 201:** Role grant record. + +--- + +### `DELETE /api/tenants/:tenantId/roles` — revoke role + +**Auth:** `authenticateToken` + tenant role `owner`. + +**Request body:** +```json +{ "userId": "uuid", "role": "manager" } +``` + +**Response 200:** `{ "data": { "removed": true } }` + +--- + +## Error codes + +| Code | HTTP | Meaning | +| --- | --- | --- | +| `TENANT_SLUG_TAKEN` | 409 | Slug already registered. | +| `TENANT_SLUG_INVALID` | 400 | Slug does not match `[a-z0-9-]{3,40}`. | +| `TENANT_NOT_FOUND` | 404 | No tenant with that id / slug / host. | +| `PREVIEW_FORBIDDEN` | 403 | Slug preview requested from a non-platform host. | +| `DOMAIN_NOT_FOUND` | 404 | Domain id does not exist or is not owned by the tenant. | +| `DOMAIN_NOT_ACTIVE` | 400 | TLS check requested before domain status is `active`. | +| `VALIDATION_ERROR` | 400 | Missing required field or invalid value (e.g. `defaultRail ∉ allowedRails`). | +| `RATE_LIMIT_EXCEEDED` | 429 | Storefront rate limiter: 120 req/min per tenant+IP. | + +--- + +## Tenant resolution middleware + +`tenantResolutionMiddleware` (`backend/src/shared/middleware/tenantResolution.ts`) runs on every storefront route. It attaches `req.tenant` and `req.tenantDomain` (when a custom domain matched). + +Resolution order: + +1. **Preview** — Host is platform base (`amn.gg`, `localhost`) **and** `req.params.slug` or `req.query.t` is present → `resolveTenantBySlug(slug, { previewOnly: true })`. +2. **Subdomain** — Host ends with `.amn.gg` (single label only, e.g. `seller.amn.gg`) → `resolveTenantByHost` → slug lookup. +3. **Custom domain** — Any other host → `resolveTenantByHost` → `findByHostname`. + +On no match, `req.tenant` is `undefined` and the route handler returns 404. + +> [!important] Security invariants +> - Never reads `X-Tenant-ID` or any client-supplied header. +> - Only resolves preview by slug when the `Host` is the platform base. +> - Fail-open: resolution errors call `next()` without crashing the request. + +Related: [[Tenant]], [[Authentication API]], [[PRD - Seller-Owned White-Label Shops and Bots]]. diff --git a/04 - Flows/Telegram Mini App.md b/04 - Flows/Telegram Mini App.md index 1704c4c..5501941 100644 --- a/04 - Flows/Telegram Mini App.md +++ b/04 - Flows/Telegram Mini App.md @@ -6,8 +6,8 @@ related_apis: ["POST /api/auth/telegram", "[[Auth API]]"] task: "5.4" --- -> **Last updated:** 2026-06-08 -> **Status:** IN PROGRESS — Task 5.4 (dependencies: 5.1 auth infra, 5.2 Telegram sign-in endpoint) +> **Last updated:** 2026-06-12 +> **Status:** LARGELY COMPLETE — Task 5.4 core implementation done; open items are `startapp` deep-link auto-routing, backend Socket.IO room scoping, archived-chat surfacing, review-prompt integration, and cross-platform QA. > **Frontend branch:** `integrate-main-into-development` · v2.8.94+ > **Entry point:** `src/sections/telegram/` · route `/telegram` @@ -68,15 +68,26 @@ The shell is a **single-page, no-router** design: all navigation (tabs, overlays ## 2. Launch Points -| Entry | Mechanism | `startapp` context | -|---|---|---| -| Bot profile | User opens bot → taps "Open App" | none | -| Menu button | Pinned button in any chat with the bot | none | -| Inline button | Bot sends a card with an embedded button | `req_` | -| Direct deep link | `https://t.me/AmanehBot/app?startapp=req_` | `req_` | -| Web fallback | Browser at `/telegram` | none (unsupported state) | +| Entry | Mechanism | `startapp` context | Result | +|---|---|---|---| +| Bot profile | User opens bot → taps "Open App" | none | Shell loads at Home tab | +| Menu button | Pinned button in any chat with the bot | none | Shell loads at Home tab | +| Inline button | Bot sends a message card with an embedded button | `req_` | Shell loads; request deep-link (see below) | +| Direct deep link | `https://t.me/AmanehBot/app?startapp=req_` | `req_` | Shell loads; request deep-link (see below) | +| Web fallback | Browser at `/telegram` (no Telegram SDK) | none | `TelegramUnsupportedState` — "Open in Telegram" prompt + web dashboard link | -`startapp` / `tgWebAppStartParam` is read from either the WebApp SDK (`window.Telegram.WebApp`) or from URL query/hash params (for older Telegram clients that append them directly). +### 2.1 startapp Context Parsing + +`startapp` / `tgWebAppStartParam` is read from two sources in priority order: + +1. `window.Telegram.WebApp.initDataUnsafe.start_param` — primary source when the SDK is injected. +2. URL query/hash params (`tgWebAppStartParam`) — fallback for older Telegram clients that append params directly to the URL. + +Both are normalised into `context.startParam` by `getTelegramContext()` in `src/utils/telegram-webapp.ts`. + +### 2.2 startapp Deep-Link Routing (Partial) + +When `context.startParam` matches `req_`, the intent is to auto-open `TelegramRequestDetailView` for that request on first render. **This routing is not yet wired** — `startParam` is parsed and available in context but the shell does not yet act on it. This is open item #1 in section 16. --- @@ -172,9 +183,18 @@ When `initData` is absent (accessed via a path that skips Telegram context), onl ### 6.3 Backend Endpoint -`POST /api/auth/telegram` — expects `{ initData: string }`. Backend verifies the HMAC using the Telegram bot token, extracts `user` from the payload, upserts a `User` record (`telegramId`, `telegramVerified: true`), and issues a JWT + refresh token. Returns `{ token, refreshToken, isNewUser }`. +`POST /api/auth/telegram` — expects `{ initData: string }`. -Registered at `authRoutes.ts` line 24: `router.post("/telegram", ctrl.telegramAuth.bind(ctrl))` — public route, no auth middleware. +**Verification steps (backend):** +1. Parse the `initData` query string into key-value pairs. +2. Extract `hash` from the pairs; remove it from the set. +3. Build the data-check string: sort remaining pairs alphabetically, join as `key=value\n`. +4. Compute `HMAC-SHA256(data_check_string, HMAC-SHA256("WebAppData", TELEGRAM_BOT_TOKEN))`. +5. Compare computed hash with the extracted `hash` — reject with 401 on mismatch. +6. Parse `user` JSON from `initDataUnsafe`; upsert `User` record with `telegramId`, `telegramVerified: true`. +7. Issue JWT + refresh token. Return `{ token, refreshToken, isNewUser }`. + +Registered at `authRoutes.ts` line 24: `router.post("/telegram", ctrl.telegramAuth.bind(ctrl))` — public route, no auth middleware required (HMAC is the authentication proof). ### 6.4 Session Linking (Telegram ↔ Amaneh Account) @@ -415,7 +435,16 @@ The shell has **five bottom tabs** rendered by `TelegramTabBar`: - Fetches user points via `use-telegram-points.ts`. - Shows points balance and transaction history. -### 9.16 Notifications Overlay +### 9.16 Dispute Surface + +The Mini App does not yet have a dedicated dispute-filing view. Dispute access is handled via two escape hatches: + +- **Request detail "View full details" link** (`openTelegramExternalLink`) — opens the web dashboard request detail page where dispute filing is available. +- **Support chat** — buyer or seller can reach a support agent from the Account tab or the Home tab quick-action cards; the support agent can escalate to a formal dispute. + +A native in-shell dispute flow (matching the web dashboard `DisputeView`) is planned but not yet implemented. This is a known gap for the Task 5.4 feature surface. + +### 9.17 Notifications Overlay - `TelegramNotificationsView` is rendered as `overlayScreen = 'notifications'`. - Fetches via `useTelegramNotifications` → `getNotifications(userId, 1, 50)` → `GET /api/notifications?userId=...&page=1&limit=50`. @@ -453,6 +482,8 @@ The shell has **five bottom tabs** rendered by `TelegramTabBar`: Cart operations (add/remove/quantity) are **pure localStorage** — no API calls until checkout. +Dispute endpoints (`POST /api/disputes`, `GET /api/disputes/:id`) are not yet called from the Mini App shell — dispute access is delegated to the web dashboard via `openTelegramExternalLink`. + --- ## 11. Bilingual Support (EN / FA) @@ -730,15 +761,19 @@ src/ | amanat-assist integration | Done | "Open Assist" CTA in Home + New Request; window.location hand-off with access_token | | Deep link `startapp` routing | Partial | `startParam` parsed; auto-navigation to specific request not yet wired | | Backend room-scoped Socket.IO | Partial | Global socket broadcast fixed client-side (v2.8.4); server-side room scoping is a follow-up | +| Dispute filing (in-shell) | Not started | Escape hatch via web dashboard link + support chat; native view planned | +| Review prompt integration | Partial | `TelegramReviewPrompt` component exists; trigger point (post-payment/delivery) not yet wired | +| Archived chats | Partial | `TelegramArchivedChatsView` exists; not yet surfaced in navigation | | Client matrix QA (iOS/Android/Desktop) | Pending | Needs cross-platform testing pass | ### Open Items -1. **`startapp` deep link routing:** if `context.startParam` matches `req_`, auto-open `TelegramRequestDetailView` on first render. -2. **Backend room-scoped Socket.IO:** server-side scoping for real-time chat updates (follow-up from client-side fix in v2.8.4). -3. **Client matrix QA:** iOS Telegram, Android Telegram, Telegram Desktop, and web clients all need a full feature pass. -4. **Review prompt:** `TelegramReviewPrompt` component exists but integration point (post-payment / post-delivery) is TBD. -5. **Archived chats:** `TelegramArchivedChatsView` exists but is not yet surfaced in the navigation. +1. **`startapp` deep link routing:** if `context.startParam` matches `req_`, auto-open `TelegramRequestDetailView` on first render. `startParam` is already parsed and available in context; the shell needs a one-time effect on mount to act on it. +2. **Backend room-scoped Socket.IO:** server-side scoping for real-time chat and notification events (follow-up from client-side provider gate in v2.8.4 that fixed global cart-wipe). +3. **In-shell dispute filing:** add `TelegramDisputeView` matching the web dashboard dispute surface; currently only accessible via `openTelegramExternalLink` escape hatch. +4. **Review prompt:** wire `TelegramReviewPrompt` to trigger after payment confirmation or delivery acknowledgement. +5. **Archived chats:** surface `TelegramArchivedChatsView` from `TelegramChatView` (e.g. an "Archived" row at the bottom of the conversation list). +6. **Client matrix QA:** iOS Telegram, Android Telegram, Telegram Desktop, and web clients all need a full feature pass with particular attention to safe-area insets and BackButton behaviour on each platform. --- diff --git a/04 - Flows/Tenant Storefront Flow.md b/04 - Flows/Tenant Storefront Flow.md new file mode 100644 index 0000000..05923c3 --- /dev/null +++ b/04 - Flows/Tenant Storefront Flow.md @@ -0,0 +1,215 @@ +--- +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]]. diff --git a/09 - Audits/Activity Log.md b/09 - Audits/Activity Log.md index 9747fcb..52b518a 100644 --- a/09 - Audits/Activity Log.md +++ b/09 - Audits/Activity Log.md @@ -2030,4 +2030,12 @@ Added `10 - Services/README.md` index. All docs now reflect current codebase sta - `fix(tables/audit): remove hardcoded rgba(194,65,12,0.04) hover colors` — replaced with MUI built-in hover across admin tables, points leaderboard, and blog; violates AGENTS.md rule (no inline hex/rgba colors). - `fix(payment-list): remove fontFamily:var(--amn-sans) from header cells` — hardcoded fontFamily violation removed; inherits theme font correctly. +### 2026-06-08 — nick-doc sync — added sub-project service docs and updated core docs + +Added 4 new service docs to `10 - Services/`: backend, frontend, scanner, deployment. +Updated amanat-assist.md to latest version. Updated Telegram Mini App flow doc and Scanner Architecture doc. +Added `10 - Services/README.md` index. All docs now reflect current codebase state as of 2026-06-08. + +--- + diff --git a/09 - Audits/Audit Index - 2026-05-24.md b/09 - Audits/Audit Index - 2026-05-24.md index bd4357e..bf631b8 100644 --- a/09 - Audits/Audit Index - 2026-05-24.md +++ b/09 - Audits/Audit Index - 2026-05-24.md @@ -14,6 +14,8 @@ Full-system audit triggered by completion of Telegram first-class auth, Request | [[Security Audit - 2026-05-24]] | 6 critical · 5 high · 7 medium · 4 low | | [[Logic Audit - 2026-05-24]] | 4 critical · 5 high · 7 medium · 2 low | | [[Performance Audit - 2026-05-24]] | 6 high · 8 medium · 4 low | +| [[Multi-Shop Branch Project Scan - 2026-06-10]] | Full nested-repo scan plus `feature/white-label-shops` documentation sync | +| [[Comprehensive Workspace Audit - 2026-06-10]] | Full all-repo security, frontend/backend, deployment, scanner, assist, dependency, and quality audit | --- diff --git a/09 - Audits/Multi-Shop Branch Project Scan - 2026-06-10.md b/09 - Audits/Multi-Shop Branch Project Scan - 2026-06-10.md new file mode 100644 index 0000000..d4771a3 --- /dev/null +++ b/09 - Audits/Multi-Shop Branch Project Scan - 2026-06-10.md @@ -0,0 +1,64 @@ +--- +title: Multi-Shop Branch Project Scan - 2026-06-10 +tags: [audit, repo-scan, multi-shop, white-label, documentation-sync] +created: 2026-06-10 +--- + +# Multi-Shop Branch Project Scan - 2026-06-10 + +> Scope: full workspace scan of nested Git repositories under `/Users/manwe/CascadeProjects/escrow`, with special focus on `frontend/` and `backend/` `feature/white-label-shops`. + +## Repository snapshot + +| Repo | Branch | Head | Status summary | Notes | +| --- | --- | --- | --- | --- | +| `frontend/` | `feature/white-label-shops` | `df679a4` | Ahead of `forgejo/feature/white-label-shops` by 43 commits; dirty worktree | Version `2.11.49`. Multi-shop frontend, admin tenants UI, `WEBAPP_ENABLED` gate, many untracked E2E specs/report artifacts. | +| `backend/` | `feature/white-label-shops` | `ce06f47` | Ahead of `forgejo/feature/white-label-shops` by 35 commits; clean | Version `2.11.49`. Tenant services, storefront routes, tenant bot webhook, custom-domain/Caddy provisioning. | +| `deployment/` | `main` | `08fca31` | Ahead of `origin/main` by 2 commits; dirty worktree | Adds `escrow-multi` stack for `multi.amn.gg`; `escrow-multi/docker-compose.yml` modified; `dev-amn/` untracked. | +| `scanner/` | `development` | `1911c3a` | Ahead of `origin/development` by 8 commits; clean | Version `0.1.10`. Recent BSC Testnet/tUSDT alignment. | +| `amanat-assist/` | `main` | `821601a` | Dirty worktree | Version `1.1.0`. Recent Telegram theme/auth/review UX work; local `docker-compose.yml` modified and `nginx.conf` untracked. | +| `nick-doc/` | `main` | `6724422` | Dirty worktree | Existing tenant docs were untracked before this sync; `.obsidian/graph.json` already modified. | + +## Multi-shop branch summary + +The active multi-shop implementation is split across `frontend/`, `backend/`, and `deployment/`: + +- `backend/src/db/schema/tenant.ts` defines six PG-native tenant tables: `tenants`, `tenant_domains`, `tenant_bots`, `tenant_integrations`, `tenant_payment_policies`, and `tenant_user_roles`. +- `backend/src/routes/tenantRoutes.ts` exposes tenant CRUD, activation/suspension, domains, bot registration/deletion/claim links, payment policies, and tenant roles. +- `backend/src/routes/storefrontRoutes.ts` exposes public tenant bootstrap and reserved catalog/checkout/order stubs. +- `backend/src/routes/tenantWebhookRoutes.ts` handles tenant Telegram bot webhooks and `/start ` admin activation. +- `backend/src/services/tenant/domainProvisioningService.ts` verifies DNS, provisions Caddy routes, checks TLS, syncs active routes at startup, and runs a polling loop. +- `frontend/src/contexts/TenantContext.tsx` fetches `/api/storefront/bootstrap` and falls back to Amanat defaults on expected tenant misses. +- `frontend/src/app/dashboard/admin/tenants` and `frontend/src/sections/admin/tenants` provide tenant list/detail UI, DNS/TLS controls, bot activation links, payment policy editing, and member role controls. +- `deployment/escrow-multi/docker-compose.yml` defines the isolated `escrow-multi` stack with `:multi` frontend/backend images, one-shot migrations, isolated Postgres/Redis, and `shared-web` ingress. + +## Documentation updated in this sync + +| Doc | Update | +| --- | --- | +| [[System Overview]] | Reframed the platform as a multi-repo workspace and added the active multi-shop branch role. | +| [[10 - Services/README]] | Added tenant/white-label service row and `multi.amn.gg` routing. | +| [[frontend]] | Updated version/status/remote and noted tenant admin UI plus `WEBAPP_ENABLED`. | +| [[backend]] | Updated version/status and added tenant/storefront/tenant-webhook route groups. | +| [[deployment]] | Added `escrow-multi` stack details and branch isolation warning. | +| [[Tenant]] | Added bot claim fields and current domain lifecycle. | +| [[Tenant API]] | Added domain verify/TLS/delete routes, bot claim/delete/webhook routes, and current request/response behavior. | +| [[Tenant Storefront Flow]] | Updated domain provisioning and Telegram bot claim sequences. | +| [[tenant]] | Added Caddy/domain services, tenant webhook route, current env vars, and frontend/backend member-route mismatch. | + +## Open findings + +| Priority | Finding | Evidence | Suggested next step | +| --- | --- | --- | --- | +| P1 | Tenant member UI and backend route names do not match. | Frontend Members tab calls `/tenants/:tenantId/members` and `/tenants/:tenantId/members/:memberId`; backend exposes `POST /tenants/:tenantId/roles` and `DELETE /tenants/:tenantId/roles`. | Align frontend hooks/UI to backend routes or add backend member aliases before relying on tenant member management. | +| P2 | `useTenantDomains().addDomain()` sends `mode: "primary"` when `isPrimary` is true, but backend/domain enum accepts `cname` or `managed_ns`. | `frontend/src/hooks/use-tenants.ts` maps `isPrimary` to `"primary"`; `tenantDomainMode` enum is `managed_ns`, `cname`. | Remove `isPrimary` mapping or introduce a separate primary-domain model. | +| P2 | Tenant API docs and code now show bot webhook auto-registration, but production readiness depends on correct public `APP_URL`/`FRONTEND_URL`, Telegram secret header delivery, and tenant bot notification routing. | `tenantBotService.registerBot()` fire-and-forgets `setWebhook`; non-claim updates are currently acknowledged and ignored. | Add smoke tests for bot claim and document how tenant seller notifications will route after claim. | +| P3 | The docs vault now reflects Postgres/Drizzle as current runtime, but older pages still contain Mongo-era language. | `System Overview` was corrected; deeper flow/data pages may still mention legacy Mongo models. | Run a later doc-audit pass focused on Mongo/Mongoose references after code migration status is final. | + +## Guardrails confirmed + +- No frontend/backend code changes were made in this documentation sync, so no version bump is required. +- Do not touch the `escrow-dev` / `dev-amn` stack while working on `feature/white-label-shops`; target only `escrow-multi`. +- Do not print or copy `.env` contents, BotFather tokens, private keys, database credentials, or Woodpecker agent tokens into docs or chat. + +Related: [[Tenant]], [[Tenant API]], [[Tenant Storefront Flow]], [[tenant]], [[deployment]], [[PRD - Seller-Owned White-Label Shops and Bots]]. diff --git a/10 - Services/README.md b/10 - Services/README.md index 487c741..d18a0cd 100644 --- a/10 - Services/README.md +++ b/10 - Services/README.md @@ -1,20 +1,18 @@ # 10 - Services -This section documents each deployable service (sub-project) in the Amanat/Escrow platform. Each article covers the service's purpose, configuration, build process, and operational notes. - -See also: [[01 - Architecture]] · [[08 - Operations]] · [[03 - API Reference]] +This section documents every deployable service (sub-project) in the Amanat / Escrow platform. Each page covers the service's purpose, tech stack, configuration, and operational notes. --- -## Service Inventory +## Service Directory | Service | Language / Framework | Status | URL | Doc | |---|---|---|---|---| -| Backend | Node.js / TypeScript (Express) | Live | `api.dev.amn.gg` | [[backend]] | -| Frontend | Next.js / React / TypeScript | Live | `dev.amn.gg` | [[frontend]] | +| Backend API | Node.js / TypeScript (Express) | Live | `dev.amn.gg/api`, `multi.amn.gg/api` | [[backend]] | +| Frontend | Next.js 14 / React / TypeScript | Live | `dev.amn.gg`, `multi.amn.gg` | [[frontend]] | | Scanner | Go | Live | internal | [[scanner]] | -| Amanat Assist | Node.js / TypeScript + LLM proxy | Live | `assist.dev.amn.gg` | [[amanat-assist]] | -| Deployment | Docker Compose + Caddy + Watchtower | Live | — | [[deployment]] | +| Amanat Assist | Node.js / TypeScript + Telegram Bot API | Live | `assist.dev.amn.gg` | [[amanat-assist]] | +| Deployment | Docker Compose + Caddy + Watchtower | Live | `arcane.tbs.amn.gg` | [[deployment]] | --- @@ -24,30 +22,34 @@ See also: [[01 - Architecture]] · [[08 - Operations]] · [[03 - API Reference]] Browser / Telegram Mini App │ ▼ - infra-caddy (reverse proxy, TLS) - ├── dev.amn.gg → [[frontend]] (Next.js SSR) - ├── api.dev.amn.gg → [[backend]] (Express REST + WebSocket) - └── assist.dev.amn.gg → [[amanat-assist]] (LLM proxy / Telegram bot) + infra-caddy (reverse proxy, TLS, ports 80/443) + ├── dev.amn.gg / multi.amn.gg → [[frontend]] (Next.js SSR) + ├── */api → [[backend]] (Express REST + WebSocket) + └── assist.dev.amn.gg → [[amanat-assist]] (LLM proxy + Telegram mini-app) [[backend]] - ├── MongoDB / PostgreSQL (dual-write seam, PG cutover in progress) - ├── Redis (sessions, rate-limit, pub-sub) + ├── PostgreSQL (Drizzle ORM — primary store) + ├── MongoDB (legacy read path, being retired) + ├── Redis (sessions, rate-limit, pub-sub) └── emits payment events │ ▼ [[scanner]] (Go — watches EVM chains for on-chain payments) - │ webhook callback - └──────────────────▶ [[backend]] /api/payment/callback + │ HTTP webhook on confirmation + └──────────────▶ [[backend]] POST /api/payment/callback ``` -- All containers share the `shared-web` Docker network managed by [[deployment]]. -- [[amanat-assist]] is a separate Telegram Mini App; it calls [[backend]] APIs on behalf of users. -- [[scanner]] is stateless; it probes RPC endpoints and forwards confirmations to the backend. +Integration points: +- **[[frontend]] → [[backend]]**: REST `/api/*` and WebSocket via infra-caddy +- **[[scanner]] → [[backend]]**: webhook POST on each confirmed on-chain payment +- **[[amanat-assist]] → [[backend]]**: reads offers/requests; sends AI-generated replies via Telegram Bot API +- **[[backend]] → Telegram**: step notifications for buyer/seller workflow +- All services run as Docker containers in Arcane-managed projects on `89.58.32.32`; see [[deployment]] for compose files, env vars, and the `shared-web` network. --- ## Related Sections - [[01 - Architecture]] — system-wide design decisions, data model, and sequence diagrams -- [[03 - API Reference]] — REST endpoints, WebSocket events, auth headers -- [[08 - Operations]] — deployment runbooks, monitoring, secrets management +- [[03 - API Reference]] — full REST endpoint and WebSocket event reference +- [[08 - Operations]] — runbooks, monitoring, secrets management, backup diff --git a/10 - Services/amanat-assist.md b/10 - Services/amanat-assist.md index f546a18..526f65d 100644 --- a/10 - Services/amanat-assist.md +++ b/10 - Services/amanat-assist.md @@ -1,6 +1,6 @@ # amanat-assist — AI Request Assistant -**Status:** Live at `assist.amn.gg` (v1.1.0) +**Status:** Live at `assist.amn.gg` (v1.1.1) **Repo:** `/amanat-assist` (separate repo, no Amanat DB or internal-service access) **Owner:** Amanat Platform **PRD:** [PRD — AI Request Assistant Mini App](../PRD%20-%20AI%20Request%20Assistant%20Mini%20App.md) @@ -252,8 +252,8 @@ steps: 2. deploy (docker:27-cli, docker socket volume-mounted — no registry push): - Copy dist/ to /opt/amanat-assist/dist/ (nginx bind-mount) - Sync docker-compose.yml to /opt/amanat-assist/ - - Rebuild amanat-llm-proxy Docker image in-place (locally, never pushed) - - docker compose up -d (recreates llm-proxy container) + - Rebuild `amanat-assist-llm-proxy` Docker image in-place (locally, never pushed) + - docker compose up -d llm-proxy (recreates llm-proxy container only) 3. notify (node:22-alpine): - Runs scripts/ci/tg-notify.cjs on success or failure - Uses TG_TOKEN + TG_USERS secrets @@ -301,3 +301,6 @@ See `src/sections/assist/` in the frontend repo for the implementation. - **categoryId from vision disabled** — vision returns category names, not ObjectIds; name→ID matching is left to the LLM in the follow-up turn - **llm-proxy is zero-dependency** — `llm-proxy/index.mjs` uses only Node.js built-ins (`http`, native `fetch`); no npm packages. Logs rotate at 10 MB. - **No registry push** — CI builds the llm-proxy image directly on the host via a docker socket volume mount; `docker pull` will always fail (intentional — image is local-only) +- **Telegram theme override is --primary accent only** — applying full Telegram theme tokens causes invisible text on cream backgrounds; only the primary accent colour is overridden from the Telegram theme +- **iframe auth handoff** — when embedded via iframe, auth is delivered via `access_token` + `user_json` URL params; the app decodes the JWT client-side as a fallback when the backend `/api/auth/me` call is not possible +- **slotsRef stale-closure guard** — review submit uses a ref (`slotsRef`) instead of state directly to avoid a stale-closure bug that could cause the wrong slot values to be submitted diff --git a/10 - Services/backend.md b/10 - Services/backend.md index b27eabe..e4536f7 100644 --- a/10 - Services/backend.md +++ b/10 - Services/backend.md @@ -2,375 +2,293 @@ ## 1. Overview -**amn-backend** is the Express 5 / TypeScript API server powering the Amanat escrow marketplace (`dev.amn.gg`). It handles all buyer-seller escrow workflow logic, crypto payment processing across multiple chains and providers, real-time socket events, authentication, admin tooling, and the in-progress Mongo→PostgreSQL migration. +`amn-backend` is the Express 5 / TypeScript API server that powers the Amanat escrow marketplace. It is the single authoritative backend for the dev.amn.gg (escrow-dev) and multi.amn.gg (escrow-multi) stacks. | Field | Value | |---|---| -| Current version | **2.10.5** | -| Status | Active — production at `dev.amn.gg` | +| Current version | **2.11.43** | +| Status | Production — receiving active feature development | +| Runtime | Node ≥ 22 | +| Framework | Express 5 (TypeScript) | +| Primary DB | PostgreSQL via Drizzle ORM | +| Mongo status | **Removed** — Mongoose was fully stripped; PostgreSQL is the sole persistence layer | | Repo | `git@git.tbs.amn.gg:escrow/backend.git` | -| Runtime port | 8083 (production Docker), 8080 (dev Docker), 5001 (dev default) | -| Database | PostgreSQL (Drizzle ORM) — sole persistence layer as of v2.9.12 | -| Node version | 22 (`.nvmrc`) | - -PostgreSQL is the sole active database. MongoDB references remain in some env-var config for the dual-write seam during migration, but no Mongo-backed stores remain active in normal operation (all 11 repository domains use Drizzle repos). +| Dev stack host | `root@89.58.32.32` — Arcane project `escrow-dev` | --- ## 2. Tech Stack -| Layer | Technology | -|---|---| -| Framework | Express 5 (TypeScript) | -| Runtime | Node.js 22 | -| Primary DB | PostgreSQL via Drizzle ORM (`drizzle-orm ^0.45.2`, `pg ^8.21.0`) | -| Migrations | Drizzle Kit (`drizzle-kit ^0.31.1`) — 19 landed SQL migrations | -| Session / Cache | Redis (`ioredis`) with Socket.IO pub-sub adapter | -| Realtime | Socket.IO with Redis adapter (seller/buyer rooms) | -| Auth | JWT (`jsonwebtoken`), Google OAuth, WebAuthn passkeys (`@simplewebauthn/server`), Telegram Mini App initData | -| Crypto payments | Request Network, amn.scanner (in-house), DePay, SHKeeper | -| Rate limiting | In-memory (express-rate-limit) — Redis adapter planned | -| AI integration | OpenAI (listing descriptions, moderation) | -| Email | Nodemailer via Resend SMTP | -| Telegram | Bot webhook + Mini App session + identity linking | -| Security | Helmet, CORS, Cloudflare Turnstile CAPTCHA, HMAC webhook verification | -| Containerization | Docker (Dockerfile.prod, Dockerfile.dev) | -| CI/CD | Woodpecker CI (4 pipelines) | +| Layer | Technology | Notes | +|---|---|---| +| HTTP framework | Express 5 | Async error propagation built in | +| Language | TypeScript (strict) | tsc gate on every CI push | +| Runtime | Node ≥ 22 | Also used for CI typecheck step | +| Database | PostgreSQL 15 via Drizzle ORM (`drizzle-orm ^0.45.2`, `pg ^8.21.0`) | Single source of truth; 19+ migrations landed | +| Auth | JWT (access + refresh) + WebAuthn/Passkey + Google OAuth | `JWT_SECRET`, `REFRESH_TOKEN_EXPIRES_IN` | +| Session / Mini App | Telegram Mini App `initData` verification | `TELEGRAM_WEBAPP_URL` | +| Realtime | Socket.IO with Redis adapter (`@socket.io/redis-adapter`) | Room-scoped events | +| Cache / Pub-Sub | Redis | `REDIS_URI` | +| Rate limiting | express-rate-limit (in-memory; Redis adapter planned) | Auth 10/15 min, payment 30/15 min, AI 20/15 min, global 100/15 min | +| Security headers | Helmet | CSP, X-Frame-Options, etc. | +| File uploads | Multer | MIME validation, `UPLOAD_PATH` | +| Email | Nodemailer (SMTP) + Resend | `SMTP_*` / `RESEND_API_KEY` | +| Price oracle | Chainlink + OffchainFX | Depeg protection, `ORACLE_MAX_STALENESS_S` | +| AML | Chainalysis + OFAC SDN | `CHAINALYSIS_API_KEY`, `OFAC_SDN_URL` | +| AI | OpenAI | Descriptions, moderation | +| CI | Woodpecker CI | `.woodpecker/*.yml` | +| Process model | Node cluster | `CLUSTER_WORKERS` workers + master | --- ## 3. Directory Structure ``` -backend/src/ -├── app.ts # Express bootstrap, middleware chain, route registration, graceful shutdown -├── cluster.ts # Node.js cluster mode entry point (multi-core) -├── controllers/ # HTTP request handlers — thin layer, delegate to services -├── db/ # Drizzle/Postgres layer -│ ├── schema/ # Per-table Drizzle schema files + index.ts barrel -│ ├── migrations/ # 19 numbered SQL migration files (0000–0018) -│ └── repositories/ # Drizzle repos, factory.ts, backfill scripts, verify utilities -├── infrastructure/ -│ └── socket/ # Socket.IO server init, room helpers, emit wrappers -├── models/ # Removed — replaced by Drizzle schemas in db/schema/ -├── routes/ # Express Router definitions (mounted in app.ts) -│ ├── amnScannerWebhookRoutes.ts -│ ├── blogRoutes.ts -│ ├── disputeRoutes.ts -│ └── pointsRoutes.ts -├── scripts/ # CLI utilities (seed:users, seed:categories, backfill, etc.) -├── seeds/ # Seed data fixtures (Postgres-capable, store-aware, idempotent) -├── services/ # Feature domain services (self-contained per domain) -│ ├── address/ # Address management -│ ├── admin/ # Admin-only operations, AML config, break-glass, data cleanup -│ ├── ai/ # OpenAI integration (descriptions, moderation) -│ ├── auth/ # JWT, OAuth, Passkey, Telegram, password reset -│ ├── blockchain/ # Web3 read/verify helpers -│ ├── blog/ # Posts, categories, comments -│ ├── chat/ # Conversations, messages, attachments -│ ├── config/ # Runtime config service -│ ├── delivery/ # Delivery tracking -│ ├── dispute/ # Dispute lifecycle, evidence, mediator assignment -│ ├── email/ # Nodemailer transport + templates -│ ├── file/ # Multer uploads, MIME validation -│ ├── health/ # Health check endpoint logic -│ ├── marketplace/ # PurchaseRequest, SellerOffer, Template, Shop -│ ├── notification/ # Templates, delivery, mark-as-read -│ ├── payment/ # Payment orchestration + provider adapters + ledger -│ │ ├── adapters/ # Provider-neutral adapter interface + registry -│ │ ├── amnScanner/ # amn.scanner in-house pay-in detection -│ │ ├── ledger/ # Internal funds ledger (available / held / releasable) -│ │ ├── migration/ # Legacy data backfill utilities -│ │ ├── observability/ # Logging and incident controls -│ │ ├── orchestration/ # High-level payment flow coordination -│ │ ├── priceOracle/ # Chainlink + off-chain FX oracle, depeg protection -│ │ ├── reconciliation/ # Webhook + status reconciliation per provider -│ │ ├── request-network/# Request Network routes and webhook signature -│ │ ├── requestNetwork/ # Request Network service logic -│ │ ├── safety/ # Transaction Safety Provider + confirmation thresholds -│ │ ├── tokens/ # On-chain token registry / decimals lookup -│ │ └── wallets/ # Derived destination wallets + sweep orchestration -│ ├── points/ # Loyalty points, levels, redemption -│ ├── redis/ # Redis client, cache helpers, pub-sub -│ ├── telegram/ # Bot webhook, Mini App session, identity linking, notifications -│ ├── trezor/ # Trezor hardware-wallet signing for admin approvals -│ └── user/ # Profile, preferences, addresses -├── shared/ -│ ├── config/index.ts # Centralised typed env-var loader -│ ├── middleware/ # authMiddleware, errorHandler, roleGuard, validators -│ ├── types/ # Cross-cutting TypeScript types -│ └── utils/response-handler.ts # Standard success/error response envelope -└── utils/ # Pure utilities (logger, currencyUtils, etc.) +backend/ +├── src/ +│ ├── app.ts # Express bootstrap: middleware chain, route registration, server creation +│ ├── cluster.ts # Node cluster master — forks CLUSTER_WORKERS child processes +│ ├── controllers/ # Thin HTTP handlers that delegate to services +│ ├── db/ # Drizzle/Postgres layer +│ │ ├── schema/ # Per-table Drizzle schema files + index.ts barrel +│ │ ├── migrations/ # Numbered SQL migration files (0000–0018+) +│ │ └── repositories/ # DrizzleXxxRepo classes + factory.ts +│ ├── infrastructure/ +│ │ └── socket/ # Socket.IO server init, room helpers, emit wrappers +│ ├── models/ # Legacy placeholder (Mongoose removed; schemas now in db/schema/) +│ ├── routes/ # Standalone Express Router files (dispute, blog, points, amn-scanner webhook) +│ ├── scripts/ # CLI utilities — seed:users, seed:categories, tg-notify.cjs (CI) +│ ├── seeds/ # Fixture data for local dev (Postgres-capable, idempotent) +│ ├── services/ # Domain service modules (see §4) +│ ├── shared/ +│ │ ├── config/index.ts # Typed env-var loader — single import for all config +│ │ ├── middleware/ # authMiddleware, roleGuard, errorHandler, validators +│ │ ├── types/ # Cross-cutting TypeScript types and enums +│ │ └── utils/response-handler.ts # Standard success/error envelope +│ └── utils/ # Pure utility functions: logger, currencyUtils, etc. +├── .woodpecker/ # CI pipeline definitions (cleanup, development, manual, production) +├── Dockerfile.prod # Multi-stage production image +└── drizzle.config.ts # Drizzle Kit configuration ``` -Each service folder is self-contained: `Service.ts`, `Controller.ts`, `Routes.ts`, `Validation.ts`. This design allows future extraction to microservices with minimal coupling. - --- ## 4. Key Services / Modules -| Module | Description | +| Service path | Description | |---|---| -| `services/auth/` | JWT issuance/refresh, Google OAuth, WebAuthn passkeys, Telegram initData verification, password reset | -| `services/marketplace/` | Core escrow domain: PurchaseRequest, SellerOffer, Template, Shop lifecycle | -| `services/payment/` | Payment orchestration, provider adapters, internal ledger, reconciliation | -| `services/payment/ledger/` | Double-spend guard: tracks available / held / releasable balances per payment | -| `services/payment/wallets/` | Derived destination address derivation (xpub) + sweep orchestration | -| `services/payment/priceOracle/` | Chainlink + off-chain FX oracle for multi-currency pricing + stablecoin depeg protection | -| `services/payment/safety/` | Transaction Safety Provider: confirmation thresholds, tx hash and transfer match enforcement | -| `services/payment/amnScanner/` | In-house blockchain scanner webhook adapter (replaces Request Network for pay-in detection) | -| `services/payment/requestNetwork/` | Request Network pay-in routes, webhook signature verification, invoice creation | -| `infrastructure/socket/` | Socket.IO server init, buyer/seller room management, emit helpers | -| `services/redis/` | Redis client wrapper, pub-sub channel helpers, session cache | -| `services/chat/` | Conversations and message threading between buyer and seller | -| `services/dispute/` | Dispute lifecycle: open, evidence, mediator, resolution | -| `services/admin/` | Admin RBAC operations: AML config, break-glass, dispute management, data cleanup | -| `services/telegram/` | Bot webhook handler, Mini App session auth, Telegram identity linking, push notifications | -| `services/trezor/` | Trezor hardware-wallet approval gate for high-value admin actions (break-glass overrideable) | -| `services/notification/` | In-app notification templates, delivery, mark-as-read | -| `services/ai/` | OpenAI integration: AI-assisted listing descriptions and content moderation | -| `services/email/` | Nodemailer transport via Resend SMTP, HTML email templates | -| `services/points/` | Loyalty points engine, tier levels, redemption | -| `services/blog/` | Blog posts, categories, comments | -| `services/file/` | Multer-based file upload handler, MIME validation, upload path management | -| `services/blockchain/` | Low-level Web3 read helpers: balance checks, tx confirmation polling | -| `db/repositories/` | Drizzle ORM repository layer for all 11 domain entities | -| `seeds/` | Idempotent Postgres seed fixtures for users, categories, shops, configs | -| `scripts/` | CLI backfill, migration verify, seeding, and maintenance scripts | +| `services/auth/` | JWT issue/refresh, Google OAuth, WebAuthn/Passkey registration and assertion, password reset | +| `services/user/` | User profile CRUD, preferences, address book | +| `services/marketplace/` | PurchaseRequest, SellerOffer, RequestTemplate, ShopSettings — core escrow marketplace | +| `services/payment/` | Payment orchestration: provider adapters, internal ledger (available/held/releasable), reconciliation, safety confirmations | +| `services/payment/adapters/` | Provider-neutral adapter interface + registry; plugs in DePay, SHKeeper, amn.scanner, Request Network | +| `services/payment/requestNetwork/` | Request Network pay-in creation, in-house checkout rehydration, HMAC-verified webhook | +| `services/payment/wallets/` | HD-derived destination addresses, sweep orchestration, gas top-up | +| `services/payment/ledger/` | Funds ledger tracking available / held / releasable balances per payment | +| `services/payment/safety/` | Transaction Safety Provider: AML screening, min-confirmation thresholds | +| `services/blockchain/` | Web3 read helpers: balance checks, tx verification across ETH / BSC / Base / TON | +| `services/chat/` | Conversations, messages, attachments | +| `services/dispute/` | Dispute lifecycle: open, evidence upload, mediator assignment, release-hold | +| `services/notification/` | Template-based notification delivery (in-app + Telegram); mark-as-read | +| `services/telegram/` | Bot webhook handler, Mini App `initData` verification, identity link/unlink, seller notifications | +| `services/points/` | Loyalty points accrual, levels, referrals, redemption | +| `services/blog/` | Blog posts, categories, comments (public read / admin write) | +| `services/digital-goods/` | Encrypted digital-goods delivery; key stored under `DIGITAL_GOODS_ENC_KEY` | +| `services/file/` | Multer multipart upload, MIME validation, static serving under `/uploads` | +| `services/email/` | Nodemailer SMTP + Resend transport, templated emails | +| `services/ai/` | OpenAI-backed request description generation and content moderation | +| `services/redis/` | Redis client singleton, cache helpers, pub-sub wrappers | +| `services/admin/` | Admin-only endpoints: data cleanup (provider-scoped), confirmation thresholds, awaiting-confirmation view | +| `services/collection/` | Collection management (multi-seller feature) | +| `services/delivery/` | Delivery tracking and status | +| `infrastructure/socket/` | Socket.IO server attached to HTTP server; Redis adapter for multi-process pub-sub | --- ## 5. API Surface Summary -All routes are mounted under `/api/*`. See [[03 - API Reference/API Overview]] for the full endpoint reference. +All API routes are mounted under `/api/`. The table below lists top-level route groups. -Key route groups: +| Mount path | Service module | Auth | Purpose | +|---|---|---|---| +| `/api/auth` | `services/auth/authRoutes.ts` | mixed | Login, register, refresh, OAuth, passkey | +| `/api/user` / `/api/users` | `services/user/userRoutes.ts` | JWT | Profile, preferences | +| `/api/address` | `services/user/addressRoutes.ts` | JWT | Address CRUD | +| `/api/marketplace/requests` | `services/marketplace/` | JWT | PurchaseRequest CRUD | +| `/api/marketplace/offers` | `services/marketplace/` | JWT (seller) | SellerOffer CRUD | +| `/api/marketplace/templates` | `services/marketplace/` | JWT (seller) | RequestTemplate CRUD | +| `/api/marketplace/categories` | `services/marketplace/` | public read | Category list | +| `/api/marketplace/shop-settings` | `services/marketplace/shopSettingsController.ts` | JWT (seller) | Shop profile | +| `/api/payment` | `services/payment/paymentControllerRoutes.ts` | JWT | Payment CRUD, status, export | +| `/api/payment/decentralized` | `services/payment/decentralizedPaymentRoutes.ts` | mixed | Legacy/manual Web3 save and verify | +| `/api/payment/request-network` | `services/payment/requestNetwork/` | mixed + HMAC | RN pay-in, checkout rehydrate, webhook | +| `/api/payment/derived-destinations` | `services/payment/wallets/` | JWT (admin) | HD address list, sweeps, cron config | +| `/api/telegram` | `services/telegram/telegramRoutes.ts` | mixed | Mini App session, bot webhook, identity link | +| `/api/chat` | `services/chat/chatRoutes.ts` | JWT | Conversations, messages | +| `/api/notification` | `services/notification/` | JWT | List, mark-as-read | +| `/api/disputes` | `services/dispute/` | JWT | Dispute CRUD, evidence, release-hold | +| `/api/blog` | `services/blog/blogRoutes.ts` | mixed | Public reads, admin writes | +| `/api/points` | `services/points/pointsRoutes.ts` | JWT | Points, levels, referrals | +| `/api/ai` | `services/ai/aiRoutes.ts` | JWT | OpenAI helpers | +| `/api/files` | `services/file/fileRoutes.ts` | JWT | Multipart upload | +| `/api/email` | `services/email/emailRoutes.ts` | JWT | Email dispatch | +| `/api/trezor` | `services/trezor/trezorRoutes.ts` | JWT | Trezor hardware-wallet ops | +| `/api/admin/cleanup` | `services/admin/dataCleanupRoutes.ts` | JWT (admin) | Data cleanup (must be provider-scoped) | +| `/api/admin/rn/networks` | `services/payment/requestNetwork/networkRegistryRoutes.ts` | JWT (admin) | RN chain/token registry | +| `/api/admin/settings/confirmation-thresholds` | `services/admin/confirmationThresholdRoutes.ts` | JWT (admin) | Runtime confirmation thresholds | +| `/health` | `app.ts` | public | Docker healthcheck; surfaces active Postgres store modes | -| Prefix | Domain | -|---|---| -| `/api/auth/*` | Registration, login, OAuth, passkeys, Telegram auth | -| `/api/payment/*` | Payment CRUD, status polling, provider webhooks | -| `/api/payment/request-network/*` | Request Network webhook + invoice endpoints | -| `/api/amn-scanner/*` | amn.scanner webhook receiver | -| `/api/marketplace/*` | Purchase requests, seller offers, templates | -| `/api/chat/*` | Conversations, messages, attachments | -| `/api/dispute/*` | Dispute lifecycle | -| `/api/admin/*` | Admin operations (role-gated) | -| `/api/notification/*` | In-app notifications | -| `/api/blog/*` | Blog posts and comments | -| `/api/points/*` | Loyalty points | -| `/api/user/*` | User profile, preferences | -| `/health` | Docker healthcheck + active store mode listing | - -**Rate limits (active):** - -| Scope | Limit | -|---|---| -| Auth endpoints | 10 req / 15 min | -| Payment endpoints | 30 req / 15 min | -| AI endpoints | 20 req / 15 min | -| Global | 100 req / 15 min | -| `GET /api/payment/:id` | Exempt (polling route) | -| RN + Telegram webhooks | Exempt from global limiter | +Full per-endpoint details: [[03 - API Reference/API Overview]] --- ## 6. Database -### PostgreSQL (primary — active) +### PostgreSQL (primary) -- **ORM:** Drizzle ORM (`drizzle-orm ^0.45.2`) -- **Driver:** `pg ^8.21.0` -- **Migrations:** 19 SQL files under `src/db/migrations/` (0000–0018), managed by `drizzle-kit` -- **Schemas:** per-table files in `src/db/schema/`, exported via `index.ts` barrel -- **Repositories:** `src/db/repositories/` — one Drizzle repo per domain; `factory.ts` provides DI -- **Connection:** `PG_URL` env var (`postgres://user:pass@host:5432/db`) -- **Migrations run:** `npx drizzle-kit migrate` (or via `drizzle.config.ts`) +- Driver: `pg ^8.21.0` via Drizzle ORM (`drizzle-orm ^0.45.2`) +- Connection: `PG_URL` (primary pool); `PG_VITAL_URL` / `PG_NONVITAL_URL` for split-pool configuration +- Pool tuning: `PG_POOL_MAX`, `PG_POOL_SIZE`, `PG_NONVITAL_POOL_MAX` +- Migrations: numbered SQL files in `src/db/migrations/` (0000–0018+), applied via Drizzle Kit (`npx drizzle-kit migrate`) +- Repositories: `DrizzleXxxRepo` classes in `src/db/repositories/`; factory pattern via `factory.ts` +- Seeds: idempotent Postgres-capable seed scripts under `src/seeds/`; auto-run on start when `AUTO_SEED_ON_START=true` -### MongoDB (legacy — migration in progress) +### MongoDB (retired) -MongoDB and Mongoose were removed at the code level as of v2.9.12. The `MONGO_CONNECT_MODE` env var and `*_STORE` vars remain for the dual-write seam but all active domain stores use Drizzle exclusively. Remaining migration work: +MongoDB and Mongoose have been **fully removed** from the runtime. The `MONGODB_URI` and `MIGRATION_MONGO_URL` env vars exist only for optional data backfill tooling in `src/db/repositories/migration/`. No Mongo connection is established at server boot. `MONGO_CONNECT_MODE=never` is the effective runtime mode. -- Backfill execution for remaining legacy records -- Per-domain read cutover verification -- Chat domain normalization (current blocker) -- Full runtime coupling severance - -See [[PRD - Mongo Retirement (Full Nuke).md]] and [[MIGRATION_TODO.md]] for status. +The `DATABASE_URL` / `POSTGRES_URL` aliases are accepted for compatibility; prefer `PG_URL`. --- ## 7. Auth Model -| Method | Mechanism | -|---|---| -| Password | bcrypt hashed, JWT access + refresh token pair | -| Google OAuth | OAuth 2.0 code flow via `google-auth-library` | -| WebAuthn / Passkeys | `@simplewebauthn/server` — RP ID: `dev.amn.gg` | -| Telegram Mini App | initData HMAC verification (bot token), replay window: 120 s, TTL: 24 h | -| Telegram Bot | Webhook secret token header verification | -| Sessions | Stateless JWT; refresh token stored in Redis | -| CAPTCHA | Cloudflare Turnstile, triggered after 3 failed login attempts from same IP | +### JWT -**RBAC roles:** `admin`, `buyer`, `seller`, `resolver`, `guard` +- Access tokens signed with `JWT_SECRET`; expiry controlled by `JWT_EXPIRES_IN` +- Refresh tokens with `REFRESH_TOKEN_EXPIRES_IN`; stored and rotated server-side +- `authMiddleware` in `shared/middleware/` verifies tokens and attaches `req.user` +- Role-based access via `roleGuard('admin' | 'seller' | 'buyer' | 'resolver' | 'guard')` -`roleGuard(role)` middleware is applied per-route after `authMiddleware`. The admin role unlocks break-glass, AML config, dispute management, and data cleanup endpoints. +### WebAuthn / Passkey -**Trezor safekeeping:** when `TREZOR_SAFEKEEPING_REQUIRED=true`, high-value admin actions (release, refund, payout) require a Trezor-signed approval message. Break-glass overrides this for 1 hour and fires a Telegram alarm. +- Passkey registration and assertion handled in `services/auth/` +- Enables passwordless login on supported clients + +### Google OAuth + +- `GOOGLE_CLIENT_ID` enables Google OAuth 2.0 sign-in + +### Telegram Mini App + +- Mini App sessions verified via Telegram `initData` HMAC in `services/telegram/` +- Identity linking ties a Telegram user to a platform account +- `TELEGRAM_WEBAPP_URL` controls the allowed Mini App origin + +### Rate limits on auth endpoints + +- Login: 10 requests per 15-minute window (`LOGIN_RATE_LIMIT_ENABLED` to toggle) +- Cloudflare Turnstile CAPTCHA support: `TURNSTILE_SECRET_KEY` --- ## 8. Realtime (Socket.IO) -- **Adapter:** Redis pub-sub (`@socket.io/redis-adapter`) — scales across multiple backend instances -- **Init:** `infrastructure/socket/socketService.ts` — attaches to the HTTP server after Express bootstraps -- **Room model:** - - `buyer:` — buyer-facing events (payment status, offer updates, cart) - - `seller:` — seller-facing events (new requests, offer accepted) - - Admin rooms for dispute/notification broadcasts -- **Auth:** Socket handshake verified with JWT before room join -- **Known issue:** Global payment broadcasts previously wiped all users' carts (fixed in frontend v2.8.4 with a provider gate). Backend room-scoping is an open follow-up item. - -Key emitted events (non-exhaustive): - -| Event | Direction | Description | -|---|---|---| -| `payment:status` | Server → client | Payment state change (pending → confirmed → released) | -| `offer:new` | Server → seller | New purchase request from buyer | -| `offer:accepted` | Server → buyer | Seller accepted the offer | -| `notification:new` | Server → client | In-app notification delivery | -| `dispute:update` | Server → both | Dispute state change | -| `chat:message` | Server → both | New chat message in conversation | +- Socket.IO server is attached to the HTTP server at bootstrap (`infrastructure/socket/socketService.ts`) +- Redis adapter (`@socket.io/redis-adapter`) enables pub-sub across Node cluster workers +- **Room conventions:** + - `user:` — personal notifications, payment status updates + - `payment:` — scoped payment lifecycle events (added in v2.8.4 to prevent global cart-wipe) + - `dispute:` — dispute chat and status + - `chat:` — chat messages +- **Key emitted events:** `payment:update`, `notification:new`, `dispute:update`, `chat:message`, `offer:update` +- Server verifies JWT on `connection` and room join; frontend must join the correct room after authenticating --- ## 9. Payment Providers -The payment layer uses a provider-neutral adapter interface (`services/payment/adapters/`). All providers register in the adapter registry. The ledger (`services/payment/ledger/`) enforces double-spend prevention across all providers. - -| Provider | Type | Chains | Status | +| Provider | Type | Chains / Tokens | Notes | |---|---|---|---| -| **amn.scanner** | In-house blockchain scanner | ETH, BSC, Base, TON | Active — default for new payments when `AMN_SCANNER_DEFAULT=true` | -| **Request Network** | Decentralized payment protocol | BSC (USDC/USDT) + ETH | Active — legacy in-flight payments; webhook-driven | -| **DePay** | Widget-based crypto payments | Multi-chain | Available via adapter | -| **SHKeeper** | Self-hosted crypto gateway | Bitcoin + EVM | Available via adapter | +| **amn.scanner** | In-house on-chain scanner | ETH, BSC (USDT/USDC) | Bearer auth via `AMN_SCANNER_API_KEY`; webhook secret `AMN_SCANNER_WEBHOOK_SECRET`; provider tag `"amn.scanner"` | +| **Request Network** | Decentralized invoicing | ETH, Base (USDC/DAI) | `REQUEST_NETWORK_*` env block; HMAC webhook signature; canonical proxy addresses differ per chain (ETH `0x370DE2…`, Base `0x189219…`) | +| **SHKeeper** | Self-hosted crypto gateway | BTC, ETH, BNB, USDT, others | `SHKEEPER_NETWORK`, `SHKEEPER_NETWORKS`, `SHKEEPER_ALLOWED_TOKENS` | +| **DePay** | Web3 payment widget | EVM chains | Legacy path; `PAYMENT_CALLBACK_SECRET` | +| **Derived Destinations** | HD-wallet receive addresses | ETH / BSC | `DERIVED_DESTINATION_XPUB/XPRIV`; sweep orchestration runs on configurable interval | -### Payment flow +### Payment orchestration -1. Buyer creates intent (`POST /api/payment`) → provider adapter creates invoice / watch address -2. Provider webhook arrives → HMAC-verified → reconciliation service updates ledger -3. Escrow holds funds → seller fulfills → admin/resolver releases or refunds -4. Ledger enforces: held → releasable → released (no double-spend) +- `PAYMENT_PROVIDER_MODE` selects active provider(s) at runtime +- Internal ledger tracks `available`, `held`, and `releasable` balances per payment record +- Transaction Safety Provider: AML screening (Chainalysis / OFAC SDN), minimum on-chain confirmation thresholds configurable at runtime (`TRANSACTION_SAFETY_MIN_CONFIRMATIONS`, `TRANSACTION_SAFETY_AML_PROVIDER`) +- `GET /api/payment/:id` is exempt from the payment rate limiter (polling-safe) +- Cleanup endpoints must always be scoped by `provider:` to avoid wiping unrelated payment records -### amn.scanner specifics +### Price Oracle -- Webhook endpoint: `POST /api/amn-scanner/webhook` -- HMAC verification via `AMN_SCANNER_WEBHOOK_SECRET` -- Discriminator field: `payload.event` (not `eventType`) — always check this field -- Provider scoped by `provider: "amn.scanner"` in payment records -- Read token decimals on-chain, not from registry - -### Request Network specifics - -- Webhook endpoint: `POST /api/payment/request-network/webhook` -- Webhook secret: `REQUEST_NETWORK_WEBHOOK_SECRET` -- Network: BSC mainnet, currency: USDC -- Canonical proxy addresses differ per chain (ETH: `0x370DE2…`, Base: `0x189219…`) — probe before trusting - -### Safety layer - -- `TRANSACTION_SAFETY_MIN_CONFIRMATIONS=12` (default) -- Requires tx hash match and on-chain transfer match before releasing funds -- AML screening: `none` (default), `ofac` (OFAC SDN list, local, free), or `chainalysis` - -### Price oracle / depeg protection - -- Providers: Chainlink + off-chain FX (`OFFCHAIN_FX_URL`) -- Chains: ETH (RPC via `CHAINLINK_RPC_1`), BSC (via `CHAINLINK_RPC_56`) -- Depeg hard cap: `DEPEG_HARD_CAP_BPS` (default 500 bps = 5%) -- Oracle max staleness: `ORACLE_MAX_STALENESS_S=120` -- Currently disabled (`ORACLE_QUOTING_ENABLED=false`) — enable after FX feeds are configured +- Chainlink + OffchainFX feeds; `ORACLE_MAX_STALENESS_S` sets maximum acceptable quote age +- Depeg protection rejects or flags stablecoin payments when peg deviation exceeds threshold +- `ORACLE_BYPASS_ENABLED=true` disables staleness check (dev/test only) --- ## 10. CI/CD (Woodpecker) -Four pipelines in `backend/.woodpecker/`: +Four Woodpecker pipeline files under `.woodpecker/`: -### `production.yml` — primary deploy pipeline +| File | Trigger | Purpose | +|---|---|---| +| `production.yml` | push to `main` / `master` | Typecheck → build Docker image locally on host → `docker compose up -d backend` | +| `development.yml` | cron (parked) | Was the dev-stack auto-deploy; currently inactive | +| `manual.yml` | manual trigger | Builds image to `git.tbs.amn.gg` registry (escrow-dev stack ignores registry pulls) | +| `cleanup.yml` | scheduled | Housekeeping tasks (prune old images, stale data) | -Trigger: `push` to `main`/`master` · Platform: `linux/arm64` +### Production pipeline steps -| Step | Description | -|---|---| -| `get-version` | Reads `package.json` version, writes `dev-` to `.tags` | -| `typecheck` | `npm ci` + `npm run typecheck` — gates image build on clean TypeScript (cached npm on host) | -| `build-and-deploy` | `docker build -t git.tbs.amn.gg/escrow/backend:dev` locally on the agent, then `docker compose up -d --no-deps --pull never backend` — no registry push, image stays local | -| `notify` | Posts plain-text result to Telegram via `scripts/ci/tg-notify.cjs` (no parse_mode) | +1. **get-version** — reads `package.json` version, writes `dev-` to `.tags` +2. **typecheck** — `npm ci` (cached at `/opt/woodpecker-cache/backend-npm`) then `npm run typecheck`; push is blocked if tsc errors exist +3. **build-and-deploy** — `docker build -f Dockerfile.prod -t escrow-backend-local:dev .` on the agent co-located with the stack; then `docker compose up -d --no-deps --pull never backend` +4. **notify** — `node scripts/ci/tg-notify.cjs` posts success/failure to Telegram (no `parse_mode` to avoid HTML/Markdown breakage) -> No registry push on production pipeline — agent is co-located with the stack; pushing large images over Tailscale times out. +### escrow-multi stack -### `development.yml` — parked +The `escrow-multi` stack (branch `feature/white-label-shops`) uses `.woodpecker/multi.yml`. Always deploy via `git push forgejo feature/white-label-shops` — never via manual SSH or rsync. Woodpecker CLI credentials are in `~/CascadeProjects/escrow/.env`. -Trigger: `event: cron` (no cron configured — effectively disabled). Targets legacy `git.manko.yoga` registry and retired Arcane deploy. Use `manual.yml` for manual playground builds. +### Version bump requirement -### `manual.yml` — manual build playground - -Trigger: manual. Builds and pushes to `git.tbs.amn.gg/escrow/backend`. Used for testing the pipeline independently. - -### `cleanup.yml` — image cleanup - -Trigger: scheduled/manual. Removes old image tags from the registry. - -**Important CI notes:** -- Always bump `package.json` version before pushing a CI-triggering commit — otherwise the build tag doesn't change and the deployed image may be stale. -- CI green does not guarantee the image was pushed — verify `git.tbs.amn.gg` has the `dev-` tag before trusting the deploy. -- Woodpecker eats `${VAR}` in commands — use `$VAR` or `$$VAR`; prefer plugins over raw curl for notifications. +Bump `package.json` version before every CI-triggering push, or the deployed image will not be distinguishable from the previous build. See memory note `version_bump_before_ci.md`. --- ## 11. Local Development Quick-Start ```bash -# Clone and install +# 1. Clone git clone git@git.tbs.amn.gg:escrow/backend.git cd backend + +# 2. Install dependencies npm install -# Copy environment file -cp .env.example .env.local -# Edit .env.local — set PG_URL, REDIS_URI, JWT_SECRET at minimum +# 3. Copy and populate env +cp .env.example .env.development +# Edit .env.development — minimum required: PG_URL, REDIS_URI, JWT_SECRET, FRONTEND_URL -# Start dependencies (Postgres + Redis) -docker compose -f docker-compose.local.yml up -d +# 4. Start Postgres and Redis (Docker) +docker compose up -d postgres redis -# Run DB migrations +# 5. Run migrations npx drizzle-kit migrate -# Start dev server (hot-reload) +# 6. Start dev server (seeds run automatically if SEED_USERS=true) npm run dev -# → listens on http://localhost:5001 +# Server starts on process.env.PORT -# OR run in dev Docker -docker compose -f docker-compose.dev.yml up -# → listens on http://localhost:8080 - -# Seed database -npm run seed:users -npm run seed:categories -``` - -**Typecheck (required before push):** -```bash +# 7. Type-check only (no run) npm run typecheck ``` -A pre-push git hook blocks the push on tsc errors. If a parallel agent's mid-refactor tree has errors, use explicit `git add ` — never `git add -A`. -**Run tests:** -```bash -npm test -``` -Test files live in `__tests__/`. +> The pre-push git hook runs a full `tsc` check. If a parallel agent's mid-refactor tree is checked out, this hook may block your push. Stage only your specific files — never `git add -A` blindly. See memory note `backend_prepush_tsc_hook.md`. --- @@ -378,89 +296,130 @@ Test files live in `__tests__/`. | Variable | Description | |---|---| -| `NODE_ENV` | `production` / `development` / `test` | -| `PORT` | HTTP listen port (default 5001) | -| `TRUST_PROXY_HOPS` | Number of reverse-proxy hops in front of app | -| `FRONTEND_URL` | Allowed CORS origin for frontend | -| `BACKEND_URL` | Public backend base URL | -| `PG_URL` | PostgreSQL connection string | -| `POSTGRES_USER` | Postgres username (Docker init) | -| `POSTGRES_PASSWORD` | Postgres password (Docker init) | -| `POSTGRES_DB` | Postgres database name (Docker init) | -| `MONGO_CONNECT_MODE` | `always` / `never` / `optional` — Mongo connection behavior (legacy) | -| `REDIS_URI` | Redis connection URI | -| `JWT_SECRET` | HS256 signing secret for access tokens | -| `JWT_EXPIRES_IN` | Access token TTL (e.g. `7d`) | -| `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `30d`) | -| `ADMIN_EMAIL` | Bootstrap admin account email | -| `ADMIN_PASSWORD` | Bootstrap admin account password | -| `SEED_USERS` | `true` to auto-seed users on dev boot | -| `SEED_PASSWORD_ADMIN` | Admin seed account password | -| `SEED_PASSWORD_SUPPORT` | Support seed account password | -| `SEED_PASSWORD_BUYER` | Buyer seed account password | -| `SEED_PASSWORD_SELLER` | Seller seed account password | -| `GOOGLE_CLIENT_ID` | Google OAuth client ID | -| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | -| `WEBAUTHN_RP_ID` | WebAuthn relying party ID (e.g. `dev.amn.gg`) | -| `WEBAUTHN_RP_NAME` | WebAuthn relying party display name | -| `WEBAUTHN_RP_ORIGIN` | WebAuthn allowed origin | -| `SMTP_HOST` | SMTP server host | -| `SMTP_PORT` | SMTP server port | +| `PORT` | HTTP listen port | +| `NODE_ENV` | `development` / `production` / `test` | +| `FRONTEND_URL` | Allowed CORS origin (frontend base URL) | +| `BACKEND_URL` | Self-referential base URL (used for webhook callback construction) | +| `PG_URL` | Primary Postgres connection string | +| `PG_VITAL_URL` | Postgres connection for vital (write-path) pool | +| `PG_NONVITAL_URL` | Postgres connection for non-vital (read-path) pool | +| `PG_POOL_MAX` | Max connections in primary pool | +| `PG_POOL_SIZE` | Pool size alias | +| `PG_NONVITAL_POOL_MAX` | Max connections in non-vital pool | +| `DATABASE_URL` / `POSTGRES_URL` | Compatibility aliases for `PG_URL` | +| `REDIS_URI` | Redis connection string (sessions, pub-sub, Socket.IO adapter) | +| `JWT_SECRET` | HMAC secret for JWT signing | +| `JWT_EXPIRES_IN` | Access token TTL (e.g. `15m`) | +| `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `7d`) | +| `GOOGLE_CLIENT_ID` | Google OAuth 2.0 client ID | +| `TELEGRAM_WEBAPP_URL` | Allowed Telegram Mini App origin | +| `TG_NOTIFY_BOT_TOKEN` | Telegram bot token for CI/admin notifications | +| `TG_NOTIFY_CHATS` | Comma-separated Telegram chat IDs for notifications | +| `SMTP_HOST` | SMTP server hostname | +| `SMTP_PORT` | SMTP port | | `SMTP_SECURE` | `true` for TLS | -| `SMTP_USER` | SMTP username | -| `SMTP_PASS` | SMTP password | -| `SMTP_FROM` | From address for outgoing email | -| `RESEND_WEBHOOK_SECRET` | Resend inbound webhook signing secret (`whsec_…`) | -| `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile server-side secret (empty = CAPTCHA disabled) | -| `RATE_LIMIT_WINDOW_MS` | Rate limit window in milliseconds | -| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window (global) | -| `MAX_FILE_SIZE` | Upload max file size in bytes | -| `UPLOAD_PATH` | Server-side upload directory | -| `PAYMENT_PROVIDER_MODE` | `live` / `test` | -| `PAYMENT_LEDGER_ENFORCEMENT` | `true` to enforce double-spend ledger guard | +| `SMTP_USER` | SMTP auth username | +| `SMTP_PASS` | SMTP auth password | +| `SMTP_FROM` | From address for outbound email | +| `RESEND_API_KEY` | Resend email API key | +| `RESEND_WEBHOOK_SECRET` | Resend webhook signature secret | +| `PAYMENT_PROVIDER_MODE` | Active payment provider(s) | +| `PAYMENT_CALLBACK_SECRET` | DePay callback HMAC secret | +| `AMN_SCANNER_URL` | amn.scanner service base URL | +| `AMN_SCANNER_API_KEY` | Bearer token for amn.scanner API | +| `AMN_SCANNER_WEBHOOK_SECRET` | HMAC secret for amn.scanner webhook verification | +| `REQUEST_NETWORK_API_BASE_URL` | Request Network API base URL | +| `REQUEST_NETWORK_API_KEY` | Request Network API key | +| `REQUEST_NETWORK_CLIENT_ID` | RN client identifier | +| `REQUEST_NETWORK_NETWORK` | RN chain name (`mainnet` / `sepolia` / etc.) | +| `REQUEST_NETWORK_RECEIVER_ADDRESS` | Merchant wallet for RN payments | +| `REQUEST_NETWORK_PAYMENT_CURRENCY` | Payment token symbol | +| `REQUEST_NETWORK_PAYMENT_TOKEN_ADDRESS` | Payment token contract address | +| `REQUEST_NETWORK_INVOICE_CURRENCY` | Invoice denomination currency | +| `REQUEST_NETWORK_WEBHOOK_CALLBACK_URL` | RN webhook delivery URL | +| `REQUEST_NETWORK_WEBHOOK_SECRET` | HMAC secret for RN webhook | +| `REQUEST_NETWORK_MERCHANT_REFERENCE` | RN merchant reference string | +| `REQUEST_NETWORK_ORIGIN` | RN request origin header | +| `RN_API_KEY` | Alias for `REQUEST_NETWORK_API_KEY` | +| `RN_API_URL` | Alias for `REQUEST_NETWORK_API_BASE_URL` | +| `RN_CLIENT_ID` | Alias for `REQUEST_NETWORK_CLIENT_ID` | +| `RN_WEBHOOK_SECRET` | Alias for `REQUEST_NETWORK_WEBHOOK_SECRET` | +| `SHKEEPER_NETWORK` | SHKeeper primary network identifier | +| `SHKEEPER_NETWORKS` | SHKeeper supported networks (comma-separated) | +| `SHKEEPER_ALLOWED_TOKENS` | Token allowlist for SHKeeper | +| `DERIVED_DESTINATION_XPUB` | HD wallet extended public key for address derivation | +| `DERIVED_DESTINATION_XPRIV` | HD wallet extended private key (for sweep signing) | +| `DERIVED_DESTINATION_BASE_PATH` | BIP-44 derivation base path | +| `DERIVED_DESTINATION_CHAIN_ID` | EVM chain ID for derived address sweeps | +| `DERIVED_DESTINATION_MIN_SWEEP_AMOUNT` | Minimum balance to trigger a sweep | +| `DERIVED_DESTINATION_SWEEP_INTERVAL_MS` | Sweep polling interval in milliseconds | +| `DERIVED_DESTINATION_SWEEP_BALANCE_CONCURRENCY` | Parallel balance-check concurrency | +| `DERIVED_DESTINATION_SWEEP_SIGNER` | Sweep transaction signing mode | +| `DERIVED_DESTINATION_SWEEP_AUTOSTART` | Auto-start sweep cron on boot | +| `SWEEP_MASTER_PRIVKEY` | Master private key for sweep gas top-up | +| `SWEEP_GAS_MIN_BNB` | Minimum BNB balance before gas top-up is triggered | +| `SWEEP_GAS_TOP_UP_BNB` | Amount of BNB to top up for sweep gas | | `ESCROW_WALLET_ADDRESS` | Platform escrow wallet address | | `RECEIVER_WALLET_ADDRESS` | Platform receiver wallet address | -| `REQUEST_NETWORK_ENABLED` | Enable Request Network provider | -| `REQUEST_NETWORK_API_KEY` | Request Network API key | -| `REQUEST_NETWORK_NETWORK` | Target chain (`bsc`, `eth`, etc.) | -| `REQUEST_NETWORK_WEBHOOK_SECRET` | HMAC secret for RN webhook verification | -| `AMN_SCANNER_URL` | amn.scanner service base URL | -| `AMN_SCANNER_WEBHOOK_SECRET` | HMAC secret for scanner webhook verification | -| `AMN_SCANNER_DEFAULT` | `true` to make amn.scanner the default provider | -| `ORACLE_QUOTING_ENABLED` | Enable on-chain oracle pricing + depeg protection | -| `PRICE_ORACLE_PROVIDERS` | Comma-separated oracle providers (`chainlink,offchain_fx`) | -| `ORACLE_MAX_STALENESS_S` | Max oracle data age in seconds | -| `DEPEG_HARD_CAP_BPS` | Stablecoin depeg hard cap in basis points | -| `OFFCHAIN_FX_URL` | Off-chain FX rate source URL (required for IRR/TRY) | -| `CHAINLINK_RPC_1` | Private RPC override for Chainlink on ETH mainnet | -| `CHAINLINK_RPC_56` | Private RPC override for Chainlink on BSC | -| `DERIVED_DESTINATION_XPUB` | xPub for derived payment address derivation | -| `DERIVED_DESTINATION_SWEEP_SIGNER` | Sweep signing mode: `build-only` / `hot-key` / `kms` / `trezor` | -| `DERIVED_DESTINATION_SWEEP_INTERVAL_MS` | Sweep cron interval in ms (0 = disabled) | -| `SWEEP_MASTER_PRIVKEY` | Master sweep wallet private key (gas funder) | -| `TREZOR_SAFEKEEPING_REQUIRED` | `true` to require Trezor approval for admin actions | -| `TRANSACTION_SAFETY_ENABLED` | Enable transaction safety layer | -| `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` | Minimum on-chain confirmations before release | -| `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider: `none` / `ofac` / `chainalysis` | -| `CHAINALYSIS_API_KEY` | Chainalysis API key (when AML provider = chainalysis) | -| `TELEGRAM_BOT_TOKEN` | Telegram bot token | -| `TELEGRAM_WEBHOOK_SECRET_TOKEN` | Telegram webhook secret token header value | -| `TELEGRAM_INITDATA_MAX_AGE_SEC` | Max age for Telegram initData (default 86400 s) | -| `TG_NOTIFY_CHATS` | Comma-separated Telegram chat IDs for CI/admin notifications | +| `INFURA_KEY` | Infura RPC key (ETH mainnet) | +| `BSC_RPC_URL` | BSC mainnet RPC endpoint | +| `BSC_TESTNET_RPC_URL` | BSC testnet RPC endpoint | +| `BNB_TESTNET_RPC_URL` | BNB testnet RPC endpoint | +| `RPC_URL_CHAIN_56` | BSC mainnet RPC (chain ID 56) | +| `RPC_URL_CHAIN_97` | BSC testnet RPC (chain ID 97) | +| `ENABLE_TESTNET_CHAINS` | Enable testnet chain support | +| `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider: `chainalysis` / `ofac` / `none` | +| `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` | Default minimum on-chain confirmations | +| `CHAINALYSIS_API_KEY` | Chainalysis KYT API key | +| `OFAC_SDN_URL` | OFAC SDN list endpoint | +| `AML_CHECK_COST_USD` | Cost per AML check (for billing/reporting) | +| `ORACLE_MAX_STALENESS_S` | Maximum age (seconds) for oracle price quotes | +| `ORACLE_BYPASS_ENABLED` | Disable oracle staleness check (`true` in dev/test only) | +| `DIGITAL_GOODS_ENC_KEY` | AES encryption key for digital goods delivery | +| `TREZOR_SAFEKEEPING_REQUIRED` | Require Trezor safekeeping confirmation | +| `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile CAPTCHA secret | +| `RATE_LIMIT_WINDOW_MS` | Rate limit window in milliseconds | +| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window (global limiter) | +| `RATE_LIMIT_BYPASS_IPS` | Comma-separated IPs exempt from rate limiting | +| `LOGIN_RATE_LIMIT_ENABLED` | Enable/disable login rate limiter | +| `TRUST_PROXY_HOPS` | `trust proxy` hop count for X-Forwarded-For behind Traefik | +| `UPLOAD_PATH` | Filesystem path for uploaded files (default `/app/uploads`) | +| `MAX_FILE_SIZE` | Maximum upload size in bytes | +| `CLUSTER_WORKERS` | Number of Node cluster worker processes | +| `SEED_USERS` | Seed default dev users on start | +| `AUTO_SEED_ON_START` | Auto-run all seeds on process start | +| `SEED_DIGITAL_GOODS_ON_START` | Seed digital goods fixtures on start | +| `SEED_MOCK_SHOPS_ON_START` | Seed mock shop fixtures on start | +| `FORCE_SEED_TEMPLATES` | Force re-seed request templates even if already present | +| `SEED_PASSWORD_SELLER` | Password for seeded seller account | +| `SEED_PASSWORD_MOCK_SELLER` | Password for seeded mock seller account | +| `SEED_PASSWORD_SUPPORT` | Password for seeded support account | +| `ADMIN_EMAIL` | Seeded admin user email | +| `ADMIN_PASSWORD` | Seeded admin user password | +| `ADMIN_FIRST_NAME` | Seeded admin first name | +| `ADMIN_LAST_NAME` | Seeded admin last name | +| `MIGRATION_MONGO_URL` | Mongo URL used only by migration/backfill tooling (not runtime) | +| `MIGRATION_PG_URL` | Postgres URL used by migration tooling (may differ from `PG_URL`) | +| `MONGODB_URI` | Legacy Mongo URI retained for backfill scripts only | +| `DB_NAME` | Database name (legacy config field) | + +> Store-mode env vars (`AUTH_STORE`, `USER_STORE`, `BLOG_STORE`, etc.) were part of the dual-write migration scaffolding. All domains are now Postgres-only; these can be left unset or set to `postgres`. --- ## 13. Known Issues / Open Items -| Issue | Status | Notes | +| Issue | Status | Reference | |---|---|---| -| Mongo→PG migration incomplete | In progress | Chat normalization is the current blocker; read cutover and backfill exec pending | -| Backend room-scoping for socket events | Open | Frontend provider gate is in place (v2.8.4); backend should scope payment events to `seller:` rooms to prevent cross-user leakage | -| Rate limit counters are in-memory | Open | Not shared across instances; Redis adapter planned for distributed deployments | -| Oracle quoting disabled | Open | `ORACLE_QUOTING_ENABLED=false`; requires FX feed configuration before enabling | -| amn.scanner multi-seller + multi-chain gap | Open | Current scanner watches one chain; multi-seller and multi-chain support not yet verified | -| Woodpecker development.yml parked | Known | Targets legacy registry; needs repointing to `git.tbs.amn.gg` and new Arcane deploy before re-enabling | -| Trezor safekeeping off by default | By design | `TREZOR_SAFEKEEPING_REQUIRED=false`; must be enabled explicitly in production once admin xpub is registered | -| Request Network canonical proxy addresses | Known | RN's CREATE2 canonical-address claim is false for ETH and Base — probe actual address before trusting | -| JSON assets not copied to dist/ | Fixed (requires postbuild) | `tsc` does not copy `.json` files; explicit `postbuild` copy step required for any `fs.readFileSync` on JSON assets | -| Parallel agent push conflicts | Operational | mojtaba agent pushes to same branches; always `git fetch --rebase` before pushing; expect version-bump conflicts | +| Rate limit counters are in-memory | Not multi-process safe across cluster workers; Redis adapter planned | `backend_rate_limits.md` | +| `pgId` vs legacy `_id` mismatch | Auth `_id` is a legacy ObjectId; marketplace FKs use Postgres UUID (`pgId`); match offers on `pgId` | `pgid_vs_legacy_id.md` | +| Socket.IO room scoping for payments | Backend room-scoping for payment events is an open follow-up (frontend gate added in v2.8.4) | `cart_wipe_global_socket_events.md` | +| Performance is WAN-bound | Profiling shows 300–800ms on external routes = WAN RTT (~235ms); server-side is 3–12ms; PG migration does not fix this | `perf_is_network_bound_not_db.md` | +| RN webhook `event` field | Request Network sends discriminator as `payload.event` not `eventType`; parser must include `event` in fallback chain | `rn_webhook_event_field.md` | +| RN canonical proxy addresses per chain | ETH `0x370DE2…`, Base `0x189219…` — not the same CREATE2 address; always probe before using hardcoded addresses | `rn_proxy_addresses_per_chain.md` | +| JSON assets not copied to dist | `tsc` does not copy `.json` files; any `fs.readFileSync` on JSON needs explicit `postbuild` copy step | `feedback_json_assets_copy_to_dist.md` | +| Woodpecker `${VAR}` template collision | Woodpecker eats `${VAR}` in commands; use `$VAR` or `$$VAR` | `woodpecker_template_collision.md` | +| CI silent build fail | Green CI does not guarantee image was pushed to registry; verify `dev-` tag exists before trusting | `woodpecker_silent_build_fail.md` | +| Admin cleanup must be provider-scoped | Any payment cleanup query must filter by `provider:` or it silently destroys multi-seller/RN records | `feedback_payment_cleanup_provider_filter.md` | +| Store-mode env vars | Legacy dual-write `*_STORE` vars still present in codebase but are no-ops; can be pruned in a future cleanup | — | +| Mongo backfill tooling | `MIGRATION_MONGO_URL` / `MONGODB_URI` retained for backfill scripts only; server never connects to Mongo at runtime | `mongo_retirement_status.md` | diff --git a/10 - Services/deployment.md b/10 - Services/deployment.md index 613f0c7..a2927cd 100644 --- a/10 - Services/deployment.md +++ b/10 - Services/deployment.md @@ -5,7 +5,7 @@ tags: [services, deployment, infrastructure, docker] # Deployment -The `deployment/` sub-project contains all Docker Compose definitions, Caddyfile configurations, Gatus monitoring config, and environment templates for running the Amanat escrow platform. Two compose files exist side-by-side reflecting a legacy setup and the current live stack. +The `deployment/` sub-project contains Docker Compose definitions, reverse-proxy configs, Gatus monitoring, and migration bundles for running the Amanat escrow platform. It covers three distinct stacks: a **legacy compose** (reference only), the **dev-amn active dev stack** (`dev.amn.gg`), and the **escrow-multi white-label stack** (`multi.amn.gg`). --- @@ -13,124 +13,198 @@ The `deployment/` sub-project contains all Docker Compose definitions, Caddyfile | File | Status | Host | Notes | |---|---|---|---| -| `deployment/docker-compose.yml` | Legacy | Any | nginx + traefik_public network, images from `git.manko.yoga` registry | -| `deployment/dev-amn/docker-compose.yml` | **Active** | `89.58.32.32` | shared-web + infra-caddy ingress, images from `git.tbs.amn.gg/escrow` | +| `deployment/docker-compose.yml` | **Legacy / reference** | Any | nginx + traefik_public network; images from `git.manko.yoga` registry. Do not deploy from this. | +| `deployment/dev-amn/docker-compose.yml` | **Active** | `89.58.32.32` | `shared-web` + infra-caddy ingress; images from `git.tbs.amn.gg/escrow` | +| `deployment/escrow-multi/docker-compose.yml` | **Active multi-shop** | `89.58.32.32` | Isolated stack for `multi.amn.gg`; images tagged `:multi`; fresh Postgres/Redis; Drizzle migrations | -The `dev-amn` stack is the authoritative deployment. It runs under Arcane project **devEscrow** (`77c10db2…`) on the ARM64 host at `89.58.32.32`. All operational decisions, env var edits, and container restarts target this stack. +The `dev-amn` stack is the authoritative dev deployment. The `escrow-multi` stack is the only valid target for `feature/white-label-shops` branch work. -The legacy compose (`deployment/docker-compose.yml`) is kept for historical reference. It uses an nginx sidecar, Traefik labels, and images from the old `git.manko.yoga` registry. Do not deploy from it. +> [!warning] Branch / stack isolation +> Work on `feature/white-label-shops` must NEVER touch `escrow-dev` — no restart, redeploy, or env change. Work on `main` must NEVER touch `escrow-multi`. Each stack must have its own `TELEGRAM_BOT_TOKEN` (different bots). See [[deploy_architecture_two_stacks]]. --- ## 2. Services +### 2.1 dev-amn stack (active, `dev.amn.gg`) + | Service | Image | Internal Port | Role | |---|---|---|---| | `backend` | `git.tbs.amn.gg/escrow/backend:dev` | 5001 | Express 5 API + Socket.IO + admin seed | | `frontend` | `git.tbs.amn.gg/escrow/frontend:dev` | 8083 | Next.js SSR app | | `refscanner` | `git.tbs.amn.gg/escrow/scanner:dev` | 8080 | In-house AMN payment scanner (SQLite) | -| `postgres` | `postgres:18-alpine` | 5432 | Primary datastore (auth, marketplace, PG stores) | -| `redis` | `redis:8-alpine` | 6379 | Session cache, rate-limit counters, job queues | -| `mongodb` | `mongo:8.0-noble` | 27017 | Legacy datastore — dev boot parity only, retired in prod | -| `gatus` | `twinproduction/gatus:latest` | 8080 (mapped 8084) | Uptime monitoring + Telegram alerting | +| `postgres` | `postgres:18-alpine` | 5432 | Primary datastore (auth + all 8 Postgres domain stores) | +| `redis` | `redis:8-alpine` | 6379 | Session cache, rate-limit counters, pub/sub | +| `mongodb` | `mongo:8.0-noble` | 27017 | Legacy datastore — dev boot parity only; retire once remaining reads migrated | -> **Note on refscanner:** The in-house scanner (`provider: "amn.scanner"`) persists state in a SQLite file at `/data/scanner.db` inside the container. It does not expose a port on `shared-web`; the backend calls it via the `default` bridge by container alias `refscanner`. +### 2.2 escrow-multi stack (`multi.amn.gg`) -> **Note on mongodb:** The Mongo container is retained for dev stack parity because `MONGODB_URI` is still present in the env. It will be removed once the backend's remaining Mongo reads are migrated to Postgres. See [[mongo-to-pg-migration-guide]] and [[mongo_retirement_status]]. +| Service | Image | Internal Port | Role | +|---|---|---|---| +| `migrate` | `node:22-alpine` | n/a | One-shot Drizzle migration runner | +| `backend` | `git.tbs.amn.gg/escrow/backend:multi` | 5001 | Express API, tenant services, storefront API, tenant bot webhook | +| `frontend` | `git.tbs.amn.gg/escrow/frontend:multi` | 8083 | Next.js for `multi.amn.gg`, tenant subdomains, dashboards | +| `postgres` | `postgres:18-alpine` | 5432 | Isolated multi-stack database (`escrow_multi`) | +| `redis` | `redis:8-alpine` | 6379 | Isolated multi-stack cache/session/pub-sub | + +### 2.3 Legacy compose services (`deployment/docker-compose.yml`) + +> These are documented for reference only. Do not deploy from this file. + +| Service | Image | Host Port | Role | +|---|---|---|---| +| `nginx` | `nginx:alpine` | 80 (via Traefik) | Reverse proxy in front of backend and frontend | +| `nickDev-marketplace` | `git.manko.yoga/manawenuz/escrow-backend:dev` | — | Backend (legacy registry) | +| `mongodb` | `mongo:8.0-noble` | — | Mongo datastore | +| `postgres` | `postgres:18-alpine` | — | Postgres datastore | +| `redis` | `redis:8-alpine` | — | Cache/sessions | +| `nickDev-frontend` | `git.manko.yoga/manawenuz/escrow-frontend:dev` | 8083 | Frontend (legacy registry) | +| `gatus` | `twinproduction/gatus:latest` | 8084→8080 | Uptime monitoring + Telegram alerting | --- ## 3. Architecture Diagram +### dev-amn (active) + ``` Internet (HTTPS 443 / HTTP 80) │ ▼ -┌───────────────────────────┐ -│ Cloudflare CDN / Proxy │ -│ amn.gg / dev.amn.gg │ -└─────────────┬─────────────┘ - │ - ▼ (origin) -┌─────────────────────────────────────────────────┐ -│ Host: 89.58.32.32 │ -│ │ -│ ┌────────────────────────────────────────────┐ │ -│ │ infra-caddy (Arcane project "infra") │ │ -│ │ ports 80:80, 443:443 on host │ │ -│ │ reads Caddyfile at │ │ -│ │ /opt/arcane/data/projects/infra/Caddyfile │ │ -│ └───┬───────────────────────────┬────────────┘ │ -│ │ /api/* /socket.io/* │ /* │ -│ │ /uploads/* │ │ -│ ▼ ▼ │ -│ ┌────────────┐ ┌────────────────┐ │ -│ │ backend │ │ frontend │ │ -│ │ :5001 │ │ :8083 │ │ -│ │ shared-web │ │ shared-web │ │ -│ └──┬──┬──┬───┘ └────────────────┘ │ -│ │ │ │ │ -│ │ │ └──────────────────────┐ │ -│ │ │ ▼ │ -│ │ │ ┌────────────────────┐ │ -│ │ │ │ refscanner │ │ -│ │ │ │ :8080 (default │ │ -│ │ │ │ bridge only) │ │ -│ │ │ └────────────────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ -│ │ postgres │ │ redis │ │ mongodb │ │ -│ │ :5432 │ │ :6379 │ │ :27017 │ │ -│ │ (default │ │ (default │ │ (default only,│ │ -│ │ only) │ │ only) │ │ legacy) │ │ -│ └──────────┘ └──────────┘ └───────────────┘ │ -│ │ -│ ┌────────────────────────────────────────────┐ │ -│ │ gatus :8084 (mapped from :8080) │ │ -│ │ monitors dev.amn.gg + amn.gg + external │ │ -│ └────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ +┌───────────────────────────────┐ +│ Cloudflare CDN / Proxy │ +│ amn.gg / dev.amn.gg │ +└─────────────┬─────────────────┘ + │ (origin request) + ▼ +┌─────────────────────────────────────────────────────┐ +│ Host: 89.58.32.32 │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ infra-caddy (Arcane project "infra") │ │ +│ │ ports 80:80, 443:443 bound to host │ │ +│ │ Caddyfile: /opt/arcane/data/projects/ │ │ +│ │ infra/Caddyfile │ │ +│ └───────┬─────────────────────────┬────────────┘ │ +│ │ /api/* /socket.io/* │ /* │ +│ │ /uploads/* │ │ +│ ▼ ▼ │ +│ ┌───────────────┐ ┌────────────────────┐ │ +│ │ backend │ │ frontend │ │ +│ │ :5001 │ │ :8083 │ │ +│ │ shared-web │ │ shared-web │ │ +│ └──┬──┬────┬────┘ └────────────────────┘ │ +│ │ │ │ │ +│ │ │ └────────────────────┐ │ +│ │ │ ▼ │ +│ │ │ ┌──────────────────────┐ │ +│ │ │ │ refscanner │ │ +│ │ │ │ :8080 │ │ +│ │ │ │ (default only) │ │ +│ │ │ └──────────────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ +│ │ postgres │ │ redis │ │ mongodb │ │ +│ │ :5432 │ │ :6379 │ │ :27017 │ │ +│ │ (default │ │ (default │ │ (default only, │ │ +│ │ only) │ │ only) │ │ legacy) │ │ +│ └──────────┘ └──────────┘ └────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ Networks: - shared-web ─── external, attached: backend, frontend - default ─── internal bridge: all services + shared-web (external) ─ backend + frontend (reachable by infra-caddy) + default (bridge) ─ all containers on the stack +``` + +### Legacy compose (reference only) + +``` +Internet (HTTPS 443) + │ + ▼ +┌─────────────────────────┐ +│ Traefik (external) │ +│ escrowdev.ch.manko. │ +│ yoga → nginx:80 │ +│ gatus.ch.manko.yoga → │ +│ gatus:8080 │ +└────────────┬────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ nginx (traefik_public + default) │ +│ nickDev-nginx │ +│ conf: /var/data/escrowDev/nginx/ │ +└────────┬──────────────────┬────────────┘ + │ /api /socket.io │ /* + ▼ ▼ +┌────────────────┐ ┌────────────────────┐ +│ nickDev- │ │ nickDev-frontend │ +│ marketplace │ │ :8083 │ +│ backend │ │ (watchtower) │ +│ (watchtower) │ └────────────────────┘ +└──┬──┬──────────┘ + │ │ + ▼ ▼ +┌──────────┐ ┌──────────┐ ┌────────────┐ +│ mongodb │ │ postgres │ │ redis │ +│ :27017 │ │ :5432 │ │ :6379 │ +└──────────┘ └──────────┘ └────────────┘ + +┌────────────────────────────────────────┐ +│ gatus :8084→:8080 (traefik_public) │ +└────────────────────────────────────────┘ ``` --- ## 4. Networks -| Network | Type | Services Attached | Purpose | -|---|---|---|---| -| `default` (bridge) | Internal | All services | Container-to-container communication | -| `shared-web` | External (pre-existing) | `backend`, `frontend` | Allows infra-caddy to proxy by container name | -| `traefik_public` | External (legacy only) | nginx, gatus (legacy compose) | Old Traefik-based ingress on `git.manko.yoga` host | +| Network | Type | Present in | Services Attached | Purpose | +|---|---|---|---|---| +| `default` (bridge) | Internal auto | dev-amn, legacy | All services | Container-to-container communication | +| `shared-web` | External (pre-existing) | dev-amn, escrow-multi | `backend`, `frontend` | Allows infra-caddy to proxy by container name | +| `traefik_public` | External (pre-existing) | Legacy compose only | `nginx`, `gatus` | Old Traefik-based ingress on `git.manko.yoga` host | **Key rules:** -- `postgres`, `redis`, `mongodb` are on `default` only — no external exposure. +- `postgres`, `redis`, `mongodb` are on `default` only — never externally reachable. - `refscanner` is on `default` only; backend reaches it via alias `refscanner:8080`. -- Any new public-facing service must join `shared-web` AND get a Caddyfile block. See [[Shared Infra (89.58.32.32)]] and section 6 below. -- `shared-web` must exist on the host before `docker compose up`. It is created by the `infra` project. +- Any new public-facing service must join `shared-web` AND get a Caddyfile vhost block. +- `shared-web` must exist on the host before `docker compose up` — it is created by the Arcane `infra` project. --- ## 5. Volumes and Bind Mounts -All data volumes in the `dev-amn` stack use relative bind mounts under `./data/` (resolved to `/opt/arcane/data/projects/escrow-dev/data/` on the server): +### dev-amn stack + +All data volumes use relative bind mounts under `./data/` (resolved to `/opt/arcane/data/projects/escrow-dev/data/` on the server): | Service | Host Path | Container Path | Notes | |---|---|---|---| -| `backend` | `./data/uploads` | `/app/uploads` | User-uploaded files | +| `backend` | `./data/uploads` | `/app/uploads` | User-uploaded files (served via `/uploads/*`) | | `refscanner` | `./data/scanner` | `/data` | SQLite DB at `/data/scanner.db` | -| `postgres` | `./data/postgres` | `/var/lib/postgresql/data` | `PGDATA` subdir workaround: actual data at `./data/postgres/pgdata` | -| `redis` | `./data/redis` | `/data` | Persistence dump | -| `mongodb` | `./data/mongo` | `/data/db` | Legacy; can be deleted once Mongo retired | -| `gatus` | `./gatus/config.yaml` | `/config/config.yaml` (ro) | Monitoring config — part of repo | +| `postgres` | `./data/postgres` | `/var/lib/postgresql/data` | `PGDATA=/var/lib/postgresql/data/pgdata` (subdir workaround) | +| `redis` | `./data/redis` | `/data` | RDB persistence dump | +| `mongodb` | `./data/mongo` | `/data/db` | Legacy; can be removed once Mongo retired | -**Postgres volume note:** `postgres:18` introduced a version-scoped data directory layout and refuses to init directly into a volume root that already contains files from a different layout. The compose file sets `PGDATA=/var/lib/postgresql/data/pgdata` to place actual data in a subdirectory of the mount, avoiding init conflicts. +The Gatus config (`deployment/gatus/config.yaml`) is bind-mounted read-only into the gatus container at `/config/config.yaml`. It lives in the repo, not in `./data/`. -**Legacy compose** (`deployment/docker-compose.yml`) uses absolute host paths under `/var/data/escrowDev/` and does not share volumes with the dev-amn stack. +> **Postgres volume note:** `postgres:18` uses a version-scoped data directory layout and refuses to init into a volume root that already contains files from a different layout. `PGDATA` is set to a subdirectory (`/var/lib/postgresql/data/pgdata`) inside the mount to avoid init conflicts. + +### Legacy compose bind mounts (`/var/data/escrowDev/`) + +| Service | Host Path | Container Path | +|---|---|---| +| `nginx` | `/var/data/escrowDev/nginx/nginx.conf` | `/etc/nginx/nginx.conf` (ro) | +| `nginx` | `/var/data/escrowDev/nginx/logs` | `/var/log/nginx` | +| `nginx` / `backend` | `/var/data/escrowDev/uploads` | `/uploads` / `/app/uploads` | +| `mongodb` | `/var/data/escrowDev/mongodb_data` | `/data/db` | +| `mongodb` | `/var/data/escrowDev/mongo-init` | `/docker-entrypoint-initdb.d` | +| `postgres` | `/var/data/escrowDev/postgres_data` | `/var/lib/postgresql` | +| `redis` | `/var/data/escrowDev/redis_data` | `/data` | --- @@ -138,11 +212,12 @@ All data volumes in the `dev-amn` stack use relative bind mounts under `./data/` Ingress for `89.58.32.32` is handled exclusively by **infra-caddy** — the Caddy container in the Arcane project `infra`. It owns host ports 80 and 443. No service should bind those ports directly. -### Current Caddyfile block (dev.amn.gg) +### Caddyfile block for dev.amn.gg -Located at `/opt/arcane/data/projects/infra/Caddyfile` on the server (and mirrored in `deployment/dev-amn/Caddyfile` for reference): +Live location on server: `/opt/arcane/data/projects/infra/Caddyfile` +Reference copy in repo: `deployment/dev-amn/Caddyfile` -``` +```caddy { email manwe@manko.yoga auto_https disable_redirects @@ -158,25 +233,46 @@ dev.amn.gg { } ``` -- `auto_https disable_redirects` — Cloudflare proxy sits in front; Caddy should not force HTTP→HTTPS redirects at origin. -- Routes by path prefix: `/api/*`, `/socket.io/*`, `/uploads/*` go to `backend:5001`; everything else to `frontend:8083`. -- Container names are resolved via the `shared-web` network. +- `auto_https disable_redirects` — Cloudflare proxy sits in front; Caddy must not force HTTP→HTTPS redirects at origin. +- Routes by path prefix: `/api/*`, `/socket.io/*`, `/uploads/*` → `backend:5001`; everything else → `frontend:8083`. +- Container names resolve via the `shared-web` network (both `backend` and `frontend` join it). ### Adding a new public service 1. Add the service to `deployment/dev-amn/docker-compose.yml` with `networks: shared-web: {}`. 2. Edit `/opt/arcane/data/projects/infra/Caddyfile` on the server — add a new vhost block or path matcher. -3. Reload Caddy (no restart needed): +3. Reload Caddy without restarting: ```bash - docker exec infra-caddy caddy reload --config /etc/caddy/Caddyfile + ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ + "docker exec infra-caddy caddy reload --config /etc/caddy/Caddyfile" ``` -4. Verify via `curl -I https://dev.amn.gg/`. +4. Verify: `curl -I https://dev.amn.gg/` + +See also: [[Shared Infra (89.58.32.32)]] + +### Legacy compose: Traefik labels + +In the legacy compose, nginx and gatus expose themselves to Traefik via Docker labels: + +```yaml +# nginx +traefik.http.routers.escrowDev.rule=Host(`escrowdev.ch.manko.yoga`) +traefik.http.routers.escrowDev.entrypoints=https +traefik.http.services.escrowDev.loadbalancer.server.port=80 + +# gatus +traefik.http.routers.gatus.rule=Host(`gatus.ch.manko.yoga`) +traefik.http.routers.gatus.entrypoints=https +traefik.http.services.gatus.loadbalancer.server.port=8080 +``` --- ## 7. Gatus Monitoring -Gatus runs as a sidecar in the `default` network. Config is at `deployment/gatus/config.yaml` (bind-mounted read-only). Alerts are delivered via Telegram. +Gatus runs as a sidecar in the `default` network. Config is at `deployment/gatus/config.yaml` (bind-mounted read-only). Alerts are delivered via Telegram to `GATUS_TELEGRAM_CHAT_ID`. + +In the legacy compose, Gatus is exposed on host port `8084` (mapped from container `:8080`) and publicly accessible via Traefik at `gatus.ch.manko.yoga`. ### Alert policy @@ -187,69 +283,80 @@ Gatus runs as a sidecar in the `default` network. Config is at `deployment/gatus | Send on resolved | Yes | | Alert channel | Telegram (`GATUS_TELEGRAM_BOT_TOKEN` / `GATUS_TELEGRAM_CHAT_ID`) | +Prod endpoints use `failure-threshold: 2` (faster alerting). + ### Monitored endpoints | Name | Group | URL | Interval | Key Conditions | |---|---|---|---|---| -| `backend-dev-version` | backend-dev | `https://dev.amn.gg/api/version` | 60s | HTTP 200, body.version not empty | -| `backend-dev-health` | backend-dev | `https://dev.amn.gg/api/health` | 30s | HTTP 200, all PG store modes = postgres, redis ok, RN chain+token registry loaded | -| `backend-prod-version` | backend-prod | `https://amn.gg/api/version` | 60s | HTTP 200, body.version not empty (failure-threshold 2) | -| `backend-prod-health` | backend-prod | `https://amn.gg/api/health` | 30s | HTTP 200, db/postgres/redis/RN registries ok (failure-threshold 2) | -| `frontend-dev` | frontend | `https://dev.amn.gg/` | 60s | HTTP 200, response < 3000ms | -| `frontend-prod` | frontend | `https://amn.gg/` | 60s | HTTP 200, response < 3000ms (failure-threshold 2) | -| `rn-api-reachable` | external | `https://api.request.network/v2/health` | 5m | HTTP 200/401/404 (accepts auth errors — just checks reachability) | +| `backend-dev-version` | backend-dev | `https://dev.amn.gg/api/version` | 60s | HTTP 200, `body.version` not empty | +| `backend-dev-health` | backend-dev | `https://dev.amn.gg/api/health` | 30s | HTTP 200, all 8 PG store modes = postgres, redis ok, RN chain+token registry loaded | +| `backend-prod-version` | backend-prod | `https://amn.gg/api/version` | 60s | HTTP 200, `body.version` not empty | +| `backend-prod-health` | backend-prod | `https://amn.gg/api/health` | 30s | HTTP 200, db/postgres/redis/RN registries ok | +| `frontend-dev` | frontend | `https://dev.amn.gg/` | 60s | HTTP 200, response time < 3000ms | +| `frontend-prod` | frontend | `https://amn.gg/` | 60s | HTTP 200, response time < 3000ms | +| `rn-api-reachable` | external | `https://api.request.network/v2/health` | 5m | HTTP 200/401/404 (checks reachability only) | | `chainalysis-public-api` | external | `https://public.chainalysis.com/api/v1/address/0x000…` | 5m | HTTP 200 or 404 | | `bsc-rpc-publicnode` | external | `https://bsc-rpc.publicnode.com` (POST) | 2m | HTTP 200, `result == "0x38"` (BSC mainnet chain ID) | -The `backend-dev-health` check validates that **all 8 domain stores are running on Postgres** (`auth`, `config`, `address`, `category`, `levelConfig`, `shopSettings`, `review`, `notification`). A failure here means a store mode regression or a broken `PG_URL`. +The `backend-dev-health` endpoint validates **all 8 domain stores running on Postgres**: `auth`, `config`, `address`, `category`, `levelConfig`, `shopSettings`, `review`, `notification`. A failure here means a store mode regression or a broken `PG_URL`. -Gatus dashboard is accessible at `:8084` on the host (not publicly proxied by default — access via SSH tunnel or add a Caddyfile block if needed). +Gatus dashboard: `:8084` on the host locally (not publicly proxied by default — access via SSH tunnel, or add a Caddyfile block if public exposure is needed). --- ## 8. Environment Variables -All vars are passed to containers via `.env` at the stack root (`deployment/dev-amn/.env` on the server, `deployment/.env` in the repo as the live dev reference). The file is `chmod 600` and never committed. +All vars are injected via `.env` at the stack root. The server file is `chmod 600` and never committed. The `deployment/.env` in the repo serves as the live dev reference / template. -### Backend +### 8.1 Runtime / Node -| Variable | Description | Example / Default | +| Variable | Description | Default | |---|---|---| | `NODE_ENV` | Runtime environment | `production` | | `PORT` | Express listen port | `5001` | -| `TRUST_PROXY` | Express trust-proxy (required behind Caddy) | `true` | +| `TRUST_PROXY` | Express trust-proxy (required behind Caddy/nginx) | `true` | | `DEBUG` | Debug namespaces | _(empty)_ | | `LOG_LEVEL` | Winston log level | `info` | -#### Database +### 8.2 Database — Postgres -| Variable | Description | Example | +| Variable | Description | Default in compose | |---|---|---| -| `MONGODB_URI` | Mongo connection string | `mongodb://admin:pass@mongodb:27017/marketplace?authSource=admin` | -| `MONGO_INITDB_ROOT_USERNAME` | Mongo root user | `admin` | -| `MONGO_INITDB_ROOT_PASSWORD` | Mongo root password | — | -| `MONGO_INITDB_DATABASE` | Mongo init database | `marketplace` | -| `DB_NAME` | Mongo database name used by app | `amn-db` | -| `PG_URL` | Postgres DSN | `postgres://amanat:pass@amanat-postgres:5432/amanat_dev` | +| `PG_URL` | Postgres DSN | `postgres://amanat:amanat_local@postgres:5432/amanat_dev` | | `POSTGRES_USER` | Postgres superuser | `amanat` | | `POSTGRES_PASSWORD` | Postgres superuser password | — | | `POSTGRES_DB` | Postgres database name | `amanat_dev` | | `AUTO_SEED_ON_START` | Run seed on boot | `true` | -#### Store modes (dual-write seam) +### 8.3 Database — Mongo (legacy) | Variable | Description | Default | |---|---|---| -| `AUTH_STORE` | Auth domain store backend | `postgres` | -| `CONFIG_STORE` | Config domain | `postgres` | -| `ADDRESS_STORE` | Address domain | `postgres` | -| `CATEGORY_STORE` | Category domain | `postgres` | -| `LEVEL_CONFIG_STORE` | Level config domain | `postgres` | -| `SHOP_SETTINGS_STORE` | Shop settings domain | `postgres` | -| `REVIEW_STORE` | Review domain | `postgres` | -| `NOTIFICATION_STORE` | Notification domain | `postgres` | +| `MONGODB_URI` | Mongo connection string | `mongodb://admin:pass@mongodb:27017/marketplace?authSource=admin` | +| `MONGO_INITDB_ROOT_USERNAME` | Mongo root user | `admin` | +| `MONGO_INITDB_ROOT_PASSWORD` | Mongo root password | `changeme_local` | +| `MONGO_INITDB_DATABASE` | Mongo init DB | `marketplace` | +| `DB_NAME` | Mongo database name used by app | `amn-db` | -#### Auth / Sessions +### 8.4 Store modes (dual-write seam) + +All default to `postgres` in the dev-amn compose. Changing any to `mongo` re-routes that domain's reads/writes to MongoDB. + +| Variable | Domain | Default | +|---|---|---| +| `AUTH_STORE` | Auth / user accounts | `postgres` | +| `CONFIG_STORE` | App config | `postgres` | +| `ADDRESS_STORE` | User addresses | `postgres` | +| `CATEGORY_STORE` | Marketplace categories | `postgres` | +| `LEVEL_CONFIG_STORE` | Gamification level config | `postgres` | +| `SHOP_SETTINGS_STORE` | Per-shop settings | `postgres` | +| `REVIEW_STORE` | Product / seller reviews | `postgres` | +| `NOTIFICATION_STORE` | User notifications | `postgres` | + +See [[mongo_retirement_status]] and [[mongo-to-pg-migration-guide]]. + +### 8.5 Auth / Sessions | Variable | Description | |---|---| @@ -257,14 +364,14 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `JWT_EXPIRES_IN` | Access token TTL (e.g. `7d`) | | `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `30d`) | -#### Redis +### 8.6 Redis | Variable | Description | |---|---| | `REDIS_URI` | Redis connection string (includes password) | -| `REDIS_PASSWORD` | Redis auth password (standalone, if not in URI) | +| `REDIS_PASSWORD` | Redis auth password (standalone form) | -#### URLs / CORS +### 8.7 URLs / CORS | Variable | Description | |---|---| @@ -274,23 +381,23 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `BACKEND_URL` | Backend origin | | `CORS_ORIGIN` | Allowed CORS origin | -#### File uploads +### 8.8 File uploads | Variable | Description | Default | |---|---|---| | `UPLOAD_PATH` | Upload directory inside container | `/app/uploads` | | `MAX_FILE_SIZE` | Max upload bytes | `52428800` (50 MB) | -#### Rate limiting +### 8.9 Rate limiting | Variable | Description | Default | |---|---|---| | `RATE_LIMIT_WINDOW_MS` | Window for rate limiter | `900000` (15 min) | | `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | `100` | -> GET `/api/payment/:id` must bypass `paymentLimiter` — see [[backend_rate_limits]]. +> GET `/api/payment/:id` must bypass `paymentLimiter` (30 req/15 min) — see [[backend_rate_limits]]. -#### SMTP +### 8.10 SMTP | Variable | Description | |---|---| @@ -301,7 +408,7 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `SMTP_PASS` | SMTP password | | `SMTP_FROM` | From address | -#### WebAuthn (Passkeys) +### 8.11 WebAuthn (Passkeys) | Variable | Description | |---|---| @@ -309,7 +416,7 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `WEBAUTHN_RP_NAME` | Relying party display name | | `WEBAUTHN_RP_ORIGIN` | Relying party origin URL | -#### Admin seed +### 8.12 Admin seed | Variable | Description | |---|---| @@ -318,14 +425,14 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `ADMIN_FIRST_NAME` | Admin first name | | `ADMIN_LAST_NAME` | Admin last name | -#### Google OAuth +### 8.13 Google OAuth | Variable | Description | |---|---| | `GOOGLE_CLIENT_ID` | Google OAuth client ID | | `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | -#### OpenAI +### 8.14 OpenAI | Variable | Description | |---|---| @@ -334,13 +441,13 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `OPENAI_MAX_TOKENS` | Max tokens per request | | `OPENAI_TEMPERATURE` | Sampling temperature | -#### Sentry +### 8.15 Sentry | Variable | Description | |---|---| | `SENTRY_DSN` | Sentry ingest DSN | -#### Wallets / Blockchain +### 8.16 Wallets / Blockchain | Variable | Description | |---|---| @@ -349,7 +456,7 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `ADMIN_PAYOUT_WALLET_ADDRESS` | Admin payout destination | | `RECEIVER_WALLET_ADDRESS` | Default receiver wallet | -#### DePay +### 8.17 DePay | Variable | Description | |---|---| @@ -359,7 +466,7 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `DEPAY_ALLOWED_TOKENS` | Allowed payment tokens | | `DEPAY_PUBLIC_KEY` | DePay public key (PEM) | -#### SHKeeper +### 8.18 SHKeeper | Variable | Description | |---|---| @@ -375,24 +482,25 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `SHKEEPER_CALLBACK_SECRET` | Callback verification secret | | `SHKEEPER_WEBHOOK_SECRET` | Webhook verification secret | -#### Request Network +### 8.19 Request Network -| Variable | Description | -|---|---| -| `REQUEST_NETWORK_ENABLED` | Enable RN provider | -| `REQUEST_NETWORK_WEBHOOK_SECRET` | Webhook signature secret | -| `REQUEST_NETWORK_WEBHOOK_CALLBACK_URL` | Public callback URL | -| `REQUEST_NETWORK_PAYMENT_CURRENCY` | Currency (e.g. `USDC`) | -| `REQUEST_NETWORK_NETWORK` | Chain (e.g. `bsc`) | -| `REQUEST_NETWORK_MERCHANT_REFERENCE` | Merchant address reference | -| `REQUEST_NETWORK_API_BASE_URL` | RN API root | -| `REQUEST_NETWORK_API_KEY` | RN API key | -| `REQUEST_NETWORK_ORIGIN` | Origin header sent to RN | -| `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` | Allow test events (default `false`) | +| Variable | Description | Default | +|---|---|---| +| `REQUEST_NETWORK_ENABLED` | Enable RN provider | — | +| `REQUEST_NETWORK_WEBHOOK_SECRET` | Webhook signature secret | — | +| `REQUEST_NETWORK_WEBHOOK_CALLBACK_URL` | Public callback URL | — | +| `REQUEST_NETWORK_PAYMENT_CURRENCY` | Currency (e.g. `USDC`) | — | +| `REQUEST_NETWORK_NETWORK` | Chain (e.g. `bsc`) | — | +| `REQUEST_NETWORK_MERCHANT_REFERENCE` | Merchant address reference | — | +| `REQUEST_NETWORK_API_BASE_URL` | RN API root | — | +| `REQUEST_NETWORK_API_KEY` | RN API key | — | +| `REQUEST_NETWORK_ORIGIN` | Origin header sent to RN | — | +| `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` | Allow test events | `false` | > RN webhook discriminator is `payload.event` (not `eventType`) — see [[rn_webhook_event_field]]. +> RN proxy addresses differ per chain (not canonical CREATE2 addresses) — see [[rn_proxy_addresses_per_chain]]. -#### Transaction safety +### 8.20 Transaction safety | Variable | Description | Default | |---|---|---| @@ -402,7 +510,7 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` | Min block confirmations | `12` | | `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider (`none`, `chainalysis`) | `none` | -#### Payment routing +### 8.21 Payment routing | Variable | Description | |---|---| @@ -411,23 +519,25 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `PAYMENT_PROVIDER_MODE` | `live` or `test` | | `PAYMENT_ROLLBACK_PROVIDER` | Fallback provider | -#### Telegram +### 8.22 Telegram | Variable | Description | |---|---| | `TELEGRAM_FEATURE_ENABLED` | Enable Telegram integration | | `TELEGRAM_MINIAPP_ENABLED` | Enable Mini App | | `TELEGRAM_WEBHOOK_ENABLED` | Enable webhook receiver | -| `TELEGRAM_BOT_TOKEN` | Main bot token | +| `TELEGRAM_BOT_TOKEN` | Main bot token (`@amnescrow_Bot` for dev) | | `TELEGRAM_WEBHOOK_SECRET_TOKEN` | Webhook secret for validation | | `TELEGRAM_INITDATA_MAX_AGE_SEC` | Max age for initData | | `TELEGRAM_INITDATA_REPLAY_WINDOW_MS` | Replay protection window | | `TELEGRAM_WEBHOOK_REPLAY_WINDOW_MS` | Webhook replay protection window | | `TELEGRAM_SESSION_TTL_SEC` | Session TTL | -| `TG_NOTIFY_BOT_TOKEN` | Ops/monitoring bot token (amnGG_MonitorBot) | +| `TG_NOTIFY_BOT_TOKEN` | Ops/monitoring bot token (`amnGG_MonitorBot`) | | `TG_NOTIFY_CHATS` | Comma-separated chat IDs for ops notifications | -#### Pangolin / Newt (VPN mesh — optional) +> Each stack (dev, multi) must have a **different `TELEGRAM_BOT_TOKEN`** — sharing a bot token kills one stack's webhook when the other registers. See [[escrow_multi_woodpecker_deploy]] and stack isolation warning above. + +### 8.23 Pangolin / Newt (optional VPN mesh) | Variable | Description | |---|---| @@ -435,13 +545,7 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `NEWT_ID` | Newt node ID | | `NEWT_SECRET` | Newt node secret | -#### Testnet chains - -| Variable | Description | -|---|---| -| `ENABLE_TESTNET_CHAINS` | Expose testnet chain configs | Set to `true` in dev-amn compose override | - -### Frontend (NEXT_PUBLIC_*) +### 8.24 Frontend (NEXT_PUBLIC_*) | Variable | Description | |---|---| @@ -458,7 +562,7 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `NEXT_PUBLIC_PASSKEY_RP_NAME` | WebAuthn RP name | | `NEXT_PUBLIC_PASSKEY_RP_ID` | WebAuthn RP ID | | `NEXT_PUBLIC_PASSKEY_ORIGIN` | WebAuthn origin | -| `NEXT_PUBLIC_BACKEND_URL` | Backend origin (used for direct calls) | +| `NEXT_PUBLIC_BACKEND_URL` | Backend origin (direct calls) | | `NEXT_PUBLIC_DEPAY_INTEGRATION_ID` | DePay integration ID | | `NEXT_PUBLIC_IS_DEVELOPMENT` | Development flag | | `NEXT_PUBLIC_ENABLE_DEBUG` | Enable client debug logging | @@ -466,7 +570,9 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- | `NEXT_PUBLIC_TELEGRAM_BOT_ID` | Telegram bot numeric ID | | `BUILD_STATIC_EXPORT` | Enable `next export` mode (`false` for SSR) | -### Gatus +> `NEXT_PUBLIC_*` vars are baked into the frontend bundle at build time. Never put secrets in them. Frontend env changes require a fresh image build and redeploy. + +### 8.25 Gatus | Variable | Description | |---|---| @@ -477,9 +583,9 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev- ## 9. Deploy Workflow -### 9.1 Normal image update (CI-driven) +### 9.1 Normal image update (CI-driven — dev-amn) -Woodpecker CI builds `backend` and `frontend` images, pushes tags to `git.tbs.amn.gg/escrow/` on merge to `dev`, then triggers an Arcane GitOps sync which pulls the new image and recreates the container. +Woodpecker CI builds backend and frontend images, pushes to `git.tbs.amn.gg/escrow/`, then triggers an Arcane GitOps sync which pulls the new image and recreates the container. ``` git push origin dev @@ -487,31 +593,52 @@ git push origin dev └─► docker push git.tbs.amn.gg/escrow/backend:dev └─► docker push git.tbs.amn.gg/escrow/frontend:dev └─► arcane-cli gitops sync cf6c9eab… (or watchtower polls) - └─► escrow-backend container restarted with new image - └─► escrow-frontend container restarted with new image + └─► escrow-backend restarted with new image + └─► escrow-frontend restarted with new image ``` -> Always bump the version in `package.json` + lock before pushing, otherwise the CI build may not register as a new deploy. See [[version_bump_before_ci]]. +> Always bump `package.json` version before pushing. See [[version_bump_before_ci]]. -### 9.2 Manual deploy (backend hotfix — no registry) +### 9.2 escrow-multi deploys (white-label stack) -For urgent backend fixes without a full CI cycle, use the local-build pattern (the dev stack has `pull_policy: always` but the override `docker-compose.override.yml` sets `pull_policy: never` for the `escrow-backend-local:dev` image path): +**Always use Woodpecker. Never use manual rsync/docker-build/ssh for escrow-multi.** ```bash -# 1. Copy changed files to build tree on server +# 1. Make changes, bump version in package.json +# 2. Commit +git commit -m "fix: description (vX.Y.Z)" +# 3. Push to Forgejo (remote is "forgejo", not "origin") +git push forgejo feature/white-label-shops +# 4. Monitor Woodpecker pipeline +source ~/CascadeProjects/escrow/.env +WOODPECKER_SERVER=$WOODPECKER_SERVER WOODPECKER_TOKEN=$WOODPECKER_TOKEN \ + woodpecker-cli pipeline ls escrow/backend +``` + +Frontend is a separate Woodpecker project (`escrow/frontend`). Both push targets trigger their respective pipelines. + +### 9.3 Manual hotfix deploy (backend only — no registry cycle) + +For urgent fixes without a full CI cycle, build locally on the server: + +```bash +# 1. Copy changed files to build tree scp -i ~/CascadeProjects/wzp src/services/auth/authRoutes.ts \ root@89.58.32.32:/tmp/escrow-backend-build/src/services/auth/ # 2. Rebuild image on server (~3 min, ARM64) ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ - "cd /tmp/escrow-backend-build && docker build -f Dockerfile.prod -t escrow-backend-local:dev ." + "cd /tmp/escrow-backend-build && docker build -f Dockerfile.prod \ + -t escrow-backend-local:dev ." # 3. Restart the backend container ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ "cd /opt/arcane/data/projects/escrow-dev && docker compose up -d backend" ``` -### 9.3 Bringing the stack up/down +The `docker-compose.override.yml` at `/opt/arcane/data/projects/escrow-dev/docker-compose.override.yml` sets `pull_policy: never` for `escrow-backend-local:dev` so watchtower never clobbers it. + +### 9.4 Bringing the stack up/down ```bash # via Arcane CLI (preferred) @@ -526,9 +653,7 @@ ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ "cd /opt/arcane/data/projects/escrow-dev && docker compose down" ``` -### 9.4 Reloading Caddy after Caddyfile edits - -Edit `/opt/arcane/data/projects/infra/Caddyfile` on the server, then: +### 9.5 Reloading Caddy after Caddyfile edits ```bash ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ @@ -537,17 +662,17 @@ ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ No container restart needed. -### 9.5 Updating env vars +### 9.6 Updating env vars 1. Edit `.env` on the server: `/opt/arcane/data/projects/escrow-dev/.env` -2. Restart affected service: +2. Restart affected container: ```bash ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ "cd /opt/arcane/data/projects/escrow-dev && docker compose up -d backend" ``` - Frontend env vars baked at build time (via `NEXT_PUBLIC_*`) require a fresh image rebuild. +3. Frontend `NEXT_PUBLIC_*` vars are baked at build time — they require a fresh image build and full redeploy via CI. -### 9.6 Verifying a deploy +### 9.7 Verifying a deploy ```bash # Check running containers @@ -556,7 +681,7 @@ arcane-cli project status devEscrow # Check backend version curl https://dev.amn.gg/api/version -# Check health (all stores + registries) +# Check health (all stores + RN registries) curl https://dev.amn.gg/api/health | jq . # Tail backend logs @@ -564,48 +689,48 @@ ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ "docker logs -f escrow-backend --tail 100" ``` -> CI ✓ green does NOT guarantee the new image was pushed to the registry. Always verify `curl /api/version` returns the expected version. See [[woodpecker_silent_build_fail]]. +> CI ✓ green does NOT guarantee the new image was pushed. Always verify `curl /api/version` returns the expected version. See [[woodpecker_silent_build_fail]]. --- ## 10. Dev vs Prod Differences -| Aspect | dev-amn (dev.amn.gg) | Prod (amn.gg) | +| Aspect | dev-amn (`dev.amn.gg`) | Prod (`amn.gg`) | |---|---|---| | Compose file | `deployment/dev-amn/docker-compose.yml` | Separate prod stack (not in this repo) | | Image registry | `git.tbs.amn.gg/escrow` | Same registry, prod tags | | Image tag | `:dev` | `:latest` or versioned | -| MongoDB | Present (dev parity) | Retired | +| MongoDB | Present (dev parity — retired in prod) | Not present | | `ENABLE_TESTNET_CHAINS` | `true` (compose override) | Not set / `false` | -| `NODE_ENV` | `production` (same) | `production` | +| `NODE_ENV` | `production` | `production` | | `NEXT_PUBLIC_IS_DEVELOPMENT` | `false` | `false` | | `PAYMENT_PROVIDER_MODE` | `live` | `live` | -| `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` | can be `true` for RN testing | `false` | +| `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` | May be `true` for RN testing | `false` | +| `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` | `12` (default) | `12` (default) | +| Gatus monitoring | Monitors both dev + prod endpoints | Shared Gatus instance | +| TLS | Cloudflare proxy → Caddy (`disable_redirects`) | Same | +| Version bump | Required before CI push | Required | | Watchtower labels | Present in legacy compose | Prod stack may differ | -| Gatus monitoring | Monitors both dev + prod endpoints | N/A (shared gatus instance) | -| TLS | Cloudflare proxy → Caddy (disable_redirects) | Same | -| Version bump requirement | Required before CI push | Required | --- ## 11. Secret Management -**The `.env` file on the server is the single source of runtime secrets. It is never committed.** +The `.env` file on the server is the single source of runtime secrets. It is never committed. -- Location on server: `/opt/arcane/data/projects/escrow-dev/.env` -- Permissions: `chmod 600` owned by root -- Reference template: `deployment/.env` (in repo — contains live dev values, treated as low-sensitivity dev config; rotate before prod use) -- `.gitleaks.toml` in `deployment/` configures secret scanning exclusions for the repo +- **Server location:** `/opt/arcane/data/projects/escrow-dev/.env` +- **Permissions:** `chmod 600`, owned by root +- **Repo template:** `deployment/.env` — contains live dev values, treated as low-sensitivity dev config; rotate all values before using in production ### Rules 1. Never commit `.env` or any file containing real tokens, passwords, or private keys. 2. Never pass secrets as Dockerfile `ARG`/`ENV` at build time — they appear in image layers. All secrets are runtime-injected via `env_file`. -3. `NEXT_PUBLIC_*` vars are baked into the frontend bundle at build time. Do not place secrets in any `NEXT_PUBLIC_` variable. -4. Wallet addresses (e.g. `ESCROW_WALLET_ADDRESS`) are public on-chain but still kept out of the repo for operational hygiene. -5. For new deployments: copy `deployment/.env` to the server, fill in real values, then `chmod 600`. -6. Gatus bot token and chat ID go into the same `.env` — they are read by the gatus container via `environment:` directives. -7. Telegram bot tokens are high-value secrets — rotate immediately if accidentally pushed. +3. `NEXT_PUBLIC_*` vars are baked into the frontend bundle. Never place secrets in them. +4. Wallet addresses are public on-chain but still kept out of the repo for operational hygiene. +5. For new deployments: copy `deployment/.env` to the server, fill in real values, `chmod 600`. +6. Gatus vars (`GATUS_TELEGRAM_BOT_TOKEN`, `GATUS_TELEGRAM_CHAT_ID`) go into the same `.env`. +7. Telegram bot tokens are high-value — rotate immediately if accidentally pushed. ### Sensitive variable groups @@ -614,7 +739,7 @@ ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \ | JWT | `JWT_SECRET` | Full session forgery | | DB credentials | `POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `MONGO_INITDB_ROOT_PASSWORD` | Database access | | Payment webhook secrets | `REQUEST_NETWORK_WEBHOOK_SECRET`, `DEPAY_WEBHOOK_SECRET`, `SHKEEPER_CALLBACK_SECRET`, `SHKEEPER_WEBHOOK_SECRET` | Fake payment injection | -| Bot tokens | `TELEGRAM_BOT_TOKEN`, `TG_NOTIFY_BOT_TOKEN` | Bot takeover | +| Bot tokens | `TELEGRAM_BOT_TOKEN`, `TG_NOTIFY_BOT_TOKEN` | Bot takeover / webhook hijack | | OAuth secrets | `GOOGLE_CLIENT_SECRET` | OAuth impersonation | | API keys | `OPENAI_API_KEY`, `REQUEST_NETWORK_API_KEY`, `SHKEEPER_API_KEY` | Billing / data access | | Sentry DSN | `SENTRY_DSN` | Error data exfiltration | diff --git a/10 - Services/frontend.md b/10 - Services/frontend.md index 5d5d9d5..236cb18 100644 --- a/10 - Services/frontend.md +++ b/10 - Services/frontend.md @@ -2,7 +2,7 @@ title: Frontend Service — amn-frontend tags: [service, frontend, nextjs, react, web3, telegram] created: 2026-06-08 -updated: 2026-06-08 +updated: 2026-06-12 --- # Frontend Service — amn-frontend @@ -14,355 +14,305 @@ updated: 2026-06-08 | Field | Value | |---|---| | Package name | `amn-frontend` | -| Version | **2.10.5** | -| Status | Active — deployed on `dev.amn.gg` | +| Version | **2.11.89** | +| Status | Active — `main` deployed to `dev.amn.gg`; `feature/white-label-shops` deployed to `multi.amn.gg` | | Framework | Next.js 16 (App Router + Turbopack), React 19, TypeScript strict | | Dev port | `8083` (both local and Docker) | | Package manager | `yarn@1.22.22` | -| Node requirement | `>=20` (host runs v26.0.0) | -| Repo | `git@git.manko.yoga:222/nick/frontend.git` | +| Node requirement | `>=20` | +| Repo | `git@git.tbs.amn.gg:escrow/frontend.git` | -The app covers the full escrow lifecycle: request creation, multi-seller offer collection, negotiation, on-chain payment (BSC/ETH/Base), delivery confirmation, dispute handling, loyalty points, and a Telegram Mini App shell for mobile-native access. +The app covers the full escrow lifecycle: request creation, multi-seller offer collection, negotiation, on-chain payment (BSC/ETH/Base/TON), delivery confirmation, dispute handling, loyalty points, tenant admin for white-label shops, public seller-shop browsing, and a Telegram Mini App shell for mobile-native access. + +> [!note] Multi-shop branch +> `feature/white-label-shops` adds `TenantProvider`, `/dashboard/admin/tenants`, custom-domain controls, bot activation links, and the `WEBAPP_ENABLED` middleware gate that keeps `multi.amn.gg` Mini App-first while leaving dashboard/auth routes reachable. --- ## 2. Tech Stack -### Core - | Layer | Library / Version | Notes | |---|---|---| -| Framework | `next@^16.1.1` | App Router, Turbopack dev, standalone output | -| UI runtime | `react@^19.1.0` + `react-dom@^19.1.0` | | -| Language | TypeScript `^6.0.3` strict | `noEmit` check required before push | -| Component library | `@mui/material@^9.0.1` | MUI v9 + `@mui/lab`, `@mui/x-data-grid`, `@mui/x-date-pickers`, `@mui/x-tree-view` | -| Styling | `@emotion/react` + `@emotion/styled` + `stylis-plugin-rtl` | RTL support via stylis | -| Animation | `framer-motion@^12.13.0` | | -| Icon system | `@iconify/react@^6.0.0` | | - -### Data Fetching & State - -| Layer | Library | Notes | -|---|---|---| -| Server-state cache | `@tanstack/react-query@^5.83.0` | Primary async state manager | -| Lightweight fetch | `swr@^2.3.3` | Used in some hooks alongside RQ | -| HTTP client | `axios@^1.11.0` | Centralized instance with interceptors in `src/lib/axios.ts` | -| Forms | `react-hook-form@^7.77.0` + `zod@^4.0.10` + `@hookform/resolvers@^5.0.1` | | -| Real-time | `socket.io-client@^4.8.1` | `src/contexts/` socket context; used for request/offer/chat events | - -### Web3 - -| Layer | Library | Notes | -|---|---|---| -| Wallet connection | `wagmi@^2.19.5` | Primary Web3 state manager | -| EVM low-level | `viem@^2.31.7` | ABI encoding, RPC calls | -| Compat layer | `ethers@^6.15.0` | Legacy compatibility | -| Chain indexing | `alchemy-sdk@^3.6.1` | Mainnet / Sepolia / Polygon queries | -| TON wallet | `@tonconnect/ui-react@^2.4.4` + `@ton/core@^0.63.1` | TON Connect in Telegram Mini App | +| Framework | `next@^16.1.1` | App Router, Turbopack dev server | +| UI runtime | `react@^19.1.0`, `react-dom@^19.1.0` | Server + Client Components | +| Component library | `@mui/material@^9.0.1` | MUI v9 with Emotion; `@mui/lab`, `@mui/x-data-grid`, `@mui/x-date-pickers`, `@mui/x-tree-view` | +| Styling engine | `@emotion/react`, `@emotion/styled`, `stylis-plugin-rtl` | RTL support via stylis | +| State / data fetching | `@tanstack/react-query@^5.83.0`, `swr@^2.3.3` | TanStack Query is primary; SWR used in some legacy paths | +| Real-time | `socket.io-client@^4.8.1` | Bidirectional events; custom `SocketContext` | +| Forms | `react-hook-form@^7.77.0`, `@hookform/resolvers@^5.0.1`, `zod@^4.0.10` | Schema validation via Zod v4 | +| i18n | `i18next@^26.3.0`, `react-i18next@^17.0.8` | 6 locales (en, fa, ar, fr, cn, vi); RTL for fa/ar | +| Web3 — EVM | `wagmi@^2.19.5`, `viem@^2.31.7`, `ethers@^6.15.0` | WalletConnect + MetaMask + Trezor | +| Web3 — TON | `@tonconnect/ui-react@^2.4.4`, `@ton/core@^0.63.1` | TON wallet payments | | Hardware wallet | `@trezor/connect-web@^9.7.3` | Trezor signing flow | - -### Internationalization & Localisation - -| Layer | Library | Notes | -|---|---|---| -| i18n engine | `i18next@^26.3.0` + `react-i18next@^17.0.8` | | -| Language detection | `i18next-browser-languagedetector@^8.1.0` | | -| Lazy loading | `i18next-resources-to-backend@^1.2.1` | | -| Persian date | `date-fns-jalali@^4.1.0-0` | Jalali calendar date formatting | -| RTL styling | `stylis-plugin-rtl@^2.1.1` | Emotion cache flips properties for RTL | - -### Observability & Testing - -| Layer | Library | Notes | -|---|---|---| -| Error tracking | `@sentry/nextjs@^10.22.0` | Configured in `src/instrumentation.ts` | -| Unit tests | `jest@^30.4.2` + `@testing-library/react@^16.3.0` | | -| E2E tests | `@playwright/test@^1.56.1` | `e2e/` directory; performance spec included | -| Notifications | `notistack@^3.0.2` + `sonner@^2.0.3` | Toast system | - -### Editor & Rich Content - -| Layer | Library | Notes | -|---|---|---| -| Rich text | `@tiptap/react@^3.23.6` + extensions | Code blocks, links, images, alignment, underline | -| Markdown render | `react-markdown@10.1.0` + rehype plugins | With GFM, syntax highlight, sanitization | -| Maps | `mapbox-gl@^3.12.0` + `react-map-gl@^8.0.4` | Address / delivery location picker | -| Charts | `react-apexcharts@^2.1.0` | Dashboard KPI charts | -| Carousels | `embla-carousel-react@8.6.0` | Auto-scroll and autoplay plugins | +| Chain indexing | `alchemy-sdk@^3.6.1` | Alchemy for multi-chain queries | +| Rich text editor | `@tiptap/react@^3.23.6` + extensions | Used in post/blog editor | +| Charts | `apexcharts@^5.10.1`, `react-apexcharts@^2.1.0` | Dashboard KPI charts | +| Animation | `framer-motion@^12.13.0` | Page transitions and UI motion | +| Carousel | `embla-carousel-react@8.6.0` | Product / shop carousels | +| Maps | `mapbox-gl@^3.12.0`, `react-map-gl@^8.0.4` | Address / location pickers | +| HTTP client | `axios@^1.11.0` | Centralised instance with auth interceptors in `src/lib/axios.ts` | +| Notifications | `notistack@^3.0.2`, `sonner@^2.0.3` | Snackbar + toast | +| Error monitoring | `@sentry/nextjs@^10.22.0` | SDK wraps Next.js build + runtime | +| CAPTCHA | `@marsidev/react-turnstile@^1.5.2` | Cloudflare Turnstile | +| Dates | `dayjs@^1.11.13`, `date-fns-jalali@^4.1.0-0` | Jalali (Persian) calendar support | +| QR code | `qrcode@^1.5.4` | Wallet payment QR generation | +| Fonts | DM Sans, Inter, Nunito Sans, Public Sans, Barlow | Variable fonts via `@fontsource-variable` | --- ## 3. App Router Page Structure -The Next.js App Router root is `src/app/`. Pages are thin wrappers that import a View component from `src/sections//view/`. No business logic lives in `page.tsx` files. +All routes live under `frontend/src/app/`. The dev server and Docker container both bind port `8083`. -### Top-level routes +### Top-level route segments -| Route segment | Type | Purpose | +| Route | Type | Purpose | |---|---|---| -| `/` | Public | Landing / marketing page | -| `/api/health` | API route | Health check endpoint | -| `/api/llm` | API route | LLM proxy (amanat-assist integration) | -| `/auth/jwt/*` | Auth | Sign-in, sign-up, verify email, reset password, update password | -| `/checkout/` | Protected | Checkout flow entry (redirects to payment) | -| `/dashboard/` | Protected | Main authenticated shell (see sub-routes below) | -| `/design-preview/` | Dev | Component / theme preview (non-production) | -| `/error/` | Public | Global error page | -| `/payment/` | Protected | Payment status / callback landing | -| `/post/[slug]` | Public | Blog / post reader | -| `/shop/[seller]/[id]` | Public | Public seller shop and product view | -| `/telegram/` | Mini App | Telegram Mini App shell (dedicated layout, see §7) | -| `not-found.tsx` | Public | 404 page | +| `/` | Public | Landing / marketing home | +| `/api/health` | API Route | Container health-check endpoint | +| `/api/llm` | API Route | LLM proxy for amanat-assist features | +| `/auth/jwt/*` | Public | Sign-in, sign-up, OTP verify, password reset, update | +| `/checkout/request-network/*` | Public | Request Network payment checkout shell | +| `/dashboard/*` | Protected | Main authenticated app (see below) | +| `/design-preview` | Internal | Theme/component sandbox | +| `/error` | Public | Global error display | +| `/payment/callback`, `/payment/cancel` | Public | Payment gateway redirect landing | +| `/post/[slug]` | Public | Blog post reader | +| `/shop/[seller]/[id]` | Public | Public seller shop / item view | +| `/store/items`, `/store/checkout` | Public | Storefront browsing and checkout | +| `/telegram` | Mini App | Telegram Mini App shell (see §7) | -### Dashboard sub-routes (`/dashboard/*`) +### Dashboard sub-routes (AuthGuard + EmailVerificationGuard) -All dashboard routes are wrapped in `AuthGuard` + `EmailVerificationGuard`. - -| Sub-route | Purpose | +| Route | Purpose | |---|---| -| `account/` | Profile, avatar, address book, notification prefs, passkey, wallet linking | -| `admin/` | Admin control panel | -| `assist/` | AI assistant chat (amanat-assist integration) | -| `chat/` | Real-time escrow negotiation chat | -| `disputes/` | Dispute hub — raise, view, respond | -| `payment/` | Payment history and detail view | -| `points/` | Loyalty hub — transaction log, referral tracking, level tiers | -| `post/` | Admin blog editor (Tiptap) | -| `request/` | Buyer purchase request management (create, track, accept offer) | -| `request-template/` | Seller request templates management | -| `seller/` | Seller profile and analytics | -| `shop-settings/` | Seller shop configuration (name, policies, payment rails) | -| `shops/` | Browse shops / checkout within dashboard scope | -| `user/` | Admin user management | +| `/dashboard` → `/dashboard/overview` | KPI home tiles, recent activity | +| `/dashboard/chat` | Real-time escrow chat | +| `/dashboard/account/*` | Profile, address, notifications, wallet, passkey | +| `/dashboard/request/*` | Buyer purchase requests | +| `/dashboard/request-template/*` | Seller request templates | +| `/dashboard/payment/*` | Payment history and detail | +| `/dashboard/points/*` | Loyalty hub — transactions, referrals, levels | +| `/dashboard/disputes/*` | Dispute creation and management | +| `/dashboard/seller/*` | Seller-side offer management | +| `/dashboard/shop-settings/*` | Seller shop configuration (incl. Telegram config) | +| `/dashboard/shops/*` | Browse / checkout from within dashboard | +| `/dashboard/user/*` | Admin user management | +| `/dashboard/post/*` | Admin blog editor (Tiptap) | +| `/dashboard/admin/tenants/*` | Tenant admin (white-label shops; `feature/white-label-shops` only) | +| `/dashboard/assist/*` | AI assistant (amanat-assist) | --- -## 4. Key Sections & Features +## 4. Key Sections / Features -### Marketplace +### Marketplace and escrow flow -- `src/sections/shop-settings/` — seller configures shop, accepted payment chains/tokens, delivery policy. -- `src/sections/request/` — core escrow lifecycle feature. Status flow: - ``` - pending_payment → pending → active → received_offers → in_negotiation - → payment → processing → delivery → delivered → confirming → seller_paid → completed - (or cancelled at most stages) - ``` -- Shared status/urgency color and label maps live in `src/sections/request/constants.ts`. Do not redefine per-view; use `getStatusColor / getStatusLabel / getUrgencyColor / getUrgencyLabel`. -- Role-based views (buyer / seller / admin) dispatched from `role-based--view.tsx` components. +The primary buyer journey: -### Escrow Flow +1. Buyer submits a **purchase request** (`/dashboard/request/new`) — product description, budget, chain preference. +2. Sellers see the request and submit **offers** via request templates (`/dashboard/request-template`). +3. Buyer selects an offer; both sides enter the **escrow chat** (`/dashboard/chat`). +4. Buyer initiates payment — on-chain via Wagmi/Trezor or off-chain via Request Network. +5. After delivery, buyer releases escrow funds; on dispute, both parties access `/dashboard/disputes`. -The escrow flow spans multiple sections: +### Dashboard -1. **Buyer** creates a purchase request (`/dashboard/request/new`) — wizard in `src/sections/request/components/steps/`. -2. **Sellers** receive notifications via Socket.io and submit offers (`received_offers` state). -3. **Negotiation** phase: real-time chat (`/dashboard/chat/`) with offer counter-proposals. -4. **Payment**: buyer pays on-chain (BSC primary, ETH/Base/Polygon/Arbitrum supported). Funds held in escrow wallet (`NEXT_PUBLIC_ESCROW_WALLET_ADDRESS`). USDT is the primary escrow currency; BSC USDT uses 18 decimals (non-standard — handled in `src/utils/currencyUtils.ts`). -5. **Delivery & confirmation**: seller marks delivered, buyer confirms → `confirming → seller_paid → completed`. -6. **Disputes**: either party can raise at `src/sections/dispute/`. +Multi-role dashboard accessible post-login. Guards: +- `AuthGuard` — redirects unauthenticated users to `/auth/jwt/sign-in`. +- `EmailVerificationGuard` — blocks unverified accounts on key routes. -### Dashboard & Admin +Sidebar nav adapts to role: buyer, seller, admin, or multi-tenant operator. -- Overview tiles with ApexCharts KPI cards. -- Admin panel: user management, shop review, dispute arbitration, blog post management. -- Points / loyalty system: transaction ledger, referral tracking, tier levels at `src/sections/points/`. -- AI assist panel: embedded `amanat-assist` chat at `/dashboard/assist/`. +### Admin + +`/dashboard/user` and `/dashboard/post` are admin-only sections gated by role check in the layout. Tenant admin (`/dashboard/admin/tenants`) is only visible on the `feature/white-label-shops` build. ### Telegram Mini App -See §7 for full detail. +Full Telegram Mini App (TMA) at `/telegram`. See §7 for integration details. + +### White-label shops + +`feature/white-label-shops` branch adds multi-tenancy: `TenantProvider` (Context), `/dashboard/admin/tenants` CRUD, custom domain config per tenant, and a `WEBAPP_ENABLED` middleware flag to route Mini App-first for `multi.amn.gg`. + +### Blog / content + +Public blog at `/post/[slug]` rendered server-side. Admin writes posts via Tiptap rich-text editor at `/dashboard/post`. + +### AI assistant (amanat-assist) + +`/api/llm` proxies requests to the amanat-assist backend service. The dashboard `/assist` section provides the in-app chat interface. --- ## 5. State Management -The app uses a layered approach — no single global store: - -| Layer | Tool | Scope | +| Layer | Mechanism | Usage | |---|---|---| -| Server state & cache | `@tanstack/react-query` | All API calls — fetching, mutations, invalidation | -| Supplementary fetch | `swr` | Some lightweight hooks | -| Local component state | `React.useState` / `useReducer` | Component-local UI state | -| Cross-tree shared state | React Context | Socket connection (`src/contexts/`), Auth (`src/auth/context/`), Web3, Settings drawer, Localization | -| Form state | `react-hook-form` | All form instances, with `zod` schemas as resolvers | -| Settings (theme/locale) | Context + `localStorage` | Theme mode, layout direction, color preset, font — managed by `src/settings/` | +| Server cache / async state | `@tanstack/react-query` | All API data fetching, mutation, background refetch | +| Legacy async state | `swr` | Some older sections not yet migrated to TQ | +| Real-time events | `SocketContext` (`src/contexts/`) | Socket.io connection; exposes socket via `useSocket` hook | +| Global UI state | React Context (multiple providers) | Auth, Settings (theme/direction/language), Web3 | +| Form state | `react-hook-form` + Zod | All forms; validation on client | +| Zustand | Not in use | No Zustand dependency in package.json | -There is no Zustand or Redux in the dependency tree. Global state is passed via Context providers stacked in `src/app/layout.tsx`. +The root layout stacks providers in order: `ThemeProvider` → `SettingsProvider` → `AuthProvider` → `QueryClientProvider` → `SocketContextProvider` → `Web3Provider`. -Key contexts: - -- `SocketContext` (`src/contexts/`) — wraps `socket.io-client`, exposes live event subscriptions. -- `AuthContext` (`src/auth/context/`) — JWT session, user object, sign-in/out actions. -- `Web3Context` / wagmi `WagmiProvider` (`src/web3/context/`) — wallet connection, chain switching. -- `SettingsContext` (`src/settings/`) — UI preferences (RTL, color scheme, font). -- `LocalizationProvider` (`src/locales/`) — i18next + MUI date picker locale. +The Telegram layout uses a minimal provider stack: `TonConnectUIProvider` + `QueryClientProvider` only — no dashboard providers. --- ## 6. Internationalization -The app is RTL-first with Persian (Farsi) as the primary production language. - -| Aspect | Implementation | +| Detail | Value | |---|---| -| Engine | `i18next` + `react-i18next` | -| Supported languages | `fa` (Persian), `ar` (Arabic), `en` (English), `fr` (French), `cn` (Chinese), `vi` (Vietnamese) | -| Translation files | `src/locales/langs//*.json` — split by feature namespace | -| RTL flip | `stylis-plugin-rtl` applied to the Emotion cache — physical CSS properties (margin-left, padding-right, etc.) are automatically mirrored | -| LTR islands | Inline `dir="ltr"` on elements containing URLs, wallet addresses, token amounts, or other inherently LTR content | -| Persian calendar | `date-fns-jalali` for Jalali date formatting; MUI date pickers use the Jalali locale adapter | -| Direction state | Controlled via `SettingsContext` — users can toggle in the settings drawer | -| Config | `src/locales/locales-config.ts` + `src/locales/i18n-provider.tsx` | +| Library | `i18next@^26.3.0` + `react-i18next@^17.0.8` | +| Language detection | `i18next-browser-languagedetector` + `accept-language` (server hint) | +| Locales shipped | English (`en`), Persian/Farsi (`fa`), Arabic (`ar`), French (`fr`), Chinese (`cn`), Vietnamese (`vi`) | +| RTL locales | `fa`, `ar` — `direction: rtl` applied at theme level; `stylis-plugin-rtl` transforms MUI Emotion styles | +| Jalali calendar | `date-fns-jalali@^4.1.0-0` — date pickers switch to Jalali for `fa` locale | +| Translation files | `src/locales/langs/{en,fa,ar,fr,cn,vi}/*.json` (lazy-loaded via `i18next-resources-to-backend`) | +| Telegram locales | `src/sections/telegram/locales/{en,fa}.ts` — standalone namespace for TMA strings | +| Default locale | Determined by browser; Persian is the primary product locale | -Language detection priority: URL `?lng=` param → browser `Accept-Language` → localStorage fallback → `fa`. +RTL layout direction is set in `SettingsProvider` and passed to the MUI theme. The `stylis-plugin-rtl` plugin auto-mirrors margin/padding/float/border-radius CSS properties. --- ## 7. Telegram Mini App Integration -The Telegram Mini App (TMA) is a first-class feature with a dedicated route segment, layout, and 40+ purpose-built components. +### Loading mechanism -### Loading & Auth Flow +`/app/telegram/layout.tsx` injects the Telegram SDK via `next/script` with `strategy="beforeInteractive"`: -- The TMA loads via Telegram's `webApp.openWebApp()` into the `/telegram/` route. -- The root layout at `src/app/telegram/layout.tsx` provides a minimal provider stack: - - `TonConnectUIProvider` (TON wallet) - - No standard app shell (no top nav, no side drawer) — uses native Telegram chrome instead. -- User identity: `window.Telegram.WebApp.initData` is parsed by `src/utils/telegram-webapp.ts` (a custom wrapper around the `window.Telegram` global — **no `@twa-dev` or `@telegram-apps` SDK package** is used). -- Auth is linked to the existing JWT session: on first open the app prompts the user to connect their AMN account (onboarding sheet). Subsequent opens re-use the stored token. +```tsx +