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:
./swagger.jsβ generates Swagger output then bootsserver.js(the API)../schedule/index.jsβ boots only cron jobs.
The frontend is not behind PM2; it is a static SPA bundle deployed on Cloudflare (see deployment.md).
Process startup (API)
- Load env (
process.env.TIMEZONE) and setmoment-timezoneglobal default. - Configure
log4js(access / error / sql / schedule daily-rotated files inlogs/node/). - Override
console.log/console.errorto write throughlog4jsafter runningsanitizeAccessLogArgs(seeapp/utils/log-sanitizer.js). - Import and mount ~90 route files under
/api/...β full list: backend.md Β§ Routes. - Mount Swagger UI at
/doc. - Mount
express-jwtmiddleware globally after/api/loginand a few public referral endpoints; everything else requiresBearerJWT. - Mount
notFoundHandlerthenerrorHandlerlast. - Listen on
process.env.PORT. - Handle
SIGTERM/SIGINTby closing both DB pools (general + transaction) βgracefulShutdowninserver.jslines 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
- Logs:
log4jswrites daily-rotated files (yyyy-MM-dd.log) foraccess,error,sql,schedule. Retention = 365 days. Console is also teeβd. - SQL log:
app/utils/sql-logger.jsplus aconnection.querypatch inapp/models/db.jslog every query, mutation outcomes, and errors with duration. - Swagger at
/docfor the API (autogen output checked in asswagger-output.json). - No Sentry / Datadog / New Relic integration in the code.
Key cross-cutting conventions
- Soft delete β every business table has
is_deleted(0/1). All list queries pass throughaddCommonConditions(middleware/commonSqlUtil.js) which appendsWHERE (table.is_deleted = 0 OR table.is_deleted IS NULL). Forgetting this on ad-hoc queries leaks deleted rows. - Audit fields β
create_account_id,created_date,last_modified_account_id,last_modified_dateon every table.decodeToken(req.headers.authorization)is used to populate these in controllers. - Number generation β
generateNo(tableName, prefix, idColumn)produces document numbers likeINV24070001(prefix + YYMM + 5-digit running). Each entity has its own/generateXxxNoendpoint. - Response envelope β
{ status: 200, message: 'success', data }. Errors:{ status, message, error, details? }β seemiddleware/errorhandler.js. - Multiple statements enabled in MySQL connection (
multipleStatements: true) so several largeINSERT INTO commission_* ...blocks can be issued at once. See commission-engine.md.
What is NOT in the architecture
- No queue / worker layer (BullMQ / SQS / RabbitMQ): cron is the only async path.
- No Redis / Memcached.
- No GraphQL / tRPC: REST only, hand-mounted in
server.js. - No ORM. Raw SQL via the callback driver, with the helpers in
middleware/commonSqlUtil.js. - No CI config in repo (no
.github/workflows/*content delivered β there is a.github/folder, see ifdependabotetc. only). - No automated tests.