Initial commit: nick docs
This commit is contained in:
311
01 - Architecture/Backend Architecture.md
Normal file
311
01 - Architecture/Backend Architecture.md
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
title: Backend Architecture
|
||||
tags: [architecture, backend]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Backend Architecture
|
||||
|
||||
Module-level architecture of the Express 5 + TypeScript + Mongoose backend at `/Users/mojtabaheidari/code/backend` (development branch).
|
||||
|
||||
> [!info]
|
||||
> Repo: `git@git.manko.yoga:222/nick/backend.git` · Branch: `development` · Version: 2.6.3-beta (`package.json:4`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Folder tree
|
||||
|
||||
```
|
||||
backend/src/
|
||||
├── app.ts # Express bootstrap, middleware chain, route registration
|
||||
├── config/ # Per-feature config (legacy — most moved to shared/config)
|
||||
├── controllers/ # HTTP request handlers (slim — delegate to services)
|
||||
├── infrastructure/
|
||||
│ ├── database/ # Mongoose connection, retries, graceful shutdown
|
||||
│ └── socket/socketService.ts # Socket.IO server, rooms, emit helpers
|
||||
├── models/ # Mongoose models — see 02 - Data Models/
|
||||
├── routes/ # Express Router definitions (mounted in app.ts)
|
||||
├── scripts/ # CLI utilities (seed:users, seed:categories, ...)
|
||||
├── seeds/ # Seed data fixtures
|
||||
├── services/
|
||||
│ ├── ai/ # OpenAI integration (descriptions, moderation)
|
||||
│ ├── auth/ # JWT, OAuth, Passkey, password reset
|
||||
│ ├── blockchain/ # Web3 read/verify helpers
|
||||
│ ├── blog/ # Posts, categories, comments
|
||||
│ ├── chat/ # Conversations, messages, attachments
|
||||
│ ├── dispute/ # Dispute lifecycle, evidence, mediator
|
||||
│ ├── file/ # Multer uploads, MIME validation
|
||||
│ ├── marketplace/ # PurchaseRequest, SellerOffer, Template, Shop
|
||||
│ ├── notification/ # Templates, delivery, mark-as-read
|
||||
│ ├── payment/ # Payment orchestration + shkeeper/ subdir
|
||||
│ │ └── shkeeper/ # SHKeeper API, webhook, payout
|
||||
│ ├── points/ # Loyalty points, levels, redemption
|
||||
│ ├── redis/ # Redis client, cache helpers
|
||||
│ ├── user/ # Profile, preferences, addresses
|
||||
│ ├── admin/ # Admin-only operations
|
||||
│ └── email/ # Nodemailer transport + templates
|
||||
├── shared/
|
||||
│ ├── config/index.ts # Centralised env-var loader (typed)
|
||||
│ ├── middleware/ # auth, errorHandler, validators
|
||||
│ ├── types/ # Cross-cutting TypeScript types
|
||||
│ └── utils/response-handler.ts # Standard success/error response envelope
|
||||
└── utils/ # Pure utility fns (logger, currencyUtils, etc.)
|
||||
```
|
||||
|
||||
> [!tip]
|
||||
> Service folders are self-contained: each typically has `<feature>Service.ts`, `<feature>Controller.ts`, `<feature>Routes.ts`, `<feature>Validation.ts`. This makes each service movable to a microservice later with minimal coupling.
|
||||
|
||||
---
|
||||
|
||||
## 2. Bootstrap — `src/app.ts`
|
||||
|
||||
The bootstrap is intentionally linear and easy to audit. Execution order:
|
||||
|
||||
1. **Imports & env load** — `dotenv` (if used), then `import { config } from './shared/config'`.
|
||||
2. **Express app construction** — `const app = express();`
|
||||
3. **Trust proxy** — `app.set('trust proxy', config.trustProxy)` so X-Forwarded-For works behind Nginx.
|
||||
4. **Security headers** — `app.use(helmet({ ... }))`.
|
||||
5. **CORS** — `cors({ origin: config.frontendUrl, credentials: true, methods: [...] })`.
|
||||
6. **Body parsers** — `express.json({ limit: '10mb' })`, `express.urlencoded({ extended: true })`.
|
||||
7. **Static uploads** — `app.use('/uploads', express.static(uploadDir))`.
|
||||
8. **Health endpoint** — `GET /health` for Docker healthcheck and external monitors.
|
||||
9. **Route mounting** — every `/api/*` route registered before the error handler.
|
||||
10. **404 handler** — catches unmatched `/api/*`.
|
||||
11. **Error handler** — central `errorHandler` middleware formats responses via `response-handler.ts`.
|
||||
12. **HTTP server creation** — `const server = http.createServer(app)`.
|
||||
13. **Socket.IO attach** — `initSocket(server, corsOptions)` (see [[Real-time Layer]]).
|
||||
14. **DB connect** — `await connectDatabase()`.
|
||||
15. **Redis connect** — `await connectRedis()`.
|
||||
16. **Listen** — `server.listen(config.port, ...)`.
|
||||
17. **Graceful shutdown** — SIGTERM/SIGINT handlers close server, drain sockets, close Mongoose, close Redis.
|
||||
18. **Optional dev seeding** — when `NODE_ENV === 'development'` and `SEED_USERS !== 'false'`, the bootstrap calls the seed scripts to provision default test users.
|
||||
|
||||
---
|
||||
|
||||
## 3. Middleware chain
|
||||
|
||||
| Order | Middleware | Where | Purpose |
|
||||
|---|---|---|---|
|
||||
| 1 | `helmet` | global | Sets security headers (CSP, X-Frame-Options, ...). |
|
||||
| 2 | `cors` | global | Origin allow-list = `config.frontendUrl`, credentials enabled. |
|
||||
| 3 | `express.json` / `express.urlencoded` | global | Body parsers (10MB limit). |
|
||||
| 4 | `morgan` (dev only) | global | HTTP request log to stdout. |
|
||||
| 5 | `requestId` | global | Adds `X-Request-Id` for log correlation. |
|
||||
| 6 | `authMiddleware` | per-route | Verifies JWT, attaches `req.user`. Mounted only on protected routes. |
|
||||
| 7 | `roleGuard('admin'|'seller'|...)` | per-route | RBAC check after auth. |
|
||||
| 8 | `validate(schema)` | per-route | express-validator + zod inputs. |
|
||||
| 9 | `controllerFn` | per-route | Delegates to service layer. |
|
||||
| 10 | `notFound` | tail | Returns 404 envelope for unmatched routes. |
|
||||
| 11 | `errorHandler` | tail | Catches thrown errors, formats response. |
|
||||
|
||||
> [!warning]
|
||||
> Rate-limit middleware is **disabled by default for personal use** (see `app.ts:227` cited in the architecture review). Enable before any real public traffic — `express-rate-limit` is already a dependency.
|
||||
|
||||
---
|
||||
|
||||
## 4. Route registration
|
||||
|
||||
The full route table mounted by `app.ts`:
|
||||
|
||||
| Mount path | Module | Auth | Notes |
|
||||
|---|---|---|---|
|
||||
| `/api/auth` | `services/auth/authRoutes.ts` | mixed | login, register, refresh, OAuth, passkey |
|
||||
| `/api/user` | `services/user/userRoutes.ts` | JWT | profile, preferences |
|
||||
| `/api/address` | `services/user/addressRoutes.ts` | JWT | CRUD addresses |
|
||||
| `/api/marketplace/requests` | `services/marketplace/controllerRoutes.ts` | JWT | PurchaseRequest CRUD |
|
||||
| `/api/marketplace/offers` | `services/marketplace/controllerRoutes.ts` | JWT (seller) | SellerOffer CRUD |
|
||||
| `/api/marketplace/templates` | `services/marketplace/controllerRoutes.ts` | JWT (seller) | RequestTemplate CRUD |
|
||||
| `/api/marketplace/categories` | `services/marketplace/controllerRoutes.ts` | public read | Category list |
|
||||
| `/api/marketplace/shop-settings` | `services/marketplace/shopSettingsController.ts` | JWT (seller) | Shop profile |
|
||||
| `/api/payment` | `services/payment/paymentRoutes.ts` | JWT | Payment intent, status |
|
||||
| `/api/payment/shkeeper/webhook` | `services/payment/shkeeper/shkeeperWebhook.ts` | HMAC | Inbound from gateway |
|
||||
| `/api/payment/payout` | `services/payment/shkeeper/shkeeperPayoutService.ts` | JWT (seller/admin) | Withdraw to wallet |
|
||||
| `/api/chat` | `services/chat/chatRoutes.ts` | JWT | Conversations, messages |
|
||||
| `/api/notification` | `services/notification/notificationRoutes.ts` | JWT | List, mark read |
|
||||
| `/api/dispute` | `services/dispute/disputeRoutes.ts` | JWT | Open, evidence, resolve |
|
||||
| `/api/blog` | `services/blog/blogRoutes.ts` | mixed | Public read, admin write |
|
||||
| `/api/admin` | `services/admin/adminRoutes.ts` | JWT (admin) | Mod operations |
|
||||
| `/api/points` | `services/points/pointsRoutes.ts` | JWT | Balance, redemption |
|
||||
| `/api/ai` | `services/ai/aiRoutes.ts` | JWT | OpenAI-backed helpers |
|
||||
| `/api/file` | `services/file/fileRoutes.ts` | JWT | Multipart upload |
|
||||
|
||||
Full per-endpoint details → [[03 - API Reference/API Overview]] and the service-specific reference docs.
|
||||
|
||||
---
|
||||
|
||||
## 5. Service layer pattern
|
||||
|
||||
Every service module follows this contract:
|
||||
|
||||
```ts
|
||||
// services/<feature>/<feature>Service.ts
|
||||
export class FeatureService {
|
||||
static async createX(input, ctx): Promise<X> { /* business logic */ }
|
||||
static async getX(id, ctx): Promise<X | null> { /* ... */ }
|
||||
static async listX(filter, ctx): Promise<X[]> { /* ... */ }
|
||||
static async updateX(id, patch, ctx): Promise<X> { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
- Controllers are **thin** — they validate request shape, call the service, format the response.
|
||||
- Services own **business logic**, side effects (DB writes, socket emits, email sends).
|
||||
- Models are **pure schema** — only Mongoose definitions + virtuals/hooks.
|
||||
|
||||
Cross-service calls are direct imports — no event bus yet. When the system grows, the seam between services is a natural place to introduce a message queue.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dependency map (simplified)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
auth[auth]
|
||||
user[user]
|
||||
market[marketplace]
|
||||
pay[payment]
|
||||
chat[chat]
|
||||
notify[notification]
|
||||
dispute[dispute]
|
||||
points[points]
|
||||
file[file]
|
||||
email[email]
|
||||
socket[socket]
|
||||
|
||||
auth --> user
|
||||
auth --> notify
|
||||
market --> notify
|
||||
market --> chat
|
||||
market --> file
|
||||
pay --> market
|
||||
pay --> notify
|
||||
pay --> socket
|
||||
dispute --> market
|
||||
dispute --> chat
|
||||
dispute --> notify
|
||||
points --> notify
|
||||
notify --> socket
|
||||
notify --> email
|
||||
```
|
||||
|
||||
> [!note]
|
||||
> `socket` and `email` are leaf services — every notification path funnels through them. Mocking these two in tests covers most side-effect verification.
|
||||
|
||||
---
|
||||
|
||||
## 7. Error handling
|
||||
|
||||
All thrown errors are caught by the central error handler. The expected shape:
|
||||
|
||||
```ts
|
||||
class AppError extends Error {
|
||||
statusCode: number; // HTTP 4xx/5xx
|
||||
code: string; // app-specific code, e.g. "PAYMENT_ALREADY_REFUNDED"
|
||||
details?: unknown; // optional debug payload
|
||||
}
|
||||
```
|
||||
|
||||
Response envelope (success path is `{success:true,data:...}`):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_FAILED",
|
||||
"message": "email is required",
|
||||
"details": [{ "path": "email", "msg": "required" }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See `backend/src/shared/utils/response-handler.ts` and `backend/src/shared/middleware/errorHandler.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Configuration
|
||||
|
||||
Single source of truth for env vars: `src/shared/config/index.ts`. It exports a typed `config` object — anywhere you would write `process.env.X`, instead import `config.x`.
|
||||
|
||||
Full table in [[Environment Variables]]. Critical ones:
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `PORT` | `5001` | Listen port |
|
||||
| `MONGODB_URI` | `mongodb://localhost:27017/nickapp` | Includes db name |
|
||||
| `REDIS_URI` | `redis://localhost:6379` | + `REDIS_PASSWORD` |
|
||||
| `JWT_SECRET` | required | ≥32 chars |
|
||||
| `JWT_EXPIRES_IN` | `7d` | |
|
||||
| `REFRESH_TOKEN_EXPIRES_IN` | `30d` | |
|
||||
| `FRONTEND_URL` | `http://localhost:3000` | CORS origin |
|
||||
| `SHKEEPER_API_URL` | `https://pay.amn.gg` | |
|
||||
| `SHKEEPER_API_KEY` | required | |
|
||||
| `SHKEEPER_WEBHOOK_SECRET` | required | HMAC key |
|
||||
| `SMTP_*` | required | Nodemailer |
|
||||
| `OPENAI_API_KEY` | required | |
|
||||
|
||||
---
|
||||
|
||||
## 9. Database & connection management
|
||||
|
||||
- **Mongoose** is the ODM. Connection in `src/infrastructure/database/`.
|
||||
- Connection options enable retryable writes, exponential backoff on reconnect.
|
||||
- Indexes are defined on each model and auto-created on connect (Mongoose `autoIndex: true` in dev, recommend `false` in prod with explicit migration).
|
||||
- See [[Data Model Overview]] for the relational map and per-model docs.
|
||||
|
||||
Redis client (in `src/services/redis/`) provides:
|
||||
- Session caching (login attempts, lockout counters)
|
||||
- Rate-limit counters (when middleware is enabled)
|
||||
- Hot-path caches (category list, level configs)
|
||||
|
||||
---
|
||||
|
||||
## 10. Background work
|
||||
|
||||
The codebase has no dedicated queue runner — scheduled / async work is triggered inline from request handlers and uses `setTimeout` / `setInterval` patterns where needed (e.g., delayed retries). Consider introducing Bull / BullMQ if you grow:
|
||||
|
||||
- Payment status reconciliation (polling SHKeeper for stragglers)
|
||||
- Notification email digests
|
||||
- Auto-release escrow timers
|
||||
- Token / refresh-token cleanup
|
||||
|
||||
---
|
||||
|
||||
## 11. Testing
|
||||
|
||||
Jest test suites in `backend/__tests__/`:
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| `models.test.ts` | Schema validation, virtuals, hooks |
|
||||
| `payment-services.test.ts` | Payment orchestration logic |
|
||||
| `complete-backend.test.ts` | Cross-service integration |
|
||||
| `shkeeper-backend.test.ts` | SHKeeper service + webhook |
|
||||
|
||||
Run with `npm run test:all`. CI runs the same. Reach for `npm run test:models`, `npm run test:payment`, etc. when iterating on a slice.
|
||||
|
||||
---
|
||||
|
||||
## 12. Notable files for orientation
|
||||
|
||||
| File | Why it matters |
|
||||
|---|---|
|
||||
| `src/app.ts` | Bootstrap — read once to understand wiring |
|
||||
| `src/shared/config/index.ts` | All env vars, typed |
|
||||
| `src/shared/utils/response-handler.ts` | Standard response shape |
|
||||
| `src/shared/middleware/auth.ts` | JWT verify + RBAC |
|
||||
| `src/infrastructure/socket/socketService.ts` | All socket plumbing |
|
||||
| `src/services/payment/shkeeper/shkeeperWebhook.ts` | Webhook signature scheme |
|
||||
| `src/services/marketplace/PurchaseRequestService.ts` | Core marketplace state machine |
|
||||
| `src/services/auth/authService.ts` | Auth flows, lockout, hashing |
|
||||
| `src/models/User.ts` | Central entity with role/preferences |
|
||||
| `openapi.json` | Generated API spec — definitive endpoint list |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[System Architecture]] — full system topology
|
||||
- [[Frontend Architecture]] — how the FE talks to this BE
|
||||
- [[Real-time Layer]] — Socket.IO room model
|
||||
- [[Security Architecture]] — JWT, passkeys, webhook HMAC
|
||||
- [[Data Model Overview]] — entity-relationship map
|
||||
- [[Authentication Flow]] · [[Payment Flow - SHKeeper]] · [[Dispute Flow]]
|
||||
369
01 - Architecture/Frontend Architecture.md
Normal file
369
01 - Architecture/Frontend Architecture.md
Normal file
@@ -0,0 +1,369 @@
|
||||
---
|
||||
title: Frontend Architecture
|
||||
tags: [architecture, frontend, nextjs]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Frontend Architecture
|
||||
|
||||
Module-level architecture of the Next.js 16 (App Router) + TypeScript + MUI v7 frontend at `/Users/mojtabaheidari/code/frontend` (development branch).
|
||||
|
||||
> [!info]
|
||||
> Repo: `git@git.manko.yoga:222/nick/frontend.git` · Branch: `development` · Version: 1.9.6 (`package.json:4`) · Dev port `3000`, Docker port `8083`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Folder tree
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── app/ # Next.js 16 App Router (server + client islands)
|
||||
│ ├── layout.tsx # Root layout — providers, fonts, Sentry
|
||||
│ ├── (public)/ # Unauthenticated routes
|
||||
│ │ ├── post/[slug]/ # Blog reader
|
||||
│ │ └── shop/[seller]/[id] # Public seller / item view
|
||||
│ ├── auth/jwt/ # sign-in, sign-up, verify, reset, update
|
||||
│ ├── dashboard/ # AuthGuard + EmailVerificationGuard
|
||||
│ │ ├── overview/ # KPI / home tiles
|
||||
│ │ ├── chat/ # Real-time chat
|
||||
│ │ ├── account/ # Profile, address, notifications, wallet, passkey
|
||||
│ │ ├── request/ # Buyer purchase requests
|
||||
│ │ ├── request-template/ # Seller request templates
|
||||
│ │ ├── payment/ # Payment history / detail
|
||||
│ │ ├── points/ # Loyalty hub (transactions, referrals, levels)
|
||||
│ │ ├── disputes/ # Dispute hub
|
||||
│ │ ├── user/ # Admin user management
|
||||
│ │ ├── post/ # Admin blog editor
|
||||
│ │ ├── shop-settings/ # Seller shop config
|
||||
│ │ └── shops/ # Browse / checkout (dashboard scope)
|
||||
│ ├── error/ # Global error page
|
||||
│ └── not-found.tsx # 404
|
||||
├── sections/ # Page-specific composition modules (one folder per feature)
|
||||
│ └── (chat|payment|request|request-template|dispute|user|points|...)
|
||||
├── components/ # Reusable UI primitives (hook-form, table, upload, editor, ...)
|
||||
├── layouts/ # Page-template wrappers (auth-centered, auth-split, dashboard, main)
|
||||
├── theme/ # MUI theme creation, palette, typography, overrides
|
||||
├── settings/ # Settings drawer (mode, layout, direction, color, font)
|
||||
├── contexts/ # React Context providers (socket-context)
|
||||
├── hooks/ # 50+ custom hooks (use-boolean, use-socket, use-web3-wagmi, …)
|
||||
├── lib/ # Cross-cutting libs (axios.ts with interceptors)
|
||||
├── locales/ # i18next config + langs/{en,fa,ar,fr,cn,vi}/*.json
|
||||
└── types/ # Cross-app TypeScript types
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Rendering strategy
|
||||
|
||||
- **App Router** with `output: 'standalone'` in `next.config.ts` for production single-binary serving.
|
||||
- **Server components by default**, **client components** opted into with `"use client"`. Heavy interactive pages (chat, editor, dashboard) are mostly client-side; SEO routes (`shop/`, `post/[slug]`) leverage server rendering.
|
||||
- **Turbopack** in dev (`next dev --turbopack`).
|
||||
- **Streaming** via `loading.tsx` and Suspense boundaries.
|
||||
- **MUI cache** is wired up via `@mui/material-nextjs` in the root layout to avoid FOUC across server/client boundary.
|
||||
|
||||
---
|
||||
|
||||
## 3. Provider tree (root layout)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A[RootLayout]
|
||||
A --> B[AppRouterCacheProvider<br/>MUI emotion cache]
|
||||
B --> C[ThemeProvider<br/>theme + RTL stylis]
|
||||
C --> D[LocalizationProvider<br/>dayjs adapter]
|
||||
D --> E[I18nProvider<br/>i18next]
|
||||
E --> F[QueryClientProvider<br/>React Query]
|
||||
F --> G[SocketProvider<br/>Socket.IO context]
|
||||
G --> H[SnackbarProvider<br/>notistack]
|
||||
H --> I[Children — routes]
|
||||
```
|
||||
|
||||
Order matters: theme must wrap query (because mutations show snackbars styled by theme); socket wraps snackbar (so socket-driven notifications can fire snackbars).
|
||||
|
||||
---
|
||||
|
||||
## 4. Route layout & guards
|
||||
|
||||
| Route group | Layout | Guard chain |
|
||||
|---|---|---|
|
||||
| `(public)` | `layouts/main` | none |
|
||||
| `auth/jwt/*` | `layouts/auth-centered` or `auth-split` | redirect if already authed |
|
||||
| `dashboard/*` | `layouts/dashboard` (sidebar + topbar + breadcrumbs) | `AuthGuard` → `EmailVerificationGuard` |
|
||||
| `dashboard/user/*` | dashboard | + `role: admin` |
|
||||
| `dashboard/post/*` (editor) | dashboard | + `role: admin` |
|
||||
| `dashboard/shop-settings/*` | dashboard | + `role: seller` |
|
||||
|
||||
Guards live in `frontend/src/auth/` (HOC + hook). They consult the JWT-derived user context and redirect unauthenticated to `/auth/jwt/sign-in?returnTo=...`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sections vs components vs hooks
|
||||
|
||||
The codebase enforces a three-layer split:
|
||||
|
||||
| Layer | Lives in | Purpose | Example |
|
||||
|---|---|---|---|
|
||||
| **Section** | `src/sections/<feature>/` | Page-specific composition; orchestrates components | `sections/chat/view/ChatView.tsx` |
|
||||
| **Component** | `src/components/<name>/` | Reusable across sections | `components/hook-form/RHFTextField.tsx` |
|
||||
| **Hook** | `src/hooks/<name>.ts` | Encapsulated stateful behavior | `hooks/use-chat-socket.ts` |
|
||||
|
||||
> [!tip]
|
||||
> Per the cursor rules (`backend/.cursor/rules/ui-development-standards.mdc`), every component folder has `index.ts` (barrel export), `<name>.tsx` (component), `classes.ts` (styled-component class names), `types.ts` (interface). Following this layout keeps each component refactorable.
|
||||
|
||||
---
|
||||
|
||||
## 6. State management
|
||||
|
||||
The frontend deliberately uses **three** state mechanisms, each for one concern:
|
||||
|
||||
| Concern | Tool | Where |
|
||||
|---|---|---|
|
||||
| Server data | **React Query** | every `useXxxQuery` / `useXxxMutation` hook |
|
||||
| Cross-page shared state | **React Context** | `contexts/socket-context.tsx`, settings context, auth context |
|
||||
| Per-component / local UI | `useState`, `useReducer`, `use-boolean`, `use-set-state` | inside components |
|
||||
|
||||
No Redux, no MobX, no Recoil. The cursor rules also mention **Zustand** as the preferred client store if one is added, but the dev branch currently relies on React Query + Context.
|
||||
|
||||
### React Query setup
|
||||
|
||||
- `QueryClient` created once at the root layout with `defaultOptions`:
|
||||
- `queries: { staleTime: 30_000, retry: 1, refetchOnWindowFocus: false }`
|
||||
- `mutations: { retry: 0 }`
|
||||
- Query keys are tuples — convention: `['<resource>', <filter|id|...>]` e.g. `['requests', { status: 'open' }]`.
|
||||
- Mutations invalidate related keys in `onSuccess`.
|
||||
|
||||
---
|
||||
|
||||
## 7. API client (`src/lib/axios.ts`)
|
||||
|
||||
A single axios instance underpins every HTTP call:
|
||||
|
||||
```ts
|
||||
const api = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Request interceptor — attach token
|
||||
api.interceptors.request.use((cfg) => {
|
||||
const token = getStoredToken();
|
||||
if (token) cfg.headers.Authorization = `Bearer ${token}`;
|
||||
cfg.headers['X-Request-Id'] = randomUUID();
|
||||
return cfg;
|
||||
});
|
||||
|
||||
// Response interceptor — handle 401, normalize errors
|
||||
api.interceptors.response.use(
|
||||
(res) => res.data,
|
||||
async (err) => {
|
||||
if (err.response?.status === 401 && !retried) {
|
||||
// attempt refresh-token flow
|
||||
await refresh();
|
||||
return api.request(err.config);
|
||||
}
|
||||
throw normalizeError(err);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Endpoint constants live alongside the hook that uses them — no central `api/endpoints.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Real-time integration
|
||||
|
||||
The `SocketProvider` in `contexts/socket-context.tsx`:
|
||||
|
||||
1. Opens the connection after authentication (token from storage).
|
||||
2. Auto-joins user-specific rooms (`user-{id}`, `seller-{id}` or `buyer-{id}` based on role).
|
||||
3. Exposes a `useSocket()` hook returning `socket`, `connected`, helper emitters.
|
||||
|
||||
Higher-level hooks build on this:
|
||||
|
||||
| Hook | Subscribes to |
|
||||
|---|---|
|
||||
| `use-chat-socket` | `chat:new-message`, typing indicators, read receipts |
|
||||
| `use-conversations` | conversation list updates |
|
||||
| `use-notifications` | `notification:received` |
|
||||
| `use-purchase-requests` | `request:offer-received`, status changes |
|
||||
| `use-marketplace-socket` | broad market events |
|
||||
| `use-unified-real-time` | multi-event aggregator |
|
||||
|
||||
See [[Real-time Layer]] for the full event catalog.
|
||||
|
||||
---
|
||||
|
||||
## 9. Web3 integration
|
||||
|
||||
```ts
|
||||
// wagmi config (approx — confirm in src/web3/ or src/lib/wagmi.ts)
|
||||
const config = createConfig({
|
||||
chains: [bsc, polygon, mainnet, sepolia],
|
||||
transports: {
|
||||
[bsc.id]: http(`https://...alchemy.com/${KEY}`),
|
||||
[polygon.id]: http(`https://polygon-mainnet.g.alchemy.com/v2/${KEY}`),
|
||||
...
|
||||
},
|
||||
connectors: [
|
||||
injected(), // MetaMask
|
||||
walletConnect({ projectId: NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID }),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Wallet UI: connect / disconnect / show address / show balance via `use-web3-wagmi`, `use-web3-context`. The DePay widget (`@depay/widgets`) is loaded for the assisted-pay flow.
|
||||
|
||||
---
|
||||
|
||||
## 10. Internationalization
|
||||
|
||||
- `i18next` + `react-i18next` initialized in `src/locales/locales-config.ts` with 6 langs (en, fa, ar, fr, cn, vi).
|
||||
- Translation files in `langs/<locale>/*.json` (e.g., `common.json`).
|
||||
- Direction (`ltr`|`rtl`) lives in settings; `stylis-plugin-rtl` is wired into the MUI cache when `dir === 'rtl'`.
|
||||
- Date formatting via `dayjs` with locale auto-loaded.
|
||||
- Number formatting via `Intl.NumberFormat` + helper at `locales/utils/number-format-locale.ts`.
|
||||
- DataGrid has Persian-specific locale at `custom-fa-data-grid-locale.ts`.
|
||||
|
||||
See [[Internationalization & RTL]] for full detail.
|
||||
|
||||
---
|
||||
|
||||
## 11. Forms & validation
|
||||
|
||||
`react-hook-form` + `zod` schema via `@hookform/resolvers/zod`. Custom field wrappers in `components/hook-form/`:
|
||||
|
||||
| Wrapper | Wraps |
|
||||
|---|---|
|
||||
| `RHFTextField` | MUI `TextField` |
|
||||
| `RHFSelect` | MUI `Select` |
|
||||
| `RHFAutocomplete` | MUI `Autocomplete` |
|
||||
| `RHFCheckbox` | MUI `Checkbox` |
|
||||
| `RHFRadioGroup` | MUI `RadioGroup` |
|
||||
| `RHFSwitch` | MUI `Switch` |
|
||||
| `RHFUpload` | custom Dropzone (`components/upload`) |
|
||||
| `RHFEditor` | TipTap editor wrapper |
|
||||
| `RHFDatePicker` | `@mui/x-date-pickers` |
|
||||
| `RHFPhoneInput` | `react-phone-number-input` |
|
||||
| `RHFCountrySelect` | `components/country-select` |
|
||||
|
||||
A typical form section:
|
||||
|
||||
```tsx
|
||||
const schema = z.object({ email: z.string().email(), password: z.string().min(8) });
|
||||
const methods = useForm({ resolver: zodResolver(schema) });
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<RHFTextField name="email" label="Email" />
|
||||
<RHFTextField name="password" label="Password" type="password" />
|
||||
<LoadingButton type="submit" loading={methods.formState.isSubmitting}>Sign in</LoadingButton>
|
||||
</FormProvider>
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Theming
|
||||
|
||||
`src/theme/index.ts` creates the theme; `src/theme/options/` contains palette / typography / overrides. Light & dark variants share tokens; the active mode is read from the **Settings** drawer.
|
||||
|
||||
- Primary: `Public Sans Variable` (per cursor rules).
|
||||
- Secondary: `Barlow`.
|
||||
- Breakpoints: xs=600, sm=600, md=900, lg=1200, xl=1536.
|
||||
- Shape radius: 8 (default), customizable in settings.
|
||||
|
||||
See [[Theme Configuration]] and [[Design System Overview]].
|
||||
|
||||
---
|
||||
|
||||
## 13. Settings drawer
|
||||
|
||||
`src/settings/` provides a side-drawer that lets a user toggle:
|
||||
|
||||
- Mode (light / dark / system)
|
||||
- Contrast (default / high)
|
||||
- Layout (vertical / mini / horizontal nav)
|
||||
- Direction (ltr / rtl)
|
||||
- Color preset (one of N curated palettes)
|
||||
- Font family override
|
||||
|
||||
State persists in `localStorage` under `settings-key`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Editor (TipTap)
|
||||
|
||||
`components/editor/` wraps `@tiptap/react` with these extensions enabled:
|
||||
|
||||
- `StarterKit` (paragraph, headings, bold/italic, lists, blockquote)
|
||||
- `Link` (URL parsing)
|
||||
- `Image` (upload via `components/upload`)
|
||||
- `Underline`, `TextAlign`
|
||||
- `CodeBlock` + `CodeBlockLowlight` (syntax highlighting via `lowlight`)
|
||||
- `Placeholder`
|
||||
|
||||
Used in:
|
||||
- Blog post editor (`dashboard/post/new`, `dashboard/post/[id]/edit`)
|
||||
- Long-form description fields in Purchase Request & Request Template forms
|
||||
- Dispute evidence narrative
|
||||
|
||||
Content is stored as HTML in MongoDB. `react-markdown` + `remark-gfm` are available where Markdown rendering is preferred (e.g., chat messages).
|
||||
|
||||
---
|
||||
|
||||
## 15. File uploads
|
||||
|
||||
`components/upload/` provides a dropzone with:
|
||||
- Multi-file selection
|
||||
- Drag-and-drop
|
||||
- Per-file progress
|
||||
- Preview (`components/file-thumbnail/` shows by MIME)
|
||||
- Removal
|
||||
- Total-size enforcement (5 MB default, matches `MAX_FILE_SIZE`)
|
||||
|
||||
Backed by `/api/file/*` (multipart upload).
|
||||
|
||||
---
|
||||
|
||||
## 16. Sentry
|
||||
|
||||
`@sentry/nextjs` is initialised at app boot. Errors include user context (role, userId) and breadcrumbs (route changes, API failures). Source maps uploaded at build time.
|
||||
|
||||
---
|
||||
|
||||
## 17. Build & deploy
|
||||
|
||||
`package.json`:
|
||||
- `dev` → `next dev -p 8083 --turbopack`
|
||||
- `build` → `next build && cp .next/static .next/standalone/.next/ && cp -r public .next/standalone/`
|
||||
- `start` → `PORT=8083 node .next/standalone/server.js`
|
||||
|
||||
`Dockerfile` is a 2-stage multi-stage build that produces `.next/standalone/` and copies into a small `node:22-alpine` runner image with non-root user `nextjs`. Healthcheck via `curl http://localhost:8083`.
|
||||
|
||||
CI: `.gitea/workflows/deploy.yml` runs `scripts/deploy.sh` on push to `main` / `master`.
|
||||
|
||||
See [[Docker Setup]], [[CI-CD Pipeline]], and [[Deployment]].
|
||||
|
||||
---
|
||||
|
||||
## 18. Notable files for orientation
|
||||
|
||||
| File | Why it matters |
|
||||
|---|---|
|
||||
| `src/app/layout.tsx` | Provider tree |
|
||||
| `src/lib/axios.ts` | Every HTTP call goes through this |
|
||||
| `src/contexts/socket-context.tsx` | Realtime plumbing |
|
||||
| `src/theme/index.ts` | Theme creation entry |
|
||||
| `src/locales/locales-config.ts` | i18next setup |
|
||||
| `src/settings/context/` | Settings context (persistence) |
|
||||
| `next.config.ts` | Standalone build, COOP/COEP headers for Web3 popups |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[System Architecture]] — bird's-eye topology
|
||||
- [[Backend Architecture]] — server peer
|
||||
- [[Real-time Layer]] — Socket.IO plumbing
|
||||
- [[Design System Overview]] · [[Theme Configuration]] · [[Components]]
|
||||
- [[Tech Stack]] — versions
|
||||
- [[Coding Standards]] — UI cursor-rules summary
|
||||
224
01 - Architecture/Infrastructure.md
Normal file
224
01 - Architecture/Infrastructure.md
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
title: Infrastructure
|
||||
tags: [architecture, infrastructure, docker, devops]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Infrastructure
|
||||
|
||||
How the system is packaged, deployed and run in production. Read alongside [[Docker Setup]] (operations runbook) and [[CI-CD Pipeline]].
|
||||
|
||||
---
|
||||
|
||||
## 1. Runtime topology
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
DNS[DNS amn.gg] --> CF[CloudFlare / Edge SSL]
|
||||
CF --> Nginx
|
||||
subgraph Host[Single Docker host]
|
||||
Nginx[nginx:alpine<br/>nickapp-nginx<br/>:8083]
|
||||
FE[nickapp-frontend:latest<br/>Next.js standalone<br/>:8083]
|
||||
BE[nickapp-backend:latest<br/>Express + Socket.IO<br/>:5001]
|
||||
Mongo[(mongo:8.0<br/>mongodb_data volume)]
|
||||
Redis[(redis:8-alpine<br/>redis_data volume)]
|
||||
Up[/uploads volume/]
|
||||
end
|
||||
Nginx --> FE
|
||||
Nginx --> BE
|
||||
BE --> Mongo
|
||||
BE --> Redis
|
||||
BE --- Up
|
||||
FE --- Up
|
||||
Watchtower>Watchtower<br/>auto-update] -.-> BE
|
||||
Watchtower -.-> FE
|
||||
```
|
||||
|
||||
> [!info]
|
||||
> Single-host today. Horizontal scaling requires Redis pub/sub adapter for Socket.IO and externalizing the `uploads/` volume (S3 / R2).
|
||||
|
||||
---
|
||||
|
||||
## 2. Docker images
|
||||
|
||||
| Image | Source Dockerfile | Stages | Final size target | User |
|
||||
|---|---|---|---|---|
|
||||
| `nickapp-backend:latest` | `backend/Dockerfile.prod` | builder + runner | ~200 MB | `marketplace` (uid 1001) |
|
||||
| `nickapp-backend:dev` | `backend/Dockerfile.dev` | single | ~400 MB | `marketplace` (uid 1001) |
|
||||
| `nickapp-frontend:latest` | `frontend/Dockerfile` | builder + runner | ~250 MB | `nextjs` (uid 1001) |
|
||||
| `nickapp-frontend:dev` | `frontend/Dockerfile.dev` | single | ~600 MB | root |
|
||||
|
||||
All built on `node:22-alpine`. Backend images include Python+make+g++ in the builder stage for native deps (bcrypt). Frontend runner only ships the `.next/standalone` output + `public/`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Compose stacks
|
||||
|
||||
### 3.1 `docker-compose.dev.yml` (backend repo)
|
||||
|
||||
Used by developers running `npm run docker:dev`. Three services:
|
||||
|
||||
| Service | Image | Ports | Volumes | Depends on |
|
||||
|---|---|---|---|---|
|
||||
| `nickdev-backend` | built from `Dockerfile.dev` | `5001:5001` | `./src:/app/src`, `./uploads:/app/uploads` | mongodb, redis |
|
||||
| `nickdev-mongodb` | `mongo:8.0` | `27017:27017` | `mongodb_data:/data/db` | — |
|
||||
| `nickdev-redis` | `redis:8-alpine` | (internal) | `redis_data:/data` | — |
|
||||
|
||||
Network: `nickapp-network` (bridge). Env: `.env.local`.
|
||||
|
||||
Frontend dev runs OUTSIDE compose — developer launches `yarn dev` on host so React Fast Refresh works.
|
||||
|
||||
### 3.2 `docker-compose.production.yml` (backend repo)
|
||||
|
||||
Used in production. Five services:
|
||||
|
||||
| Service | Image | Ports | Healthcheck | Watchtower |
|
||||
|---|---|---|---|---|
|
||||
| `nickapp-nginx` | `nginx:alpine` | `8083:80` | implicit | — |
|
||||
| `nickapp-backend` | `nickapp-backend:latest` (pulled) | 5001 internal | `node healthcheck.js` 30s | ✓ |
|
||||
| `nickapp-frontend` | `nickapp-frontend:latest` (pulled) | 8083 internal | `curl http://localhost:8083` 30s | ✓ |
|
||||
| `nickapp-mongodb` | `mongo:8.0` | internal | `mongosh ping` 30s | — |
|
||||
| `nickapp-redis` | `redis:8-alpine` (with `--requirepass`) | internal | `redis-cli ping` 30s | — |
|
||||
|
||||
Volumes: `mongodb_data`, `redis_data`, `./uploads`, `./nginx/nginx.conf`, `./nginx/logs`. Env: single root `.env`.
|
||||
|
||||
> [!warning]
|
||||
> `REDIS_PASSWORD` MUST be set in the production `.env` before starting — otherwise the Redis container fails its healthcheck and dependents won't start.
|
||||
|
||||
---
|
||||
|
||||
## 4. Nginx configuration
|
||||
|
||||
The Nginx proxy at `./nginx/nginx.conf` (mounted read-only) is responsible for:
|
||||
|
||||
- Terminating internal HTTP (SSL handled by an external edge — CloudFlare / nginx-proxy / Caddy)
|
||||
- Routing `/api/*` and `/socket.io/*` to `nickapp-backend:5001`
|
||||
- Routing everything else to `nickapp-frontend:8083`
|
||||
- Serving `/uploads/*` directly from the shared volume (bypasses the Node process)
|
||||
- Standard gzip / compression / client_max_body_size (≥10 MB to match backend body limit)
|
||||
|
||||
> [!tip]
|
||||
> Put the Nginx access log path on a tmpfs or rotate it aggressively — the container exposes `./nginx/logs` so the host can manage retention.
|
||||
|
||||
---
|
||||
|
||||
## 5. Watchtower (auto-update)
|
||||
|
||||
Both `nickapp-backend` and `nickapp-frontend` carry the `watchtower.enable=true` label. Watchtower polls the container registry on its configured interval and re-pulls when the `latest` tag moves.
|
||||
|
||||
Release cycle:
|
||||
1. Developer pushes commits to a feature branch → merged into `development`.
|
||||
2. Manual Gitea workflow `docker-build-simple.yml` builds & pushes `nickapp-backend:latest` (and a versioned tag) to `git.manko.yoga/manawenuz/escrow-backend`.
|
||||
3. Within the next poll interval (default 5 min) Watchtower restarts the affected service.
|
||||
|
||||
> [!warning]
|
||||
> Because Watchtower restarts only on tag change, sequential pushes are safe — but a broken build pushed to `latest` will roll out automatically. Keep `dev` and `production` tags separated, or pin production to a versioned tag.
|
||||
|
||||
---
|
||||
|
||||
## 6. Persistent storage
|
||||
|
||||
| Volume | What it stores | Backup priority |
|
||||
|---|---|---|
|
||||
| `mongodb_data` | All business data (users, requests, payments, chats, disputes...) | **Critical** — daily dump |
|
||||
| `redis_data` | Cache, session, rate counters | Low — losing it logs everyone out but no data loss |
|
||||
| `./uploads` (host bind) | Avatars, product images, dispute evidence, documents | **High** — daily rsync |
|
||||
| `./nginx/logs` | Access / error logs | Medium |
|
||||
|
||||
See [[Backup & Recovery]] for the runbook.
|
||||
|
||||
---
|
||||
|
||||
## 7. CI / CD
|
||||
|
||||
Located at `backend/.gitea/workflows/`:
|
||||
|
||||
| Workflow | Trigger | Output |
|
||||
|---|---|---|
|
||||
| `docker-build-simple.yml` | manual | Build → push backend image to registry (`latest` + version) |
|
||||
| `docker-build-dev.yml` | manual | Dev image variant |
|
||||
| `docker-build-no-cache.yml` | manual | Clean build (no GH-Actions cache) |
|
||||
|
||||
Frontend at `frontend/.gitea/workflows/`:
|
||||
|
||||
| Workflow | Trigger | Output |
|
||||
|---|---|---|
|
||||
| `deploy.yml` | push to `main`/`master`, manual | Runs `scripts/deploy.sh` (deploy via SSH) |
|
||||
| `devDeploy.yml` | manual | Deploy to dev env |
|
||||
|
||||
Single secret required: `GITEATOKEN` (Gitea PAT with `write:package`).
|
||||
|
||||
Full breakdown → [[CI-CD Pipeline]].
|
||||
|
||||
---
|
||||
|
||||
## 8. Secrets
|
||||
|
||||
Never committed. Required in production `.env`:
|
||||
|
||||
- `JWT_SECRET`, `REFRESH_TOKEN_SECRET`
|
||||
- `MONGODB_URI`, `REDIS_PASSWORD`
|
||||
- `SHKEEPER_API_KEY`, `SHKEEPER_WEBHOOK_SECRET`
|
||||
- `SMTP_USER`, `SMTP_PASS`
|
||||
- `OPENAI_API_KEY`
|
||||
- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`
|
||||
- `NEXT_PUBLIC_ALCHEMY_API_KEY_*` (frontend public envs — embedded at build)
|
||||
|
||||
Recommend storing host-side in an `.env` outside the repo, mounted via Compose. For multi-host: Vault / AWS SSM / GCP Secret Manager.
|
||||
|
||||
---
|
||||
|
||||
## 9. Observability
|
||||
|
||||
| Signal | Where |
|
||||
|---|---|
|
||||
| App logs | container `stdout` → `docker logs nickapp-backend` |
|
||||
| Access logs | `./nginx/logs` |
|
||||
| Frontend errors | Sentry (`@sentry/nextjs`) |
|
||||
| Backend errors | Sentry (optional — add `@sentry/node`) |
|
||||
| Healthchecks | `GET /health` (backend) · `GET /` (frontend) · `mongosh ping` · `redis-cli ping` |
|
||||
|
||||
See [[Monitoring]] for the full table of metrics & recommended alerts.
|
||||
|
||||
---
|
||||
|
||||
## 10. Networking
|
||||
|
||||
| Direction | Port | Protocol | Notes |
|
||||
|---|---|---|---|
|
||||
| Internet → Nginx | 8083 | HTTP | SSL terminated upstream |
|
||||
| Browser → Backend | 5001 | HTTP + WS | via Nginx `/api`, `/socket.io` |
|
||||
| Backend → MongoDB | 27017 | TCP | Docker network |
|
||||
| Backend → Redis | 6379 | TCP | Docker network |
|
||||
| Backend → SHKeeper | 443 | HTTPS | External |
|
||||
| Backend → SMTP | 587 | TLS | External |
|
||||
| Backend → OpenAI | 443 | HTTPS | External |
|
||||
| Browser → Blockchain RPC | 443 | HTTPS | Alchemy URLs |
|
||||
| Browser → WalletConnect | 443 | HTTPS | Relay server |
|
||||
|
||||
---
|
||||
|
||||
## 11. Hardening checklist
|
||||
|
||||
- [ ] Non-root user inside every container (`marketplace` / `nextjs`) ✓ in Dockerfiles
|
||||
- [ ] Read-only Nginx config mount ✓
|
||||
- [ ] Redis requires password ✓
|
||||
- [ ] MongoDB has root credentials + bind to internal network only ✓
|
||||
- [ ] Rate limit enabled before public traffic ⚠ currently disabled
|
||||
- [ ] Refresh tokens rotated on use ⚠ partial
|
||||
- [ ] All public env values reviewed (no `IS_DEVELOPMENT=true` in prod build) ⚠
|
||||
- [ ] Watchtower scoped to specific repos
|
||||
- [ ] Sentry source-map upload at build time ✓ (frontend)
|
||||
|
||||
See [[Security Architecture]] for the deeper review.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[System Architecture]] · [[Backend Architecture]] · [[Frontend Architecture]]
|
||||
- [[Docker Setup]] — operational walk-through with commands
|
||||
- [[CI-CD Pipeline]] — workflow breakdown
|
||||
- [[Deployment]] — first-deploy runbook
|
||||
- [[Backup & Recovery]] · [[Monitoring]] · [[Incident Response]]
|
||||
- [[Environment Variables]] — full env-var catalog
|
||||
220
01 - Architecture/Real-time Layer.md
Normal file
220
01 - Architecture/Real-time Layer.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
title: Real-time Layer
|
||||
tags: [architecture, realtime, websocket, socket.io]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Real-time Layer
|
||||
|
||||
Socket.IO 4.8 is the single transport for all server-pushed updates: chat messages, notifications, payment status, offer arrivals, dispute updates.
|
||||
|
||||
> [!info]
|
||||
> Backend handler: `backend/src/infrastructure/socket/socketService.ts`. Frontend context: `frontend/src/contexts/socket-context.tsx` consumed via `useSocket()`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Connection lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as Browser
|
||||
participant FE as React (SocketProvider)
|
||||
participant BE as Socket.IO Server
|
||||
U->>FE: Login completes (JWT stored)
|
||||
FE->>BE: io.connect(url, { auth: { token } })
|
||||
BE->>BE: verifyJwt(token) → req.user
|
||||
BE-->>FE: "connect" + socket.id
|
||||
FE->>BE: emit "join-user-room", userId
|
||||
BE-->>FE: ack
|
||||
Note over BE,FE: Long-lived connection. Auto-reconnect handles drops.
|
||||
BE-->>FE: emit "notification:received" (when triggered)
|
||||
FE->>FE: update React Query cache / show snackbar
|
||||
```
|
||||
|
||||
On the client:
|
||||
- Socket is **created lazily** after auth state is known.
|
||||
- Connection auto-recovers on disconnect (Socket.IO default exponential backoff).
|
||||
- The provider exposes `connected: boolean` so UI can show a "reconnecting…" indicator.
|
||||
|
||||
On the server:
|
||||
- The `connection` handler verifies the JWT from `socket.handshake.auth.token`.
|
||||
- If invalid → `socket.disconnect(true)`.
|
||||
- If valid → `socket.data.user = decoded` is set for use in subsequent handlers.
|
||||
|
||||
---
|
||||
|
||||
## 2. Room model
|
||||
|
||||
Rooms are joined explicitly by the client via emitter events. Names use a `prefix-{id}` convention so socket addressing matches the canonical entity ID.
|
||||
|
||||
| Room | Joined by | Purpose |
|
||||
|---|---|---|
|
||||
| `user-{userId}` | every authenticated socket | User-targeted events (notifications, payment updates for them) |
|
||||
| `seller-{userId}` | sellers on login | Seller-targeted events (new offer requests in their category) |
|
||||
| `sellers` | all sellers | Broadcast to all sellers (e.g., new public request) |
|
||||
| `buyer-{userId}` | buyers on login | Buyer-targeted events |
|
||||
| `buyers` | all buyers | Broadcast to all buyers |
|
||||
| `request-{requestId}` | participants of a request (buyer + accepted seller) | Per-request updates (status, offer arrivals) |
|
||||
| `chat-{chatId}` | conversation participants | Live chat messages, typing, read receipts |
|
||||
| `admin` | admin users | Mod-only events (new dispute opened, suspicious activity) |
|
||||
|
||||
Client-side join/leave emitters (defined in `socketService.ts`):
|
||||
|
||||
| Emit | Args | Effect |
|
||||
|---|---|---|
|
||||
| `join-user-room` | `userId` | Adds socket to `user-{userId}` |
|
||||
| `join-request-room` | `requestId` | Adds to `request-{requestId}` |
|
||||
| `leave-request-room` | `requestId` | Removes from `request-{requestId}` |
|
||||
| `join-seller-room` | `sellerId` | Adds to `seller-{sellerId}` and `sellers` |
|
||||
| `leave-seller-room` | `sellerId` | Removes from both |
|
||||
| `join-buyer-room` | `buyerId` | Adds to `buyer-{buyerId}` and `buyers` |
|
||||
| `leave-buyer-room` | `buyerId` | Removes from both |
|
||||
| `join-chat-room` | `chatId` | Adds to `chat-{chatId}` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Emit helpers (server-side)
|
||||
|
||||
The socket service exposes typed emitters used across all services so individual modules never touch `io` directly:
|
||||
|
||||
```ts
|
||||
// src/infrastructure/socket/socketService.ts
|
||||
export function emitToRoom(room: string, event: string, payload: unknown): void;
|
||||
export function emitToUser(userId: string, event: string, payload: unknown): void;
|
||||
export function emitToSellers(event: string, payload: unknown): void;
|
||||
export function emitToBuyers(event: string, payload: unknown): void;
|
||||
export function emitGlobalEvent(event: string, payload: unknown): void;
|
||||
```
|
||||
|
||||
This indirection makes it trivial to:
|
||||
- Add per-event logging
|
||||
- Throttle / batch in the future
|
||||
- Swap to a Redis pub/sub adapter when scaling out
|
||||
|
||||
---
|
||||
|
||||
## 4. Event catalog
|
||||
|
||||
### 4.1 Notifications
|
||||
|
||||
| Event | Payload | Emitted by | Rooms |
|
||||
|---|---|---|---|
|
||||
| `notification:received` | `Notification` doc | NotificationService | `user-{recipientId}` |
|
||||
| `notification:read` | `{ id, readAt }` | NotificationService.markRead | `user-{id}` |
|
||||
|
||||
### 4.2 Marketplace
|
||||
|
||||
| Event | Payload | Emitted by | Rooms |
|
||||
|---|---|---|---|
|
||||
| `request:created` | `PurchaseRequest` | PurchaseRequestService.create | `sellers` (or category-scoped sellers in future) |
|
||||
| `request:offer-received` | `{ requestId, offer: SellerOffer }` | SellerOfferService.create | `user-{buyerId}`, `request-{requestId}` |
|
||||
| `request:status-updated` | `{ requestId, status }` | various | `request-{requestId}` |
|
||||
| `offer:negotiation-update` | `{ offerId, message }` | SellerOfferService.counter | `request-{requestId}` |
|
||||
| `offer:accepted` | `{ offerId }` | SellerOfferService.accept | `seller-{sellerId}`, `user-{buyerId}` |
|
||||
|
||||
### 4.3 Payment
|
||||
|
||||
| Event | Payload | Emitted by |
|
||||
|---|---|---|
|
||||
| `payment:status-updated` | `{ paymentId, status, providerPaymentId }` | shkeeperWebhook → PaymentService |
|
||||
| `payment:created` | `Payment` | PaymentService.createPayInIntent |
|
||||
| `payment:completed` | `{ paymentId }` | PaymentService on webhook completion |
|
||||
| `payout:created` | `Payout` | shkeeperPayoutService |
|
||||
| `payout:completed` | `{ payoutId, txHash }` | payout polling / webhook |
|
||||
|
||||
### 4.4 Chat
|
||||
|
||||
| Event | Payload | Rooms |
|
||||
|---|---|---|
|
||||
| `chat:new-message` | `Message` | `chat-{chatId}` and `user-{participantId}` (for badge) |
|
||||
| `chat:message-read` | `{ chatId, messageId, userId, readAt }` | `chat-{chatId}` |
|
||||
| `chat:typing` | `{ chatId, userId }` | `chat-{chatId}` |
|
||||
| `chat:stopped-typing` | `{ chatId, userId }` | `chat-{chatId}` |
|
||||
|
||||
### 4.5 Dispute
|
||||
|
||||
| Event | Payload | Rooms |
|
||||
|---|---|---|
|
||||
| `dispute:opened` | `Dispute` | `request-{requestId}`, `admin` |
|
||||
| `dispute:evidence-added` | `{ disputeId, evidence }` | `request-{requestId}` |
|
||||
| `dispute:resolved` | `{ disputeId, resolution }` | `request-{requestId}`, `user-{buyerId}`, `user-{sellerId}` |
|
||||
|
||||
### 4.6 System
|
||||
|
||||
| Event | Payload | Rooms |
|
||||
|---|---|---|
|
||||
| `system:maintenance` | `{ message, startsAt }` | global broadcast |
|
||||
| `system:announcement` | `{ message }` | global broadcast |
|
||||
|
||||
---
|
||||
|
||||
## 5. Client-side hooks
|
||||
|
||||
Higher-level React hooks consume the socket and integrate with React Query so UI stays consistent:
|
||||
|
||||
| Hook | Listens for | Effect |
|
||||
|---|---|---|
|
||||
| `useChatSocket(chatId)` | `chat:new-message`, `chat:typing` | Appends to chat query cache; updates typing state |
|
||||
| `useConversations()` | `chat:new-message` (any chat) | Bumps conversation in list; updates unread count |
|
||||
| `useNotifications()` | `notification:received` | Prepends to notification list; shows snackbar |
|
||||
| `usePurchaseRequests()` | `request:status-updated`, `request:offer-received` | Invalidates `['requests']` cache |
|
||||
| `useMarketplaceSocket()` | `request:created` (seller side) | Bumps seller feed |
|
||||
| `useUnifiedRealTime()` | multi | Aggregates the above for the dashboard overview |
|
||||
|
||||
Pattern: each hook subscribes inside a `useEffect` and unsubscribes in cleanup. Use `socket.off(event, handler)` to avoid handler leaks on re-render.
|
||||
|
||||
---
|
||||
|
||||
## 6. Authentication & authorization on sockets
|
||||
|
||||
- **Connection-time auth** — JWT verified in handshake; invalid token → disconnect.
|
||||
- **Per-event auth** — Critical handlers re-check role from `socket.data.user.role`. Example: `admin` room membership only added for admins.
|
||||
- **Tampering** — `userId` arguments in `join-*-room` events are **NOT trusted blindly**; the server must verify `socket.data.user.id === userId` before adding to the room (cite: needs verification in `socketService.ts`).
|
||||
|
||||
> [!warning]
|
||||
> If `join-user-room` accepts any userId from the client, malicious users could subscribe to other users' notifications. Always cross-check with `socket.data.user.id`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Reconnection & buffering
|
||||
|
||||
Socket.IO buffers up to N events per disconnected client by default — but **the server does not replay missed events after a long disconnect**. Frontend strategy:
|
||||
|
||||
1. On reconnect, the SocketProvider re-fires `join-*-room` for the user's current state.
|
||||
2. Critical hooks (notifications, chat) call `refetch()` on reconnect to backfill from REST.
|
||||
3. Last-event-id pattern is **not implemented**; consider adding for chat reliability.
|
||||
|
||||
---
|
||||
|
||||
## 8. Scaling to multiple backend nodes
|
||||
|
||||
Socket.IO requires a shared bus when running >1 backend instance. Recommended adapter:
|
||||
|
||||
```ts
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { createClient } from 'redis';
|
||||
const pub = createClient({ url: config.redisUri, password: config.redisPassword });
|
||||
const sub = pub.duplicate();
|
||||
await Promise.all([pub.connect(), sub.connect()]);
|
||||
io.adapter(createAdapter(pub, sub));
|
||||
```
|
||||
|
||||
Without this adapter, emits from node A never reach sockets connected to node B.
|
||||
|
||||
Sticky sessions on the load balancer are also required so a given client always lands on the same node (otherwise the handshake / connection upgrade fails).
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing
|
||||
|
||||
- Backend tests (`backend/__tests__/`) mock the socket emitter via the indirection layer — no real socket server in unit tests.
|
||||
- Manual smoke test: open two browser tabs as buyer + seller, accept an offer, watch both receive `offer:accepted` instantly.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[Backend Architecture]] · [[Frontend Architecture]]
|
||||
- [[Chat Flow]] · [[Notification Flow]] · [[Payment Flow - SHKeeper]] · [[Dispute Flow]]
|
||||
- [[Security Architecture]] — socket auth concerns
|
||||
- [[Socket Events]] — full event reference (developer-facing API doc)
|
||||
227
01 - Architecture/Security Architecture.md
Normal file
227
01 - Architecture/Security Architecture.md
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: Security Architecture
|
||||
tags: [architecture, security, authentication, rbac]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# Security Architecture
|
||||
|
||||
How identity, authorization, transport, and integrity are handled across the platform.
|
||||
|
||||
> [!important]
|
||||
> Read alongside [[Authentication Flow]] (user-facing), [[Passkey (WebAuthn) Flow]], and [[Payment Flow - SHKeeper]] (webhook HMAC).
|
||||
|
||||
---
|
||||
|
||||
## 1. Threat model — at a glance
|
||||
|
||||
| Threat | Mitigation |
|
||||
|---|---|
|
||||
| Credential stuffing | bcrypt 12-round hashing + account lockout + rate-limit (when enabled) |
|
||||
| Session hijacking | Short-lived JWTs (7d), opaque refresh tokens (30d), token rotation |
|
||||
| CSRF | JWT in `Authorization` header (not cookie), CORS allow-list |
|
||||
| XSS | Helmet CSP, React auto-escaping, sanitize HTML before storage |
|
||||
| SQL/NoSQL injection | Mongoose parameterized queries, no `$where` strings, schema validation |
|
||||
| Webhook spoofing | HMAC SHA-256 over body + secret, constant-time compare |
|
||||
| File upload abuse | Multer MIME validation, 5 MB cap, non-executable storage, served by Nginx not Node |
|
||||
| Replay attacks | Per-payment idempotency on `providerPaymentId`, per-request `X-Request-Id` |
|
||||
| Account takeover | Email verification required, password reset code expiry (1h), passkey support |
|
||||
| Phishing | Passkey origin binding (`NEXT_PUBLIC_PASSKEY_ORIGIN`), email domain pinning |
|
||||
| Data leakage | Role-gated endpoints, field-level projection (`select: false` on password), redacted logs |
|
||||
|
||||
---
|
||||
|
||||
## 2. Authentication layers
|
||||
|
||||
### 2.1 Email + password (primary)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as User
|
||||
participant FE as Frontend
|
||||
participant BE as Backend
|
||||
participant DB as MongoDB
|
||||
U->>FE: enters credentials
|
||||
FE->>BE: POST /api/auth/login { email, password }
|
||||
BE->>DB: User.findOne({ email })
|
||||
DB-->>BE: user doc (incl. hashed password)
|
||||
BE->>BE: bcrypt.compare(password, hash)
|
||||
alt invalid
|
||||
BE->>DB: increment loginAttempts; lock at N
|
||||
BE-->>FE: 401 / 423 locked
|
||||
else valid
|
||||
BE->>BE: sign JWT (7d), refresh (30d)
|
||||
BE->>DB: store refresh-token id; clear attempts
|
||||
BE-->>FE: 200 { user, token, refreshToken }
|
||||
end
|
||||
```
|
||||
|
||||
- Password rules enforced by `authValidation.ts`: ≥8 chars, mixed case + digit recommended (cite the validator for exact rules).
|
||||
- bcrypt rounds = 12 (`authService.ts`).
|
||||
- Lockout: after N failed attempts within window, account locked for cooldown — see `authService.ts:113-145`.
|
||||
- Reset code emailed on `passwordResetCode` request; valid 1h.
|
||||
|
||||
### 2.2 Google OAuth 2.0
|
||||
|
||||
- Frontend uses `NEXT_PUBLIC_GOOGLE_CLIENT_ID` for the Sign-In with Google button.
|
||||
- ID token sent to backend → `googleOAuthService.ts` verifies via Google's public keys → either links to existing User by email or creates a new one.
|
||||
- See [[Google OAuth Flow]].
|
||||
|
||||
### 2.3 WebAuthn / Passkey
|
||||
|
||||
- Standards-based passwordless.
|
||||
- Backend: `passkeyService.ts` orchestrates registration and assertion challenges.
|
||||
- Frontend env: `NEXT_PUBLIC_PASSKEY_RP_NAME=Amn`, `NEXT_PUBLIC_PASSKEY_RP_ID=<domain>`, `NEXT_PUBLIC_PASSKEY_ORIGIN=<origin>`.
|
||||
- See [[Passkey (WebAuthn) Flow]].
|
||||
|
||||
> [!warning]
|
||||
> Dev env files ship `NEXT_PUBLIC_PASSKEY_RP_ID=localhost`. In production this MUST be the actual eTLD+1 domain (e.g., `amn.gg`) — passkeys are scoped to the RP ID and can't be transferred.
|
||||
|
||||
### 2.4 Refresh-token rotation
|
||||
|
||||
- On `POST /api/auth/refresh`, the backend:
|
||||
- Verifies the supplied refresh token.
|
||||
- Issues a NEW access token + a NEW refresh token.
|
||||
- Invalidates the old refresh token id in MongoDB.
|
||||
- If the same refresh token is presented twice → all sessions for that user are invalidated (token reuse detection).
|
||||
|
||||
---
|
||||
|
||||
## 3. Authorization (RBAC)
|
||||
|
||||
### 3.1 Roles
|
||||
|
||||
| Role | Source | Capabilities |
|
||||
|---|---|---|
|
||||
| `buyer` (user) | default on signup | Create requests, pay, chat, dispute, rate |
|
||||
| `seller` (owner) | chosen at signup OR upgraded | Make offers, build templates, run a shop, withdraw |
|
||||
| `admin` | seed / manual | Moderate, mediate disputes, manage users/blogs/levels |
|
||||
| `support` | seed / manual | Read-only on most data, can reset passwords, escalate |
|
||||
|
||||
A single User may be `buyer` and `seller` simultaneously (combined role).
|
||||
|
||||
### 3.2 Enforcement points
|
||||
|
||||
- **Middleware** — `authMiddleware` (verifies JWT) followed by `roleGuard(role)` on every route that requires elevation.
|
||||
- **Service layer** — defensive `assertRole(ctx, 'admin')` calls inside critical service methods so even mis-mounted routes can't bypass.
|
||||
- **UI** — `AuthGuard` + `EmailVerificationGuard` + role-aware nav (`components/nav-section`) hide admin/seller menus for users without permission. This is convenience only — never the security boundary.
|
||||
|
||||
---
|
||||
|
||||
## 4. Transport security
|
||||
|
||||
- **HTTPS** terminated upstream (CloudFlare / external Nginx). Internal cluster is HTTP.
|
||||
- **HSTS** header set by upstream proxy (recommended `max-age=31536000; includeSubDomains; preload`).
|
||||
- **CORS** — exactly one origin allowed: `config.frontendUrl`. `credentials: true`.
|
||||
- **CSP** — Helmet default, currently permissive for Web3 popup compatibility (see `frontend/next.config.ts` setting COOP=`same-origin-allow-popups`, COEP=`unsafe-none`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Webhook integrity (SHKeeper)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SHK
|
||||
participant BE
|
||||
SHK->>BE: POST /api/payment/shkeeper/webhook<br/>X-Signature: sha256=<hmac>
|
||||
BE->>BE: hmac = HMAC_SHA256(SHKEEPER_WEBHOOK_SECRET, body)
|
||||
BE->>BE: crypto.timingSafeEqual(hmac, providedSig)
|
||||
alt mismatch
|
||||
BE-->>SHK: 401 Unauthorized
|
||||
else match
|
||||
BE->>BE: process payment update
|
||||
BE-->>SHK: 200 OK
|
||||
end
|
||||
```
|
||||
|
||||
- Body must be the raw bytes used for HMAC — apply `express.raw({ type: 'application/json' })` on the webhook route ONLY (the rest of the app uses parsed JSON).
|
||||
- In dev (`NODE_ENV === 'development'`) signature verification can be bypassed for local testing — confirm this is gated and never reachable in prod.
|
||||
- Idempotency: identical webhook delivered twice should be no-op. Check by `(providerPaymentId, status)` tuple before mutating.
|
||||
|
||||
See [[Payment Flow - SHKeeper]] for the full flow.
|
||||
|
||||
---
|
||||
|
||||
## 6. Input validation
|
||||
|
||||
- **Backend** — `express-validator` per route (e.g., `authValidation.ts`), centralised `validate` middleware that 422s on failure with `{ details: [...] }`.
|
||||
- **Frontend** — `zod` schemas via `@hookform/resolvers/zod`. Same schema can be re-exported to a `shared/` package for true single-source-of-truth (not yet wired).
|
||||
- **Mongoose** — schema-level `type`, `required`, `enum`, `min`/`max`, custom `validate` functions as a last line of defence.
|
||||
|
||||
---
|
||||
|
||||
## 7. File upload safety
|
||||
|
||||
- Stored under `uploads/{avatars|documents|products|temp}/` — non-executable, served by Nginx (no Node interpretation).
|
||||
- MIME allow-list in `fileService.ts`: images for avatars/products, PDFs/docs for evidence.
|
||||
- 5 MB hard cap (`MAX_FILE_SIZE=5242880`).
|
||||
- Original filenames hashed → no path traversal, no clobber.
|
||||
- Recommended: virus scan via ClamAV before exposing to other users (dispute evidence, chat attachments).
|
||||
|
||||
---
|
||||
|
||||
## 8. Secrets management
|
||||
|
||||
- Production secrets injected via host `.env`, mounted into compose `env_file`.
|
||||
- Never log secrets — logger redaction recommended (winston/pino formatter).
|
||||
- `.env*` files in `.gitignore`. Repo includes only `.env.development` / `.env.production` templates with **public** values (NEXT_PUBLIC_*).
|
||||
- Rotate `JWT_SECRET` invalidates all existing JWTs — schedule a maintenance window.
|
||||
- Rotate `SHKEEPER_WEBHOOK_SECRET` coordinated with SHKeeper dashboard (set new → verify → remove old).
|
||||
|
||||
See [[Environment Variables]] for the catalog.
|
||||
|
||||
---
|
||||
|
||||
## 9. Rate limiting & abuse
|
||||
|
||||
- Backend has `express-rate-limit` ready but currently disabled (`app.ts:227`).
|
||||
- Recommended pre-launch settings:
|
||||
- `/api/auth/*` — 10 req / 5 min / IP
|
||||
- `/api/auth/login` — 5 req / 5 min / IP **and** /email
|
||||
- global API — 100 req / 15 min / IP (current default constants)
|
||||
- Counters stored in Redis when enabled.
|
||||
- For chat and notifications, debounce at the client to avoid spamming legitimate emits.
|
||||
|
||||
---
|
||||
|
||||
## 10. Audit logging
|
||||
|
||||
The codebase currently uses `morgan` (HTTP access logs) and ad-hoc `logger.info/warn/error`. For PCI-adjacent operations (payments) consider:
|
||||
|
||||
- Append-only audit log of every payment / payout / refund / role change.
|
||||
- Include actor (userId), target, action, before/after diff, request id.
|
||||
- Persist in a separate Mongo collection or external log sink with retention ≥1y.
|
||||
|
||||
---
|
||||
|
||||
## 11. Frontend session storage
|
||||
|
||||
- JWT and refresh token stored in `localStorage` (per current implementation — cite to verify in `frontend/src/lib/`).
|
||||
- Risk: XSS = total takeover. Mitigations: strict CSP, no `dangerouslySetInnerHTML` on untrusted content, audit dependencies (`yarn audit`).
|
||||
- Alternative: store refresh token in `httpOnly` cookie and keep only short-lived access token in memory — recommended for production hardening.
|
||||
|
||||
---
|
||||
|
||||
## 12. Hardening checklist (pre-launch)
|
||||
|
||||
- [ ] Enable rate-limit middleware
|
||||
- [ ] Promote refresh tokens to `httpOnly` cookies
|
||||
- [ ] Replace `localhost` passkey RP ID with production domain
|
||||
- [ ] Disable `NEXT_PUBLIC_IS_DEVELOPMENT=true` and `ENABLE_DEBUG=true` in prod build
|
||||
- [ ] Verify `NODE_ENV=production` in backend prod env
|
||||
- [ ] Pin production Watchtower to versioned tag (not `latest`)
|
||||
- [ ] Add backend Sentry SDK + source maps
|
||||
- [ ] Rotate all dev-seeded credentials before public launch
|
||||
- [ ] Run `yarn audit` / `npm audit` and triage CVEs
|
||||
- [ ] Pentest the payment + dispute flows specifically
|
||||
- [ ] Review every `> [!warning]` callout in this vault
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[Authentication Flow]] · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]]
|
||||
- [[Backend Architecture]] · [[Frontend Architecture]] · [[Real-time Layer]]
|
||||
- [[Payment Flow - SHKeeper]] — webhook HMAC details
|
||||
- [[Environment Variables]] — secret catalog
|
||||
- [[Incident Response]] — what to do when something goes wrong
|
||||
202
01 - Architecture/System Architecture.md
Normal file
202
01 - Architecture/System Architecture.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: System Architecture
|
||||
tags: [architecture, system, overview]
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
# System Architecture
|
||||
|
||||
End-to-end architecture for the **Amn** escrow marketplace platform — a two-repo system (frontend + backend) that brokers crypto-escrowed transactions between buyers and sellers.
|
||||
|
||||
> [!info]
|
||||
> Read [[Backend Architecture]] and [[Frontend Architecture]] for component-level detail. Read [[Infrastructure]] for deployment topology.
|
||||
|
||||
---
|
||||
|
||||
## 1. High-level topology
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Browser[Browser / PWA]
|
||||
CDN[Static CDN]
|
||||
Nginx[Nginx Reverse Proxy<br/>:80/:443]
|
||||
FE[Next.js Frontend<br/>standalone server<br/>:8083]
|
||||
BE[Express Backend<br/>+ Socket.IO<br/>:5001]
|
||||
Mongo[(MongoDB 8)]
|
||||
Redis[(Redis 8)]
|
||||
SHK[SHKeeper<br/>Crypto Gateway]
|
||||
SMTP[SMTP<br/>Nodemailer]
|
||||
OAI[OpenAI API]
|
||||
BC[Blockchain RPC<br/>Alchemy / WalletConnect]
|
||||
|
||||
Browser -->|HTTPS + WSS| CDN
|
||||
Browser -->|HTTPS| Nginx
|
||||
Nginx --> FE
|
||||
Nginx --> BE
|
||||
FE -->|REST /api/*| BE
|
||||
FE -.->|Socket.IO| BE
|
||||
BE --> Mongo
|
||||
BE --> Redis
|
||||
BE -->|Pay-in / Pay-out| SHK
|
||||
SHK -.->|Webhook HMAC| BE
|
||||
BE --> SMTP
|
||||
BE --> OAI
|
||||
FE -->|Wallet Connect| BC
|
||||
BE -->|Tx verify| BC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Request lifecycle (typical authenticated REST call)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as User
|
||||
participant FE as Frontend (Next.js)
|
||||
participant AX as Axios Client
|
||||
participant BE as Backend (Express)
|
||||
participant MW as Middleware Chain
|
||||
participant SVC as Service Layer
|
||||
participant DB as MongoDB
|
||||
participant SK as Socket.IO
|
||||
|
||||
U->>FE: Interact with UI
|
||||
FE->>AX: useMutation(...).mutate(payload)
|
||||
AX->>AX: Attach Authorization: Bearer <jwt>
|
||||
AX->>BE: POST /api/marketplace/requests
|
||||
BE->>MW: helmet, cors, body-parser
|
||||
MW->>MW: authMiddleware → verify JWT
|
||||
MW->>MW: validation middleware
|
||||
MW->>SVC: controller invokes service
|
||||
SVC->>DB: Mongoose create / update
|
||||
DB-->>SVC: document
|
||||
SVC->>SK: emitToRoom("user-{id}", "request:created", payload)
|
||||
SVC-->>BE: response payload
|
||||
BE->>AX: 200 { success, data }
|
||||
AX->>FE: React Query cache update
|
||||
FE-->>U: UI re-render
|
||||
```
|
||||
|
||||
Concurrent realtime path:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as Frontend
|
||||
participant BE as Backend
|
||||
FE->>BE: socket.connect() w/ auth token
|
||||
BE-->>FE: socket connected
|
||||
FE->>BE: join-user-room <userId>
|
||||
BE-->>FE: joined user-{userId}
|
||||
Note over BE,FE: long-lived connection
|
||||
BE-->>FE: emit "notification:received"
|
||||
FE->>FE: update notification badge
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Deployment topology
|
||||
|
||||
Production runs as a single Docker Compose stack (`backend/docker-compose.production.yml`) behind an internal Nginx that proxies to both the frontend and backend containers. Watchtower watches the container registry (`git.manko.yoga/manawenuz/escrow-backend`) and auto-pulls new images tagged `latest`.
|
||||
|
||||
| Layer | Service | Image | Port | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| Edge | Nginx | `nginx:alpine` | 8083 | Reverse proxy + static assets |
|
||||
| App | Frontend | `nickapp-frontend:latest` | 8083 (internal) | Next.js standalone |
|
||||
| App | Backend | `nickapp-backend:latest` | 5001 (internal) | Express + Socket.IO |
|
||||
| Data | MongoDB | `mongo:8.0` | 27017 (internal) | Primary store |
|
||||
| Data | Redis | `redis:8-alpine` | 6379 (internal) | Cache + sessions + rate-limit counters |
|
||||
|
||||
External SSL termination, DNS, and CDN are assumed to live in front of Nginx (CloudFlare / nginx-proxy / similar).
|
||||
|
||||
> [!note]
|
||||
> Dev uses `backend/docker-compose.dev.yml` (no Nginx, no frontend container — developer runs FE locally with `yarn dev`). See [[Docker Setup]] for full compose breakdown.
|
||||
|
||||
---
|
||||
|
||||
## 4. Network ports
|
||||
|
||||
| Port | Process | Visibility | Notes |
|
||||
|---|---|---|---|
|
||||
| 8083 | Frontend (Docker) | Public via Nginx | `next start` standalone |
|
||||
| 3000 | Frontend (local dev) | localhost | `yarn dev` — hot reload |
|
||||
| 5001 | Backend (Express + Socket.IO) | Public via Nginx `/api` & `/socket.io` | One process, two protocols |
|
||||
| 27017 | MongoDB | Internal | Mapped to host only in dev |
|
||||
| 6379 | Redis | Internal | Mapped to host only in dev |
|
||||
| 222 | Gitea SSH | Public | Used for `git clone` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Data flow patterns
|
||||
|
||||
### 5.1 Read path
|
||||
|
||||
REST `GET` requests are cache-able. React Query on the frontend de-duplicates concurrent requests, retries on failure (exponential backoff), and treats data as stale after `staleTime` (default 60s, varies per query). Backend reads use Mongoose lean queries where possible for performance.
|
||||
|
||||
### 5.2 Write path
|
||||
|
||||
Mutations follow optimistic-then-confirm:
|
||||
1. Frontend `useMutation` issues the request, often with `onMutate` updating cache optimistically.
|
||||
2. Backend validates → writes to MongoDB → emits affected socket rooms → returns canonical response.
|
||||
3. Frontend invalidates related query keys; React Query refetches.
|
||||
4. Other connected clients receive the socket event and refetch on their side.
|
||||
|
||||
### 5.3 Webhook path (inbound)
|
||||
|
||||
External services (SHKeeper) POST to `/api/payment/shkeeper/webhook`. The backend verifies HMAC signature, updates the `Payment` document, advances any linked `PurchaseRequest`/`SellerOffer` state, and emits Socket.IO events to both buyer and seller rooms.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SHK as SHKeeper
|
||||
participant BE as Backend
|
||||
participant DB as MongoDB
|
||||
participant Buyer
|
||||
participant Seller
|
||||
SHK->>BE: POST /api/payment/shkeeper/webhook<br/>X-Signature: HMAC-SHA256
|
||||
BE->>BE: verifySignature(body, header, SHKEEPER_WEBHOOK_SECRET)
|
||||
BE->>DB: Payment.updateOne({providerPaymentId}, {status:"completed"})
|
||||
BE->>DB: PurchaseRequest.updateOne(..., {status:"funded"})
|
||||
BE-->>Buyer: socket emit "payment:status-updated"
|
||||
BE-->>Seller: socket emit "request:funded"
|
||||
BE-->>SHK: 200 OK
|
||||
```
|
||||
|
||||
See [[Payment Flow - SHKeeper]] for the full sequence.
|
||||
|
||||
---
|
||||
|
||||
## 6. Scaling considerations
|
||||
|
||||
| Concern | Current state | Scale-out path |
|
||||
|---|---|---|
|
||||
| Backend stateless? | Yes — JWT-only auth, no in-memory session | Run N replicas behind LB; use Redis pub/sub adapter for Socket.IO |
|
||||
| MongoDB | Single-node | Replica set → sharding by `buyerId` |
|
||||
| Redis | Single-node | Cluster mode; separate cache vs session DBs |
|
||||
| Socket.IO | Single process | `@socket.io/redis-adapter` for multi-node fan-out |
|
||||
| File uploads | Local `uploads/` mount | S3 / R2; multer-s3 adapter |
|
||||
| Logs | Container stdout | Loki / ELK |
|
||||
| Errors | Sentry (`@sentry/nextjs` on FE) | Add Sentry backend SDK |
|
||||
|
||||
> [!warning]
|
||||
> Before horizontal-scaling backend, switch Socket.IO to the Redis adapter — otherwise users on different nodes will not receive each other's events.
|
||||
|
||||
---
|
||||
|
||||
## 7. Cross-cutting concerns
|
||||
|
||||
- **Auth** — Bearer JWT on REST, same token also passed via Socket.IO `auth` payload on connect. See [[Security Architecture]] & [[Authentication Flow]].
|
||||
- **i18n** — Frontend supports en/fa/ar/fr/cn/vi via `i18next`. RTL handled by `stylis-plugin-rtl` per direction. See [[Internationalization & RTL]].
|
||||
- **Realtime** — All notification/chat/payment state changes broadcast over Socket.IO. See [[Real-time Layer]].
|
||||
- **Errors** — Standardized envelope `{ success: false, error: { code, message, details } }` via `backend/src/shared/utils/response-handler.ts`. Sentry on frontend.
|
||||
- **Idempotency** — Payment webhooks idempotent by `providerPaymentId` + status. Pay-out uses a client-supplied `reference` string.
|
||||
|
||||
---
|
||||
|
||||
## 8. Related documents
|
||||
|
||||
- [[Backend Architecture]] — module-level walk-through of the Express app
|
||||
- [[Frontend Architecture]] — Next.js App Router & section organization
|
||||
- [[Infrastructure]] — Docker, compose, registry, Watchtower
|
||||
- [[Real-time Layer]] — Socket.IO setup, rooms, events
|
||||
- [[Security Architecture]] — auth, hashing, rate-limit, webhook HMAC
|
||||
- [[Tech Stack]] — exact versions & purpose of every dependency
|
||||
- [[Payment Flow - SHKeeper]] — end-to-end crypto pay-in flow
|
||||
Reference in New Issue
Block a user