πŸ› οΈ

01 β€” Architecture

Logical view

                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚  Browser (SPA, React/Vite) β”‚
                   β”‚  property-management-main  β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚  HTTPS  + JWT (Bearer)
                                 β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚  Express API (property-management-api-main)β”‚
            β”‚  ─ Auth: express-jwt + custom verifyToken  β”‚
            β”‚  ─ Validation: Joi                         β”‚
            β”‚  ─ ORM-less: callback-style mysql driver   β”‚
            β”‚  ─ Logging: log4js (access / error / sql)  β”‚
            β”‚  ─ PDFs: pdf-lib + jsPDF                   β”‚
            β”‚  ─ Mail: nodemailer                        β”‚
            β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚          β”‚             β”‚
                   β–Ό          β–Ό             β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚  MySQL   β”‚ β”‚  MinIO  β”‚ β”‚  AWS S3        β”‚
            β”‚  (RDS)   β”‚ β”‚  (file  β”‚ β”‚  (referral form β”‚
            β”‚  propertyβ”‚ β”‚   blobs)β”‚ β”‚  uploads via   β”‚
            β”‚   *_dev  β”‚ β”‚         β”‚ β”‚   aws-sdk)     β”‚
            β”‚   _copy  β”‚ β”‚         β”‚ β”‚                β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚  Schedule process (separate PM2) β”‚
            β”‚  schedule/index.js (cron)         β”‚
            β”‚   ─ daily ranking (00:00)         β”‚
            β”‚   ─ loan notifications (every 6h) β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Runtime topology (PM2)

Defined in property-management-api-main/ecosystem.config.js:

Env API process Scheduler process
production property-management-api property-management-api-schedule
dev property-management-api-dev property-management-api-schedule-dev
staging property-management-api-staging property-management-api-schedule-staging

Each apps[] entry calls one of:

The frontend is not behind PM2; it is a static SPA bundle deployed on Cloudflare (see deployment.md).

Process startup (API)

server.js:

  1. Load env (process.env.TIMEZONE) and set moment-timezone global default.
  2. Configure log4js (access / error / sql / schedule daily-rotated files in logs/node/).
  3. Override console.log/console.error to write through log4js after running sanitizeAccessLogArgs (see app/utils/log-sanitizer.js).
  4. Import and mount ~90 route files under /api/... β€” full list: backend.md Β§ Routes.
  5. Mount Swagger UI at /doc.
  6. Mount express-jwt middleware globally after /api/login and a few public referral endpoints; everything else requires Bearer JWT.
  7. Mount notFoundHandler then errorHandler last.
  8. Listen on process.env.PORT.
  9. Handle SIGTERM / SIGINT by closing both DB pools (general + transaction) β€” gracefulShutdown in server.js lines 268–294.

Request lifecycle

HTTP request
  β†’ express.json / urlencoded
  β†’ CORS (open: app.use(cors()) with no whitelist)
  β†’ /doc β†’ Swagger UI
  β†’ /api/login* (public)
  β†’ express-jwt parses Authorization: Bearer <token>
  β†’ mounted route /api/<entity>
  β†’ controller:
      β”œβ”€ Joi.validate(req.body)      ← validation
      β”œβ”€ decodeToken(req.headers.authorization)  ← user/role/agentId
      └─ Model.<op>(data, callback)  ← MySQL via app/models/db.js
            β”‚ uses middleware/commonSqlUtil.js helpers
            β”‚ (addCondition, addLikeCondition, addPageCondition,
            β”‚  addCommonConditions=is_deleted filter, queryRecord, etc.)
            β–Ό
      callback(err, result)
        β”œβ”€ on err β†’ next(err) β†’ middleware/errorhandler.js
        β”‚     converts MySQL/Joi/JWT errors β†’ AppError β†’ JSON
        └─ on success β†’ res.send({ status: 200, message: 'success', data })

JWT details: security.md. DB layer details: backend.md Β§ Database layer.

State storage

What Where
Relational data (sales, agents, commission payouts, invoices, etc.) MySQL property (prod), property_dev (dev), property_copy (staging) β€” all on the same RDS host
User session Stateless JWT (7-day expiry); no server-side session store
File uploads (general) MinIO bucket(s), config in app/config/minioClient.js
File uploads (referral form public) AWS S3 via aws-sdk
Generated PDFs Streamed back to client (no persistent storage in code path)
Logs Local files under logs/node/ (access / error / sql / schedule) β€” see app/config/log4.config.js
Frontend user state Zustand persisted to localStorage (user-storage key)

Observability

Key cross-cutting conventions

What is NOT in the architecture