Skip to content

Architecture

High-Level Overview

The RDS App is a monolithic React + Node.js application. The Express backend serves the React frontend as static files in production, so the entire application is deployed as a single Azure App Service.

┌─────────────────────────────────────┐
│           Browser (React SPA)        │
│  Vite + TypeScript + Material-UI     │
│  Port 3000 (dev) / served by Express │
└──────────────┬──────────────────────┘
               │ HTTP API (REST)
┌─────────────────────────────────────┐
│       Express API (Node.js)          │
│  TypeScript + Mongoose + CASL        │
│  Port 1234 (dev) / process.env.PORT  │
└──────────────┬──────────────────────┘
               │ Mongoose ODM
┌─────────────────────────────────────┐
│           MongoDB                    │
│  Atlas (cloud) or Cosmos DB          │
└─────────────────────────────────────┘

Monorepo layout:

respondent-driven-sampling/
├── client/          # React SPA (Vite + TypeScript + MUI)
├── server/          # Express API (TypeScript + MongoDB)
├── docs/            # Documentation source (this site)
├── infra/           # Azure infrastructure (Pulumi + TypeScript)
├── mkdocs.yml       # MkDocs configuration
└── .github/         # CI/CD workflows, issue templates

In development, the Vite dev server runs on port 3000 and proxies API requests to Express on port 1234. In production, Express serves the compiled React dist/ directly.

Backend Architecture

The backend uses a domain-driven layered structure, distinct from typical Express apps:

server/src/
├── index.ts                    # App entry point; security middleware, route registration
├── middleware/
│   └── auth.ts                 # JWT verification, approval check, CASL ability injection
├── routes/
│   ├── auth.ts
│   ├── users.ts
│   ├── surveys.ts
│   ├── seeds.ts
│   ├── locations.ts
│   └── validateReferralCode.ts
├── database/
│   ├── user/
│   │   ├── mongoose/           # Mongoose model + hooks
│   │   ├── zod/                # Zod validation schemas
│   │   └── user.controller.ts  # Business logic
│   ├── survey/
│   ├── seed/
│   └── location/
├── permissions/                # CASL role and attribute definitions
├── scripts/                    # CLI management scripts
└── config/                     # Swagger, constants

Key pattern: Route files (routes/*.ts) call controller functions (database/{domain}/*.controller.ts). Business logic lives in controllers, not routes.

All routes are mounted under /api/* with no versioning. All routes that accept request bodies use Zod validation middleware.

Validation

All API request bodies are validated with Zod. Schema files:

  • zod/*.base.ts — Full document schema (used for Mongoose typing)
  • zod/*.validator.ts — API request validators (subset of the document schema)
// Example: route with Zod validation
router.post('/', validate(createSurveySchema), surveysController.create);

Auth Flow

1. User enters phone number →
     New user:       POST /api/auth/send-otp-signup → Twilio sends SMS OTP
     Returning user: POST /api/auth/send-otp-login  → Twilio sends SMS OTP
2. User enters OTP →
     New user:       POST /api/auth/verify-otp-signup → creates account, returns JWT
     Returning user: POST /api/auth/verify-otp-login  → returns JWT
3. Server returns JWT → Client stores in Zustand persistent store (localStorage)
4. All subsequent requests → Authorization: Bearer <JWT> header
5. Auth middleware:
   a. Verifies JWT signature (AUTH_SECRET)
   b. Checks approvalStatus === 'APPROVED' (403 if not)
   c. Fetches latest survey to derive user's current location
   d. Builds CASL Ability object and injects into req.authorization

Approval Flow

New users register with approvalStatus: PENDING. They can receive OTP codes but cannot access any protected routes until an admin or super-admin sets their status to APPROVED via the admin dashboard or the CLI.

PENDING → APPROVED   (admin approves in dashboard)
PENDING → REJECTED   (admin rejects)
APPROVED → PENDING   (admin can reset)

Permissions (CASL)

The app uses CASL for role and attribute-based access control.

Roles: SUPER_ADMIN, ADMIN, MANAGER, VOLUNTEER

Key permission conditions:

Condition Meaning
IS_CREATED_BY_SELF The resource was created by the requesting user
WAS_CREATED_TODAY The resource was created on the current calendar day
HAS_SAME_LOCATION The resource belongs to the user's current location

Example: volunteers can only update surveys they created today at their current location. Managers can read and update surveys at their location created today. Admins can update any survey created today across all locations.

Permissions are defined in server/src/permissions/abilityBuilder.ts and the same constants are imported by the frontend (client/src/hooks/useAbility.tsx) to mirror permission checks in the UI.

Deployment Architecture

Azure resources (Resource Group, MongoDB vCore cluster, App Service Plan, and Web App) are provisioned via Pulumi in the infra/ directory. See Infrastructure for setup instructions.

In production (Azure App Service):

GitHub push to deployment branch
  (kc-pit-2026 → prod, kc-pit-2026-test → test)
GitHub Actions workflow (azure-webapp-deploy-*.yml)
        ├─ npm run build (client) → client/dist/
        ├─ cp client/dist → server/dist/
        ├─ npm install (server)
        └─ Deploy server/ folder to Azure App Service
        Azure App Service (Node 22 LTS)
        node server/build/index.js
        Serves:
          /api/* → Express routes
          /*     → server/dist/ (React SPA)

Path Aliases

Both client and server use @/* imports:

  • Server: tsconfig.json + tsc-alias build step resolves @/* → src/*
  • Client: Vite resolves @/* → src/*, plus @/permissions/*../server/src/permissions/* for shared constants

This means the client can directly import server-side permission constants without duplicating them.