Initial commit: nick docs

This commit is contained in:
moojttaba
2026-05-23 20:35:34 +03:30
commit 0da235ae27
90 changed files with 18268 additions and 0 deletions

View 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]]

View 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

View 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

View 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)

View 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

View 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