Skip to main content

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.

SubdomainAudienceStatusStack
pasarseken.idBuyers + sellers✅ LiveVite + React (nginx), NestJS backend, Flutter mobile
help.pasarseken.idAll users✅ LiveReact + AI chat widget
ops.pasarseken.idInternal staff✅ Live — 52 of 52 modulesReact + AI staff bot
cuan.pasarseken.idFinance team✅ LiveNestJS 10 on Fastify (separate service, port :3001)
docs.pasarseken.idStaff + managers✅ LiveDocusaurus, 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.

RepoPurposeBranches
pasarsekenpnpm monorepo: frontend/ (Vite+React SPA), backend/ (NestJS+Fastify+Drizzle), mobile/ (Flutter, standalone)main, develop, feature branches
SEKENDocsInternal documentation, this sitemain only
Pasarseken-AI-BrainStrategic wiki, decisions, source of truthmain 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/schedule in-process — EscrowRelease, StrikesExpiry, WithdrawalExpiry, OutboxWorker, CuanDelivery; replaces Vercel Cron and external cron jobs
  • Auth: Two-header system — SekenJwtService issues the marketplace SEKEN JWT; cuan has its own JWT secret

Reliability layer

Three production-hardened patterns ship on every transaction-touching path:

PatternWhat it doesWhere
IdempotencyGuardPrevents duplicate orders, payments, webhooksOrder create, Xendit webhook, Biteship webhook
OutboxProcessorTransactional outbox (in-process @nestjs/schedule) — guarantees delivery of side effectsNotifications, fin_audit_log, downstream cuan events
CircuitBreakerWraps every external API call — fails fast when partner is downXendit, 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

ProviderPurposeStatusReliability
XenditPayments — QRIS, BCA VA, bank transfer✅ LiveHMAC webhook verification, idempotency keys, amount mismatch detection
BiteshipShipping aggregation✅ LiveServer-side rate quotes, label generation, delivery webhooks
ResendTransactional email✅ LiveTemplates in email_templates table, fallback queue
OpenAI gpt-4o-miniKTP OCR + ops AI bot✅ LivePII redaction layer, rate limiting, cost monitoring
XeroAccounting sync🟡 PlaceholderBoolean flag only — real API call not yet implemented
Meta PixelMarketing analytics✅ LiveStandard ecommerce events

See Integrations for technical detail on each.

Deployment

TargetDeploy targetBranchImage tagTrigger
pasarseken.id, ops, helpDokploy VPS (Compose + Traefik)main:prod-latestCI build → DOCR push → Dokploy API redeploy
Staging (dev.pasarseken.id)Dokploy VPSdevelop:latestCI build → DOCR push → Dokploy API redeploy
cuan.pasarseken.idDokploy VPS (separate service)main (cuan repo):prod-latestSame CI+Dokploy pattern
docs.pasarseken.idVercel (separate project)mainn/aAuto-deploy on merge

Branch discipline

Never commit directly to main. Every change goes through develop first.

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 wantWhere it lives
Frontend SPA entryfrontend/src/main.tsx
NestJS backend entrybackend/src/main.ts
Order state machinebackend/src/domain/order/
External integrations (adapters)backend/src/infrastructure/adapters/
Reliability patternsbackend/src/infrastructure/reliability/
Cron schedulersbackend/src/infrastructure/crons/
DB schema migrationssupabase/migrations/*.sql
nginx proxy configfrontend/nginx.conf

Next: User Roles & Permissions →