docs: sync vault with codebase state (2026-06-12) #2
2
.obsidian/graph.json
vendored
2
.obsidian/graph.json
vendored
@@ -17,6 +17,6 @@
|
|||||||
"repelStrength": 10,
|
"repelStrength": 10,
|
||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 250,
|
"linkDistance": 250,
|
||||||
"scale": 0.1383377348281637,
|
"scale": 0.19993564150556878,
|
||||||
"close": true
|
"close": true
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,18 @@ created: 2026-05-23
|
|||||||
|
|
||||||
## The 10,000-foot view
|
## 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.
|
- **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** (`/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.
|
- **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
|
## System map
|
||||||
|
|
||||||
@@ -40,6 +46,7 @@ flowchart TB
|
|||||||
SocketS["Socket.IO server<br/>rooms per user / chat / request"]
|
SocketS["Socket.IO server<br/>rooms per user / chat / request"]
|
||||||
Auth["Auth service<br/>JWT + Passkey + Google + Telegram"]
|
Auth["Auth service<br/>JWT + Passkey + Google + Telegram"]
|
||||||
Market["Marketplace service<br/>Requests, Offers, Templates"]
|
Market["Marketplace service<br/>Requests, Offers, Templates"]
|
||||||
|
TenantSvc["Tenant service<br/>host resolution + domain + bot"]
|
||||||
ChatSvc["Chat service"]
|
ChatSvc["Chat service"]
|
||||||
PaySvc["Payment service<br/>Request Network + ledger + custody controls"]
|
PaySvc["Payment service<br/>Request Network + ledger + custody controls"]
|
||||||
TelegramSvc["Telegram service<br/>bot + Mini App + notifications"]
|
TelegramSvc["Telegram service<br/>bot + Mini App + notifications"]
|
||||||
@@ -52,8 +59,7 @@ flowchart TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph Data["Data tier"]
|
subgraph Data["Data tier"]
|
||||||
Mongo[("MongoDB<br/>via Mongoose<br/>primary runtime")]
|
PG[("PostgreSQL 18<br/>Drizzle repositories")]
|
||||||
PG[("PostgreSQL 18<br/>Drizzle migration layer")]
|
|
||||||
RedisDB[("Redis<br/>cache + locks")]
|
RedisDB[("Redis<br/>cache + locks")]
|
||||||
Disk[("Local disk<br/>/uploads")]
|
Disk[("Local disk<br/>/uploads")]
|
||||||
end
|
end
|
||||||
@@ -81,11 +87,10 @@ flowchart TB
|
|||||||
ClientJS --> REST
|
ClientJS --> REST
|
||||||
SocketC <--> SocketS
|
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
|
SocketS --> ChatSvc & Notif & Market
|
||||||
|
|
||||||
Auth & Market & ChatSvc & PaySvc & Disp & Points & BlogSvc & TelegramSvc --> Mongo
|
Auth & Market & TenantSvc & ChatSvc & PaySvc & Disp & Points & BlogSvc & TelegramSvc --> PG
|
||||||
PaySvc -.->|oracle payment_quotes when enabled| PG
|
|
||||||
Auth & PaySvc & Notif --> RedisDB
|
Auth & PaySvc & Notif --> RedisDB
|
||||||
Files --> Disk
|
Files --> Disk
|
||||||
|
|
||||||
@@ -96,6 +101,7 @@ flowchart TB
|
|||||||
PaySvc -.tx fetch.-> Alchemy
|
PaySvc -.tx fetch.-> Alchemy
|
||||||
|
|
||||||
TelegramSvc <--> TelegramAPI
|
TelegramSvc <--> TelegramAPI
|
||||||
|
TenantSvc <--> TelegramAPI
|
||||||
TelegramAPI -.webhook.-> TelegramSvc
|
TelegramAPI -.webhook.-> TelegramSvc
|
||||||
Auth --> TelegramAPI
|
Auth --> TelegramAPI
|
||||||
Notif --> SMTP
|
Notif --> SMTP
|
||||||
@@ -151,7 +157,7 @@ Chat is built on Socket.IO rooms. Every entity that needs live updates gets its
|
|||||||
- `buyer-<id>` / `seller-<id>` — marketplace-wide updates
|
- `buyer-<id>` / `seller-<id>` — marketplace-wide updates
|
||||||
- `sellers` / `buyers` — global broadcast pools
|
- `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]]
|
### Notifications — [[Notifications]]
|
||||||
|
|
||||||
@@ -164,7 +170,7 @@ Push and SMS are tracked as **planned** in `backend/TODO.md`.
|
|||||||
|
|
||||||
### Disputes — [[Dispute System]]
|
### 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
|
> [!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.
|
> 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.
|
- [[Roles & Personas]] — who does what in the system.
|
||||||
- [[Glossary]] — a domain dictionary you will want open in another pane.
|
- [[Glossary]] — a domain dictionary you will want open in another pane.
|
||||||
- [[01 - Architecture]] — service boundaries, module layout, and deployment topology.
|
- [[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.
|
- [[03 - API Reference]] — every endpoint, its payload, and its auth requirements.
|
||||||
- [[04 - Flows]] — diagrammed user journeys for every major use case.
|
- [[04 - Flows]] — diagrammed user journeys for every major use case.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: Scanner Architecture
|
title: Scanner Architecture
|
||||||
tags: [architecture, scanner, payment]
|
tags: [architecture, scanner, payment]
|
||||||
created: 2026-05-30
|
created: 2026-05-30
|
||||||
updated: 2026-06-08
|
updated: 2026-06-12
|
||||||
---
|
---
|
||||||
|
|
||||||
# Scanner Architecture
|
# 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
|
- Retry failed webhook deliveries with exponential back-off
|
||||||
- Expire stale pending intents on a configurable TTL
|
- Expire stale pending intents on a configurable TTL
|
||||||
- Read an EVM ERC-20 balance on demand (`POST /balances/check`)
|
- 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`).
|
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 |
|
| BNB Smart Chain | 56 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 200 blocks | **true** |
|
||||||
| Ethereum Mainnet | 1 | EVM | `0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C` | 50 blocks | Yes |
|
| Ethereum Mainnet | 1 | EVM | `0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C` | 50 blocks | **true** |
|
||||||
| BSC Testnet | 97 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 5 blocks | Yes (testnet) |
|
| BNB Smart Chain Testnet | 97 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 5 blocks | **true** (testnet) |
|
||||||
| Arbitrum One | 42161 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 2400 blocks | No |
|
| Arbitrum One | 42161 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 2400 blocks | false |
|
||||||
| Polygon | 137 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 300 blocks | No |
|
| Polygon | 137 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 300 blocks | false |
|
||||||
| Base | 8453 | EVM | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` | 300 blocks | No |
|
| Base | 8453 | EVM | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` | 300 blocks | false |
|
||||||
| Tron Mainnet | 728126428 | Tron | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` (USDT TRC20) | 200 (API-confirmed) | No |
|
| Tron Mainnet | 728126428 | Tron | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` (USDT TRC20 contract) | 200 (API-confirmed) | false |
|
||||||
| TON Mainnet | 1100 | TON | `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` (USDT Jetton master) | 120 (API-finalized) | No |
|
| TON Mainnet | 1100 | TON | `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` (USDT Jetton master) | 120 (API-finalized) | false |
|
||||||
|
|
||||||
> [!note] Proxy address variations
|
> [!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).
|
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 <callbackUrl>` | Payment confirmed; signed with `X-AMN-Signature` HMAC-SHA256 |
|
| Scanner → Backend | `POST <callbackUrl>` | 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 /balances/check` | Synchronous ERC-20 balance read (direct-address rail) |
|
||||||
| Backend → Scanner | `POST /balance-watches` | Start async balance watch (direct-address rail) |
|
| Backend → Scanner | `POST /balance-watches` | Start async balance watch (direct-address rail) |
|
||||||
| Scanner → Backend | `POST <callbackUrl>` | Balance changed; `X-AMN-Event-Type: balance_changed` |
|
| Backend → Scanner | `GET /balance-watches/{id}` | Get balance-watch status |
|
||||||
| Backend → Scanner | `DELETE /balance-watches/{id}` | Stop watch after payment accepted or cancelled |
|
| Scanner → Backend | `POST <callbackUrl>` | 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 | `GET /scanner/status` | Chain lag + pending counts (ops/monitoring) |
|
||||||
| Backend → Scanner | `POST /admin/webhooks/retry` | Force re-delivery of `webhook_failed` intents |
|
| 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 |
|
| 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. |
|
| 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. |
|
| 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 but checkout still uses the v0.1.0 ABI. |
|
| 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`. |
|
||||||
|
|||||||
289
02 - Data Models/Tenant.md
Normal file
289
02 - Data Models/Tenant.md
Normal file
@@ -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 <token>` 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 <claimToken>` 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]].
|
||||||
459
03 - API Reference/Tenant API.md
Normal file
459
03 - API Reference/Tenant API.md
Normal file
@@ -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 <jwt>` 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 `<numeric_id>:<secret>` 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://<tenantSlug>.<TENANT_BASE_DOMAIN>`. The menu button opens `<miniAppUrl>/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 <claimToken>` 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]].
|
||||||
@@ -6,8 +6,8 @@ related_apis: ["POST /api/auth/telegram", "[[Auth API]]"]
|
|||||||
task: "5.4"
|
task: "5.4"
|
||||||
---
|
---
|
||||||
|
|
||||||
> **Last updated:** 2026-06-08
|
> **Last updated:** 2026-06-12
|
||||||
> **Status:** IN PROGRESS — Task 5.4 (dependencies: 5.1 auth infra, 5.2 Telegram sign-in endpoint)
|
> **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+
|
> **Frontend branch:** `integrate-main-into-development` · v2.8.94+
|
||||||
> **Entry point:** `src/sections/telegram/` · route `/telegram`
|
> **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
|
## 2. Launch Points
|
||||||
|
|
||||||
| Entry | Mechanism | `startapp` context |
|
| Entry | Mechanism | `startapp` context | Result |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| Bot profile | User opens bot → taps "Open App" | none |
|
| 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 |
|
| Menu button | Pinned button in any chat with the bot | none | Shell loads at Home tab |
|
||||||
| Inline button | Bot sends a card with an embedded button | `req_<requestId>` |
|
| Inline button | Bot sends a message card with an embedded button | `req_<requestId>` | Shell loads; request deep-link (see below) |
|
||||||
| Direct deep link | `https://t.me/AmanehBot/app?startapp=req_<id>` | `req_<requestId>` |
|
| Direct deep link | `https://t.me/AmanehBot/app?startapp=req_<id>` | `req_<requestId>` | Shell loads; request deep-link (see below) |
|
||||||
| Web fallback | Browser at `/telegram` | none (unsupported state) |
|
| 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_<requestId>`, 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
|
### 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)
|
### 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`.
|
- Fetches user points via `use-telegram-points.ts`.
|
||||||
- Shows points balance and transaction history.
|
- 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'`.
|
- `TelegramNotificationsView` is rendered as `overlayScreen = 'notifications'`.
|
||||||
- Fetches via `useTelegramNotifications` → `getNotifications(userId, 1, 50)` → `GET /api/notifications?userId=...&page=1&limit=50`.
|
- 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.
|
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)
|
## 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Client matrix QA (iOS/Android/Desktop) | Pending | Needs cross-platform testing pass |
|
||||||
|
|
||||||
### Open Items
|
### Open Items
|
||||||
|
|
||||||
1. **`startapp` deep link routing:** if `context.startParam` matches `req_<id>`, auto-open `TelegramRequestDetailView` on first render.
|
1. **`startapp` deep link routing:** if `context.startParam` matches `req_<id>`, 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 updates (follow-up from client-side fix in v2.8.4).
|
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. **Client matrix QA:** iOS Telegram, Android Telegram, Telegram Desktop, and web clients all need a full feature pass.
|
3. **In-shell dispute filing:** add `TelegramDisputeView` matching the web dashboard dispute surface; currently only accessible via `openTelegramExternalLink` escape hatch.
|
||||||
4. **Review prompt:** `TelegramReviewPrompt` component exists but integration point (post-payment / post-delivery) is TBD.
|
4. **Review prompt:** wire `TelegramReviewPrompt` to trigger after payment confirmation or delivery acknowledgement.
|
||||||
5. **Archived chats:** `TelegramArchivedChatsView` exists but is not yet surfaced in the navigation.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
215
04 - Flows/Tenant Storefront Flow.md
Normal file
215
04 - Flows/Tenant Storefront Flow.md
Normal file
@@ -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 `<slug>.amn.gg` automatically once active. Custom domains are now implemented through DNS verification plus dynamic Caddy Admin API routes in the multi-stack.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Seller
|
||||||
|
participant API as Backend
|
||||||
|
participant DNS as Seller DNS
|
||||||
|
participant Caddy as infra-caddy
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
Seller->>API: POST /api/tenants/:id/domains { hostname: "shop.example.com" }
|
||||||
|
API->>DB: INSERT tenant_domains status=pending tlsStatus=pending
|
||||||
|
API-->>Seller: 201 { domain, status: "pending", verificationToken }
|
||||||
|
|
||||||
|
Seller->>DNS: Add CNAME shop.example.com -> multi.amn.gg
|
||||||
|
|
||||||
|
Seller->>API: POST /api/tenants/:id/domains/:domainId/verify
|
||||||
|
API->>DNS: resolve A/CNAME
|
||||||
|
DNS-->>API: hostname points to configured ingress
|
||||||
|
API->>Caddy: add route for hostname
|
||||||
|
API->>DB: UPDATE status=active, tlsStatus=pending
|
||||||
|
API-->>Seller: 200 { dnsVerified: true }
|
||||||
|
|
||||||
|
Seller->>API: POST /api/tenants/:id/domains/:domainId/tls-check
|
||||||
|
API->>Caddy: HTTPS probe
|
||||||
|
API->>DB: UPDATE tlsStatus=issued | pending | failed
|
||||||
|
```
|
||||||
|
|
||||||
|
The background poller also runs `verifyAndProvision()` for pending domains and re-checks active domains whose TLS status is still pending. On backend startup, `syncActiveDomains()` replays active domain routes into Caddy because API-injected routes are not the source of truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Buyer landing — storefront bootstrap
|
||||||
|
|
||||||
|
The frontend fetches `/api/storefront/bootstrap` on every page load. The tenant is resolved entirely server-side from the `Host` header — the browser supplies no tenant hint.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Buyer
|
||||||
|
participant FE as Frontend (TenantProvider)
|
||||||
|
participant API as GET /api/storefront/bootstrap
|
||||||
|
participant MW as tenantResolutionMiddleware
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
Buyer->>FE: Opens shop.example.com (or seller.amn.gg)
|
||||||
|
FE->>API: GET /api/storefront/bootstrap
|
||||||
|
Note right of API: Host: shop.example.com
|
||||||
|
|
||||||
|
API->>MW: tenantResolutionMiddleware
|
||||||
|
MW->>DB: SELECT * FROM tenant_domains WHERE hostname='shop.example.com' AND status='active'
|
||||||
|
DB-->>MW: domain row
|
||||||
|
MW->>DB: SELECT * FROM tenants WHERE id=domain.tenantId AND status='active'
|
||||||
|
DB-->>MW: tenant row
|
||||||
|
MW-->>API: req.tenant = tenant
|
||||||
|
|
||||||
|
API->>DB: SELECT * FROM tenant_payment_policies WHERE tenant_id=...
|
||||||
|
DB-->>API: policy row
|
||||||
|
API-->>FE: 200 { tenantId, slug, brand, features, paymentRails, localeDefaults }
|
||||||
|
|
||||||
|
FE->>FE: TenantProvider stores bootstrap
|
||||||
|
FE->>FE: useTenantTheme() derives CSS vars from brand.primaryColor
|
||||||
|
FE-->>Buyer: Branded storefront renders
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fallback:** If `GET /api/storefront/bootstrap` returns 404 (no tenant for this host), `TenantProvider` uses `AMANAT_DEFAULTS` with `isAmanatDefault: true`. The frontend renders unchanged Amanat branding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tenant resolution paths
|
||||||
|
|
||||||
|
Three resolution paths are supported simultaneously:
|
||||||
|
|
||||||
|
| Host pattern | Example | Resolution method |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `<slug>.amn.gg` | `myshop.amn.gg` | Slug extracted from subdomain label → `findBySlug` |
|
||||||
|
| Custom CNAME | `shop.example.com` | `findByHostname` → `findById` |
|
||||||
|
| Preview (platform only) | `amn.gg/t/:slug/bootstrap` | Slug from URL param, host must be `amn.gg` / `localhost` |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[HTTP Request] --> B{Is host platform base?\namn.gg / localhost}
|
||||||
|
B -- yes + slug param --> C[resolveTenantBySlug\npreviewOnly=true]
|
||||||
|
B -- yes, no slug --> D[req.tenant = undefined\nAmanat default]
|
||||||
|
B -- no --> E{Ends with .amn.gg?}
|
||||||
|
E -- yes, single label --> F[resolveTenantByHost\nfindBySlug]
|
||||||
|
E -- no --> G[resolveTenantByHost\nfindByHostname]
|
||||||
|
C --> H{Found?}
|
||||||
|
F --> H
|
||||||
|
G --> H
|
||||||
|
H -- yes --> I[req.tenant = TenantRecord]
|
||||||
|
H -- no --> D
|
||||||
|
I --> J[Route handler]
|
||||||
|
D --> J
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Telegram bot registration and claim
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Developer
|
||||||
|
participant API as POST /api/tenants/:id/telegram/bot
|
||||||
|
participant BotSvc as tenantBotService
|
||||||
|
participant TG as Telegram Bot API
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
Developer->>API: { botToken, username?, miniAppUrl? }
|
||||||
|
Note right of Developer: botToken is write-only
|
||||||
|
|
||||||
|
API->>BotSvc: registerBot(tenantId, { botToken, username?, miniAppUrl? })
|
||||||
|
BotSvc->>TG: getMe when username omitted
|
||||||
|
BotSvc->>BotSvc: AES-256-GCM encrypt(botToken, TENANT_SECRET_KEY)
|
||||||
|
BotSvc->>BotSvc: generate webhookSecret + claimToken
|
||||||
|
BotSvc->>DB: INSERT tenant_bots (status=pending, encryptedToken, webhookSecret, claimToken)
|
||||||
|
BotSvc->>TG: setWebhook /api/telegram/tenant-webhook/:botId
|
||||||
|
API->>BotSvc: configureBotMenu(bot.id, shopUrl)
|
||||||
|
BotSvc->>TG: setChatMenuButton -> shopUrl/telegram/
|
||||||
|
BotSvc-->>API: public bot record with claimUrl
|
||||||
|
API-->>Developer: 201 { id, telegramBotId, username, status: "pending", claimUrl }
|
||||||
|
|
||||||
|
Developer->>TG: Open claimUrl and send /start <claimToken>
|
||||||
|
TG->>API: POST /api/telegram/tenant-webhook/:botId with secret header
|
||||||
|
API->>BotSvc: claimAdmin(botId, claimToken, telegramUserId)
|
||||||
|
BotSvc->>DB: UPDATE status=active, adminTelegramUserId
|
||||||
|
BotSvc->>TG: send confirmation message
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Payment policy
|
||||||
|
|
||||||
|
Payment rails available to a tenant's buyers are controlled by `tenant_payment_policies`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
PP[tenant_payment_policies] -->|allowedRails| R{Buyer checkout}
|
||||||
|
R -->|amn_escrow| E[Amanat escrow — full protection]
|
||||||
|
R -->|amn_direct| D[Amanat scanner — no escrow hold\nstrict buyer disclosure required]
|
||||||
|
R -->|external_provider| X[External processor — Amanat records evidence only]
|
||||||
|
R -->|manual_invoice| M[Operator / merchant confirms payment]
|
||||||
|
```
|
||||||
|
|
||||||
|
`buyerDisclosureMode = 'strict'` (default) mandates a prominent "not escrow protected" notice when `amn_direct` or external rails are used. The frontend reads `features.escrowCheckout` / `features.directCheckout` from the bootstrap payload to decide which checkout paths to expose.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Frontend context tree
|
||||||
|
|
||||||
|
```
|
||||||
|
<TenantProvider> ← fetches bootstrap, provides useTenant()
|
||||||
|
<ThemeProvider> ← existing MUI theme
|
||||||
|
<App>
|
||||||
|
useTenant() ← brand, features, paymentRails
|
||||||
|
useTenantTheme() ← primaryColor, cssVars (--tenant-primary)
|
||||||
|
```
|
||||||
|
|
||||||
|
`TenantProvider` wraps the application shell. All downstream components read tenant context via `useTenant()`. No tenant-specific props need to be threaded through the component tree.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase roadmap
|
||||||
|
|
||||||
|
| Phase | What ships | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 0 | Drizzle schema (6 tables), enums, repositories, tenant auth roles | ✅ `feature/white-label-shops` |
|
||||||
|
| 1 | Hosted subdomain (`seller.amn.gg`), tenant bootstrap endpoint, `TenantProvider`, admin tenant UI | ✅ `feature/white-label-shops` |
|
||||||
|
| 2 | Custom domain + DNS verification + Caddy route + TLS status checks | ✅ `feature/white-label-shops` |
|
||||||
|
| 3 | Tenant Telegram bot token storage, webhook registration, menu button, admin claim link | Partial — implemented for claim activation; multi-bot notification routing still planned |
|
||||||
|
| 4 | `amn_direct` payment rail + buyer disclosure | ⬜ Planned |
|
||||||
|
| 5 | Catalog / delivery / external payment adapters, billing events, stronger isolation | ⬜ Planned |
|
||||||
|
|
||||||
|
Related: [[Tenant]], [[Tenant API]], [[PRD - Seller-Owned White-Label Shops and Bots]], [[Escrow Flow]], [[Telegram Mini App]].
|
||||||
@@ -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(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.
|
- `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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<!-- Add new entries above this line. Newest at top. -->
|
<!-- Add new entries above this line. Newest at top. -->
|
||||||
|
|||||||
@@ -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 |
|
| [[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 |
|
| [[Logic Audit - 2026-05-24]] | 4 critical · 5 high · 7 medium · 2 low |
|
||||||
| [[Performance Audit - 2026-05-24]] | 6 high · 8 medium · 4 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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
64
09 - Audits/Multi-Shop Branch Project Scan - 2026-06-10.md
Normal file
64
09 - Audits/Multi-Shop Branch Project Scan - 2026-06-10.md
Normal file
@@ -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 <claimToken>` 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]].
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
# 10 - Services
|
# 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.
|
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.
|
||||||
|
|
||||||
See also: [[01 - Architecture]] · [[08 - Operations]] · [[03 - API Reference]]
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Service Inventory
|
## Service Directory
|
||||||
|
|
||||||
| Service | Language / Framework | Status | URL | Doc |
|
| Service | Language / Framework | Status | URL | Doc |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Backend | Node.js / TypeScript (Express) | Live | `api.dev.amn.gg` | [[backend]] |
|
| Backend API | Node.js / TypeScript (Express) | Live | `dev.amn.gg/api`, `multi.amn.gg/api` | [[backend]] |
|
||||||
| Frontend | Next.js / React / TypeScript | Live | `dev.amn.gg` | [[frontend]] |
|
| Frontend | Next.js 14 / React / TypeScript | Live | `dev.amn.gg`, `multi.amn.gg` | [[frontend]] |
|
||||||
| Scanner | Go | Live | internal | [[scanner]] |
|
| Scanner | Go | Live | internal | [[scanner]] |
|
||||||
| Amanat Assist | Node.js / TypeScript + LLM proxy | Live | `assist.dev.amn.gg` | [[amanat-assist]] |
|
| Amanat Assist | Node.js / TypeScript + Telegram Bot API | Live | `assist.dev.amn.gg` | [[amanat-assist]] |
|
||||||
| Deployment | Docker Compose + Caddy + Watchtower | Live | — | [[deployment]] |
|
| 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
|
Browser / Telegram Mini App
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
infra-caddy (reverse proxy, TLS)
|
infra-caddy (reverse proxy, TLS, ports 80/443)
|
||||||
├── dev.amn.gg → [[frontend]] (Next.js SSR)
|
├── dev.amn.gg / multi.amn.gg → [[frontend]] (Next.js SSR)
|
||||||
├── api.dev.amn.gg → [[backend]] (Express REST + WebSocket)
|
├── */api → [[backend]] (Express REST + WebSocket)
|
||||||
└── assist.dev.amn.gg → [[amanat-assist]] (LLM proxy / Telegram bot)
|
└── assist.dev.amn.gg → [[amanat-assist]] (LLM proxy + Telegram mini-app)
|
||||||
|
|
||||||
[[backend]]
|
[[backend]]
|
||||||
├── MongoDB / PostgreSQL (dual-write seam, PG cutover in progress)
|
├── PostgreSQL (Drizzle ORM — primary store)
|
||||||
|
├── MongoDB (legacy read path, being retired)
|
||||||
├── Redis (sessions, rate-limit, pub-sub)
|
├── Redis (sessions, rate-limit, pub-sub)
|
||||||
└── emits payment events
|
└── emits payment events
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
[[scanner]] (Go — watches EVM chains for on-chain payments)
|
[[scanner]] (Go — watches EVM chains for on-chain payments)
|
||||||
│ webhook callback
|
│ HTTP webhook on confirmation
|
||||||
└──────────────────▶ [[backend]] /api/payment/callback
|
└──────────────▶ [[backend]] POST /api/payment/callback
|
||||||
```
|
```
|
||||||
|
|
||||||
- All containers share the `shared-web` Docker network managed by [[deployment]].
|
Integration points:
|
||||||
- [[amanat-assist]] is a separate Telegram Mini App; it calls [[backend]] APIs on behalf of users.
|
- **[[frontend]] → [[backend]]**: REST `/api/*` and WebSocket via infra-caddy
|
||||||
- [[scanner]] is stateless; it probes RPC endpoints and forwards confirmations to the backend.
|
- **[[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
|
## Related Sections
|
||||||
|
|
||||||
- [[01 - Architecture]] — system-wide design decisions, data model, and sequence diagrams
|
- [[01 - Architecture]] — system-wide design decisions, data model, and sequence diagrams
|
||||||
- [[03 - API Reference]] — REST endpoints, WebSocket events, auth headers
|
- [[03 - API Reference]] — full REST endpoint and WebSocket event reference
|
||||||
- [[08 - Operations]] — deployment runbooks, monitoring, secrets management
|
- [[08 - Operations]] — runbooks, monitoring, secrets management, backup
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# amanat-assist — AI Request Assistant
|
# 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)
|
**Repo:** `/amanat-assist` (separate repo, no Amanat DB or internal-service access)
|
||||||
**Owner:** Amanat Platform
|
**Owner:** Amanat Platform
|
||||||
**PRD:** [PRD — AI Request Assistant Mini App](../PRD%20-%20AI%20Request%20Assistant%20Mini%20App.md)
|
**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):
|
2. deploy (docker:27-cli, docker socket volume-mounted — no registry push):
|
||||||
- Copy dist/ to /opt/amanat-assist/dist/ (nginx bind-mount)
|
- Copy dist/ to /opt/amanat-assist/dist/ (nginx bind-mount)
|
||||||
- Sync docker-compose.yml to /opt/amanat-assist/
|
- Sync docker-compose.yml to /opt/amanat-assist/
|
||||||
- Rebuild amanat-llm-proxy Docker image in-place (locally, never pushed)
|
- Rebuild `amanat-assist-llm-proxy` Docker image in-place (locally, never pushed)
|
||||||
- docker compose up -d (recreates llm-proxy container)
|
- docker compose up -d llm-proxy (recreates llm-proxy container only)
|
||||||
3. notify (node:22-alpine):
|
3. notify (node:22-alpine):
|
||||||
- Runs scripts/ci/tg-notify.cjs on success or failure
|
- Runs scripts/ci/tg-notify.cjs on success or failure
|
||||||
- Uses TG_TOKEN + TG_USERS secrets
|
- 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
|
- **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.
|
- **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)
|
- **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
|
||||||
|
|||||||
@@ -2,375 +2,293 @@
|
|||||||
|
|
||||||
## 1. Overview
|
## 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 |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Current version | **2.10.5** |
|
| Current version | **2.11.43** |
|
||||||
| Status | Active — production at `dev.amn.gg` |
|
| 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` |
|
| Repo | `git@git.tbs.amn.gg:escrow/backend.git` |
|
||||||
| Runtime port | 8083 (production Docker), 8080 (dev Docker), 5001 (dev default) |
|
| Dev stack host | `root@89.58.32.32` — Arcane project `escrow-dev` |
|
||||||
| 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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Tech Stack
|
## 2. Tech Stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology | Notes |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| Framework | Express 5 (TypeScript) |
|
| HTTP framework | Express 5 | Async error propagation built in |
|
||||||
| Runtime | Node.js 22 |
|
| Language | TypeScript (strict) | tsc gate on every CI push |
|
||||||
| Primary DB | PostgreSQL via Drizzle ORM (`drizzle-orm ^0.45.2`, `pg ^8.21.0`) |
|
| Runtime | Node ≥ 22 | Also used for CI typecheck step |
|
||||||
| Migrations | Drizzle Kit (`drizzle-kit ^0.31.1`) — 19 landed SQL migrations |
|
| Database | PostgreSQL 15 via Drizzle ORM (`drizzle-orm ^0.45.2`, `pg ^8.21.0`) | Single source of truth; 19+ migrations landed |
|
||||||
| Session / Cache | Redis (`ioredis`) with Socket.IO pub-sub adapter |
|
| Auth | JWT (access + refresh) + WebAuthn/Passkey + Google OAuth | `JWT_SECRET`, `REFRESH_TOKEN_EXPIRES_IN` |
|
||||||
| Realtime | Socket.IO with Redis adapter (seller/buyer rooms) |
|
| Session / Mini App | Telegram Mini App `initData` verification | `TELEGRAM_WEBAPP_URL` |
|
||||||
| Auth | JWT (`jsonwebtoken`), Google OAuth, WebAuthn passkeys (`@simplewebauthn/server`), Telegram Mini App initData |
|
| Realtime | Socket.IO with Redis adapter (`@socket.io/redis-adapter`) | Room-scoped events |
|
||||||
| Crypto payments | Request Network, amn.scanner (in-house), DePay, SHKeeper |
|
| Cache / Pub-Sub | Redis | `REDIS_URI` |
|
||||||
| Rate limiting | In-memory (express-rate-limit) — Redis adapter planned |
|
| 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 |
|
||||||
| AI integration | OpenAI (listing descriptions, moderation) |
|
| Security headers | Helmet | CSP, X-Frame-Options, etc. |
|
||||||
| Email | Nodemailer via Resend SMTP |
|
| File uploads | Multer | MIME validation, `UPLOAD_PATH` |
|
||||||
| Telegram | Bot webhook + Mini App session + identity linking |
|
| Email | Nodemailer (SMTP) + Resend | `SMTP_*` / `RESEND_API_KEY` |
|
||||||
| Security | Helmet, CORS, Cloudflare Turnstile CAPTCHA, HMAC webhook verification |
|
| Price oracle | Chainlink + OffchainFX | Depeg protection, `ORACLE_MAX_STALENESS_S` |
|
||||||
| Containerization | Docker (Dockerfile.prod, Dockerfile.dev) |
|
| AML | Chainalysis + OFAC SDN | `CHAINALYSIS_API_KEY`, `OFAC_SDN_URL` |
|
||||||
| CI/CD | Woodpecker CI (4 pipelines) |
|
| AI | OpenAI | Descriptions, moderation |
|
||||||
|
| CI | Woodpecker CI | `.woodpecker/*.yml` |
|
||||||
|
| Process model | Node cluster | `CLUSTER_WORKERS` workers + master |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Directory Structure
|
## 3. Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
backend/src/
|
backend/
|
||||||
├── app.ts # Express bootstrap, middleware chain, route registration, graceful shutdown
|
├── src/
|
||||||
├── cluster.ts # Node.js cluster mode entry point (multi-core)
|
│ ├── app.ts # Express bootstrap: middleware chain, route registration, server creation
|
||||||
├── controllers/ # HTTP request handlers — thin layer, delegate to services
|
│ ├── cluster.ts # Node cluster master — forks CLUSTER_WORKERS child processes
|
||||||
├── db/ # Drizzle/Postgres layer
|
│ ├── controllers/ # Thin HTTP handlers that delegate to services
|
||||||
│ ├── schema/ # Per-table Drizzle schema files + index.ts barrel
|
│ ├── db/ # Drizzle/Postgres layer
|
||||||
│ ├── migrations/ # 19 numbered SQL migration files (0000–0018)
|
│ │ ├── schema/ # Per-table Drizzle schema files + index.ts barrel
|
||||||
│ └── repositories/ # Drizzle repos, factory.ts, backfill scripts, verify utilities
|
│ │ ├── migrations/ # Numbered SQL migration files (0000–0018+)
|
||||||
├── infrastructure/
|
│ │ └── repositories/ # DrizzleXxxRepo classes + factory.ts
|
||||||
│ └── socket/ # Socket.IO server init, room helpers, emit wrappers
|
│ ├── infrastructure/
|
||||||
├── models/ # Removed — replaced by Drizzle schemas in db/schema/
|
│ │ └── socket/ # Socket.IO server init, room helpers, emit wrappers
|
||||||
├── routes/ # Express Router definitions (mounted in app.ts)
|
│ ├── models/ # Legacy placeholder (Mongoose removed; schemas now in db/schema/)
|
||||||
│ ├── amnScannerWebhookRoutes.ts
|
│ ├── routes/ # Standalone Express Router files (dispute, blog, points, amn-scanner webhook)
|
||||||
│ ├── blogRoutes.ts
|
│ ├── scripts/ # CLI utilities — seed:users, seed:categories, tg-notify.cjs (CI)
|
||||||
│ ├── disputeRoutes.ts
|
│ ├── seeds/ # Fixture data for local dev (Postgres-capable, idempotent)
|
||||||
│ └── pointsRoutes.ts
|
│ ├── services/ # Domain service modules (see §4)
|
||||||
├── scripts/ # CLI utilities (seed:users, seed:categories, backfill, etc.)
|
│ ├── shared/
|
||||||
├── seeds/ # Seed data fixtures (Postgres-capable, store-aware, idempotent)
|
│ │ ├── config/index.ts # Typed env-var loader — single import for all config
|
||||||
├── services/ # Feature domain services (self-contained per domain)
|
│ │ ├── middleware/ # authMiddleware, roleGuard, errorHandler, validators
|
||||||
│ ├── address/ # Address management
|
│ │ ├── types/ # Cross-cutting TypeScript types and enums
|
||||||
│ ├── admin/ # Admin-only operations, AML config, break-glass, data cleanup
|
│ │ └── utils/response-handler.ts # Standard success/error envelope
|
||||||
│ ├── ai/ # OpenAI integration (descriptions, moderation)
|
│ └── utils/ # Pure utility functions: logger, currencyUtils, etc.
|
||||||
│ ├── auth/ # JWT, OAuth, Passkey, Telegram, password reset
|
├── .woodpecker/ # CI pipeline definitions (cleanup, development, manual, production)
|
||||||
│ ├── blockchain/ # Web3 read/verify helpers
|
├── Dockerfile.prod # Multi-stage production image
|
||||||
│ ├── blog/ # Posts, categories, comments
|
└── drizzle.config.ts # Drizzle Kit configuration
|
||||||
│ ├── 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.)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Each service folder is self-contained: `<feature>Service.ts`, `<feature>Controller.ts`, `<feature>Routes.ts`, `<feature>Validation.ts`. This design allows future extraction to microservices with minimal coupling.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Key Services / Modules
|
## 4. Key Services / Modules
|
||||||
|
|
||||||
| Module | Description |
|
| Service path | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `services/auth/` | JWT issuance/refresh, Google OAuth, WebAuthn passkeys, Telegram initData verification, password reset |
|
| `services/auth/` | JWT issue/refresh, Google OAuth, WebAuthn/Passkey registration and assertion, password reset |
|
||||||
| `services/marketplace/` | Core escrow domain: PurchaseRequest, SellerOffer, Template, Shop lifecycle |
|
| `services/user/` | User profile CRUD, preferences, address book |
|
||||||
| `services/payment/` | Payment orchestration, provider adapters, internal ledger, reconciliation |
|
| `services/marketplace/` | PurchaseRequest, SellerOffer, RequestTemplate, ShopSettings — core escrow marketplace |
|
||||||
| `services/payment/ledger/` | Double-spend guard: tracks available / held / releasable balances per payment |
|
| `services/payment/` | Payment orchestration: provider adapters, internal ledger (available/held/releasable), reconciliation, safety confirmations |
|
||||||
| `services/payment/wallets/` | Derived destination address derivation (xpub) + sweep orchestration |
|
| `services/payment/adapters/` | Provider-neutral adapter interface + registry; plugs in DePay, SHKeeper, amn.scanner, Request Network |
|
||||||
| `services/payment/priceOracle/` | Chainlink + off-chain FX oracle for multi-currency pricing + stablecoin depeg protection |
|
| `services/payment/requestNetwork/` | Request Network pay-in creation, in-house checkout rehydration, HMAC-verified webhook |
|
||||||
| `services/payment/safety/` | Transaction Safety Provider: confirmation thresholds, tx hash and transfer match enforcement |
|
| `services/payment/wallets/` | HD-derived destination addresses, sweep orchestration, gas top-up |
|
||||||
| `services/payment/amnScanner/` | In-house blockchain scanner webhook adapter (replaces Request Network for pay-in detection) |
|
| `services/payment/ledger/` | Funds ledger tracking available / held / releasable balances per payment |
|
||||||
| `services/payment/requestNetwork/` | Request Network pay-in routes, webhook signature verification, invoice creation |
|
| `services/payment/safety/` | Transaction Safety Provider: AML screening, min-confirmation thresholds |
|
||||||
| `infrastructure/socket/` | Socket.IO server init, buyer/seller room management, emit helpers |
|
| `services/blockchain/` | Web3 read helpers: balance checks, tx verification across ETH / BSC / Base / TON |
|
||||||
| `services/redis/` | Redis client wrapper, pub-sub channel helpers, session cache |
|
| `services/chat/` | Conversations, messages, attachments |
|
||||||
| `services/chat/` | Conversations and message threading between buyer and seller |
|
| `services/dispute/` | Dispute lifecycle: open, evidence upload, mediator assignment, release-hold |
|
||||||
| `services/dispute/` | Dispute lifecycle: open, evidence, mediator, resolution |
|
| `services/notification/` | Template-based notification delivery (in-app + Telegram); mark-as-read |
|
||||||
| `services/admin/` | Admin RBAC operations: AML config, break-glass, dispute management, data cleanup |
|
| `services/telegram/` | Bot webhook handler, Mini App `initData` verification, identity link/unlink, seller notifications |
|
||||||
| `services/telegram/` | Bot webhook handler, Mini App session auth, Telegram identity linking, push notifications |
|
| `services/points/` | Loyalty points accrual, levels, referrals, redemption |
|
||||||
| `services/trezor/` | Trezor hardware-wallet approval gate for high-value admin actions (break-glass overrideable) |
|
| `services/blog/` | Blog posts, categories, comments (public read / admin write) |
|
||||||
| `services/notification/` | In-app notification templates, delivery, mark-as-read |
|
| `services/digital-goods/` | Encrypted digital-goods delivery; key stored under `DIGITAL_GOODS_ENC_KEY` |
|
||||||
| `services/ai/` | OpenAI integration: AI-assisted listing descriptions and content moderation |
|
| `services/file/` | Multer multipart upload, MIME validation, static serving under `/uploads` |
|
||||||
| `services/email/` | Nodemailer transport via Resend SMTP, HTML email templates |
|
| `services/email/` | Nodemailer SMTP + Resend transport, templated emails |
|
||||||
| `services/points/` | Loyalty points engine, tier levels, redemption |
|
| `services/ai/` | OpenAI-backed request description generation and content moderation |
|
||||||
| `services/blog/` | Blog posts, categories, comments |
|
| `services/redis/` | Redis client singleton, cache helpers, pub-sub wrappers |
|
||||||
| `services/file/` | Multer-based file upload handler, MIME validation, upload path management |
|
| `services/admin/` | Admin-only endpoints: data cleanup (provider-scoped), confirmation thresholds, awaiting-confirmation view |
|
||||||
| `services/blockchain/` | Low-level Web3 read helpers: balance checks, tx confirmation polling |
|
| `services/collection/` | Collection management (multi-seller feature) |
|
||||||
| `db/repositories/` | Drizzle ORM repository layer for all 11 domain entities |
|
| `services/delivery/` | Delivery tracking and status |
|
||||||
| `seeds/` | Idempotent Postgres seed fixtures for users, categories, shops, configs |
|
| `infrastructure/socket/` | Socket.IO server attached to HTTP server; Redis adapter for multi-process pub-sub |
|
||||||
| `scripts/` | CLI backfill, migration verify, seeding, and maintenance scripts |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. API Surface Summary
|
## 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 |
|
Full per-endpoint details: [[03 - API Reference/API Overview]]
|
||||||
|---|---|
|
|
||||||
| `/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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Database
|
## 6. Database
|
||||||
|
|
||||||
### PostgreSQL (primary — active)
|
### PostgreSQL (primary)
|
||||||
|
|
||||||
- **ORM:** Drizzle ORM (`drizzle-orm ^0.45.2`)
|
- Driver: `pg ^8.21.0` via Drizzle ORM (`drizzle-orm ^0.45.2`)
|
||||||
- **Driver:** `pg ^8.21.0`
|
- Connection: `PG_URL` (primary pool); `PG_VITAL_URL` / `PG_NONVITAL_URL` for split-pool configuration
|
||||||
- **Migrations:** 19 SQL files under `src/db/migrations/` (0000–0018), managed by `drizzle-kit`
|
- Pool tuning: `PG_POOL_MAX`, `PG_POOL_SIZE`, `PG_NONVITAL_POOL_MAX`
|
||||||
- **Schemas:** per-table files in `src/db/schema/`, exported via `index.ts` barrel
|
- Migrations: numbered SQL files in `src/db/migrations/` (0000–0018+), applied via Drizzle Kit (`npx drizzle-kit migrate`)
|
||||||
- **Repositories:** `src/db/repositories/` — one Drizzle repo per domain; `factory.ts` provides DI
|
- Repositories: `DrizzleXxxRepo` classes in `src/db/repositories/`; factory pattern via `factory.ts`
|
||||||
- **Connection:** `PG_URL` env var (`postgres://user:pass@host:5432/db`)
|
- Seeds: idempotent Postgres-capable seed scripts under `src/seeds/`; auto-run on start when `AUTO_SEED_ON_START=true`
|
||||||
- **Migrations run:** `npx drizzle-kit migrate` (or via `drizzle.config.ts`)
|
|
||||||
|
|
||||||
### 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
|
The `DATABASE_URL` / `POSTGRES_URL` aliases are accepted for compatibility; prefer `PG_URL`.
|
||||||
- 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Auth Model
|
## 7. Auth Model
|
||||||
|
|
||||||
| Method | Mechanism |
|
### JWT
|
||||||
|---|---|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
**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)
|
## 8. Realtime (Socket.IO)
|
||||||
|
|
||||||
- **Adapter:** Redis pub-sub (`@socket.io/redis-adapter`) — scales across multiple backend instances
|
- Socket.IO server is attached to the HTTP server at bootstrap (`infrastructure/socket/socketService.ts`)
|
||||||
- **Init:** `infrastructure/socket/socketService.ts` — attaches to the HTTP server after Express bootstraps
|
- Redis adapter (`@socket.io/redis-adapter`) enables pub-sub across Node cluster workers
|
||||||
- **Room model:**
|
- **Room conventions:**
|
||||||
- `buyer:<userId>` — buyer-facing events (payment status, offer updates, cart)
|
- `user:<userId>` — personal notifications, payment status updates
|
||||||
- `seller:<userId>` — seller-facing events (new requests, offer accepted)
|
- `payment:<paymentId>` — scoped payment lifecycle events (added in v2.8.4 to prevent global cart-wipe)
|
||||||
- Admin rooms for dispute/notification broadcasts
|
- `dispute:<disputeId>` — dispute chat and status
|
||||||
- **Auth:** Socket handshake verified with JWT before room join
|
- `chat:<conversationId>` — chat messages
|
||||||
- **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:** `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
|
||||||
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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Payment Providers
|
## 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 / Tokens | Notes |
|
||||||
|
|
||||||
| Provider | Type | Chains | Status |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **amn.scanner** | In-house blockchain scanner | ETH, BSC, Base, TON | Active — default for new payments when `AMN_SCANNER_DEFAULT=true` |
|
| **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 payment protocol | BSC (USDC/USDT) + ETH | Active — legacy in-flight payments; webhook-driven |
|
| **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…`) |
|
||||||
| **DePay** | Widget-based crypto payments | Multi-chain | Available via adapter |
|
| **SHKeeper** | Self-hosted crypto gateway | BTC, ETH, BNB, USDT, others | `SHKEEPER_NETWORK`, `SHKEEPER_NETWORKS`, `SHKEEPER_ALLOWED_TOKENS` |
|
||||||
| **SHKeeper** | Self-hosted crypto gateway | Bitcoin + EVM | Available via adapter |
|
| **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
|
- `PAYMENT_PROVIDER_MODE` selects active provider(s) at runtime
|
||||||
2. Provider webhook arrives → HMAC-verified → reconciliation service updates ledger
|
- Internal ledger tracks `available`, `held`, and `releasable` balances per payment record
|
||||||
3. Escrow holds funds → seller fulfills → admin/resolver releases or refunds
|
- Transaction Safety Provider: AML screening (Chainalysis / OFAC SDN), minimum on-chain confirmation thresholds configurable at runtime (`TRANSACTION_SAFETY_MIN_CONFIRMATIONS`, `TRANSACTION_SAFETY_AML_PROVIDER`)
|
||||||
4. Ledger enforces: held → releasable → released (no double-spend)
|
- `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`
|
- Chainlink + OffchainFX feeds; `ORACLE_MAX_STALENESS_S` sets maximum acceptable quote age
|
||||||
- HMAC verification via `AMN_SCANNER_WEBHOOK_SECRET`
|
- Depeg protection rejects or flags stablecoin payments when peg deviation exceeds threshold
|
||||||
- Discriminator field: `payload.event` (not `eventType`) — always check this field
|
- `ORACLE_BYPASS_ENABLED=true` disables staleness check (dev/test only)
|
||||||
- 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. CI/CD (Woodpecker)
|
## 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 |
|
1. **get-version** — reads `package.json` version, writes `dev-<version>` to `.tags`
|
||||||
|---|---|
|
2. **typecheck** — `npm ci` (cached at `/opt/woodpecker-cache/backend-npm`) then `npm run typecheck`; push is blocked if tsc errors exist
|
||||||
| `get-version` | Reads `package.json` version, writes `dev-<version>` to `.tags` |
|
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`
|
||||||
| `typecheck` | `npm ci` + `npm run typecheck` — gates image build on clean TypeScript (cached npm on host) |
|
4. **notify** — `node scripts/ci/tg-notify.cjs` posts success/failure to Telegram (no `parse_mode` to avoid HTML/Markdown breakage)
|
||||||
| `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) |
|
|
||||||
|
|
||||||
> 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
|
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`.
|
||||||
|
|
||||||
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-<version>` tag before trusting the deploy.
|
|
||||||
- Woodpecker eats `${VAR}` in commands — use `$VAR` or `$$VAR`; prefer plugins over raw curl for notifications.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Local Development Quick-Start
|
## 11. Local Development Quick-Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone and install
|
# 1. Clone
|
||||||
git clone git@git.tbs.amn.gg:escrow/backend.git
|
git clone git@git.tbs.amn.gg:escrow/backend.git
|
||||||
cd backend
|
cd backend
|
||||||
|
|
||||||
|
# 2. Install dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Copy environment file
|
# 3. Copy and populate env
|
||||||
cp .env.example .env.local
|
cp .env.example .env.development
|
||||||
# Edit .env.local — set PG_URL, REDIS_URI, JWT_SECRET at minimum
|
# Edit .env.development — minimum required: PG_URL, REDIS_URI, JWT_SECRET, FRONTEND_URL
|
||||||
|
|
||||||
# Start dependencies (Postgres + Redis)
|
# 4. Start Postgres and Redis (Docker)
|
||||||
docker compose -f docker-compose.local.yml up -d
|
docker compose up -d postgres redis
|
||||||
|
|
||||||
# Run DB migrations
|
# 5. Run migrations
|
||||||
npx drizzle-kit migrate
|
npx drizzle-kit migrate
|
||||||
|
|
||||||
# Start dev server (hot-reload)
|
# 6. Start dev server (seeds run automatically if SEED_USERS=true)
|
||||||
npm run dev
|
npm run dev
|
||||||
# → listens on http://localhost:5001
|
# Server starts on process.env.PORT
|
||||||
|
|
||||||
# OR run in dev Docker
|
# 7. Type-check only (no run)
|
||||||
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
|
|
||||||
npm run typecheck
|
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 <path>` — never `git add -A`.
|
|
||||||
|
|
||||||
**Run 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`.
|
||||||
```bash
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
Test files live in `__tests__/`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -378,89 +296,130 @@ Test files live in `__tests__/`.
|
|||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `NODE_ENV` | `production` / `development` / `test` |
|
| `PORT` | HTTP listen port |
|
||||||
| `PORT` | HTTP listen port (default 5001) |
|
| `NODE_ENV` | `development` / `production` / `test` |
|
||||||
| `TRUST_PROXY_HOPS` | Number of reverse-proxy hops in front of app |
|
| `FRONTEND_URL` | Allowed CORS origin (frontend base URL) |
|
||||||
| `FRONTEND_URL` | Allowed CORS origin for frontend |
|
| `BACKEND_URL` | Self-referential base URL (used for webhook callback construction) |
|
||||||
| `BACKEND_URL` | Public backend base URL |
|
| `PG_URL` | Primary Postgres connection string |
|
||||||
| `PG_URL` | PostgreSQL connection string |
|
| `PG_VITAL_URL` | Postgres connection for vital (write-path) pool |
|
||||||
| `POSTGRES_USER` | Postgres username (Docker init) |
|
| `PG_NONVITAL_URL` | Postgres connection for non-vital (read-path) pool |
|
||||||
| `POSTGRES_PASSWORD` | Postgres password (Docker init) |
|
| `PG_POOL_MAX` | Max connections in primary pool |
|
||||||
| `POSTGRES_DB` | Postgres database name (Docker init) |
|
| `PG_POOL_SIZE` | Pool size alias |
|
||||||
| `MONGO_CONNECT_MODE` | `always` / `never` / `optional` — Mongo connection behavior (legacy) |
|
| `PG_NONVITAL_POOL_MAX` | Max connections in non-vital pool |
|
||||||
| `REDIS_URI` | Redis connection URI |
|
| `DATABASE_URL` / `POSTGRES_URL` | Compatibility aliases for `PG_URL` |
|
||||||
| `JWT_SECRET` | HS256 signing secret for access tokens |
|
| `REDIS_URI` | Redis connection string (sessions, pub-sub, Socket.IO adapter) |
|
||||||
| `JWT_EXPIRES_IN` | Access token TTL (e.g. `7d`) |
|
| `JWT_SECRET` | HMAC secret for JWT signing |
|
||||||
| `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `30d`) |
|
| `JWT_EXPIRES_IN` | Access token TTL (e.g. `15m`) |
|
||||||
| `ADMIN_EMAIL` | Bootstrap admin account email |
|
| `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `7d`) |
|
||||||
| `ADMIN_PASSWORD` | Bootstrap admin account password |
|
| `GOOGLE_CLIENT_ID` | Google OAuth 2.0 client ID |
|
||||||
| `SEED_USERS` | `true` to auto-seed users on dev boot |
|
| `TELEGRAM_WEBAPP_URL` | Allowed Telegram Mini App origin |
|
||||||
| `SEED_PASSWORD_ADMIN` | Admin seed account password |
|
| `TG_NOTIFY_BOT_TOKEN` | Telegram bot token for CI/admin notifications |
|
||||||
| `SEED_PASSWORD_SUPPORT` | Support seed account password |
|
| `TG_NOTIFY_CHATS` | Comma-separated Telegram chat IDs for notifications |
|
||||||
| `SEED_PASSWORD_BUYER` | Buyer seed account password |
|
| `SMTP_HOST` | SMTP server hostname |
|
||||||
| `SEED_PASSWORD_SELLER` | Seller seed account password |
|
| `SMTP_PORT` | SMTP port |
|
||||||
| `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 |
|
|
||||||
| `SMTP_SECURE` | `true` for TLS |
|
| `SMTP_SECURE` | `true` for TLS |
|
||||||
| `SMTP_USER` | SMTP username |
|
| `SMTP_USER` | SMTP auth username |
|
||||||
| `SMTP_PASS` | SMTP password |
|
| `SMTP_PASS` | SMTP auth password |
|
||||||
| `SMTP_FROM` | From address for outgoing email |
|
| `SMTP_FROM` | From address for outbound email |
|
||||||
| `RESEND_WEBHOOK_SECRET` | Resend inbound webhook signing secret (`whsec_…`) |
|
| `RESEND_API_KEY` | Resend email API key |
|
||||||
| `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile server-side secret (empty = CAPTCHA disabled) |
|
| `RESEND_WEBHOOK_SECRET` | Resend webhook signature secret |
|
||||||
| `RATE_LIMIT_WINDOW_MS` | Rate limit window in milliseconds |
|
| `PAYMENT_PROVIDER_MODE` | Active payment provider(s) |
|
||||||
| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window (global) |
|
| `PAYMENT_CALLBACK_SECRET` | DePay callback HMAC secret |
|
||||||
| `MAX_FILE_SIZE` | Upload max file size in bytes |
|
| `AMN_SCANNER_URL` | amn.scanner service base URL |
|
||||||
| `UPLOAD_PATH` | Server-side upload directory |
|
| `AMN_SCANNER_API_KEY` | Bearer token for amn.scanner API |
|
||||||
| `PAYMENT_PROVIDER_MODE` | `live` / `test` |
|
| `AMN_SCANNER_WEBHOOK_SECRET` | HMAC secret for amn.scanner webhook verification |
|
||||||
| `PAYMENT_LEDGER_ENFORCEMENT` | `true` to enforce double-spend ledger guard |
|
| `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 |
|
| `ESCROW_WALLET_ADDRESS` | Platform escrow wallet address |
|
||||||
| `RECEIVER_WALLET_ADDRESS` | Platform receiver wallet address |
|
| `RECEIVER_WALLET_ADDRESS` | Platform receiver wallet address |
|
||||||
| `REQUEST_NETWORK_ENABLED` | Enable Request Network provider |
|
| `INFURA_KEY` | Infura RPC key (ETH mainnet) |
|
||||||
| `REQUEST_NETWORK_API_KEY` | Request Network API key |
|
| `BSC_RPC_URL` | BSC mainnet RPC endpoint |
|
||||||
| `REQUEST_NETWORK_NETWORK` | Target chain (`bsc`, `eth`, etc.) |
|
| `BSC_TESTNET_RPC_URL` | BSC testnet RPC endpoint |
|
||||||
| `REQUEST_NETWORK_WEBHOOK_SECRET` | HMAC secret for RN webhook verification |
|
| `BNB_TESTNET_RPC_URL` | BNB testnet RPC endpoint |
|
||||||
| `AMN_SCANNER_URL` | amn.scanner service base URL |
|
| `RPC_URL_CHAIN_56` | BSC mainnet RPC (chain ID 56) |
|
||||||
| `AMN_SCANNER_WEBHOOK_SECRET` | HMAC secret for scanner webhook verification |
|
| `RPC_URL_CHAIN_97` | BSC testnet RPC (chain ID 97) |
|
||||||
| `AMN_SCANNER_DEFAULT` | `true` to make amn.scanner the default provider |
|
| `ENABLE_TESTNET_CHAINS` | Enable testnet chain support |
|
||||||
| `ORACLE_QUOTING_ENABLED` | Enable on-chain oracle pricing + depeg protection |
|
| `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider: `chainalysis` / `ofac` / `none` |
|
||||||
| `PRICE_ORACLE_PROVIDERS` | Comma-separated oracle providers (`chainlink,offchain_fx`) |
|
| `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` | Default minimum on-chain confirmations |
|
||||||
| `ORACLE_MAX_STALENESS_S` | Max oracle data age in seconds |
|
| `CHAINALYSIS_API_KEY` | Chainalysis KYT API key |
|
||||||
| `DEPEG_HARD_CAP_BPS` | Stablecoin depeg hard cap in basis points |
|
| `OFAC_SDN_URL` | OFAC SDN list endpoint |
|
||||||
| `OFFCHAIN_FX_URL` | Off-chain FX rate source URL (required for IRR/TRY) |
|
| `AML_CHECK_COST_USD` | Cost per AML check (for billing/reporting) |
|
||||||
| `CHAINLINK_RPC_1` | Private RPC override for Chainlink on ETH mainnet |
|
| `ORACLE_MAX_STALENESS_S` | Maximum age (seconds) for oracle price quotes |
|
||||||
| `CHAINLINK_RPC_56` | Private RPC override for Chainlink on BSC |
|
| `ORACLE_BYPASS_ENABLED` | Disable oracle staleness check (`true` in dev/test only) |
|
||||||
| `DERIVED_DESTINATION_XPUB` | xPub for derived payment address derivation |
|
| `DIGITAL_GOODS_ENC_KEY` | AES encryption key for digital goods delivery |
|
||||||
| `DERIVED_DESTINATION_SWEEP_SIGNER` | Sweep signing mode: `build-only` / `hot-key` / `kms` / `trezor` |
|
| `TREZOR_SAFEKEEPING_REQUIRED` | Require Trezor safekeeping confirmation |
|
||||||
| `DERIVED_DESTINATION_SWEEP_INTERVAL_MS` | Sweep cron interval in ms (0 = disabled) |
|
| `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile CAPTCHA secret |
|
||||||
| `SWEEP_MASTER_PRIVKEY` | Master sweep wallet private key (gas funder) |
|
| `RATE_LIMIT_WINDOW_MS` | Rate limit window in milliseconds |
|
||||||
| `TREZOR_SAFEKEEPING_REQUIRED` | `true` to require Trezor approval for admin actions |
|
| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window (global limiter) |
|
||||||
| `TRANSACTION_SAFETY_ENABLED` | Enable transaction safety layer |
|
| `RATE_LIMIT_BYPASS_IPS` | Comma-separated IPs exempt from rate limiting |
|
||||||
| `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` | Minimum on-chain confirmations before release |
|
| `LOGIN_RATE_LIMIT_ENABLED` | Enable/disable login rate limiter |
|
||||||
| `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider: `none` / `ofac` / `chainalysis` |
|
| `TRUST_PROXY_HOPS` | `trust proxy` hop count for X-Forwarded-For behind Traefik |
|
||||||
| `CHAINALYSIS_API_KEY` | Chainalysis API key (when AML provider = chainalysis) |
|
| `UPLOAD_PATH` | Filesystem path for uploaded files (default `/app/uploads`) |
|
||||||
| `TELEGRAM_BOT_TOKEN` | Telegram bot token |
|
| `MAX_FILE_SIZE` | Maximum upload size in bytes |
|
||||||
| `TELEGRAM_WEBHOOK_SECRET_TOKEN` | Telegram webhook secret token header value |
|
| `CLUSTER_WORKERS` | Number of Node cluster worker processes |
|
||||||
| `TELEGRAM_INITDATA_MAX_AGE_SEC` | Max age for Telegram initData (default 86400 s) |
|
| `SEED_USERS` | Seed default dev users on start |
|
||||||
| `TG_NOTIFY_CHATS` | Comma-separated Telegram chat IDs for CI/admin notifications |
|
| `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
|
## 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 |
|
| Rate limit counters are in-memory | Not multi-process safe across cluster workers; Redis adapter planned | `backend_rate_limits.md` |
|
||||||
| Backend room-scoping for socket events | Open | Frontend provider gate is in place (v2.8.4); backend should scope payment events to `seller:<id>` rooms to prevent cross-user leakage |
|
| `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` |
|
||||||
| Rate limit counters are in-memory | Open | Not shared across instances; Redis adapter planned for distributed deployments |
|
| 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` |
|
||||||
| Oracle quoting disabled | Open | `ORACLE_QUOTING_ENABLED=false`; requires FX feed configuration before enabling |
|
| 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` |
|
||||||
| amn.scanner multi-seller + multi-chain gap | Open | Current scanner watches one chain; multi-seller and multi-chain support not yet verified |
|
| 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` |
|
||||||
| Woodpecker development.yml parked | Known | Targets legacy registry; needs repointing to `git.tbs.amn.gg` and new Arcane deploy before re-enabling |
|
| 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` |
|
||||||
| Trezor safekeeping off by default | By design | `TREZOR_SAFEKEEPING_REQUIRED=false`; must be enabled explicitly in production once admin xpub is registered |
|
| 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` |
|
||||||
| Request Network canonical proxy addresses | Known | RN's CREATE2 canonical-address claim is false for ETH and Base — probe actual address before trusting |
|
| Woodpecker `${VAR}` template collision | Woodpecker eats `${VAR}` in commands; use `$VAR` or `$$VAR` | `woodpecker_template_collision.md` |
|
||||||
| 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 |
|
| CI silent build fail | Green CI does not guarantee image was pushed to registry; verify `dev-<version>` tag exists before trusting | `woodpecker_silent_build_fail.md` |
|
||||||
| Parallel agent push conflicts | Operational | mojtaba agent pushes to same branches; always `git fetch --rebase` before pushing; expect version-bump conflicts |
|
| 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` |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ tags: [services, deployment, infrastructure, docker]
|
|||||||
|
|
||||||
# Deployment
|
# 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 |
|
| File | Status | Host | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `deployment/docker-compose.yml` | Legacy | Any | nginx + traefik_public network, images from `git.manko.yoga` registry |
|
| `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/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. Services
|
||||||
|
|
||||||
|
### 2.1 dev-amn stack (active, `dev.amn.gg`)
|
||||||
|
|
||||||
| Service | Image | Internal Port | Role |
|
| Service | Image | Internal Port | Role |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `backend` | `git.tbs.amn.gg/escrow/backend:dev` | 5001 | Express 5 API + Socket.IO + admin seed |
|
| `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 |
|
| `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) |
|
| `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) |
|
| `postgres` | `postgres:18-alpine` | 5432 | Primary datastore (auth + all 8 Postgres domain stores) |
|
||||||
| `redis` | `redis:8-alpine` | 6379 | Session cache, rate-limit counters, job queues |
|
| `redis` | `redis:8-alpine` | 6379 | Session cache, rate-limit counters, pub/sub |
|
||||||
| `mongodb` | `mongo:8.0-noble` | 27017 | Legacy datastore — dev boot parity only, retired in prod |
|
| `mongodb` | `mongo:8.0-noble` | 27017 | Legacy datastore — dev boot parity only; retire once remaining reads migrated |
|
||||||
| `gatus` | `twinproduction/gatus:latest` | 8080 (mapped 8084) | Uptime monitoring + Telegram alerting |
|
|
||||||
|
|
||||||
> **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
|
## 3. Architecture Diagram
|
||||||
|
|
||||||
|
### dev-amn (active)
|
||||||
|
|
||||||
```
|
```
|
||||||
Internet (HTTPS 443 / HTTP 80)
|
Internet (HTTPS 443 / HTTP 80)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌───────────────────────────┐
|
┌───────────────────────────────┐
|
||||||
│ Cloudflare CDN / Proxy │
|
│ Cloudflare CDN / Proxy │
|
||||||
│ amn.gg / dev.amn.gg │
|
│ amn.gg / dev.amn.gg │
|
||||||
└─────────────┬─────────────┘
|
└─────────────┬─────────────────┘
|
||||||
│
|
│ (origin request)
|
||||||
▼ (origin)
|
▼
|
||||||
┌─────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────┐
|
||||||
│ Host: 89.58.32.32 │
|
│ Host: 89.58.32.32 │
|
||||||
│ │
|
│ │
|
||||||
│ ┌────────────────────────────────────────────┐ │
|
│ ┌──────────────────────────────────────────────┐ │
|
||||||
│ │ infra-caddy (Arcane project "infra") │ │
|
│ │ infra-caddy (Arcane project "infra") │ │
|
||||||
│ │ ports 80:80, 443:443 on host │ │
|
│ │ ports 80:80, 443:443 bound to host │ │
|
||||||
│ │ reads Caddyfile at │ │
|
│ │ Caddyfile: /opt/arcane/data/projects/ │ │
|
||||||
│ │ /opt/arcane/data/projects/infra/Caddyfile │ │
|
│ │ infra/Caddyfile │ │
|
||||||
│ └───┬───────────────────────────┬────────────┘ │
|
│ └───────┬─────────────────────────┬────────────┘ │
|
||||||
│ │ /api/* /socket.io/* │ /* │
|
│ │ /api/* /socket.io/* │ /* │
|
||||||
│ │ /uploads/* │ │
|
│ │ /uploads/* │ │
|
||||||
│ ▼ ▼ │
|
│ ▼ ▼ │
|
||||||
│ ┌────────────┐ ┌────────────────┐ │
|
│ ┌───────────────┐ ┌────────────────────┐ │
|
||||||
│ │ backend │ │ frontend │ │
|
│ │ backend │ │ frontend │ │
|
||||||
│ │ :5001 │ │ :8083 │ │
|
│ │ :5001 │ │ :8083 │ │
|
||||||
│ │ shared-web │ │ shared-web │ │
|
│ │ shared-web │ │ shared-web │ │
|
||||||
│ └──┬──┬──┬───┘ └────────────────┘ │
|
│ └──┬──┬────┬────┘ └────────────────────┘ │
|
||||||
│ │ │ │ │
|
│ │ │ │ │
|
||||||
│ │ │ └──────────────────────┐ │
|
│ │ │ └────────────────────┐ │
|
||||||
│ │ │ ▼ │
|
│ │ │ ▼ │
|
||||||
│ │ │ ┌────────────────────┐ │
|
│ │ │ ┌──────────────────────┐ │
|
||||||
│ │ │ │ refscanner │ │
|
│ │ │ │ refscanner │ │
|
||||||
│ │ │ │ :8080 (default │ │
|
│ │ │ │ :8080 │ │
|
||||||
│ │ │ │ bridge only) │ │
|
│ │ │ │ (default only) │ │
|
||||||
│ │ │ └────────────────────┘ │
|
│ │ │ └──────────────────────┘ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ ▼ ▼ │
|
│ ▼ ▼ │
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
|
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
|
||||||
│ │ postgres │ │ redis │ │ mongodb │ │
|
│ │ postgres │ │ redis │ │ mongodb │ │
|
||||||
│ │ :5432 │ │ :6379 │ │ :27017 │ │
|
│ │ :5432 │ │ :6379 │ │ :27017 │ │
|
||||||
│ │ (default │ │ (default │ │ (default only, │ │
|
│ │ (default │ │ (default │ │ (default only, │ │
|
||||||
│ │ only) │ │ only) │ │ legacy) │ │
|
│ │ only) │ │ only) │ │ legacy) │ │
|
||||||
│ └──────────┘ └──────────┘ └───────────────┘ │
|
│ └──────────┘ └──────────┘ └────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ ┌────────────────────────────────────────────┐ │
|
└─────────────────────────────────────────────────────┘
|
||||||
│ │ gatus :8084 (mapped from :8080) │ │
|
|
||||||
│ │ monitors dev.amn.gg + amn.gg + external │ │
|
|
||||||
│ └────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
Networks:
|
Networks:
|
||||||
shared-web ─── external, attached: backend, frontend
|
shared-web (external) ─ backend + frontend (reachable by infra-caddy)
|
||||||
default ─── internal bridge: all services
|
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
|
## 4. Networks
|
||||||
|
|
||||||
| Network | Type | Services Attached | Purpose |
|
| Network | Type | Present in | Services Attached | Purpose |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `default` (bridge) | Internal | All services | Container-to-container communication |
|
| `default` (bridge) | Internal auto | dev-amn, legacy | All services | Container-to-container communication |
|
||||||
| `shared-web` | External (pre-existing) | `backend`, `frontend` | Allows infra-caddy to proxy by container name |
|
| `shared-web` | External (pre-existing) | dev-amn, escrow-multi | `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 |
|
| `traefik_public` | External (pre-existing) | Legacy compose only | `nginx`, `gatus` | Old Traefik-based ingress on `git.manko.yoga` host |
|
||||||
|
|
||||||
**Key rules:**
|
**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`.
|
- `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.
|
- 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 `infra` project.
|
- `shared-web` must exist on the host before `docker compose up` — it is created by the Arcane `infra` project.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Volumes and Bind Mounts
|
## 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 |
|
| 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` |
|
| `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` |
|
| `postgres` | `./data/postgres` | `/var/lib/postgresql/data` | `PGDATA=/var/lib/postgresql/data/pgdata` (subdir workaround) |
|
||||||
| `redis` | `./data/redis` | `/data` | Persistence dump |
|
| `redis` | `./data/redis` | `/data` | RDB persistence dump |
|
||||||
| `mongodb` | `./data/mongo` | `/data/db` | Legacy; can be deleted once Mongo retired |
|
| `mongodb` | `./data/mongo` | `/data/db` | Legacy; can be removed once Mongo retired |
|
||||||
| `gatus` | `./gatus/config.yaml` | `/config/config.yaml` (ro) | Monitoring config — part of repo |
|
|
||||||
|
|
||||||
**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.
|
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
|
email manwe@manko.yoga
|
||||||
auto_https disable_redirects
|
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.
|
- `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/*` go to `backend:5001`; everything else to `frontend:8083`.
|
- Routes by path prefix: `/api/*`, `/socket.io/*`, `/uploads/*` → `backend:5001`; everything else → `frontend:8083`.
|
||||||
- Container names are resolved via the `shared-web` network.
|
- Container names resolve via the `shared-web` network (both `backend` and `frontend` join it).
|
||||||
|
|
||||||
### Adding a new public service
|
### Adding a new public service
|
||||||
|
|
||||||
1. Add the service to `deployment/dev-amn/docker-compose.yml` with `networks: shared-web: {}`.
|
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.
|
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
|
```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: `curl -I https://dev.amn.gg/<new-path>`
|
||||||
|
|
||||||
|
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
|
||||||
```
|
```
|
||||||
4. Verify via `curl -I https://dev.amn.gg/<new-path>`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Gatus Monitoring
|
## 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
|
### Alert policy
|
||||||
|
|
||||||
@@ -187,69 +283,80 @@ Gatus runs as a sidecar in the `default` network. Config is at `deployment/gatus
|
|||||||
| Send on resolved | Yes |
|
| Send on resolved | Yes |
|
||||||
| Alert channel | Telegram (`GATUS_TELEGRAM_BOT_TOKEN` / `GATUS_TELEGRAM_CHAT_ID`) |
|
| Alert channel | Telegram (`GATUS_TELEGRAM_BOT_TOKEN` / `GATUS_TELEGRAM_CHAT_ID`) |
|
||||||
|
|
||||||
|
Prod endpoints use `failure-threshold: 2` (faster alerting).
|
||||||
|
|
||||||
### Monitored endpoints
|
### Monitored endpoints
|
||||||
|
|
||||||
| Name | Group | URL | Interval | Key Conditions |
|
| 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-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-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 (failure-threshold 2) |
|
| `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 (failure-threshold 2) |
|
| `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 < 3000ms |
|
| `frontend-dev` | frontend | `https://dev.amn.gg/` | 60s | HTTP 200, response time < 3000ms |
|
||||||
| `frontend-prod` | frontend | `https://amn.gg/` | 60s | HTTP 200, response < 3000ms (failure-threshold 2) |
|
| `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 (accepts auth errors — just checks reachability) |
|
| `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 |
|
| `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) |
|
| `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
|
## 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` |
|
| `NODE_ENV` | Runtime environment | `production` |
|
||||||
| `PORT` | Express listen port | `5001` |
|
| `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)_ |
|
| `DEBUG` | Debug namespaces | _(empty)_ |
|
||||||
| `LOG_LEVEL` | Winston log level | `info` |
|
| `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` |
|
| `PG_URL` | Postgres DSN | `postgres://amanat:amanat_local@postgres:5432/amanat_dev` |
|
||||||
| `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` |
|
|
||||||
| `POSTGRES_USER` | Postgres superuser | `amanat` |
|
| `POSTGRES_USER` | Postgres superuser | `amanat` |
|
||||||
| `POSTGRES_PASSWORD` | Postgres superuser password | — |
|
| `POSTGRES_PASSWORD` | Postgres superuser password | — |
|
||||||
| `POSTGRES_DB` | Postgres database name | `amanat_dev` |
|
| `POSTGRES_DB` | Postgres database name | `amanat_dev` |
|
||||||
| `AUTO_SEED_ON_START` | Run seed on boot | `true` |
|
| `AUTO_SEED_ON_START` | Run seed on boot | `true` |
|
||||||
|
|
||||||
#### Store modes (dual-write seam)
|
### 8.3 Database — Mongo (legacy)
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `AUTH_STORE` | Auth domain store backend | `postgres` |
|
| `MONGODB_URI` | Mongo connection string | `mongodb://admin:pass@mongodb:27017/marketplace?authSource=admin` |
|
||||||
| `CONFIG_STORE` | Config domain | `postgres` |
|
| `MONGO_INITDB_ROOT_USERNAME` | Mongo root user | `admin` |
|
||||||
| `ADDRESS_STORE` | Address domain | `postgres` |
|
| `MONGO_INITDB_ROOT_PASSWORD` | Mongo root password | `changeme_local` |
|
||||||
| `CATEGORY_STORE` | Category domain | `postgres` |
|
| `MONGO_INITDB_DATABASE` | Mongo init DB | `marketplace` |
|
||||||
| `LEVEL_CONFIG_STORE` | Level config domain | `postgres` |
|
| `DB_NAME` | Mongo database name used by app | `amn-db` |
|
||||||
| `SHOP_SETTINGS_STORE` | Shop settings domain | `postgres` |
|
|
||||||
| `REVIEW_STORE` | Review domain | `postgres` |
|
|
||||||
| `NOTIFICATION_STORE` | Notification domain | `postgres` |
|
|
||||||
|
|
||||||
#### 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 |
|
| 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`) |
|
| `JWT_EXPIRES_IN` | Access token TTL (e.g. `7d`) |
|
||||||
| `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `30d`) |
|
| `REFRESH_TOKEN_EXPIRES_IN` | Refresh token TTL (e.g. `30d`) |
|
||||||
|
|
||||||
#### Redis
|
### 8.6 Redis
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `REDIS_URI` | Redis connection string (includes password) |
|
| `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 |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -274,23 +381,23 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev-
|
|||||||
| `BACKEND_URL` | Backend origin |
|
| `BACKEND_URL` | Backend origin |
|
||||||
| `CORS_ORIGIN` | Allowed CORS origin |
|
| `CORS_ORIGIN` | Allowed CORS origin |
|
||||||
|
|
||||||
#### File uploads
|
### 8.8 File uploads
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `UPLOAD_PATH` | Upload directory inside container | `/app/uploads` |
|
| `UPLOAD_PATH` | Upload directory inside container | `/app/uploads` |
|
||||||
| `MAX_FILE_SIZE` | Max upload bytes | `52428800` (50 MB) |
|
| `MAX_FILE_SIZE` | Max upload bytes | `52428800` (50 MB) |
|
||||||
|
|
||||||
#### Rate limiting
|
### 8.9 Rate limiting
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `RATE_LIMIT_WINDOW_MS` | Window for rate limiter | `900000` (15 min) |
|
| `RATE_LIMIT_WINDOW_MS` | Window for rate limiter | `900000` (15 min) |
|
||||||
| `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | `100` |
|
| `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 |
|
| 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_PASS` | SMTP password |
|
||||||
| `SMTP_FROM` | From address |
|
| `SMTP_FROM` | From address |
|
||||||
|
|
||||||
#### WebAuthn (Passkeys)
|
### 8.11 WebAuthn (Passkeys)
|
||||||
|
|
||||||
| Variable | Description |
|
| 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_NAME` | Relying party display name |
|
||||||
| `WEBAUTHN_RP_ORIGIN` | Relying party origin URL |
|
| `WEBAUTHN_RP_ORIGIN` | Relying party origin URL |
|
||||||
|
|
||||||
#### Admin seed
|
### 8.12 Admin seed
|
||||||
|
|
||||||
| Variable | Description |
|
| 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_FIRST_NAME` | Admin first name |
|
||||||
| `ADMIN_LAST_NAME` | Admin last name |
|
| `ADMIN_LAST_NAME` | Admin last name |
|
||||||
|
|
||||||
#### Google OAuth
|
### 8.13 Google OAuth
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
|
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
|
||||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
|
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
|
||||||
|
|
||||||
#### OpenAI
|
### 8.14 OpenAI
|
||||||
|
|
||||||
| Variable | Description |
|
| 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_MAX_TOKENS` | Max tokens per request |
|
||||||
| `OPENAI_TEMPERATURE` | Sampling temperature |
|
| `OPENAI_TEMPERATURE` | Sampling temperature |
|
||||||
|
|
||||||
#### Sentry
|
### 8.15 Sentry
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `SENTRY_DSN` | Sentry ingest DSN |
|
| `SENTRY_DSN` | Sentry ingest DSN |
|
||||||
|
|
||||||
#### Wallets / Blockchain
|
### 8.16 Wallets / Blockchain
|
||||||
|
|
||||||
| Variable | Description |
|
| 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 |
|
| `ADMIN_PAYOUT_WALLET_ADDRESS` | Admin payout destination |
|
||||||
| `RECEIVER_WALLET_ADDRESS` | Default receiver wallet |
|
| `RECEIVER_WALLET_ADDRESS` | Default receiver wallet |
|
||||||
|
|
||||||
#### DePay
|
### 8.17 DePay
|
||||||
|
|
||||||
| Variable | Description |
|
| 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_ALLOWED_TOKENS` | Allowed payment tokens |
|
||||||
| `DEPAY_PUBLIC_KEY` | DePay public key (PEM) |
|
| `DEPAY_PUBLIC_KEY` | DePay public key (PEM) |
|
||||||
|
|
||||||
#### SHKeeper
|
### 8.18 SHKeeper
|
||||||
|
|
||||||
| Variable | Description |
|
| 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_CALLBACK_SECRET` | Callback verification secret |
|
||||||
| `SHKEEPER_WEBHOOK_SECRET` | Webhook verification secret |
|
| `SHKEEPER_WEBHOOK_SECRET` | Webhook verification secret |
|
||||||
|
|
||||||
#### Request Network
|
### 8.19 Request Network
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description | Default |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `REQUEST_NETWORK_ENABLED` | Enable RN provider |
|
| `REQUEST_NETWORK_ENABLED` | Enable RN provider | — |
|
||||||
| `REQUEST_NETWORK_WEBHOOK_SECRET` | Webhook signature secret |
|
| `REQUEST_NETWORK_WEBHOOK_SECRET` | Webhook signature secret | — |
|
||||||
| `REQUEST_NETWORK_WEBHOOK_CALLBACK_URL` | Public callback URL |
|
| `REQUEST_NETWORK_WEBHOOK_CALLBACK_URL` | Public callback URL | — |
|
||||||
| `REQUEST_NETWORK_PAYMENT_CURRENCY` | Currency (e.g. `USDC`) |
|
| `REQUEST_NETWORK_PAYMENT_CURRENCY` | Currency (e.g. `USDC`) | — |
|
||||||
| `REQUEST_NETWORK_NETWORK` | Chain (e.g. `bsc`) |
|
| `REQUEST_NETWORK_NETWORK` | Chain (e.g. `bsc`) | — |
|
||||||
| `REQUEST_NETWORK_MERCHANT_REFERENCE` | Merchant address reference |
|
| `REQUEST_NETWORK_MERCHANT_REFERENCE` | Merchant address reference | — |
|
||||||
| `REQUEST_NETWORK_API_BASE_URL` | RN API root |
|
| `REQUEST_NETWORK_API_BASE_URL` | RN API root | — |
|
||||||
| `REQUEST_NETWORK_API_KEY` | RN API key |
|
| `REQUEST_NETWORK_API_KEY` | RN API key | — |
|
||||||
| `REQUEST_NETWORK_ORIGIN` | Origin header sent to RN |
|
| `REQUEST_NETWORK_ORIGIN` | Origin header sent to RN | — |
|
||||||
| `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` | Allow test events (default `false`) |
|
| `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` | Allow test events | `false` |
|
||||||
|
|
||||||
> RN webhook discriminator is `payload.event` (not `eventType`) — see [[rn_webhook_event_field]].
|
> 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 |
|
| 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_MIN_CONFIRMATIONS` | Min block confirmations | `12` |
|
||||||
| `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider (`none`, `chainalysis`) | `none` |
|
| `TRANSACTION_SAFETY_AML_PROVIDER` | AML provider (`none`, `chainalysis`) | `none` |
|
||||||
|
|
||||||
#### Payment routing
|
### 8.21 Payment routing
|
||||||
|
|
||||||
| Variable | Description |
|
| 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_PROVIDER_MODE` | `live` or `test` |
|
||||||
| `PAYMENT_ROLLBACK_PROVIDER` | Fallback provider |
|
| `PAYMENT_ROLLBACK_PROVIDER` | Fallback provider |
|
||||||
|
|
||||||
#### Telegram
|
### 8.22 Telegram
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `TELEGRAM_FEATURE_ENABLED` | Enable Telegram integration |
|
| `TELEGRAM_FEATURE_ENABLED` | Enable Telegram integration |
|
||||||
| `TELEGRAM_MINIAPP_ENABLED` | Enable Mini App |
|
| `TELEGRAM_MINIAPP_ENABLED` | Enable Mini App |
|
||||||
| `TELEGRAM_WEBHOOK_ENABLED` | Enable webhook receiver |
|
| `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_WEBHOOK_SECRET_TOKEN` | Webhook secret for validation |
|
||||||
| `TELEGRAM_INITDATA_MAX_AGE_SEC` | Max age for initData |
|
| `TELEGRAM_INITDATA_MAX_AGE_SEC` | Max age for initData |
|
||||||
| `TELEGRAM_INITDATA_REPLAY_WINDOW_MS` | Replay protection window |
|
| `TELEGRAM_INITDATA_REPLAY_WINDOW_MS` | Replay protection window |
|
||||||
| `TELEGRAM_WEBHOOK_REPLAY_WINDOW_MS` | Webhook replay protection window |
|
| `TELEGRAM_WEBHOOK_REPLAY_WINDOW_MS` | Webhook replay protection window |
|
||||||
| `TELEGRAM_SESSION_TTL_SEC` | Session TTL |
|
| `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 |
|
| `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 |
|
| 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_ID` | Newt node ID |
|
||||||
| `NEWT_SECRET` | Newt node secret |
|
| `NEWT_SECRET` | Newt node secret |
|
||||||
|
|
||||||
#### Testnet chains
|
### 8.24 Frontend (NEXT_PUBLIC_*)
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|---|---|
|
|
||||||
| `ENABLE_TESTNET_CHAINS` | Expose testnet chain configs | Set to `true` in dev-amn compose override |
|
|
||||||
|
|
||||||
### Frontend (NEXT_PUBLIC_*)
|
|
||||||
|
|
||||||
| Variable | Description |
|
| 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_NAME` | WebAuthn RP name |
|
||||||
| `NEXT_PUBLIC_PASSKEY_RP_ID` | WebAuthn RP ID |
|
| `NEXT_PUBLIC_PASSKEY_RP_ID` | WebAuthn RP ID |
|
||||||
| `NEXT_PUBLIC_PASSKEY_ORIGIN` | WebAuthn origin |
|
| `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_DEPAY_INTEGRATION_ID` | DePay integration ID |
|
||||||
| `NEXT_PUBLIC_IS_DEVELOPMENT` | Development flag |
|
| `NEXT_PUBLIC_IS_DEVELOPMENT` | Development flag |
|
||||||
| `NEXT_PUBLIC_ENABLE_DEBUG` | Enable client debug logging |
|
| `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 |
|
| `NEXT_PUBLIC_TELEGRAM_BOT_ID` | Telegram bot numeric ID |
|
||||||
| `BUILD_STATIC_EXPORT` | Enable `next export` mode (`false` for SSR) |
|
| `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 |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -477,9 +583,9 @@ All vars are passed to containers via `.env` at the stack root (`deployment/dev-
|
|||||||
|
|
||||||
## 9. Deploy Workflow
|
## 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
|
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/backend:dev
|
||||||
└─► docker push git.tbs.amn.gg/escrow/frontend:dev
|
└─► docker push git.tbs.amn.gg/escrow/frontend:dev
|
||||||
└─► arcane-cli gitops sync cf6c9eab… (or watchtower polls)
|
└─► arcane-cli gitops sync cf6c9eab… (or watchtower polls)
|
||||||
└─► escrow-backend container restarted with new image
|
└─► escrow-backend restarted with new image
|
||||||
└─► escrow-frontend container 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
|
```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 \
|
scp -i ~/CascadeProjects/wzp src/services/auth/authRoutes.ts \
|
||||||
root@89.58.32.32:/tmp/escrow-backend-build/src/services/auth/
|
root@89.58.32.32:/tmp/escrow-backend-build/src/services/auth/
|
||||||
|
|
||||||
# 2. Rebuild image on server (~3 min, ARM64)
|
# 2. Rebuild image on server (~3 min, ARM64)
|
||||||
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
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
|
# 3. Restart the backend container
|
||||||
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
||||||
"cd /opt/arcane/data/projects/escrow-dev && docker compose up -d backend"
|
"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
|
```bash
|
||||||
# via Arcane CLI (preferred)
|
# 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"
|
"cd /opt/arcane/data/projects/escrow-dev && docker compose down"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9.4 Reloading Caddy after Caddyfile edits
|
### 9.5 Reloading Caddy after Caddyfile edits
|
||||||
|
|
||||||
Edit `/opt/arcane/data/projects/infra/Caddyfile` on the server, then:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
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.
|
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`
|
1. Edit `.env` on the server: `/opt/arcane/data/projects/escrow-dev/.env`
|
||||||
2. Restart affected service:
|
2. Restart affected container:
|
||||||
```bash
|
```bash
|
||||||
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
||||||
"cd /opt/arcane/data/projects/escrow-dev && docker compose up -d backend"
|
"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
|
```bash
|
||||||
# Check running containers
|
# Check running containers
|
||||||
@@ -556,7 +681,7 @@ arcane-cli project status devEscrow
|
|||||||
# Check backend version
|
# Check backend version
|
||||||
curl https://dev.amn.gg/api/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 .
|
curl https://dev.amn.gg/api/health | jq .
|
||||||
|
|
||||||
# Tail backend logs
|
# Tail backend logs
|
||||||
@@ -564,48 +689,48 @@ ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
|||||||
"docker logs -f escrow-backend --tail 100"
|
"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
|
## 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) |
|
| 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 registry | `git.tbs.amn.gg/escrow` | Same registry, prod tags |
|
||||||
| Image tag | `:dev` | `:latest` or versioned |
|
| 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` |
|
| `ENABLE_TESTNET_CHAINS` | `true` (compose override) | Not set / `false` |
|
||||||
| `NODE_ENV` | `production` (same) | `production` |
|
| `NODE_ENV` | `production` | `production` |
|
||||||
| `NEXT_PUBLIC_IS_DEVELOPMENT` | `false` | `false` |
|
| `NEXT_PUBLIC_IS_DEVELOPMENT` | `false` | `false` |
|
||||||
| `PAYMENT_PROVIDER_MODE` | `live` | `live` |
|
| `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 |
|
| 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
|
## 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`
|
- **Server location:** `/opt/arcane/data/projects/escrow-dev/.env`
|
||||||
- Permissions: `chmod 600` owned by root
|
- **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)
|
- **Repo template:** `deployment/.env` — contains live dev values, treated as low-sensitivity dev config; rotate all values before using in production
|
||||||
- `.gitleaks.toml` in `deployment/` configures secret scanning exclusions for the repo
|
|
||||||
|
|
||||||
### Rules
|
### Rules
|
||||||
|
|
||||||
1. Never commit `.env` or any file containing real tokens, passwords, or private keys.
|
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`.
|
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.
|
3. `NEXT_PUBLIC_*` vars are baked into the frontend bundle. Never place secrets in them.
|
||||||
4. Wallet addresses (e.g. `ESCROW_WALLET_ADDRESS`) are public on-chain but still kept out of the repo for operational hygiene.
|
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, then `chmod 600`.
|
5. For new deployments: copy `deployment/.env` to the server, fill in real values, `chmod 600`.
|
||||||
6. Gatus bot token and chat ID go into the same `.env` — they are read by the gatus container via `environment:` directives.
|
6. Gatus vars (`GATUS_TELEGRAM_BOT_TOKEN`, `GATUS_TELEGRAM_CHAT_ID`) go into the same `.env`.
|
||||||
7. Telegram bot tokens are high-value secrets — rotate immediately if accidentally pushed.
|
7. Telegram bot tokens are high-value — rotate immediately if accidentally pushed.
|
||||||
|
|
||||||
### Sensitive variable groups
|
### Sensitive variable groups
|
||||||
|
|
||||||
@@ -614,7 +739,7 @@ ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
|||||||
| JWT | `JWT_SECRET` | Full session forgery |
|
| JWT | `JWT_SECRET` | Full session forgery |
|
||||||
| DB credentials | `POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `MONGO_INITDB_ROOT_PASSWORD` | Database access |
|
| 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 |
|
| 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 |
|
| OAuth secrets | `GOOGLE_CLIENT_SECRET` | OAuth impersonation |
|
||||||
| API keys | `OPENAI_API_KEY`, `REQUEST_NETWORK_API_KEY`, `SHKEEPER_API_KEY` | Billing / data access |
|
| API keys | `OPENAI_API_KEY`, `REQUEST_NETWORK_API_KEY`, `SHKEEPER_API_KEY` | Billing / data access |
|
||||||
| Sentry DSN | `SENTRY_DSN` | Error data exfiltration |
|
| Sentry DSN | `SENTRY_DSN` | Error data exfiltration |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: Frontend Service — amn-frontend
|
title: Frontend Service — amn-frontend
|
||||||
tags: [service, frontend, nextjs, react, web3, telegram]
|
tags: [service, frontend, nextjs, react, web3, telegram]
|
||||||
created: 2026-06-08
|
created: 2026-06-08
|
||||||
updated: 2026-06-08
|
updated: 2026-06-12
|
||||||
---
|
---
|
||||||
|
|
||||||
# Frontend Service — amn-frontend
|
# Frontend Service — amn-frontend
|
||||||
@@ -14,355 +14,305 @@ updated: 2026-06-08
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Package name | `amn-frontend` |
|
| Package name | `amn-frontend` |
|
||||||
| Version | **2.10.5** |
|
| Version | **2.11.89** |
|
||||||
| Status | Active — deployed on `dev.amn.gg` |
|
| 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 |
|
| Framework | Next.js 16 (App Router + Turbopack), React 19, TypeScript strict |
|
||||||
| Dev port | `8083` (both local and Docker) |
|
| Dev port | `8083` (both local and Docker) |
|
||||||
| Package manager | `yarn@1.22.22` |
|
| Package manager | `yarn@1.22.22` |
|
||||||
| Node requirement | `>=20` (host runs v26.0.0) |
|
| Node requirement | `>=20` |
|
||||||
| Repo | `git@git.manko.yoga:222/nick/frontend.git` |
|
| 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
|
## 2. Tech Stack
|
||||||
|
|
||||||
### Core
|
|
||||||
|
|
||||||
| Layer | Library / Version | Notes |
|
| Layer | Library / Version | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Framework | `next@^16.1.1` | App Router, Turbopack dev, standalone output |
|
| Framework | `next@^16.1.1` | App Router, Turbopack dev server |
|
||||||
| UI runtime | `react@^19.1.0` + `react-dom@^19.1.0` | |
|
| UI runtime | `react@^19.1.0`, `react-dom@^19.1.0` | Server + Client Components |
|
||||||
| Language | TypeScript `^6.0.3` strict | `noEmit` check required before push |
|
| 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` |
|
||||||
| Component library | `@mui/material@^9.0.1` | MUI v9 + `@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 |
|
||||||
| Styling | `@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 |
|
||||||
| Animation | `framer-motion@^12.13.0` | |
|
| Real-time | `socket.io-client@^4.8.1` | Bidirectional events; custom `SocketContext` |
|
||||||
| Icon system | `@iconify/react@^6.0.0` | |
|
| 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 |
|
||||||
### Data Fetching & State
|
| 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 |
|
||||||
| 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 |
|
|
||||||
| Hardware wallet | `@trezor/connect-web@^9.7.3` | Trezor signing flow |
|
| Hardware wallet | `@trezor/connect-web@^9.7.3` | Trezor signing flow |
|
||||||
|
| Chain indexing | `alchemy-sdk@^3.6.1` | Alchemy for multi-chain queries |
|
||||||
### Internationalization & Localisation
|
| 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 |
|
||||||
| Layer | Library | Notes |
|
| Animation | `framer-motion@^12.13.0` | Page transitions and UI motion |
|
||||||
|---|---|---|
|
| Carousel | `embla-carousel-react@8.6.0` | Product / shop carousels |
|
||||||
| i18n engine | `i18next@^26.3.0` + `react-i18next@^17.0.8` | |
|
| Maps | `mapbox-gl@^3.12.0`, `react-map-gl@^8.0.4` | Address / location pickers |
|
||||||
| Language detection | `i18next-browser-languagedetector@^8.1.0` | |
|
| HTTP client | `axios@^1.11.0` | Centralised instance with auth interceptors in `src/lib/axios.ts` |
|
||||||
| Lazy loading | `i18next-resources-to-backend@^1.2.1` | |
|
| Notifications | `notistack@^3.0.2`, `sonner@^2.0.3` | Snackbar + toast |
|
||||||
| Persian date | `date-fns-jalali@^4.1.0-0` | Jalali calendar date formatting |
|
| Error monitoring | `@sentry/nextjs@^10.22.0` | SDK wraps Next.js build + runtime |
|
||||||
| RTL styling | `stylis-plugin-rtl@^2.1.1` | Emotion cache flips properties for RTL |
|
| 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 |
|
||||||
### Observability & Testing
|
| QR code | `qrcode@^1.5.4` | Wallet payment QR generation |
|
||||||
|
| Fonts | DM Sans, Inter, Nunito Sans, Public Sans, Barlow | Variable fonts via `@fontsource-variable` |
|
||||||
| 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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. App Router Page Structure
|
## 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/<feature>/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 |
|
| `/` | Public | Landing / marketing home |
|
||||||
| `/api/health` | API route | Health check endpoint |
|
| `/api/health` | API Route | Container health-check endpoint |
|
||||||
| `/api/llm` | API route | LLM proxy (amanat-assist integration) |
|
| `/api/llm` | API Route | LLM proxy for amanat-assist features |
|
||||||
| `/auth/jwt/*` | Auth | Sign-in, sign-up, verify email, reset password, update password |
|
| `/auth/jwt/*` | Public | Sign-in, sign-up, OTP verify, password reset, update |
|
||||||
| `/checkout/` | Protected | Checkout flow entry (redirects to payment) |
|
| `/checkout/request-network/*` | Public | Request Network payment checkout shell |
|
||||||
| `/dashboard/` | Protected | Main authenticated shell (see sub-routes below) |
|
| `/dashboard/*` | Protected | Main authenticated app (see below) |
|
||||||
| `/design-preview/` | Dev | Component / theme preview (non-production) |
|
| `/design-preview` | Internal | Theme/component sandbox |
|
||||||
| `/error/` | Public | Global error page |
|
| `/error` | Public | Global error display |
|
||||||
| `/payment/` | Protected | Payment status / callback landing |
|
| `/payment/callback`, `/payment/cancel` | Public | Payment gateway redirect landing |
|
||||||
| `/post/[slug]` | Public | Blog / post reader |
|
| `/post/[slug]` | Public | Blog post reader |
|
||||||
| `/shop/[seller]/[id]` | Public | Public seller shop and product view |
|
| `/shop/[seller]/[id]` | Public | Public seller shop / item view |
|
||||||
| `/telegram/` | Mini App | Telegram Mini App shell (dedicated layout, see §7) |
|
| `/store/items`, `/store/checkout` | Public | Storefront browsing and checkout |
|
||||||
| `not-found.tsx` | Public | 404 page |
|
| `/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`.
|
| Route | Purpose |
|
||||||
|
|
||||||
| Sub-route | Purpose |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `account/` | Profile, avatar, address book, notification prefs, passkey, wallet linking |
|
| `/dashboard` → `/dashboard/overview` | KPI home tiles, recent activity |
|
||||||
| `admin/` | Admin control panel |
|
| `/dashboard/chat` | Real-time escrow chat |
|
||||||
| `assist/` | AI assistant chat (amanat-assist integration) |
|
| `/dashboard/account/*` | Profile, address, notifications, wallet, passkey |
|
||||||
| `chat/` | Real-time escrow negotiation chat |
|
| `/dashboard/request/*` | Buyer purchase requests |
|
||||||
| `disputes/` | Dispute hub — raise, view, respond |
|
| `/dashboard/request-template/*` | Seller request templates |
|
||||||
| `payment/` | Payment history and detail view |
|
| `/dashboard/payment/*` | Payment history and detail |
|
||||||
| `points/` | Loyalty hub — transaction log, referral tracking, level tiers |
|
| `/dashboard/points/*` | Loyalty hub — transactions, referrals, levels |
|
||||||
| `post/` | Admin blog editor (Tiptap) |
|
| `/dashboard/disputes/*` | Dispute creation and management |
|
||||||
| `request/` | Buyer purchase request management (create, track, accept offer) |
|
| `/dashboard/seller/*` | Seller-side offer management |
|
||||||
| `request-template/` | Seller request templates management |
|
| `/dashboard/shop-settings/*` | Seller shop configuration (incl. Telegram config) |
|
||||||
| `seller/` | Seller profile and analytics |
|
| `/dashboard/shops/*` | Browse / checkout from within dashboard |
|
||||||
| `shop-settings/` | Seller shop configuration (name, policies, payment rails) |
|
| `/dashboard/user/*` | Admin user management |
|
||||||
| `shops/` | Browse shops / checkout within dashboard scope |
|
| `/dashboard/post/*` | Admin blog editor (Tiptap) |
|
||||||
| `user/` | Admin user management |
|
| `/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.
|
The primary buyer journey:
|
||||||
- `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-<feature>-view.tsx` components.
|
|
||||||
|
|
||||||
### 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/`.
|
Multi-role dashboard accessible post-login. Guards:
|
||||||
2. **Sellers** receive notifications via Socket.io and submit offers (`received_offers` state).
|
- `AuthGuard` — redirects unauthenticated users to `/auth/jwt/sign-in`.
|
||||||
3. **Negotiation** phase: real-time chat (`/dashboard/chat/`) with offer counter-proposals.
|
- `EmailVerificationGuard` — blocks unverified accounts on key routes.
|
||||||
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/`.
|
|
||||||
|
|
||||||
### Dashboard & Admin
|
Sidebar nav adapts to role: buyer, seller, admin, or multi-tenant operator.
|
||||||
|
|
||||||
- Overview tiles with ApexCharts KPI cards.
|
### Admin
|
||||||
- Admin panel: user management, shop review, dispute arbitration, blog post management.
|
|
||||||
- Points / loyalty system: transaction ledger, referral tracking, tier levels at `src/sections/points/`.
|
`/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.
|
||||||
- AI assist panel: embedded `amanat-assist` chat at `/dashboard/assist/`.
|
|
||||||
|
|
||||||
### Telegram Mini App
|
### 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
|
## 5. State Management
|
||||||
|
|
||||||
The app uses a layered approach — no single global store:
|
| Layer | Mechanism | Usage |
|
||||||
|
|
||||||
| Layer | Tool | Scope |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Server state & cache | `@tanstack/react-query` | All API calls — fetching, mutations, invalidation |
|
| Server cache / async state | `@tanstack/react-query` | All API data fetching, mutation, background refetch |
|
||||||
| Supplementary fetch | `swr` | Some lightweight hooks |
|
| Legacy async state | `swr` | Some older sections not yet migrated to TQ |
|
||||||
| Local component state | `React.useState` / `useReducer` | Component-local UI state |
|
| Real-time events | `SocketContext` (`src/contexts/`) | Socket.io connection; exposes socket via `useSocket` hook |
|
||||||
| Cross-tree shared state | React Context | Socket connection (`src/contexts/`), Auth (`src/auth/context/`), Web3, Settings drawer, Localization |
|
| Global UI state | React Context (multiple providers) | Auth, Settings (theme/direction/language), Web3 |
|
||||||
| Form state | `react-hook-form` | All form instances, with `zod` schemas as resolvers |
|
| Form state | `react-hook-form` + Zod | All forms; validation on client |
|
||||||
| Settings (theme/locale) | Context + `localStorage` | Theme mode, layout direction, color preset, font — managed by `src/settings/` |
|
| 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:
|
The Telegram layout uses a minimal provider stack: `TonConnectUIProvider` + `QueryClientProvider` only — no dashboard providers.
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Internationalization
|
## 6. Internationalization
|
||||||
|
|
||||||
The app is RTL-first with Persian (Farsi) as the primary production language.
|
| Detail | Value |
|
||||||
|
|
||||||
| Aspect | Implementation |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| Engine | `i18next` + `react-i18next` |
|
| Library | `i18next@^26.3.0` + `react-i18next@^17.0.8` |
|
||||||
| Supported languages | `fa` (Persian), `ar` (Arabic), `en` (English), `fr` (French), `cn` (Chinese), `vi` (Vietnamese) |
|
| Language detection | `i18next-browser-languagedetector` + `accept-language` (server hint) |
|
||||||
| Translation files | `src/locales/langs/<lang>/*.json` — split by feature namespace |
|
| Locales shipped | English (`en`), Persian/Farsi (`fa`), Arabic (`ar`), French (`fr`), Chinese (`cn`), Vietnamese (`vi`) |
|
||||||
| RTL flip | `stylis-plugin-rtl` applied to the Emotion cache — physical CSS properties (margin-left, padding-right, etc.) are automatically mirrored |
|
| RTL locales | `fa`, `ar` — `direction: rtl` applied at theme level; `stylis-plugin-rtl` transforms MUI Emotion styles |
|
||||||
| LTR islands | Inline `dir="ltr"` on elements containing URLs, wallet addresses, token amounts, or other inherently LTR content |
|
| Jalali calendar | `date-fns-jalali@^4.1.0-0` — date pickers switch to Jalali for `fa` locale |
|
||||||
| Persian calendar | `date-fns-jalali` for Jalali date formatting; MUI date pickers use the Jalali locale adapter |
|
| Translation files | `src/locales/langs/{en,fa,ar,fr,cn,vi}/*.json` (lazy-loaded via `i18next-resources-to-backend`) |
|
||||||
| Direction state | Controlled via `SettingsContext` — users can toggle in the settings drawer |
|
| Telegram locales | `src/sections/telegram/locales/{en,fa}.ts` — standalone namespace for TMA strings |
|
||||||
| Config | `src/locales/locales-config.ts` + `src/locales/i18n-provider.tsx` |
|
| 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
|
## 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.
|
```tsx
|
||||||
- The root layout at `src/app/telegram/layout.tsx` provides a minimal provider stack:
|
<Script src="https://telegram.org/js/telegram-web-app.js" strategy="beforeInteractive" />
|
||||||
- `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.
|
|
||||||
|
|
||||||
### Key File Locations
|
No `@telegram-apps/sdk` npm package is used. The native CDN script approach is chosen for compatibility with Telegram's own versioned releases.
|
||||||
|
|
||||||
| Path | Purpose |
|
### WebApp wrapper
|
||||||
|
|
||||||
|
`src/utils/telegram-webapp.ts` provides a typed wrapper around `window.Telegram.WebApp`, exposing:
|
||||||
|
- `initData` / `initDataUnsafe` — raw launch parameters
|
||||||
|
- `colorScheme`, `themeParams` — Telegram UI theme
|
||||||
|
- `MainButton`, `BackButton` — native Telegram controls
|
||||||
|
- `close()`, `expand()`, `ready()` helper calls
|
||||||
|
|
||||||
|
### Auth flow
|
||||||
|
|
||||||
|
1. On TMA load, the app extracts `initData` from `window.Telegram.WebApp`.
|
||||||
|
2. The frontend calls `POST /api/auth/telegram` with the signed `initData` string.
|
||||||
|
3. Backend verifies the HMAC signature against `TELEGRAM_BOT_TOKEN` and issues a JWT.
|
||||||
|
4. The JWT is stored in memory / cookie, and subsequent API calls use the standard auth header.
|
||||||
|
|
||||||
|
Replay protection on this path is intentionally absent — Telegram may reuse `initData` across reloads. The backend relies on HMAC verification + `auth_date` freshness only.
|
||||||
|
|
||||||
|
### TMA route structure
|
||||||
|
|
||||||
|
| Route | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/app/telegram/layout.tsx` | TMA root layout — minimal providers |
|
| `/telegram` | Entry point — reads initData, authenticates, redirects |
|
||||||
| `src/app/telegram/page.tsx` | TMA entry point |
|
| `/telegram/shop` | Seller list and product browsing |
|
||||||
| `src/utils/telegram-webapp.ts` | Custom `window.Telegram.WebApp` wrapper / SDK util |
|
| `/telegram/cart` | In-app cart and checkout handoff |
|
||||||
| `src/sections/telegram/` | All TMA feature code |
|
| `/telegram/account` | Account tab (mirrors dashboard/account) |
|
||||||
| `src/sections/telegram/view/` | ~18 view components (one per TMA screen) |
|
|
||||||
| `src/sections/telegram/components/` | ~28 TMA-specific UI primitives |
|
|
||||||
| `src/sections/telegram/hooks/` | TMA-scoped hooks including `use-telegram-live-context` |
|
|
||||||
| `src/sections/telegram/telegram-shell-css.ts` | Native Telegram shell CSS variables integration |
|
|
||||||
|
|
||||||
### TMA Views
|
### Components
|
||||||
|
|
||||||
| View file | Screen |
|
`src/sections/telegram/components/` contains TMA-native components:
|
||||||
|---|---|
|
- `telegram-header.tsx` — top navigation bar styled to Telegram theme
|
||||||
| `telegram-mini-app-view.tsx` | Main shell / router (23 KB — primary orchestrator) |
|
- `telegram-chat-row.tsx`, `telegram-chat-bubble.tsx`, `telegram-chat-composer.tsx` — inline chat UI
|
||||||
| `telegram-home-view.tsx` | Home tab |
|
- `telegram-request-stepper.tsx` — step-through purchase request wizard
|
||||||
| `telegram-shop-view.tsx` | Shop list |
|
- `telegram-cart-fab.tsx` — floating cart icon
|
||||||
| `telegram-seller-shop-view.tsx` | Individual seller shop products |
|
- `telegram-onboarding-sheet.tsx` — first-run onboarding bottom sheet
|
||||||
| `telegram-cart-view.tsx` | Cart |
|
- `telegram-filter-drawer.tsx`, `telegram-list-row.tsx`, `telegram-list-controls.tsx` — marketplace list views
|
||||||
| `telegram-checkout-view.tsx` | Checkout |
|
- `telegram-theme-toggle.tsx`, `telegram-language-toggle.tsx` — in-app settings
|
||||||
| `telegram-payment-view.tsx` | Payment status |
|
- `telegram-unlinked-state.tsx` — shown when no shop is linked to the bot
|
||||||
| `telegram-requests-view.tsx` | Buyer requests list |
|
|
||||||
| `telegram-request-detail-view.tsx` | Request detail + offer management (31 KB) |
|
|
||||||
| `telegram-new-request-view.tsx` | New request wizard |
|
|
||||||
| `telegram-template-detail-view.tsx` | Seller template detail |
|
|
||||||
| `telegram-chat-view.tsx` | In-app chat thread list |
|
|
||||||
| `telegram-chat-thread-view.tsx` | Single chat thread |
|
|
||||||
| `telegram-archived-chats-view.tsx` | Archived chats |
|
|
||||||
| `telegram-account-view.tsx` | Account settings (18 KB) |
|
|
||||||
| `telegram-addresses-view.tsx` | Address book (15 KB) |
|
|
||||||
| `telegram-points-view.tsx` | Loyalty points |
|
|
||||||
| `telegram-notifications-view.tsx` | Notification centre |
|
|
||||||
| `telegram-settings-view.tsx` | App settings (14 KB) |
|
|
||||||
|
|
||||||
### TMA-specific Components
|
### Shop-settings Telegram config
|
||||||
|
|
||||||
Key primitives: `telegram-chat-row`, `telegram-request-stepper`, `telegram-onboarding-sheet`, `telegram-tab-bar`, `telegram-header`, `telegram-quick-actions`, `telegram-cart-fab`, `telegram-support-fab`, `telegram-welcome-banner`, `telegram-unlinked-state`, `telegram-unsupported-state`.
|
`/dashboard/shop-settings` includes a Telegram configuration UI where sellers can:
|
||||||
|
- Link their Telegram bot to their shop
|
||||||
### TON Wallet in TMA
|
- Set the Mini App URL
|
||||||
|
- Preview the bot launch button (`TelegramAppButton` component)
|
||||||
TON Connect (`@tonconnect/ui-react`, `@ton/core`) is active only inside the TMA layout. BSC payments via wagmi/viem are also available in-TMA but TON is the preferred rail for Telegram users.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Web3 Integration
|
## 8. Web3 Integration
|
||||||
|
|
||||||
All Web3 code lives under `src/web3/`.
|
### EVM (Ethereum / BSC / Base / Polygon / Arbitrum)
|
||||||
|
|
||||||
### Architecture
|
| Component | Detail |
|
||||||
|
|
||||||
```
|
|
||||||
src/web3/
|
|
||||||
├── config.ts # WEB3_CONFIG — chains, WalletConnect project ID
|
|
||||||
├── index.ts # Public barrel
|
|
||||||
├── types.ts # Shared Web3 types
|
|
||||||
├── utils.ts # Misc helpers
|
|
||||||
├── payment-rails.ts # Chain+token routing logic
|
|
||||||
├── decentralizedPayment.ts # Core payment execution (16 KB)
|
|
||||||
├── web3Service.ts # Service layer (9 KB)
|
|
||||||
├── paymentBackendService.ts # Backend sync after on-chain tx (12 KB)
|
|
||||||
├── tonconnect-provider.tsx # TonConnect provider wrapper
|
|
||||||
├── context/ # Web3Context provider
|
|
||||||
├── contracts/ # ABI definitions
|
|
||||||
├── hooks/
|
|
||||||
│ ├── use-web3-wagmi.ts # wagmi-based wallet + tx hooks (5.5 KB)
|
|
||||||
│ ├── use-alchemy.ts # Alchemy SDK hooks — balance, tx history (3.8 KB)
|
|
||||||
│ ├── use-chainlink.ts # Chainlink price feed hooks (2.6 KB)
|
|
||||||
│ └── use-web3-context.ts # Context consumer hook
|
|
||||||
├── services/ # Additional service modules
|
|
||||||
├── trezor/ # Trezor Connect integration
|
|
||||||
└── utils/ # Chain-specific utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
### Supported Chains
|
|
||||||
|
|
||||||
Declared in `WEB3_CONFIG.supportedChains`: **BSC** (default, lowest fees), Base, Polygon, Arbitrum, Ethereum.
|
|
||||||
|
|
||||||
Primary escrow payments run on BSC. BSC USDT is 18 decimals (non-standard; handled in `src/utils/currencyUtils.ts` — do not hardcode decimals).
|
|
||||||
|
|
||||||
### Wallet Support
|
|
||||||
|
|
||||||
| Wallet | Integration |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| MetaMask / injected | wagmi `injected()` connector |
|
| Wagmi | `wagmi@^2.19.5` — React hooks for wallet connection, transaction signing, contract reads |
|
||||||
| WalletConnect | wagmi WalletConnect connector (`NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID`) |
|
| Viem | `viem@^2.31.7` — low-level EVM client used by wagmi internally |
|
||||||
| Trezor | `@trezor/connect-web` in `src/web3/trezor/` |
|
| Ethers | `ethers@^6.15.0` — used in `src/web3/web3Service.ts` for legacy contract interaction |
|
||||||
| TON wallet | `@tonconnect/ui-react` (TMA only) |
|
| WalletConnect | Project ID via `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` |
|
||||||
|
| Alchemy | Per-chain API keys (`NEXT_PUBLIC_ALCHEMY_API_KEY_{MAINNET,ARBITRUM,BASE,POLYGON,SEPOLIA}`) for RPC and indexing |
|
||||||
|
| Escrow wallet | `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS` — the platform escrow contract / EOA |
|
||||||
|
|
||||||
### Oracle / Price Feeds
|
Chains supported: Ethereum Mainnet, BSC (BNB Chain), Base, Polygon, Arbitrum, Sepolia (testnet).
|
||||||
|
|
||||||
- Chainlink price feeds via `use-chainlink.ts` — used for USDT/USD peg monitoring.
|
Provider hierarchy in `src/web3/context/`:
|
||||||
- Alchemy SDK (`alchemy-sdk`) for on-chain data queries (balances, tx receipts).
|
- `wagmi-provider.tsx` — wraps app in `WagmiProvider` + `QueryClientProvider`
|
||||||
- Depeg protection feature in development — see `nick-doc/` oracle depeg protection design doc.
|
- `web3-provider.tsx` — DePay + custom payment orchestration layer
|
||||||
|
- `web3-context.tsx` + `use-web3-context.ts` — React Context for payment state
|
||||||
|
|
||||||
|
### Trezor
|
||||||
|
|
||||||
|
`@trezor/connect-web@^9.7.3` handles hardware wallet signing. `src/web3/components/web3-signing-card.tsx` surfaces the Trezor confirmation UI within the payment flow.
|
||||||
|
|
||||||
|
### TON
|
||||||
|
|
||||||
|
`@tonconnect/ui-react@^2.4.4` and `@ton/core@^0.63.1` enable TON wallet connections and payments. The Telegram layout wraps the Mini App in `TonConnectUIProvider` so TON payments work natively within Telegram.
|
||||||
|
|
||||||
|
### Payment UI components
|
||||||
|
|
||||||
|
| Component | Location |
|
||||||
|
|---|---|
|
||||||
|
| `web3-connect-card.tsx` | Wallet selection / connection modal |
|
||||||
|
| `web3-payment.tsx` | Payment execution with status tracking |
|
||||||
|
| `web3-signing-card.tsx` | Trezor/hardware wallet signing prompt |
|
||||||
|
|
||||||
|
### Request Network
|
||||||
|
|
||||||
|
`/app/checkout/request-network/` is the payment shell for Request Network-based payments. The checkout flow is server-side rendered and uses a dedicated layout outside the dashboard guard.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. CI/CD
|
## 9. CI/CD
|
||||||
|
|
||||||
### Pipeline
|
Two Woodpecker pipelines run on the CI host at `89.58.32.32` (`linux/arm64`).
|
||||||
|
|
||||||
CI is managed by Woodpecker at `frontend/.woodpecker/production.yml`.
|
### `production.yml` — `main` branch → dev.amn.gg
|
||||||
|
|
||||||
**Trigger:** push to `main` or `master` branch.
|
Trigger: push to `main` or `master`.
|
||||||
|
|
||||||
**Agent:** `platform: linux/arm64` (netcup agent on `89.58.32.32`).
|
| Step | Action |
|
||||||
|
|---|---|
|
||||||
|
| `get-version` | Reads `package.json` version → writes `dev-<version>` to `.tags` |
|
||||||
|
| `build-and-deploy` | `docker build -t git.tbs.amn.gg/escrow/frontend:dev .` then `docker compose up -d --no-deps --pull never frontend` against `/opt/escrow-dev/docker-compose.yml` |
|
||||||
|
| `notify` | `node scripts/ci/tg-notify.cjs` → Telegram notification (success/failure) |
|
||||||
|
|
||||||
**Steps:**
|
Image tag: `git.tbs.amn.gg/escrow/frontend:dev`. No registry push — image is built locally on the CI host. `pull_policy: never` in the compose override prevents watchtower from pulling a stale remote image.
|
||||||
|
|
||||||
| Step | Image | Action |
|
### `multi.yml` — `feature/white-label-shops` → multi.amn.gg
|
||||||
|---|---|---|
|
|
||||||
| `get-version` | `node:22-alpine` | Reads `package.json` version → writes `dev-<version>` to `.tags` |
|
|
||||||
| `build-and-deploy` | `docker:27-cli` | `docker build -t git.tbs.amn.gg/escrow/frontend:dev .` then `docker compose up -d --no-deps --pull never frontend` against `/opt/escrow-dev/docker-compose.yml` |
|
|
||||||
| `notify` | `node:22-alpine` | Posts Telegram notification (success or failure) via `scripts/ci/tg-notify.cjs` using `TG_TOKEN` + `TG_USERS` secrets |
|
|
||||||
|
|
||||||
**Important CI notes:**
|
Trigger: push to `feature/white-label-shops`.
|
||||||
|
|
||||||
- The image is built locally on the host — it does **not** pull from a registry. `docker-compose.override.yml` sets `pull_policy: never`.
|
| Step | Action |
|
||||||
- Turbopack is dev-only. The production build uses standard `next build` (webpack).
|
|---|---|
|
||||||
- `next build` runs a strict TypeScript type-check. The build fails on type errors.
|
| `build-local` | `docker build` with hardcoded `NEXT_PUBLIC_*` build-args for `multi.amn.gg` → tags as `escrow-multi-frontend:local` |
|
||||||
- **Always bump `package.json` version before pushing** to `main`/`master`. Docker tags use `dev-<version>`. Reusing the same version overwrites the previous image tag and breaks rollback.
|
| `deploy` | `docker compose up -d --force-recreate --no-deps frontend` against `/opt/arcane/data/projects/escrow-multi` |
|
||||||
- A CI green check does not guarantee the image was pushed to the registry. Verify the registry tag manually if deployment seems stale.
|
| `notify` | `node scripts/ci/tg-notify.cjs` → Telegram notification |
|
||||||
|
|
||||||
### Docker Build
|
> [!important] Version bump required
|
||||||
|
> Every push that triggers a build must increment the patch version in `package.json`. Container images are tagged by version — an unchanged version overwrites the previous image and loses history. See RTK.md version policy.
|
||||||
|
|
||||||
- Output mode: `standalone` (set in `next.config.js`).
|
### Telegram CI notifications
|
||||||
- Start command: `PORT=8083 node .next/standalone/server.js`.
|
|
||||||
- Post-build: `cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/` (required for static assets with standalone output).
|
`scripts/ci/tg-notify.cjs` is the CI notification script. It reads `TG_TOKEN` and `TG_USERS` from Woodpecker secrets. Messages must not use `parse_mode` (HTML/Markdown) to avoid Telegram API 400 errors from unescaped characters in commit messages.
|
||||||
- Build cache: `--mount=type=cache` for apk, yarn, and `.next/cache` — incremental Next.js rebuilds on unchanged packages.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -376,77 +326,93 @@ cd frontend/
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Copy env file and fill in values
|
# Copy and populate env vars
|
||||||
cp .env.local.example .env.local
|
cp .env.local.example .env.local
|
||||||
# Edit .env.local — see §11 for required vars
|
# Edit .env.local with backend URL, API keys, etc.
|
||||||
|
|
||||||
# Start dev server (Turbopack, port 8083)
|
# Start dev server (Turbopack, port 8083)
|
||||||
yarn dev
|
yarn dev
|
||||||
|
|
||||||
# Alternative: webpack (slower, more compatible)
|
# Alternative: webpack (for debugging Turbopack-specific issues)
|
||||||
yarn dev:webpack
|
yarn dev:webpack
|
||||||
|
|
||||||
# Type check (must pass before push)
|
# Type-check
|
||||||
npx tsc --noEmit --ignoreDeprecations 6.0
|
npx tsc --noEmit
|
||||||
|
|
||||||
# Lint
|
# Lint
|
||||||
yarn lint
|
yarn lint
|
||||||
yarn lint:fix
|
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
yarn test
|
yarn test
|
||||||
|
|
||||||
# E2E tests (requires running app)
|
# E2E tests (requires running backend)
|
||||||
yarn playwright:install
|
yarn playwright:install # once
|
||||||
yarn test:e2e
|
yarn test:e2e
|
||||||
|
|
||||||
# Production build (validates types + builds)
|
# Production build (outputs standalone server)
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
# Run standalone production build
|
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** The dev server binds to `http://localhost:8083`. When proxied via infra-caddy on the dev server, it maps to `https://dev.amn.gg`.
|
The standalone server output is at `.next/standalone/server.js`. The `build` script copies static assets and public folder into the standalone bundle automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Environment Variables
|
## 11. Environment Variables
|
||||||
|
|
||||||
All public vars are prefixed `NEXT_PUBLIC_` and baked into the client bundle at build time.
|
All `NEXT_PUBLIC_*` variables are baked into the client bundle at build time. Server-side and secret variables are runtime-only.
|
||||||
|
|
||||||
| Variable | Required | Description |
|
### Client-side (build-time baked)
|
||||||
|---|---|---|
|
|
||||||
| `NEXT_PUBLIC_APP_NAME` | Yes | Application display name (e.g. `Amanat`) |
|
|
||||||
| `NEXT_PUBLIC_APP_VERSION` | Yes | App version string — should match `package.json` version |
|
|
||||||
| `NEXT_PUBLIC_BACKEND_URL` | Yes | Base URL for backend API (e.g. `https://api.dev.amn.gg`) |
|
|
||||||
| `NEXT_PUBLIC_API_URL` | Yes | API endpoint root (often same as `BACKEND_URL` + `/api`) |
|
|
||||||
| `NEXT_PUBLIC_SOCKET_URL` | No | Socket.IO server URL — falls back to `NEXT_PUBLIC_BACKEND_URL` |
|
|
||||||
| `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS` | Yes | On-chain escrow holding wallet address |
|
|
||||||
| `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` | Yes | WalletConnect Cloud project ID |
|
|
||||||
| `NEXT_PUBLIC_ALCHEMY_API_KEY_MAINNET` | Yes | Alchemy API key for Ethereum mainnet |
|
|
||||||
| `NEXT_PUBLIC_ALCHEMY_API_KEY_SEPOLIA` | Yes | Alchemy API key for Sepolia testnet |
|
|
||||||
| `NEXT_PUBLIC_ALCHEMY_API_KEY_POLYGON` | Yes | Alchemy API key for Polygon |
|
|
||||||
| `NEXT_PUBLIC_MAPBOX_API_KEY` | No | Mapbox GL token for map components |
|
|
||||||
| `NEXT_PUBLIC_ASSETS_DIR` | No | Custom assets base URL — defaults to empty (local `/public`) |
|
|
||||||
| `BUILD_STATIC_EXPORT` | No | Set `true` to enable static export mode |
|
|
||||||
| `NODE_ENV` | Auto | Set by Next.js (`development` / `production`) |
|
|
||||||
|
|
||||||
Actual values for the dev deployment are stored in `~/.agentSecrets/escrow/CLAUDE.md` (not in the repo).
|
| Variable | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `NEXT_PUBLIC_API_URL` | Backend API base URL (e.g. `https://dev.amn.gg/api`) |
|
||||||
|
| `NEXT_PUBLIC_BACKEND_URL` | Backend root URL (used for non-API paths) |
|
||||||
|
| `NEXT_PUBLIC_APP_URL` | Canonical frontend URL |
|
||||||
|
| `NEXT_PUBLIC_SOCKET_URL` | Socket.io server URL |
|
||||||
|
| `NEXT_PUBLIC_APP_NAME` | Display name of the application |
|
||||||
|
| `NEXT_PUBLIC_APP_VERSION` | App version string (mirrors `package.json` version) |
|
||||||
|
| `NEXT_PUBLIC_ASSETS_DIR` | Public assets base path |
|
||||||
|
| `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` | WalletConnect Cloud project ID |
|
||||||
|
| `NEXT_PUBLIC_ALCHEMY_API_KEY_MAINNET` | Alchemy RPC key — Ethereum Mainnet |
|
||||||
|
| `NEXT_PUBLIC_ALCHEMY_API_KEY_BASE` | Alchemy RPC key — Base |
|
||||||
|
| `NEXT_PUBLIC_ALCHEMY_API_KEY_ARBITRUM` | Alchemy RPC key — Arbitrum |
|
||||||
|
| `NEXT_PUBLIC_ALCHEMY_API_KEY_POLYGON` | Alchemy RPC key — Polygon |
|
||||||
|
| `NEXT_PUBLIC_ALCHEMY_API_KEY_SEPOLIA` | Alchemy RPC key — Sepolia testnet |
|
||||||
|
| `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS` | Platform escrow wallet / contract address |
|
||||||
|
| `NEXT_PUBLIC_TELEGRAM_BOT_ID` | Telegram Bot ID for Mini App auth |
|
||||||
|
| `NEXT_PUBLIC_TELEGRAM_MINI_APP_URL` | Deep link URL for TMA launches |
|
||||||
|
| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | Cloudflare Turnstile site key |
|
||||||
|
| `NEXT_PUBLIC_GOOGLE_CLIENT_ID` | Google OAuth client ID |
|
||||||
|
| `NEXT_PUBLIC_MAPBOX_API_KEY` | Mapbox GL access token |
|
||||||
|
| `NEXT_PUBLIC_ENABLE_TEST_PAYMENT` | `"true"` to show test payment UI |
|
||||||
|
| `NEXT_PUBLIC_PASSKEY_RP_ID` | WebAuthn relying party ID |
|
||||||
|
| `NEXT_PUBLIC_PASSKEY_ORIGIN` | WebAuthn origin |
|
||||||
|
|
||||||
|
### Multi-stack build-arg overrides (Woodpecker `multi.yml`)
|
||||||
|
|
||||||
|
The multi pipeline passes `NEXT_PUBLIC_APP_URL`, `NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_BACKEND_URL`, `NEXT_PUBLIC_SERVER_URL`, `NEXT_PUBLIC_SOCKET_URL`, `NEXT_PUBLIC_PASSKEY_RP_ID`, and `NEXT_PUBLIC_PASSKEY_ORIGIN` as `--build-arg` to target `multi.amn.gg`.
|
||||||
|
|
||||||
|
> [!warning] Two stacks, two bot tokens
|
||||||
|
> `NEXT_PUBLIC_TELEGRAM_BOT_ID` must differ between the `escrow-dev` and `escrow-multi` stacks. Sharing a bot token causes Telegram webhook delivery to break for one of the stacks.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Known Issues / Open Items
|
## 12. Known Issues / Open Items
|
||||||
|
|
||||||
| # | Issue | Status |
|
| ID | Area | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | **Socket room scoping** — global payment socket broadcasts previously wiped every user's cart. A provider gate was added in frontend `v2.8.4` to filter by provider. Backend-side room scoping is still an open follow-up. | Open |
|
| FE-01 | Performance | Measured 300–800ms API response times on `dev.amn.gg` are WAN RTT-bound (~235ms), not DB-bound. Server-side requests from loopback `:8083` show 3–12ms. Fix: CDN/edge delivery or server-side rendering that avoids client roundtrips. |
|
||||||
| 2 | **Backend rate limiter on GET /payment/:id** — `paymentLimiter` (30 req/15 min) applies to the payment status poll endpoint. Results in 429 during rapid callback polling, leaving payments stuck in "processing". Fix is on backend side but frontend polling interval could be increased as a mitigation. | Open (backend fix pending) |
|
| FE-02 | Cart isolation | Global payment socket broadcasts previously wiped every user's cart. Provider gate added in v2.8.4; backend room-scoping remains an open follow-up. |
|
||||||
| 3 | **Offer rejection UI** — "all sellers stuck at step 4" was a UI-only bug; backend rejects and notifies correctly. Telegram seller step must use `mojtaba`'s `StepContext` (introduced in `fe v2.9.13`). | Resolved in v2.9.13 |
|
| FE-03 | Real-time | Socket.io rooms are not yet fully scoped by provider on the backend — global broadcast events can still leak cross-user in some edge cases. |
|
||||||
| 4 | **Cart wipe regression risk** — any new global socket event handler must be scoped by `provider:` to avoid touching RN or other payment records. | Ongoing convention |
|
| FE-04 | TMA auth | Telegram `initData` replay protection is intentionally absent. If the threat model changes, a server-side session deduplication layer would be needed at `/api/auth/telegram`. |
|
||||||
| 5 | **Performance is network-bound** — Mongo API profiling shows 300–800 ms response times due to WAN RTT (~235 ms). Server-side processing is 3–12 ms. Frontend-side CDN / edge caching is the recommended fix; DB migration will not help. | Open |
|
| FE-05 | State migration | Some data-fetching paths still use `swr` rather than `@tanstack/react-query`. Incremental migration to TQ is ongoing. |
|
||||||
| 6 | **Oracle depeg protection** — server-side oracle quoting for multi-currency pricing + stablecoin depeg protection is designed and approved. Build starts on a new dev branch. | In progress |
|
| FE-06 | Multi-chain scanner | The AMN scanner payment rail watches specific chains. Cross-chain support (multi-seller + multi-chain) is not fully wired. |
|
||||||
| 7 | **Multi-chain for amn.scanner** — the in-house scanner pay-in path is not yet multi-chain; verify scanner watches mainnet addresses before enabling multi-chain selection in the UI for scanner provider. | Open |
|
| FE-07 | Trezor | `@trezor/connect-web` requires a popup; CSP and popup-blocker edge cases exist in some Telegram In-App Browser contexts. |
|
||||||
| 8 | **Parallel agent pushes** — a second agent (moojttaba) pushes to the same branches. Always `git fetch --rebase` before pushing. Version-bump conflicts are expected. | Ongoing |
|
| FE-08 | Pre-push hook | The backend repo has a pre-push TSC hook that blocks on full-tree errors; a parallel agent's mid-refactor tree can block clean frontend commits. Always use explicit `git add <paths>`, never `git add -A`. |
|
||||||
| 9 | **Tiptap / rich text in TMA** — the Tiptap editor is desktop-optimised; its usability on mobile Telegram is untested at scale. | Not verified |
|
| FE-09 | CI version policy | Every CI-triggering push must bump the patch version. An unchanged version silently overwrites the previous image tag. |
|
||||||
| 10 | **`design-preview/` route** — present in the app router but should be excluded or protected in production builds. | Low priority |
|
| FE-10 | TON payments | TON wallet integration is present but not fully tested end-to-end on mainnet. Verify TON mainnet contract addresses before enabling for buyers. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 2026-06-12 — reflects v2.11.89*
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ title: AMN Pay Scanner
|
|||||||
tags: [service, scanner, payment, go, blockchain]
|
tags: [service, scanner, payment, go, blockchain]
|
||||||
version: 0.1.10
|
version: 0.1.10
|
||||||
created: 2026-06-08
|
created: 2026-06-08
|
||||||
|
updated: 2026-06-12
|
||||||
---
|
---
|
||||||
|
|
||||||
# AMN Pay Scanner
|
# AMN Pay Scanner
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> Version: **0.1.10** — Go 1.25 — Status: **production (BSC + Ethereum + BSC Testnet)**, other chains staged
|
> Version: **0.1.10** — Go 1.25 — Status: **production (BSC + Ethereum + BSC Testnet)**, other chains staged behind `SCANNER_ENABLED_CHAINS`
|
||||||
> Repo: `scanner/` within the escrow monorepo.
|
> Repo: `scanner/` within the escrow monorepo.
|
||||||
> Cross-ref: [[Scanner Architecture]] | [[Scanner API]]
|
> Cross-ref: [[Scanner Architecture]] | [[Scanner API]]
|
||||||
|
|
||||||
@@ -16,25 +17,37 @@ created: 2026-06-08
|
|||||||
|
|
||||||
## 1. Overview
|
## 1. Overview
|
||||||
|
|
||||||
AMN Pay Scanner is a standalone Go microservice that watches on-chain payment events and notifies the backend via signed webhook when a payment is confirmed.
|
AMN Pay Scanner is a standalone Go microservice that watches on-chain payment events across EVM chains, Tron, and TON, and notifies the backend via signed webhook when a payment is confirmed.
|
||||||
|
|
||||||
### What it replaces
|
### What it replaces
|
||||||
|
|
||||||
Previously, the AMN escrow platform relied on **Request Network** as the payment infrastructure layer. Request Network introduced:
|
The platform previously relied on **Request Network** as its payment infrastructure layer. That dependency introduced:
|
||||||
|
|
||||||
- An external smart-contract dependency (`ERC20FeeProxy`) on RN's deployment schedule
|
- An external smart-contract registry whose canonical proxy addresses differ per chain and cannot be trusted without on-chain verification (see memory note on RN proxy addresses)
|
||||||
- A closed fee-proxy address registry that differs per chain and is not reliably canonical (see memory note on RN proxy addresses)
|
- A closed RN event/webhook pipeline that the backend had no control over
|
||||||
- A separate webhook/event pipeline managed by RN's infrastructure
|
- A hard SDK coupling between the backend and RN's versioned contracts
|
||||||
- A hard coupling between the backend and RN's SDK
|
- Inability to support Tron or TON (not in RN's network)
|
||||||
|
|
||||||
AMN Pay Scanner removes all of these by:
|
AMN Pay Scanner replaces this entirely by:
|
||||||
|
|
||||||
1. Deploying the same `ERC20FeeProxy` contract under our own control
|
1. Deploying an in-house `ERC20FeeProxy` contract on each EVM chain under our own control
|
||||||
2. Polling RPC endpoints directly (no RN nodes)
|
2. Polling RPC endpoints directly — no RN nodes, no RN SDK
|
||||||
3. Deriving payment references in-house using the same keccak256 formula as the proxy contract
|
3. Deriving payment references in-house using the same keccak256 formula the proxy contract expects
|
||||||
4. Delivering webhooks signed with a backend-controlled HMAC secret
|
4. Delivering signed webhooks using a backend-controlled HMAC secret
|
||||||
|
5. Supporting **direct-address rails** (Tron, TON, manual EVM) where no proxy contract is needed
|
||||||
|
|
||||||
The scanner also supports **direct-address payment rails** (Tron, TON, and manual EVM flows) where no proxy contract is involved at all.
|
### Current status
|
||||||
|
|
||||||
|
| Chain | Status |
|
||||||
|
|---|---|
|
||||||
|
| BNB Smart Chain (56) | Production |
|
||||||
|
| Ethereum Mainnet (1) | Production |
|
||||||
|
| BSC Testnet (97) | Production (testnet) |
|
||||||
|
| Arbitrum One (42161) | Staged — `verified: false` |
|
||||||
|
| Polygon (137) | Staged — `verified: false` |
|
||||||
|
| Base (8453) | Staged — `verified: false` |
|
||||||
|
| Tron Mainnet (728126428) | Staged — `verified: false` |
|
||||||
|
| TON Mainnet (1100) | Staged — `verified: false` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,8 +63,10 @@ Backend Scanner Chain
|
|||||||
│ destination, callbackUrl}│ │
|
│ destination, callbackUrl}│ │
|
||||||
├──────────────────────────► │ │
|
├──────────────────────────► │ │
|
||||||
│ │ persist intent (SQLite) │
|
│ │ persist intent (SQLite) │
|
||||||
│ {intentId, │ derive paymentReference │
|
│ │ derive paymentReference │
|
||||||
│ paymentReference, │ compute topicRef (EVM) │
|
│ │ compute topicRef (EVM) │
|
||||||
|
│ {intentId, │ │
|
||||||
|
│ paymentReference, │ │
|
||||||
│ checkoutBlock} │ │
|
│ checkoutBlock} │ │
|
||||||
◄──────────────────────────── │ │
|
◄──────────────────────────── │ │
|
||||||
│ │ │
|
│ │ │
|
||||||
@@ -70,12 +85,12 @@ Backend Scanner Chain
|
|||||||
│ │ blocks / finality signal) │
|
│ │ blocks / finality signal) │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ POST callbackUrl │ │
|
│ POST callbackUrl │ │
|
||||||
|
│ X-AMN-Signature: ... │ │
|
||||||
│ {intentId, txHash, │ │
|
│ {intentId, txHash, │ │
|
||||||
│ status:"confirmed", ...} │ │
|
│ status:"confirmed", ...} │ │
|
||||||
◄──────────────────────────── │ │
|
◄──────────────────────────── │ │
|
||||||
│ 200 OK │ │
|
│ 200 OK │ │
|
||||||
├──────────────────────────► │ │
|
├──────────────────────────► │ │
|
||||||
│ │ status → confirmed │
|
|
||||||
│ │ record webhookDeliveredAt │
|
│ │ record webhookDeliveredAt │
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -88,78 +103,83 @@ pending ──(tx seen)──► confirming ──(depth reached)──► confi
|
|||||||
└────────────────────────┴──────────────► expired webhook_failed
|
└────────────────────────┴──────────────► expired webhook_failed
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Tron / TON** skip `confirming` — their API only returns finalized events, so the status jumps directly to `confirmed`.
|
- **Tron / TON** skip `confirming` — their chain APIs only surface already-finalized transactions. Status jumps directly to `confirmed`.
|
||||||
- **Startup reconciliation**: on startup, any `confirmed` intent with `webhook_delivered_at IS NULL` created in the last 7 days has its webhook re-delivered. This covers crashes between finalization and delivery.
|
- **Startup reconciliation**: on startup, any `confirmed` intent with `webhook_delivered_at IS NULL` created within the last 7 days has its webhook re-delivered. This recovers from crashes between `finalizeIntent` and `deliverWebhook`.
|
||||||
- **`webhook_failed`** intents are retried on `WEBHOOK_RETRY_HOURS` schedule (default 6 h) and immediately via `POST /admin/webhooks/retry`.
|
- **`webhook_failed`** intents are retried every `WEBHOOK_RETRY_HOURS` (default 6 h) and immediately on `POST /admin/webhooks/retry`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Supported Chains
|
## 3. Supported Chains
|
||||||
|
|
||||||
> Chains marked **verified: false** in `supported-chains.json` do NOT start a worker goroutine at runtime. Override with `SCANNER_ENABLED_CHAINS` env var to force-enable specific chain IDs without a code change.
|
> [!note]
|
||||||
|
> Chains marked `verified: false` in `supported-chains.json` do **not** start a worker goroutine at runtime. Force-enable specific chain IDs without a rebuild by setting `SCANNER_ENABLED_CHAINS=56,1,42161`.
|
||||||
|
|
||||||
| Chain | Chain ID | Type | Proxy Address | Confirmation Depth | Verified |
|
| Chain | Chain ID | Type | Proxy Address | Confirmation Depth | Active by Default |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| BNB Smart Chain | 56 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 200 blocks | **yes** |
|
| BNB Smart Chain | 56 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 200 blocks (~10 min) | **yes** |
|
||||||
| Ethereum Mainnet | 1 | EVM | `0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C` | 50 blocks | **yes** |
|
| Ethereum Mainnet | 1 | EVM | `0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C` | 50 blocks (~10 min) | **yes** |
|
||||||
| BSC Testnet | 97 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 5 blocks | **yes** (testnet) |
|
| BSC Testnet | 97 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 5 blocks | **yes** (testnet) |
|
||||||
| Arbitrum One | 42161 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 2400 blocks | no |
|
| Arbitrum One | 42161 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 2400 blocks (~54 min) | no |
|
||||||
| Polygon | 137 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 300 blocks | no |
|
| Polygon | 137 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 300 blocks | no |
|
||||||
| Base | 8453 | EVM | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` | 300 blocks | no |
|
| Base | 8453 | EVM | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` | 300 blocks | no |
|
||||||
| Tron Mainnet | 728126428 | Tron | TRC20 USDT contract (`TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`) | TronGrid confirmed (~200 reported) | no |
|
| Tron Mainnet | 728126428 | Tron | TRC20 USDT contract `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | TronGrid confirmed (~200 reported) | no |
|
||||||
| TON Mainnet | 1100 | TON | USDT Jetton master (`EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs`) | TonCenter finalized (~120 reported) | no |
|
| TON Mainnet | 1100 | TON | USDT Jetton master `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` | TonCenter finalized (~120 reported) | no |
|
||||||
|
|
||||||
> [!warning] Chain notes
|
> [!warning] Chain-specific notes
|
||||||
> - **Ethereum**: uses the older v0.1.0 proxy ABI at `0x370DE27…`. A v0.2.0-next proxy is also deployed at `0x0DfbEe…` on ETH but the scanner checkout uses the v0.1.0 ABI — do not swap addresses silently.
|
> - **Ethereum**: uses the v0.1.0 proxy ABI at `0x370DE27…`. A v0.2.0-next proxy is also deployed at `0x0DfbEe…` on ETH but checkout uses the v0.1.0 ABI — do not swap addresses.
|
||||||
> - **Base**: proxy address is non-canonical (differs from the CREATE2 expected address per RN smart-contracts artifact v0.2.0). See memory note on RN proxy addresses.
|
> - **Arbitrum**: 2400-block threshold covers the optimistic rollup challenge window (~54 min at ~1.3 s/block).
|
||||||
> - **Tron**: no fee-proxy contract exists. Matching is by unique destination address, not payment reference.
|
> - **Base**: proxy address `0x1892196…` is non-canonical — it differs from the RN CREATE2 expected address for this chain. Verify on-chain before enabling in production.
|
||||||
> - **TON**: lag is reported in **seconds** (not blocks); per-intent polling is O(pending intents) API calls per cycle — known scaling concern.
|
> - **Tron**: no fee-proxy contract exists on Tron. Matching is by unique HD-derived destination address, not payment reference.
|
||||||
|
> - **TON**: lag is reported in **seconds**, not blocks. Per-intent polling is O(pending intents) TonCenter calls per cycle.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Architecture Diagram
|
## 4. Architecture Diagram
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
│ scanner binary │
|
│ scanner binary │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─────────────┐ ┌──────────────────────────────────────────┐ │
|
│ ┌──────────────────┐ ┌────────────────────────────────────────┐ │
|
||||||
│ │ HTTP API │ │ Worker Pool │ │
|
│ │ HTTP API │ │ Worker Pool │ │
|
||||||
│ │ (api.go) │ │ │ │
|
│ │ (api.go) │ │ │ │
|
||||||
│ │ │ │ ┌──────────────┐ eth_getLogs / eth_ │ │
|
│ │ │ │ ┌────────────────┐ eth_getLogs / │ │
|
||||||
│ │ POST /intents│ │ │ ChainWorker │─► blockNumber (JSON-RPC│ │
|
│ │ POST /intents │ │ │ ChainWorker ├─► eth_blockNumber │ │
|
||||||
│ │ GET /intents│ │ │ (EVM×N) │ per chain) │ │
|
│ │ GET /intents │ │ │ (EVM × N) │ (JSON-RPC) │ │
|
||||||
│ │ /balances │ │ └──────────────┘ │ │
|
│ │ DELETE /intents │ │ └────────────────┘ │ │
|
||||||
│ │ /balance- │ │ ┌──────────────┐ TronGrid REST API │ │
|
│ │ POST /balances │ │ ┌────────────────┐ TronGrid REST │ │
|
||||||
│ │ watches │ │ │ TronChain- │─► /v1/contracts/events │ │
|
│ │ /check │ │ │ TronChain- ├─► /v1/contracts/ │ │
|
||||||
│ │ /scanner/ │ │ │ Worker │ │ │
|
│ │ POST /balance- │ │ │ Worker │ {addr}/events │ │
|
||||||
│ │ status │ │ └──────────────┘ │ │
|
│ │ watches │ │ └────────────────┘ │ │
|
||||||
│ │ /admin/ │ │ ┌──────────────┐ TonCenter v3 REST │ │
|
│ │ GET /balance- │ │ ┌────────────────┐ TonCenter v3 │ │
|
||||||
│ │ webhooks/ │ │ │ TonChain- │─► /jetton/transfers │ │
|
│ │ watches/id │ │ │ TonChain- ├─► /jetton/ │ │
|
||||||
│ │ retry │ │ │ Worker │ │ │
|
│ │ DEL /balance- │ │ │ Worker │ transfers │ │
|
||||||
│ └──────┬──────┘ │ └──────────────┘ │ │
|
│ │ watches/id │ │ └────────────────┘ │ │
|
||||||
│ │ └─────────────┬────────────────────────────┘ │
|
│ │ GET /scanner/ │ └─────────────┬──────────────────────── ┘ │
|
||||||
│ │ │ match / confirm │
|
│ │ status │ │ match / confirm │
|
||||||
│ ▼ ▼ │
|
│ │ POST /admin/ │ ▼ │
|
||||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
│ │ webhooks/retry │ ┌────────────────────────────────────────┐ │
|
||||||
│ │ SQLite (WAL) │ │
|
│ └────────┬──────────┘ │ SQLite (WAL mode) │ │
|
||||||
│ │ intents · checkpoints · balance_watches │ │
|
│ │ │ intents · checkpoints · balance_watches│ │
|
||||||
│ └──────────────────────────────────────────────────────────┘ │
|
│ │ └───────────────┬────────────────────────┘ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ ▼ ▼ │
|
│ ▼ ▼ │
|
||||||
│ ┌────────────────┐ ┌─────────────────────────────────────┐ │
|
│ ┌─────────────────┐ ┌────────────────────────────────────────┐ │
|
||||||
│ │ BalanceWatch- │ │ webhook.go │ │
|
│ │ BalanceWatch- │ │ webhook.go │ │
|
||||||
│ │ Scheduler │ │ HMAC-SHA256 sign → POST callbackUrl │ │
|
│ │ Scheduler │ │ HMAC-SHA256 sign → POST callbackUrl │ │
|
||||||
│ │(balance.go) │ │ retry: 5s→30s→2m→10m→1h→failed │ │
|
│ │ (balance_ │ │ retry: 5s → 30s → 2m → 10m → 1h │ │
|
||||||
│ └────────────────┘ └─────────────────────────────────────┘ │
|
│ │ watch.go) │ │ → webhook_failed│ │
|
||||||
|
│ └─────────────────┘ └────────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ Background loops (main.go): │
|
│ Background loops (main.go): │
|
||||||
│ • intent TTL expiry (INTENT_TTL_HOURS) │
|
│ • intent TTL expiry (INTENT_TTL_HOURS) │
|
||||||
│ • webhook retry loop (WEBHOOK_RETRY_HOURS) │
|
│ • webhook retry loop (WEBHOOK_RETRY_HOURS) │
|
||||||
│ • startup reconciliation (confirmed, no delivery) │
|
│ • startup reconciliation (confirmed intents, no delivery) │
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
One worker goroutine is spawned per active chain. All three chain types implement a common `Worker` interface (`start()`, `stop()`, `getHead()`). Workers poll on `POLL_INTERVAL_SEC` (default 15 s).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. API Routes
|
## 5. API Routes
|
||||||
@@ -169,14 +189,14 @@ All endpoints except `/health` require `Authorization: Bearer <SCANNER_API_KEY>`
|
|||||||
| Method | Path | Auth | Purpose |
|
| Method | Path | Auth | Purpose |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `GET` | `/health` | none | Liveness probe — returns `{"status":"ok"}` |
|
| `GET` | `/health` | none | Liveness probe — returns `{"status":"ok"}` |
|
||||||
| `GET` | `/scanner/status` | Bearer | Chain lag, last scanned block, pending intent counts per chain |
|
| `GET` | `/scanner/status` | Bearer | Chain lag, last scanned block, pending intent count, active balance-watch count per chain |
|
||||||
| `POST` | `/intents` | Bearer | Register a payment intent; returns `intentId`, `paymentReference`, `checkoutBlock` |
|
| `POST` | `/intents` | Bearer | Register a payment intent; returns `intentId`, `paymentReference`, `checkoutBlock` |
|
||||||
| `GET` | `/intents/{id}` | Bearer | Fetch full intent record including current status and tx details |
|
| `GET` | `/intents/{id}` | Bearer | Fetch full intent record including current status and tx details |
|
||||||
| `DELETE` | `/intents/{id}` | Bearer | Cancel a pending intent (sets status to `expired`) |
|
| `DELETE` | `/intents/{id}` | Bearer | Cancel a pending intent (sets status to `expired`) |
|
||||||
| `POST` | `/balances/check` | Bearer | Read current ERC-20 balance for an address+token on a given chain |
|
| `POST` | `/balances/check` | Bearer | Read current ERC-20 balance for an address+token on a given EVM chain |
|
||||||
| `POST` | `/balance-watches` | Bearer | Start a balance-change watch on an EVM address/token |
|
| `POST` | `/balance-watches` | Bearer | Start an async balance-change watch on an EVM address/token pair |
|
||||||
| `GET` | `/balance-watches/{id}` | Bearer | Fetch balance-watch status and current balance |
|
| `GET` | `/balance-watches/{id}` | Bearer | Fetch balance-watch status, current balance, and check schedule |
|
||||||
| `DELETE` | `/balance-watches/{id}` | Bearer | Stop a balance watch |
|
| `DELETE` | `/balance-watches/{id}` | Bearer | Stop a balance watch (also: `POST /balance-watches/{id}/stop`) |
|
||||||
| `POST` | `/admin/webhooks/retry` | Bearer | Force immediate retry of all `webhook_failed` intents |
|
| `POST` | `/admin/webhooks/retry` | Bearer | Force immediate retry of all `webhook_failed` intents |
|
||||||
|
|
||||||
Full request/response schemas: [[Scanner API]]
|
Full request/response schemas: [[Scanner API]]
|
||||||
@@ -185,25 +205,27 @@ Full request/response schemas: [[Scanner API]]
|
|||||||
|
|
||||||
## 6. Payment Reference Derivation (EVM)
|
## 6. Payment Reference Derivation (EVM)
|
||||||
|
|
||||||
The ERC20FeeProxy contract indexes payments by a `bytes8` reference. The scanner derives it deterministically so the worker's scan loop only needs a single indexed DB lookup per log.
|
The `ERC20FeeProxy` contract indexes payments by a `bytes8` reference. The scanner derives it deterministically so the scan loop needs only one indexed DB lookup per log.
|
||||||
|
|
||||||
```
|
```
|
||||||
# Step 1: build raw reference
|
# Step 1 — derive the bytes8 payment reference
|
||||||
input = lower(intentId) + lower(salt) + lower(destination)
|
input = lower(intentId) + lower(salt) + lower(destination)
|
||||||
paymentReference = last8Bytes(keccak256(input)) ← bytes8, 16 hex chars
|
paymentReference = last8Bytes(keccak256(input)) ← bytes8, stored as 16 hex chars
|
||||||
|
|
||||||
# Step 2: build EVM log index key
|
# Step 2 — derive the EVM log topic index key
|
||||||
topicRef = keccak256(paymentReferenceBytes) ← bytes32, 64 hex chars
|
topicRef = keccak256(paymentReferenceBytes) ← bytes32, 64 hex chars
|
||||||
↑ this is Topics[1] in the emitted log
|
↑ this is Topics[1] in every TransferWithReferenceAndFee log
|
||||||
```
|
```
|
||||||
|
|
||||||
- `salt` is a 32-byte random hex string generated at intent creation time.
|
- `salt` is a 32-byte random hex string generated at intent creation time to prevent reference collisions.
|
||||||
- `destination` is the EVM address of the AMN treasury / seller wallet, lowercased.
|
- `destination` is the EVM treasury/seller wallet address, always lowercased before hashing.
|
||||||
- Both `paymentReference` and `topicRef` are stored in the `intents` table at creation time. The scan loop performs `SELECT * FROM intents WHERE topic_ref = ? AND status = 'pending'` — O(1) with index, regardless of how many pending intents exist.
|
- Both `paymentReference` and `topicRef` are written to the `intents` table at creation time.
|
||||||
|
- The scan inner loop executes `SELECT * FROM intents WHERE topic_ref = ? AND status = 'pending'` — O(1) with index regardless of how many pending intents exist.
|
||||||
|
|
||||||
**Event signature** (used as `Topics[0]` filter):
|
**Event signature** used as `Topics[0]` filter:
|
||||||
```
|
```
|
||||||
TransferWithReferenceAndFee → keccak256 = 0xbc8b749850bdc0c5e4a49d50f87ec40dca2acdb70a84e7c6823ccdc7b3b3d4b3
|
TransferWithReferenceAndFee
|
||||||
|
keccak256 = 0xbc8b749850bdc0c5e4a49d50f87ec40dca2acdb70a84e7c6823ccdc7b3b3d4b3
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -212,34 +234,33 @@ TransferWithReferenceAndFee → keccak256 = 0xbc8b749850bdc0c5e4a49d50f87ec40dca
|
|||||||
|
|
||||||
### EVM
|
### EVM
|
||||||
|
|
||||||
1. Worker fetches `eth_getLogs` for the proxy contract address + `TransferWithReferenceAndFee` event topic in 2000-block chunks.
|
1. Worker calls `eth_getLogs` for the proxy contract address + `TransferWithReferenceAndFee` event topic in 2 000-block chunks.
|
||||||
2. For each log, extract `Topics[1]` (= `topicRef`).
|
2. For each log, extract `Topics[1]` (the `topicRef`).
|
||||||
3. Query DB: `WHERE topic_ref = ? AND status = 'pending'`.
|
3. Query DB: `WHERE topic_ref = ? AND status = 'pending'`.
|
||||||
4. On match: decode `log.Data` to extract `tokenAddress`, `amount`, `destination`, `feeAmount`. Validate all four against the intent record.
|
4. On match: decode `log.Data` to extract `tokenAddress`, `amount`, `destination`, `feeAmount`. Validate all four against the intent record.
|
||||||
5. Update status to `confirming`, record `txHash`, `blockNumber`, `logIndex`.
|
5. Update status to `confirming`; record `txHash`, `blockNumber`, `logIndex`.
|
||||||
6. On next poll: check `head - blockNumber + 1 >= confirmationsRequired`. When met, finalize and deliver webhook.
|
6. On subsequent polls: if `chainHead - blockNumber + 1 >= confirmationsRequired`, finalize and deliver webhook.
|
||||||
|
|
||||||
**Reorg protection**: the checkpoint is rewound by `3 × confirmationThreshold` blocks (clamped to 20–500) on each poll. Any log from a recently reorganized block will be re-fetched and re-matched.
|
**Reorg protection**: the EVM checkpoint is rewound by `3 × confirmationThreshold` blocks (clamped 20–500) on every tick. Any log from a reorganized block will be re-fetched and re-matched. The unique index on `(tx_hash, log_index)` prevents double-confirmation if the same log is matched on two consecutive ticks.
|
||||||
|
|
||||||
### Tron
|
### Tron
|
||||||
|
|
||||||
- No proxy contract — each intent receives a unique HD-derived destination address.
|
- No proxy contract on Tron. Each intent receives a unique HD-derived destination address.
|
||||||
- Worker polls TronGrid `/v1/contracts/{usdtContract}/events?event_name=Transfer` filtered to the intent's destination address.
|
- Worker polls TronGrid `/v1/contracts/{usdtTrc20}/events?event_name=Transfer` filtered to the intent's destination address.
|
||||||
- Match criterion: `to == destination AND amount >= intent.Amount`.
|
- Match criterion: `to == destination AND amount >= intent.Amount`.
|
||||||
- TronGrid returns only already-confirmed transactions. No multi-block wait — status jumps directly to `confirmed`.
|
- TronGrid only surfaces already-confirmed transactions — status jumps directly to `confirmed` with no `confirming` intermediate state.
|
||||||
- Addresses from TronGrid arrive as `41xxxx` (21-byte hex). The worker normalizes these to `0x`-prefixed 20-byte EVM style for storage and comparison.
|
- Addresses from TronGrid arrive in `41xxxx` (21-byte hex) format. The worker normalizes them to `0x`-prefixed 20-byte EVM format for storage and comparison.
|
||||||
- Checkpoint stored as a millisecond Unix timestamp in `last_scanned_block`.
|
- Checkpoint is stored as a millisecond Unix timestamp in `last_scanned_block`.
|
||||||
- Pagination follows `meta.links.next` until nil.
|
- Pagination follows `meta.links.next` until nil.
|
||||||
|
|
||||||
### TON
|
### TON
|
||||||
|
|
||||||
- Also uses per-intent unique destination addresses (no proxy contract).
|
- Also uses per-intent unique destination addresses (no proxy contract).
|
||||||
- Worker calls TonCenter v3 `/jetton/transfers?account={destination}&jetton_master={usdtJetton}&start_utime={checkpoint}` for each pending TON intent.
|
- Worker calls TonCenter v3 `/jetton/transfers?account={destination}&jetton_master={usdtJetton}&start_utime={checkpoint}` for each pending TON intent individually.
|
||||||
- Match criterion: `destination == intent.Destination AND amount >= intent.Amount`.
|
- Match criterion: `destination == intent.Destination AND amount >= intent.Amount`.
|
||||||
- TonCenter returns only finalized transactions — status jumps directly to `confirmed`.
|
- TonCenter returns only finalized transactions — status jumps directly to `confirmed`.
|
||||||
- TON addresses are base64url (`EQ…`/`UQ…`), case-sensitive. Never lowercased.
|
- TON addresses are base64url (`EQ…`/`UQ…`), case-sensitive. They must never be lowercased.
|
||||||
- Checkpoint stored as Unix seconds.
|
- Checkpoint stored as Unix seconds. Lag reported in seconds, not blocks.
|
||||||
- Lag is reported in seconds, not blocks.
|
|
||||||
- **Scaling note**: each pending TON intent triggers one HTTP round-trip per scan cycle. High concurrency will exhaust TonCenter rate limits.
|
- **Scaling note**: each pending TON intent triggers one HTTP round-trip per scan cycle. High concurrency will exhaust TonCenter rate limits.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -248,7 +269,7 @@ TransferWithReferenceAndFee → keccak256 = 0xbc8b749850bdc0c5e4a49d50f87ec40dca
|
|||||||
|
|
||||||
### Payment confirmed (intent webhook)
|
### Payment confirmed (intent webhook)
|
||||||
|
|
||||||
Posted to `callbackUrl` on intent confirmation:
|
Posted to `callbackUrl` when an intent reaches `confirmed` status:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -264,17 +285,18 @@ Posted to `callbackUrl` on intent confirmation:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Header: `X-AMN-Signature: hex(HMAC-SHA256(rawBody, callbackSecret))`
|
Headers:
|
||||||
|
- `X-AMN-Signature: hex(HMAC-SHA256(rawBody, callbackSecret))`
|
||||||
|
|
||||||
The `confirmations` value is **capped** at the chain's acceptance threshold once confirmed. The scanner does not continue incrementing after the payment is safe to credit.
|
The `confirmations` value is **capped** at the chain acceptance threshold once confirmed. The scanner does not keep incrementing after the payment is safe to credit.
|
||||||
|
|
||||||
**Retry schedule on delivery failure**: `5 s → 30 s → 2 min → 10 min → 1 h → webhook_failed`
|
**Retry schedule on delivery failure**: `5 s → 30 s → 2 min → 10 min → 1 h → webhook_failed`
|
||||||
|
|
||||||
After exhausting retries the intent is set to `webhook_failed`. Manual recovery: `POST /admin/webhooks/retry` or wait for the `WEBHOOK_RETRY_HOURS` background sweep.
|
After exhausting retries the intent status becomes `webhook_failed`. Recovery: `POST /admin/webhooks/retry` or wait for the `WEBHOOK_RETRY_HOURS` background sweep (default 6 h).
|
||||||
|
|
||||||
### Balance changed (balance-watch webhook)
|
### Balance changed (balance-watch webhook)
|
||||||
|
|
||||||
Posted to the watch's `callbackUrl` when balance delta is detected:
|
Posted to the watch's `callbackUrl` when a balance delta is detected:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -295,65 +317,77 @@ Posted to the watch's `callbackUrl` when balance delta is detected:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Additional headers: `X-AMN-Delivery-ID: <watchId>`, `X-AMN-Event-Type: balance_changed`
|
Additional headers:
|
||||||
|
- `X-AMN-Delivery-ID: <watchId>`
|
||||||
|
- `X-AMN-Event-Type: balance_changed`
|
||||||
|
- `X-AMN-Signature: hex(HMAC-SHA256(rawBody, callbackSecret))`
|
||||||
|
|
||||||
The scanner only advances `current_balance` after a successful (2xx) delivery, so a down backend will retry on the next scheduled check.
|
The scanner only advances `current_balance` in the DB after a successful (2xx) delivery. A down backend will get the same notification on the next scheduled check.
|
||||||
|
|
||||||
|
**Watch polling cadence** (age-decayed):
|
||||||
|
- First 24 hours: every 5 minutes
|
||||||
|
- 24–48 hours: every 10 minutes
|
||||||
|
- 48–96 hours: every 20 minutes
|
||||||
|
- 96+ hours: every 40 minutes
|
||||||
|
- Hard expiry: 7 days after creation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. SQLite DB Schema
|
## 9. SQLite DB Schema
|
||||||
|
|
||||||
Database at `DB_PATH` (default `./scanner.db`; Docker: `/data/scanner.db`). WAL mode enabled, busy timeout 5 000 ms.
|
Database at `DB_PATH` (default `./scanner.db`; Docker: `/data/scanner.db`). WAL mode with 5 000 ms busy timeout. Connection pool capped at 1 to serialize writes.
|
||||||
|
|
||||||
### `intents`
|
### `intents`
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `intent_id` | TEXT PK | caller-supplied UUID |
|
| `intent_id` | TEXT PK | Caller-supplied UUID |
|
||||||
| `chain_id` | INTEGER | numeric chain ID |
|
| `chain_id` | INTEGER | Numeric chain ID |
|
||||||
| `chain_type` | TEXT | `evm` / `tron` / `ton` |
|
| `chain_type` | TEXT | `evm` / `tron` / `ton` |
|
||||||
| `token_address` | TEXT | EVM/Tron: lowercase `0x` hex; TON: base64url |
|
| `token_address` | TEXT | EVM/Tron: lowercase `0x` hex; TON: base64url |
|
||||||
| `destination` | TEXT | receiving address |
|
| `destination` | TEXT | Receiving address |
|
||||||
| `amount` | TEXT | base-10 wei / token smallest unit |
|
| `amount` | TEXT | Base-10 smallest unit (wei / TRC-20 units / nanoton) |
|
||||||
| `payment_reference` | TEXT | 8-byte hex — EVM only |
|
| `payment_reference` | TEXT | 8-byte hex — EVM proxy rail only |
|
||||||
| `topic_ref` | TEXT | keccak256 of paymentReference — scan index for EVM |
|
| `topic_ref` | TEXT | `keccak256(paymentReferenceBytes)` — EVM scan index |
|
||||||
| `status` | TEXT | `pending` / `confirming` / `confirmed` / `expired` / `webhook_failed` |
|
| `status` | TEXT | `pending` / `confirming` / `confirmed` / `expired` / `webhook_failed` |
|
||||||
| `callback_url` | TEXT | backend webhook endpoint |
|
| `callback_url` | TEXT | Backend webhook endpoint |
|
||||||
| `callback_secret` | TEXT | HMAC key — excluded from all JSON responses |
|
| `callback_secret` | TEXT | HMAC key — excluded from all JSON responses (`json:"-"`) |
|
||||||
| `confirmations_required` | INTEGER | floored at the chain acceptance threshold |
|
| `confirmations_required` | INTEGER | Set to chain acceptance floor at intent creation |
|
||||||
| `tx_hash` | TEXT NULL | set once the transaction is seen on-chain |
|
| `tx_hash` | TEXT NULL | Set once the transaction is seen on-chain |
|
||||||
| `log_index` | INTEGER NULL | log position within tx (EVM only) |
|
| `log_index` | INTEGER NULL | Log position within tx (EVM only) |
|
||||||
| `block_number` | INTEGER NULL | block when seen (EVM); ms timestamp (Tron); unix s (TON) |
|
| `block_number` | INTEGER NULL | Block number (EVM); ms timestamp (Tron); unix seconds (TON) |
|
||||||
| `confirmations` | INTEGER | depth while confirming; capped at threshold after confirmation |
|
| `confirmations` | INTEGER | Depth while confirming; capped at threshold after confirmation |
|
||||||
| `salt` | TEXT | 32-byte random hex used in reference derivation |
|
| `salt` | TEXT | 32-byte random hex used in reference derivation |
|
||||||
| `webhook_delivered_at` | TEXT NULL | RFC3339 timestamp of first successful delivery |
|
| `webhook_delivered_at` | TEXT NULL | RFC3339 timestamp of first successful delivery |
|
||||||
| `created_at` / `updated_at` | DATETIME | UTC |
|
| `created_at` / `updated_at` | DATETIME | UTC |
|
||||||
|
|
||||||
Unique index on `(tx_hash, log_index)` prevents double-confirmation.
|
Indexes: `(status)`, `(chain_id, status)`, `(payment_reference)`, `(topic_ref)`.
|
||||||
|
Unique index: `(tx_hash, log_index) WHERE tx_hash IS NOT NULL` — prevents double-confirmation.
|
||||||
|
|
||||||
### `checkpoints`
|
### `checkpoints`
|
||||||
|
|
||||||
| Column | Notes |
|
| Column | Notes |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `chain_id` PK | |
|
| `chain_id` PK | Numeric chain ID |
|
||||||
| `last_scanned_block` | block number (EVM), ms timestamp (Tron), unix seconds (TON) |
|
| `last_scanned_block` | Block number (EVM), ms timestamp (Tron), unix seconds (TON) |
|
||||||
|
| `updated_at` | UTC |
|
||||||
|
|
||||||
### `balance_watches`
|
### `balance_watches`
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `watch_id` | TEXT PK | caller-supplied idempotency key |
|
| `watch_id` | TEXT PK | Caller-supplied idempotency key |
|
||||||
| `chain_id` / `chain_type` | INTEGER / TEXT | currently EVM only |
|
| `chain_id` / `chain_type` | INTEGER / TEXT | Currently EVM only |
|
||||||
| `token_address` / `token_symbol` | TEXT | ERC-20 contract + optional registry symbol |
|
| `token_address` / `token_symbol` | TEXT | ERC-20 contract + optional registry symbol |
|
||||||
| `decimals` | INTEGER | registry decimals for display |
|
| `decimals` | INTEGER | Token decimals for display |
|
||||||
| `address` | TEXT | watched holder address |
|
| `address` | TEXT | Watched holder address |
|
||||||
| `baseline_balance` | TEXT | base-unit balance at watch creation |
|
| `baseline_balance` | TEXT | Base-unit balance at watch creation |
|
||||||
| `current_balance` | TEXT | last successfully delivered balance |
|
| `current_balance` | TEXT | Last successfully delivered balance |
|
||||||
| `status` | TEXT | `watching` / `stopped` / `expired` |
|
| `status` | TEXT | `watching` / `stopped` / `expired` |
|
||||||
| `callback_url` / `callback_secret` | TEXT | signed webhook destination |
|
| `callback_url` / `callback_secret` | TEXT | Signed webhook destination + HMAC key |
|
||||||
| `last_checked_at` / `next_check_at` | DATETIME | scheduler state |
|
| `last_checked_at` / `next_check_at` | DATETIME | Scheduler state |
|
||||||
| `change_count` / `last_notified_at` | INTEGER / DATETIME | notification audit |
|
| `change_count` / `last_notified_at` | INTEGER / DATETIME | Notification audit |
|
||||||
| `expires_at` | DATETIME | hard stop after 7 days |
|
| `expires_at` | DATETIME | Hard stop 7 days after creation |
|
||||||
| `created_at` / `updated_at` | DATETIME | UTC |
|
| `created_at` / `updated_at` | DATETIME | UTC |
|
||||||
|
|
||||||
Indexes: `(status, next_check_at)` for due-scan queries; `(chain_id, status)` for status reporting.
|
Indexes: `(status, next_check_at)` for due-scan queries; `(chain_id, status)` for status reporting.
|
||||||
@@ -362,15 +396,15 @@ Indexes: `(status, next_check_at)` for due-scan queries; `(chain_id, status)` fo
|
|||||||
|
|
||||||
## 10. Configuration
|
## 10. Configuration
|
||||||
|
|
||||||
All configuration via environment variables. Copy `.env.example` and populate before first run.
|
All configuration via environment variables. Copy `.env.example` before first run.
|
||||||
|
|
||||||
| Variable | Default | Required | Notes |
|
| Variable | Default | Required | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `PORT` | `8080` | no | HTTP listen port |
|
| `PORT` | `8080` | no | HTTP listen port |
|
||||||
| `DB_PATH` | `./scanner.db` | no | SQLite file path. Docker: mount `/data` and set `/data/scanner.db` |
|
| `DB_PATH` | `./scanner.db` | no | SQLite file path. Docker: mount `/data`, set `/data/scanner.db` |
|
||||||
| `CHAINS_JSON_PATH` | `./supported-chains.json` | no | Chain registry file |
|
| `CHAINS_JSON_PATH` | `./supported-chains.json` | no | Chain registry JSON file |
|
||||||
| `TOKENS_JSON_PATH` | `./tokens.json` | no | Token registry for symbol/decimals metadata |
|
| `TOKENS_JSON_PATH` | `./tokens.json` | no | Token registry for symbol/decimals metadata |
|
||||||
| `SCANNER_API_KEY` | _(none)_ | **yes (prod)** | Shared secret for Bearer auth. Generate: `openssl rand -hex 32` |
|
| `SCANNER_API_KEY` | _(none)_ | **yes (prod)** | Shared secret for Bearer auth. Generate: `openssl rand -hex 32`. Unset = all requests allowed (dev only) |
|
||||||
| `POLL_INTERVAL_SEC` | `15` | no | Chain polling interval in seconds |
|
| `POLL_INTERVAL_SEC` | `15` | no | Chain polling interval in seconds |
|
||||||
| `INTENT_TTL_HOURS` | `24` | no | Expire pending intents after N hours. `0` = disabled |
|
| `INTENT_TTL_HOURS` | `24` | no | Expire pending intents after N hours. `0` = disabled |
|
||||||
| `WEBHOOK_RETRY_HOURS` | `6` | no | Background re-delivery interval for `webhook_failed` intents. `0` = disabled |
|
| `WEBHOOK_RETRY_HOURS` | `6` | no | Background re-delivery interval for `webhook_failed` intents. `0` = disabled |
|
||||||
@@ -381,10 +415,10 @@ All configuration via environment variables. Copy `.env.example` and populate be
|
|||||||
| `RPC_ETH` | chain config | no | Override Ethereum JSON-RPC URL |
|
| `RPC_ETH` | chain config | no | Override Ethereum JSON-RPC URL |
|
||||||
| `RPC_POLYGON` | chain config | no | Override Polygon JSON-RPC URL |
|
| `RPC_POLYGON` | chain config | no | Override Polygon JSON-RPC URL |
|
||||||
| `RPC_BASE` | chain config | no | Override Base JSON-RPC URL |
|
| `RPC_BASE` | chain config | no | Override Base JSON-RPC URL |
|
||||||
| `TRONGRID_API_KEY` | _(none)_ | recommended | Free tier is very low; required for any real Tron traffic |
|
| `TRONGRID_API_KEY` | _(none)_ | strongly recommended | Free tier is severely rate-limited; required for real Tron traffic |
|
||||||
| `TONCENTER_API_KEY` | _(none)_ | recommended | Rate-limited without key |
|
| `TONCENTER_API_KEY` | _(none)_ | recommended | Rate-limited without key |
|
||||||
| `SCANNER_ENABLED_CHAINS` | JSON `verified` flags | no | Comma-separated chain IDs to enable, overriding `verified`. E.g. `56,1` |
|
| `SCANNER_ENABLED_CHAINS` | JSON `verified` flags | no | Comma-separated chain IDs to activate, overriding `verified` field. E.g. `56,1` |
|
||||||
| `SCANNER_CALLBACK_ALLOWED_HOSTS` | _(none)_ | prod recommended | Comma-separated hosts for SSRF guard on `callbackUrl` targets |
|
| `SCANNER_CALLBACK_ALLOWED_HOSTS` | _(none)_ | prod recommended | Comma-separated hosts/IPs allowed as `callbackUrl` targets (SSRF guard) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -394,7 +428,7 @@ All configuration via environment variables. Copy `.env.example` and populate be
|
|||||||
# Build
|
# Build
|
||||||
docker build -t amn-scanner .
|
docker build -t amn-scanner .
|
||||||
|
|
||||||
# Run
|
# Run (standalone)
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name amn-scanner \
|
--name amn-scanner \
|
||||||
--network shared-web \
|
--network shared-web \
|
||||||
@@ -405,28 +439,41 @@ docker run -d \
|
|||||||
amn-scanner
|
amn-scanner
|
||||||
```
|
```
|
||||||
|
|
||||||
**On the dev server** (`89.58.32.32`): the scanner is part of the `escrow-dev` Arcane project. Images are built locally from source at `/tmp/escrow-backend-build/` — the dev stack does **not** pull from any registry.
|
### Dev server (89.58.32.32)
|
||||||
|
|
||||||
|
The scanner is part of the `escrow-dev` Arcane project. The dev stack builds images locally — it does **not** pull from any registry.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Copy changed scanner source files
|
# 1. Copy changed scanner source files
|
||||||
scp -i ~/CascadeProjects/wzp scanner/*.go root@89.58.32.32:/tmp/escrow-backend-build/scanner/
|
scp -i ~/CascadeProjects/wzp scanner/*.go root@89.58.32.32:/tmp/escrow-backend-build/scanner/
|
||||||
|
|
||||||
# Rebuild + restart (on server)
|
# 2. Rebuild image on server (~2–3 min)
|
||||||
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
ssh -i ~/CascadeProjects/wzp root@89.58.32.32 \
|
||||||
"cd /tmp/escrow-backend-build/scanner && docker build -t amn-scanner-local:dev . && \
|
"cd /tmp/escrow-backend-build/scanner && \
|
||||||
cd /opt/arcane/data/projects/escrow-dev && docker compose up -d scanner"
|
docker build -t amn-scanner-local:dev . && \
|
||||||
|
cd /opt/arcane/data/projects/escrow-dev && \
|
||||||
|
docker compose up -d scanner"
|
||||||
```
|
```
|
||||||
|
|
||||||
Health check URL (via infra-caddy): check project Caddyfile for the current vhost. Direct internal: `http://amn-scanner:8080/health`.
|
Health check: `curl http://amn-scanner:8080/health` (internal) or via the Caddyfile vhost.
|
||||||
|
|
||||||
|
### Health probe
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /health
|
||||||
|
→ {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Integration with the Backend
|
## 12. Integration with the Backend
|
||||||
|
|
||||||
|
The backend wires the scanner through the `amn.scanner` provider. See memory note [[amn_scanner_payin_wiring]] for full service/dispatch registration and the 6 required env vars.
|
||||||
|
|
||||||
### Registering a payment intent
|
### Registering a payment intent
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// backend: src/services/amnScanner/...
|
// src/services/amnScanner/...
|
||||||
const resp = await fetch(`${SCANNER_URL}/intents`, {
|
const resp = await fetch(`${SCANNER_URL}/intents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -438,22 +485,22 @@ const resp = await fetch(`${SCANNER_URL}/intents`, {
|
|||||||
chainId: 56,
|
chainId: 56,
|
||||||
tokenAddress: '0x55d398326f99059fF775485246999027B3197955', // USDT BSC
|
tokenAddress: '0x55d398326f99059fF775485246999027B3197955', // USDT BSC
|
||||||
destination: sellerWalletAddress,
|
destination: sellerWalletAddress,
|
||||||
amount: amountInWei, // base-10 string
|
amount: amountInWei, // base-10 string, smallest unit
|
||||||
callbackUrl: `${BACKEND_URL}/api/payment/amn-scanner/webhook`,
|
callbackUrl: `${BACKEND_URL}/api/payment/amn-scanner/webhook`,
|
||||||
callbackSecret: process.env.SCANNER_CALLBACK_SECRET,
|
callbackSecret: process.env.SCANNER_CALLBACK_SECRET,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const { intentId, paymentReference, checkoutBlock } = await resp.json();
|
const { intentId, paymentReference, checkoutBlock } = await resp.json();
|
||||||
// store intentId + checkoutBlock in the payment record
|
// Store intentId in the payment record
|
||||||
// pass checkoutBlock to the frontend for transaction construction
|
// Pass checkoutBlock to the frontend for transaction construction
|
||||||
```
|
```
|
||||||
|
|
||||||
The `checkoutBlock` contains everything the frontend needs to call the `ERC20FeeProxy.transferWithReferenceAndFee()` function:
|
The `checkoutBlock` response contains everything the frontend needs to call `ERC20FeeProxy.transferWithReferenceAndFee()`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"destination": "0x...",
|
"destination": "0x...",
|
||||||
"tokenAddress": "0x55d...",
|
"tokenAddress": "0x55d398326f99059fF775485246999027B3197955",
|
||||||
"tokenSymbol": "USDT",
|
"tokenSymbol": "USDT",
|
||||||
"decimals": 18,
|
"decimals": 18,
|
||||||
"chainId": 56,
|
"chainId": 56,
|
||||||
@@ -465,6 +512,9 @@ The `checkoutBlock` contains everything the frontend needs to call the `ERC20Fee
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!note] Token decimals
|
||||||
|
> Read token decimals on-chain, not from an internal registry. The scanner's `checkoutBlock.decimals` comes from `tokens.json`, which may lag registry updates.
|
||||||
|
|
||||||
### Receiving the webhook callback
|
### Receiving the webhook callback
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -475,39 +525,64 @@ app.post('/api/payment/amn-scanner/webhook', async (req, res) => {
|
|||||||
if (!timingSafeEqual(signature, expected)) return res.status(401).end();
|
if (!timingSafeEqual(signature, expected)) return res.status(401).end();
|
||||||
|
|
||||||
const { intentId, status, txHash, amount, chainId } = req.body;
|
const { intentId, status, txHash, amount, chainId } = req.body;
|
||||||
if (status !== 'confirmed') return res.status(200).end(); // ignore non-confirmed
|
if (status !== 'confirmed') return res.status(200).end(); // ignore non-terminal
|
||||||
|
|
||||||
await paymentService.markConfirmed({ intentId, txHash, amount, chainId });
|
await paymentService.markConfirmed({ intentId, txHash, amount, chainId });
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!warning]
|
> [!warning] Always scope by provider
|
||||||
> The backend must always scope payment lookups by `provider: "amn.scanner"`. Sweeping all pending payments to mark them confirmed/failed will destroy records from other providers (RN, manual). See memory note on payment cleanup + provider filter.
|
> The backend must always scope payment lookups to `provider: "amn.scanner"`. Sweeping all pending payments will destroy records from other providers (RN, manual). See memory note on payment cleanup + provider filter.
|
||||||
|
|
||||||
### Backend env vars required
|
### Using direct balance checks (non-proxy flows)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Synchronous balance read (manual payment flow)
|
||||||
|
const { balance } = await scannerClient.post('/balances/check', {
|
||||||
|
chainId: 56,
|
||||||
|
address: sellerWalletAddress,
|
||||||
|
token: 'USDT',
|
||||||
|
});
|
||||||
|
// Store baseline, then re-check when buyer clicks "I paid"
|
||||||
|
|
||||||
|
// Async balance watch
|
||||||
|
await scannerClient.post('/balance-watches', {
|
||||||
|
watchId: `payment-${paymentId}-c56-USDT`,
|
||||||
|
chainId: 56,
|
||||||
|
address: sellerWalletAddress,
|
||||||
|
token: 'USDT',
|
||||||
|
callbackUrl: `${BACKEND_URL}/api/payment/amn-scanner/webhook`,
|
||||||
|
callbackSecret: process.env.SCANNER_CALLBACK_SECRET,
|
||||||
|
baselineBalance: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop watch after payment resolved
|
||||||
|
await scannerClient.delete(`/balance-watches/payment-${paymentId}-c56-USDT`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend environment variables
|
||||||
|
|
||||||
```
|
```
|
||||||
SCANNER_URL=http://amn-scanner:8080
|
SCANNER_URL=http://amn-scanner:8080
|
||||||
SCANNER_API_KEY=<same value as scanner SCANNER_API_KEY>
|
SCANNER_API_KEY=<same value as scanner SCANNER_API_KEY>
|
||||||
SCANNER_CALLBACK_SECRET=<same value as scanner intent callbackSecret>
|
SCANNER_CALLBACK_SECRET=<shared HMAC key, same value used in callbackSecret field>
|
||||||
```
|
```
|
||||||
|
|
||||||
See memory note: [[amn_scanner_payin_wiring]] for full wiring details and token-decimal notes.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Known Limitations / Open Items
|
## 13. Known Limitations / Open Items
|
||||||
|
|
||||||
| # | Area | Description |
|
| # | Area | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | **TON scaling** | O(pending intents) TonCenter API calls per scan cycle. Becomes a bottleneck above ~50 concurrent TON intents. Future fix: batch by address or use a streaming API. |
|
| 1 | **TON scaling** | O(pending intents) TonCenter API calls per scan cycle. Becomes a bottleneck above ~50 concurrent TON intents. Future fix: batch by address or use a streaming/webhook API. |
|
||||||
| 2 | **Tron/TON balance watches** | `POST /balances/check` and `POST /balance-watches` currently only support EVM ERC-20 reads. Tron TRC-20 and TON Jetton balance reads are future scope. |
|
| 2 | **Tron/TON balance watches** | `POST /balances/check` and `POST /balance-watches` only support EVM ERC-20 (`eth_call` balanceOf). Tron TRC-20 and TON Jetton balance reads are future scope. |
|
||||||
| 3 | **Non-EVM chains unverified** | Arbitrum, Polygon, Base, Tron, TON are `"verified": false` — workers do not start by default. Needs production testing + `SCANNER_ENABLED_CHAINS` opt-in before enabling for real traffic. |
|
| 3 | **Non-EVM chains unverified** | Arbitrum, Polygon, Base, Tron, TON are `"verified": false` — workers do not start by default. Needs production testing and `SCANNER_ENABLED_CHAINS` opt-in before enabling for real traffic. |
|
||||||
| 4 | **Base proxy address** | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` is non-canonical (differs from RN's CREATE2 expected address for that chain). Verify before enabling Base in production. |
|
| 4 | **Base proxy address non-canonical** | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` differs from the RN CREATE2 expected address for Base. Must be verified on-chain before enabling Base in production. |
|
||||||
| 5 | **Ethereum proxy version** | Ethereum uses the v0.1.0 proxy at `0x370DE27…`. A v0.2.0 proxy is also deployed on ETH. The two have different ABI signatures — do not silently swap addresses. |
|
| 5 | **Ethereum proxy version** | Chain 1 uses the v0.1.0 proxy at `0x370DE27…`. A v0.2.0 proxy is also deployed on ETH. The two have different ABI signatures — do not silently swap addresses. |
|
||||||
| 6 | **Single SQLite instance** | The embedded SQLite database is not horizontally scalable. Running multiple scanner replicas would require coordination or migration to a shared DB. Acceptable for current load. |
|
| 6 | **Single SQLite instance** | The embedded SQLite database is not horizontally scalable. Running multiple scanner replicas would require coordination or migration to a shared DB (Postgres). Acceptable for current load. |
|
||||||
| 7 | **No native-token support** | Only ERC-20/TRC-20/Jetton transfers are scanned. Native token (BNB, ETH, TRX, TON coin) payments are not supported. |
|
| 7 | **No native-token support** | Only ERC-20, TRC-20, and Jetton (TON) transfers are scanned. Native token payments (BNB, ETH, TRX, TON coin) are not supported. |
|
||||||
| 8 | **Multi-seller / multi-chain** | AMN Scanner pay-in supports single-seller flow only. Multi-seller cart payments and cross-chain routing are not implemented. |
|
| 8 | **Single-seller only** | AMN Scanner pay-in supports single-seller flow. Multi-seller cart payments and cross-chain routing are not implemented. |
|
||||||
| 9 | **Webhook signature algorithm** | HMAC-SHA256 with a pre-shared secret. There is no key rotation mechanism — changing `callbackSecret` requires intent re-registration. |
|
| 9 | **No webhook key rotation** | HMAC-SHA256 with a pre-shared `callbackSecret`. There is no key rotation mechanism — changing the secret requires re-registering intents. |
|
||||||
| 10 | **No EVM testnet for ETH/Polygon/Base** | Only BSC Testnet (97) has a testnet entry in `supported-chains.json`. Developers testing on Ethereum Sepolia or Polygon Amoy need to add chain entries manually. |
|
| 10 | **No EVM testnet for ETH/Polygon/Base** | Only BSC Testnet (97) has a testnet entry in `supported-chains.json`. Testing on Ethereum Sepolia or Polygon Amoy requires manually adding chain entries. |
|
||||||
|
| 11 | **Arbitrum threshold latency** | The 2400-block Arbitrum threshold (~54 min) is deliberately conservative for the optimistic rollup challenge window. This makes Arbitrum slow for real-time escrow use. |
|
||||||
|
|||||||
304
10 - Services/tenant.md
Normal file
304
10 - Services/tenant.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# Tenant Service
|
||||||
|
|
||||||
|
> **Last updated:** 2026-06-10 — current `feature/white-label-shops` scan.
|
||||||
|
|
||||||
|
Tenant lifecycle, custom-domain provisioning, encrypted Telegram bot management, tenant webhook handling, and request-time tenant resolution for the white-label multi-shop branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Directory layout
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/src/services/tenant/
|
||||||
|
├── tenantService.ts # Core lifecycle + bootstrap payload
|
||||||
|
├── tenantBotService.ts # Encrypted bot token management
|
||||||
|
├── tenantAuthService.ts # Tenant role middleware factories
|
||||||
|
├── domainProvisioningService.ts # DNS verification + Caddy route lifecycle
|
||||||
|
└── caddyService.ts # Caddy Admin API wrapper
|
||||||
|
|
||||||
|
backend/src/shared/middleware/
|
||||||
|
└── tenantResolution.ts # Request-time Host → tenant resolver
|
||||||
|
|
||||||
|
backend/src/routes/
|
||||||
|
├── tenantRoutes.ts # Authenticated tenant admin API
|
||||||
|
├── storefrontRoutes.ts # Public storefront bootstrap/stubs
|
||||||
|
└── tenantWebhookRoutes.ts # Telegram tenant bot webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## tenantService.ts
|
||||||
|
|
||||||
|
**Singleton export:** `tenantService` (also default export).
|
||||||
|
|
||||||
|
### Typed errors
|
||||||
|
|
||||||
|
| Class | `code` | Thrown when |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `TenantSlugInvalidError` | `TENANT_SLUG_INVALID` | Slug does not match `/^[a-z0-9-]{3,40}$/` |
|
||||||
|
| `TenantSlugTakenError` | `TENANT_SLUG_TAKEN` | Slug already registered |
|
||||||
|
| `TenantNotFoundError` | `TENANT_NOT_FOUND` | `updateTenant` / `suspendTenant` / `activateTenant` on missing id |
|
||||||
|
|
||||||
|
Route handlers map these to `400 / 409 / 404` via the shared `handleServiceError` helper in `tenantRoutes.ts`.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
#### `createTenant(input)`
|
||||||
|
|
||||||
|
Creates a tenant, auto-grants the `owner` role to the creating user, and seeds a default `amn_escrow` payment policy (all in sequence, not a transaction — acceptable for Phase 0/1).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await tenantService.createTenant({
|
||||||
|
ownerUserId: 'uuid',
|
||||||
|
slug: 'myshop',
|
||||||
|
displayName: 'My Shop',
|
||||||
|
type: 'hosted_seller', // optional, default 'hosted_seller'
|
||||||
|
brand: { primaryColor: '#1F6FEB' }, // optional
|
||||||
|
features: {}, // optional
|
||||||
|
localeDefaults: ['en'], // optional
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Slug is lowercased before the uniqueness check.
|
||||||
|
|
||||||
|
#### `getTenantById(id)` / `getTenantBySlug(slug)`
|
||||||
|
|
||||||
|
Direct DB lookups. Return `null` if not found.
|
||||||
|
|
||||||
|
#### `resolveTenantByHost(host)`
|
||||||
|
|
||||||
|
The main resolution path for HTTP requests.
|
||||||
|
|
||||||
|
```
|
||||||
|
(a) host ends with .amn.gg → strip suffix → findBySlug (must be 'active')
|
||||||
|
(b) custom host → findByHostname (must be 'active') → findById
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns `{ tenant, domain? }` or `null`. Never throws — callers can treat null as a 404.
|
||||||
|
|
||||||
|
#### `resolveTenantBySlug(slug, { previewOnly })`
|
||||||
|
|
||||||
|
Used only for `/t/:slug/bootstrap` preview paths. `previewOnly: true` allows `pending` tenants. `previewOnly: false` requires `status = 'active'`.
|
||||||
|
|
||||||
|
#### `buildBootstrapPayload(tenant)` → `TenantBootstrapPayload`
|
||||||
|
|
||||||
|
Assembles the public bootstrap object from the tenant row and its payment policy. Feature flags are derived from policy rails and overridden by `tenant.features` JSONB.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface TenantBootstrapPayload {
|
||||||
|
tenantId: string;
|
||||||
|
shopId?: string;
|
||||||
|
slug: string;
|
||||||
|
brand: { name: string; logoUrl?: string; primaryColor?: string; supportEmail?: string };
|
||||||
|
features: { escrowCheckout: boolean; directCheckout: boolean; externalPayments: boolean; telegramMiniApp: boolean };
|
||||||
|
paymentRails: TenantPaymentRail[];
|
||||||
|
localeDefaults: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security:** never includes `ownerUserId`, `brand.supportEmail` is only included if set, no encrypted fields.
|
||||||
|
|
||||||
|
#### `updateTenant(id, patch)` / `suspendTenant(id)` / `activateTenant(id)` / `listTenants(opts?)`
|
||||||
|
|
||||||
|
Standard CRUD. All throw `TenantNotFoundError` on missing id.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## tenantBotService.ts
|
||||||
|
|
||||||
|
**Singleton export:** `tenantBotService`.
|
||||||
|
|
||||||
|
Manages Telegram bot token registration with AES-256-GCM encryption. The service encrypts on write; repositories only store ciphertext. The raw token is never logged or returned.
|
||||||
|
|
||||||
|
**Required env var:** `TENANT_SECRET_KEY` — a 32-byte key provided as 64 hex chars or 44 base64 chars. Missing or wrongly sized keys fail fast before bot registration can proceed.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
#### `registerBot(tenantId, { telegramBotId, username, botToken, miniAppUrl? })`
|
||||||
|
|
||||||
|
1. Encrypts `botToken` with AES-256-GCM using `TENANT_SECRET_KEY`.
|
||||||
|
2. Resolves the bot username via Telegram `getMe` when `username` is omitted.
|
||||||
|
3. Generates a random `webhookSecret` and `claimToken`.
|
||||||
|
4. Calls `tenantBotRepo.create(...)` — stores `encryptedToken`, `encryptedTokenIv`, `encryptedTokenTag`, `webhookSecret`, and `claimToken`.
|
||||||
|
5. If `APP_URL` or the first `FRONTEND_URL` value is configured, fire-and-forget registers a Telegram webhook at `/api/telegram/tenant-webhook/:botId`.
|
||||||
|
6. Returns the public bot record **without** encrypted fields or webhook secret. Pending bots include a derived `claimUrl`.
|
||||||
|
|
||||||
|
#### `listBotsForTenant(tenantId)`
|
||||||
|
|
||||||
|
Returns all bots for the tenant, with encrypted fields stripped from the response.
|
||||||
|
|
||||||
|
#### `configureBotMenu(botId, shopUrl)`
|
||||||
|
|
||||||
|
Decrypts the token internally and calls Telegram `setChatMenuButton` so the bot opens `shopUrl/telegram/`. Errors are logged but do not block bot registration.
|
||||||
|
|
||||||
|
#### `claimAdmin(botId, claimToken, telegramUserId)`
|
||||||
|
|
||||||
|
Called by `tenantWebhookRoutes` on `/start <claimToken>`. Verifies the pending bot claim token, stores `adminTelegramUserId`, flips the bot to `active`, and sends a confirmation message.
|
||||||
|
|
||||||
|
#### `revokeBot(botId)`
|
||||||
|
|
||||||
|
Sets `status = 'revoked'` on the bot row.
|
||||||
|
|
||||||
|
> [!warning] Secret handling
|
||||||
|
> `getDecryptedToken()` and token decryption are internal-only. Never call them from an HTTP route handler and never log plaintext BotFather tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## domainProvisioningService.ts
|
||||||
|
|
||||||
|
Owns the custom-domain lifecycle for `multi.amn.gg` and tenant-owned hostnames.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
#### `verifyAndProvision(domainId)`
|
||||||
|
|
||||||
|
1. Touches `lastCheckedAt`.
|
||||||
|
2. Accepts either an A record pointing to `CADDY_SERVER_IP` or a CNAME pointing to `CADDY_CNAME_TARGET`.
|
||||||
|
3. Adds an idempotent Caddy route via `caddyService.addRoute(hostname)`.
|
||||||
|
4. Updates the domain to `status = 'active'`, `tlsStatus = 'pending'`.
|
||||||
|
5. Marks `status = 'degraded'`, `tlsStatus = 'failed'` when Caddy provisioning fails.
|
||||||
|
|
||||||
|
Returns `'active'` or `'pending'`.
|
||||||
|
|
||||||
|
#### `checkTlsStatus(domainId)`
|
||||||
|
|
||||||
|
Performs an HTTPS probe through Caddy and updates `tlsStatus` to `issued`, `pending`, or `failed`.
|
||||||
|
|
||||||
|
#### `deprovision(domainId)`
|
||||||
|
|
||||||
|
Removes the Caddy route and marks the domain `suspended` with `tlsStatus = 'expired'`.
|
||||||
|
|
||||||
|
#### `syncActiveDomains()`
|
||||||
|
|
||||||
|
On backend startup, pings Caddy Admin API and re-adds all active domain routes. The database is the source of truth because Caddy API routes can be lost on Caddy restart.
|
||||||
|
|
||||||
|
#### `startPoller()`
|
||||||
|
|
||||||
|
Polls pending domains and active domains with pending TLS status every `DOMAIN_POLL_INTERVAL_MS` (default 60000 ms).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## caddyService.ts
|
||||||
|
|
||||||
|
Thin Caddy Admin API wrapper.
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `ping()` | Check Admin API reachability. |
|
||||||
|
| `addRoute(hostname)` | Add a host route. `/api/*`, `/socket.io/*`, and `/uploads/*` proxy to backend; all other paths proxy to frontend. |
|
||||||
|
| `removeRoute(hostname)` | Delete the route by Caddy `@id`. |
|
||||||
|
| `hasRoute(hostname)` | Check if the route exists. |
|
||||||
|
| `checkTls(hostname)` | Probe HTTPS and classify TLS as `issued`, `pending`, or `failed`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## tenantAuthService.ts
|
||||||
|
|
||||||
|
Provides Express middleware factories for tenant-scoped authorization.
|
||||||
|
|
||||||
|
#### `requireTenantRole(...roles: TenantUserRoleName[])`
|
||||||
|
|
||||||
|
Returns an Express middleware that:
|
||||||
|
1. Reads `req.params.tenantId`.
|
||||||
|
2. Checks `tenant_user_roles` for `(tenantId, req.user.id, role ∈ roles)`.
|
||||||
|
3. Also passes if `req.user.role === 'admin'` (platform admin bypasses tenant role checks).
|
||||||
|
4. Returns `403` if no matching role found.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```ts
|
||||||
|
router.get('/:tenantId/settings',
|
||||||
|
authenticateToken,
|
||||||
|
requireTenantRole('owner', 'manager'),
|
||||||
|
handler
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `requireTenantOwner`
|
||||||
|
|
||||||
|
Shortcut for `requireTenantRole('owner')`.
|
||||||
|
|
||||||
|
#### `requirePlatformAdmin`
|
||||||
|
|
||||||
|
Returns `403` unless `req.user.role === 'admin'`. Thin wrapper around `authorizeRoles('admin')`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## tenantResolutionMiddleware
|
||||||
|
|
||||||
|
`backend/src/shared/middleware/tenantResolution.ts`
|
||||||
|
|
||||||
|
Express middleware for the public storefront surface. Attaches `req.tenant: TenantRecord | undefined` and `req.tenantDomain: TenantDomainRecord | undefined`.
|
||||||
|
|
||||||
|
Resolution order and security invariants are documented in [[Tenant API#Tenant resolution middleware]].
|
||||||
|
|
||||||
|
The Express `Request` type augmentation lives in this file:
|
||||||
|
```ts
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
tenant?: TenantRecord;
|
||||||
|
tenantDomain?: TenantDomainRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## tenantWebhookRoutes.ts
|
||||||
|
|
||||||
|
Mounted at `/api/telegram` before the normal authenticated route groups.
|
||||||
|
|
||||||
|
`POST /tenant-webhook/:botId`:
|
||||||
|
1. Requires `X-Telegram-Bot-Api-Secret-Token`.
|
||||||
|
2. Fetches the bot row and compares the header to `webhookSecret`.
|
||||||
|
3. Touches `lastWebhookAt`.
|
||||||
|
4. Handles `/start <claimToken>` for pending bots by calling `tenantBotService.claimAdmin()`.
|
||||||
|
5. Acknowledges other updates with `200 { ok: true }`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend — TenantContext
|
||||||
|
|
||||||
|
`frontend/src/contexts/TenantContext.tsx`
|
||||||
|
|
||||||
|
Fetches `/api/storefront/bootstrap` on mount. Exposes:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface TenantContextValue {
|
||||||
|
tenant: TenantBootstrapPayload | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isAmanatDefault: boolean;
|
||||||
|
error: string | null;
|
||||||
|
reload: () => Promise<void>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`useTenant()` is a guard hook — throws if called outside `TenantProvider`.
|
||||||
|
|
||||||
|
On `404 TENANT_NOT_FOUND` or network error the provider falls back to `AMANAT_DEFAULTS` with `isAmanatDefault: true`. This means the rest of the frontend works unchanged on `amn.gg` — no tenant resolution required there.
|
||||||
|
|
||||||
|
`frontend/src/hooks/use-tenant-theme.ts` derives `primaryColor`, `cssVars`, and `brandName` from `useTenant()`. `--tenant-primary` CSS variable defaults to `#1F6FEB` when no tenant color is set.
|
||||||
|
|
||||||
|
Admin UI lives in `frontend/src/app/dashboard/admin/tenants` and `frontend/src/sections/admin/tenants`. It includes list/detail views, domain Check DNS / Check TLS actions, bot registration with activation links, payment policy editing, and member-role controls.
|
||||||
|
|
||||||
|
> [!warning] Current frontend/backend mismatch
|
||||||
|
> The Members tab posts to `/tenants/:tenantId/members` and deletes `/tenants/:tenantId/members/:memberId`, while the backend currently exposes `POST /tenants/:tenantId/roles` and `DELETE /tenants/:tenantId/roles`. Fix one side before relying on member management in production.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Env vars
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `TENANT_BASE_DOMAIN` | no | Base domain for subdomain tenants. Default `amn.gg`. |
|
||||||
|
| `TENANT_SECRET_KEY` | yes (when registering bots) | 32-byte AES key, provided as 64 hex chars or 44 base64 chars. |
|
||||||
|
| `APP_URL` / `FRONTEND_URL` | yes for webhook auto-registration | Base URL used to register Telegram `setWebhook`. `APP_URL` wins; otherwise first comma-separated `FRONTEND_URL` value is used. |
|
||||||
|
| `CADDY_ADMIN_URL` | no | Caddy Admin API URL. Default `http://infra-caddy:2019`. |
|
||||||
|
| `CADDY_BACKEND_UPSTREAM` | no | Backend upstream for dynamic tenant routes. Default `escrow-multi-backend:5001`. |
|
||||||
|
| `CADDY_FRONTEND_UPSTREAM` | no | Frontend upstream for dynamic tenant routes. Default `escrow-multi-frontend:8083`. |
|
||||||
|
| `CADDY_SERVER_IP` | no | Public IP accepted by DNS verification. |
|
||||||
|
| `CADDY_CNAME_TARGET` | no | CNAME target accepted by DNS verification. Default `multi.amn.gg`. |
|
||||||
|
| `DOMAIN_POLL_INTERVAL_MS` | no | Pending-domain/TLS poll interval. Default `60000`. |
|
||||||
|
|
||||||
|
Related: [[Tenant]], [[Tenant API]], [[PRD - Seller-Owned White-Label Shops and Bots]].
|
||||||
13
README.md
13
README.md
@@ -10,7 +10,7 @@ created: 2026-05-23
|
|||||||
Complete technical & operational documentation for the **Amn** (a.k.a. "nick app") crypto-escrow marketplace platform. This vault is exhaustive enough to **re-implement the system from scratch** with no access to the source code.
|
Complete technical & operational documentation for the **Amn** (a.k.a. "nick app") crypto-escrow marketplace platform. This vault is exhaustive enough to **re-implement the system from scratch** with no access to the source code.
|
||||||
|
|
||||||
> [!info]
|
> [!info]
|
||||||
> **Repos:** `git@git.manko.yoga:222/nick/{backend,frontend}.git` · **Active backend integration branch:** `integrate-main-into-development` · **Current backend baseline:** `2.6.79` at `3a50dc4` · **Vault generated:** 2026-05-23
|
> **Repos:** `backend/`, `frontend/`, `deployment/`, `scanner/`, `amanat-assist/`, `nick-doc/` under `git@git.tbs.amn.gg:escrow/*` · **Current multi-shop branch:** `feature/white-label-shops` · **Frontend/backend baseline:** `2.11.49` · **Latest full repo scan:** [[Multi-Shop Branch Project Scan - 2026-06-10]]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ Project context, the cast of characters, and shared vocabulary.
|
|||||||
How the system is composed at every layer.
|
How the system is composed at every layer.
|
||||||
|
|
||||||
- [[System Architecture]] — end-to-end topology + request lifecycle
|
- [[System Architecture]] — end-to-end topology + request lifecycle
|
||||||
- [[Backend Architecture]] — Express 5 + Mongoose + Socket.IO module map, plus current Postgres migration layer status
|
- [[Backend Architecture]] — Express 5 + Socket.IO module map, plus current Postgres/Drizzle runtime status
|
||||||
- [[Database Strategy - Mongo vs Postgres Assessment]] — current Mongo primary posture and Postgres cutover assessment
|
- [[Database Strategy - Mongo vs Postgres Assessment]] — migration context and historical assessment
|
||||||
- [[Frontend Architecture]] — Next.js 16 App Router + provider tree
|
- [[Frontend Architecture]] — Next.js 16 App Router + provider tree
|
||||||
- [[Request Network Integration Constraints]] — current RN integration constraints and rollout gates
|
- [[Request Network Integration Constraints]] — current RN integration constraints and rollout gates
|
||||||
- [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] — custody decentralization and smart-contract decision roadmap
|
- [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] — custody decentralization and smart-contract decision roadmap
|
||||||
@@ -58,7 +58,8 @@ How the system is composed at every layer.
|
|||||||
Per-entity Mongoose schemas — fields, relationships, state machines.
|
Per-entity Mongoose schemas — fields, relationships, state machines.
|
||||||
|
|
||||||
- [[Data Model Overview]] — ER-style map + reading order
|
- [[Data Model Overview]] — ER-style map + reading order
|
||||||
- [[Postgres Runtime Cutover Status]] — what is actually using Postgres vs still Mongo-backed on `integrate-main-into-development`
|
- [[Postgres Runtime Cutover Status]] — what is actually using Postgres vs legacy migration context
|
||||||
|
- [[Tenant]] — white-label tenant tables, domains, bots, integrations, payment policies, roles
|
||||||
- Core entities: [[User]] · [[PurchaseRequest]] · [[SellerOffer]] · [[Payment]] · [[Chat]] · [[Notification]] · [[Dispute]]
|
- Core entities: [[User]] · [[PurchaseRequest]] · [[SellerOffer]] · [[Payment]] · [[Chat]] · [[Notification]] · [[Dispute]]
|
||||||
- Marketplace extras: [[RequestTemplate]] · [[ShopSettings]] · [[Category]] · [[Review]]
|
- Marketplace extras: [[RequestTemplate]] · [[ShopSettings]] · [[Category]] · [[Review]]
|
||||||
- User extras: [[Address]] · [[TempVerification]]
|
- User extras: [[Address]] · [[TempVerification]]
|
||||||
@@ -76,7 +77,7 @@ Every endpoint, grouped by service. Auth, request/response shapes, errors, socke
|
|||||||
- Real-time / messaging: [[Chat API]] · [[Notification API]] · [[Socket Events]]
|
- Real-time / messaging: [[Chat API]] · [[Notification API]] · [[Socket Events]]
|
||||||
- Disputes & ratings: [[Dispute API]]
|
- Disputes & ratings: [[Dispute API]]
|
||||||
- Content: [[Blog API]]
|
- Content: [[Blog API]]
|
||||||
- Admin & ops: [[Admin API]]
|
- Admin & ops: [[Admin API]] · [[Tenant API]]
|
||||||
- Loyalty: [[Points API]]
|
- Loyalty: [[Points API]]
|
||||||
- Utility: [[File API]] · [[AI API]]
|
- Utility: [[File API]] · [[AI API]]
|
||||||
- Errors: [[Error Codes]]
|
- Errors: [[Error Codes]]
|
||||||
@@ -89,7 +90,7 @@ End-to-end narratives for every user-visible interaction, with Mermaid sequence/
|
|||||||
- [[Authentication Flow]] · [[Registration Flow]] · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]]
|
- [[Authentication Flow]] · [[Registration Flow]] · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]]
|
||||||
|
|
||||||
**Marketplace**
|
**Marketplace**
|
||||||
- [[Purchase Request Flow]] · [[Seller Offer Flow]] · [[Negotiation Flow]]
|
- [[Purchase Request Flow]] · [[Seller Offer Flow]] · [[Negotiation Flow]] · [[Tenant Storefront Flow]]
|
||||||
|
|
||||||
**Money**
|
**Money**
|
||||||
- [[PRD - Request Network In-House Checkout]] · [[Payment Flow - DePay & Web3]] · [[Escrow Flow]] · [[Payout Flow]] · [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]]
|
- [[PRD - Request Network In-House Checkout]] · [[Payment Flow - DePay & Web3]] · [[Escrow Flow]] · [[Payout Flow]] · [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]]
|
||||||
|
|||||||
Reference in New Issue
Block a user