System Architecture
The whole platform on one page. Five subdomains, three repositories, one backend, six external integrations.
The full picture
Subdomains
Five distinct surfaces, each serving a specific audience. Four share the pnpm monorepo served by nginx + NestJS on a Dokploy VPS. Cuan runs as a separate NestJS service on the same VPS. Docs is a separate Vercel project.
| Subdomain | Audience | Status | Stack |
|---|---|---|---|
pasarseken.id | Buyers + sellers | ✅ Live | Vite + React (nginx), NestJS backend, Flutter mobile |
help.pasarseken.id | All users | ✅ Live | React + AI chat widget |
ops.pasarseken.id | Internal staff | ✅ Live — 52 of 52 modules | React + AI staff bot |
cuan.pasarseken.id | Finance team | ✅ Live | NestJS 10 on Fastify (separate service, port :3001) |
docs.pasarseken.id | Staff + managers | ✅ Live | Docusaurus, separate Vercel project |
Routing: nginx in the frontend container proxies /api/* to the NestJS backend on :3000. Cuan's separate NestJS service runs on :3001 at cuan.pasarseken.id. There is no runtime subdomain detection in the React bundle — the SPA is the marketplace only; ops and help are separate entry points.
Repository structure
Three repositories cooperate to deliver the platform.
| Repo | Purpose | Branches |
|---|---|---|
| pasarseken | pnpm monorepo: frontend/ (Vite+React SPA), backend/ (NestJS+Fastify+Drizzle), mobile/ (Flutter, standalone) | main, develop, feature branches |
| SEKENDocs | Internal documentation, this site | main only |
| Pasarseken-AI-Brain | Strategic wiki, decisions, source of truth | main only |
Tech stack
Frontend
- Web: React 18 + Vite + TypeScript + Tailwind CSS — built as a static SPA, nginx-served in a Docker container (
frontend/) - Mobile: Flutter 3.41 standalone in
mobile/— parity with web feature set - State: TanStack Query for server state, lightweight context for UI state
- Forms: React Hook Form + Zod validation
Backend
- Runtime: Node 22 (NestJS 10 on Fastify) —
backend/ - ORM: Drizzle over
postgres.js,prepare:false(external Postgres via Supavisor session pooler) - Architecture: Domain-Driven Design — bounded contexts, application use cases, infrastructure adapters
- Storage: DigitalOcean Spaces (prefix-less keys) + imgproxy for image transforms
- Crons:
@nestjs/schedulein-process — EscrowRelease, StrikesExpiry, WithdrawalExpiry, OutboxWorker, CuanDelivery; replaces Vercel Cron and external cron jobs - Auth: Two-header system —
SekenJwtServiceissues the marketplace SEKEN JWT; cuan has its own JWT secret
Reliability layer
Three production-hardened patterns ship on every transaction-touching path:
| Pattern | What it does | Where |
|---|---|---|
| IdempotencyGuard | Prevents duplicate orders, payments, webhooks | Order create, Xendit webhook, Biteship webhook |
| OutboxProcessor | Transactional outbox (in-process @nestjs/schedule) — guarantees delivery of side effects | Notifications, fin_audit_log, downstream cuan events |
| CircuitBreaker | Wraps every external API call — fails fast when partner is down | Xendit, Biteship, Resend, OpenAI |
Auth pattern
Every request to the backend carries two headers:
Authorization: Bearer <anon JWT>
X-User-Token: <SEKEN JWT (issued by SekenJwtService)>
The anon JWT satisfies external Postgres RLS. The X-User-Token carries the actual SEKEN user identity, role, and account state. The NestJS auth guard verifies both before any business logic runs. Cuan's backend uses its own SEKEN JWT secret — cuan tokens are not valid on the marketplace backend and vice versa.
External integrations
| Provider | Purpose | Status | Reliability |
|---|---|---|---|
| Xendit | Payments — QRIS, BCA VA, bank transfer | ✅ Live | HMAC webhook verification, idempotency keys, amount mismatch detection |
| Biteship | Shipping aggregation | ✅ Live | Server-side rate quotes, label generation, delivery webhooks |
| Resend | Transactional email | ✅ Live | Templates in email_templates table, fallback queue |
| OpenAI gpt-4o-mini | KTP OCR + ops AI bot | ✅ Live | PII redaction layer, rate limiting, cost monitoring |
| Xero | Accounting sync | 🟡 Placeholder | Boolean flag only — real API call not yet implemented |
| Meta Pixel | Marketing analytics | ✅ Live | Standard ecommerce events |
See Integrations for technical detail on each.
Deployment
| Target | Deploy target | Branch | Image tag | Trigger |
|---|---|---|---|---|
| pasarseken.id, ops, help | Dokploy VPS (Compose + Traefik) | main | :prod-latest | CI build → DOCR push → Dokploy API redeploy |
| Staging (dev.pasarseken.id) | Dokploy VPS | develop | :latest | CI build → DOCR push → Dokploy API redeploy |
| cuan.pasarseken.id | Dokploy VPS (separate service) | main (cuan repo) | :prod-latest | Same CI+Dokploy pattern |
| docs.pasarseken.id | Vercel (separate project) | main | n/a | Auto-deploy on merge |
Branch discipline
Never commit directly to
main. Every change goes throughdevelopfirst.
Standard flow:
feature/X → develop → main
Every PR to main requires the change to soak on develop first.
Where to look in the codebase
| What you want | Where it lives |
|---|---|
| Frontend SPA entry | frontend/src/main.tsx |
| NestJS backend entry | backend/src/main.ts |
| Order state machine | backend/src/domain/order/ |
| External integrations (adapters) | backend/src/infrastructure/adapters/ |
| Reliability patterns | backend/src/infrastructure/reliability/ |
| Cron schedulers | backend/src/infrastructure/crons/ |
| DB schema migrations | supabase/migrations/*.sql |
| nginx proxy config | frontend/nginx.conf |