Phase 5 — Technical Debt & Modernization Register
⚠️ SUPERSEDED / ถูกแทนที่แล้ว — This audit snapshot is not authoritative.
อย่าใช้ตัดสินใจ operational โดยไม่ verify โค้ด / Do not use for operational decisions without verifying code.Authoritative docs: VitePress volumes (
docs/volume-00…docs/volume-16) ·docs/audits/docs-verify-2026-07-11-delta-vol2-pass.md·docs/volume-17-appendices/08-changelog-errata.mdหมายเหตุ: เนื้อหาด้านล่างเป็น snapshot เก่า — รายการ debt ที่ authoritative คือ Volume 14; อย่าอ้าง package บน cu-central จากตารางด้านล่างโดยไม่เปิด
package.jsonตรวจ (เช่นhtml-pdf/firebase-adminอยู่ที่ vtrc-api เท่านั้น)
i3 Gateway Knowledge Base · VTRC Platform Consolidated debt register spanning all five sub-repos. Every entry is evidence-based (file:line) and mapped to the i3 Gateway modernization target: Go (Hexagonal / DDD) for backends · Nuxt 3 (Feature-Sliced Design) for frontends. Severity: Critical = active security/data-loss/availability risk · High = correctness/maintainability hazard under load · Med = friction, hygiene, velocity drag.
5.1 Debt Register (Master Table)
| # | Component | Vulnerability / Issue | Severity | Evidence | i3 Gateway Modernization Strategy (stack focus) |
|---|---|---|---|---|---|
| SEC-1 | vtrc-api/api/src/routes.js PDF endpoints | Command injection via execSync(qpdf --encrypt ${password} ...). password derives from identityCard (user-controlled national ID). A crafted identityCard with shell metachars ("; rm -rf / #) executes as the Node process. | Critical | routes.js:73,120,210,260,312 — let cmd = \qpdf --encrypt ${allData.passwordPDF} ...`; execSync(cmd)` | Go document-renderer service (Hex/DDD outbound port). Use os/exec with arg slicing (never a shell string). Or outsource to a headless-Chrome + pdfcpu sidecar. Eliminate execSync entirely. |
| SEC-2 | vtrc-api/api/src/index.js:68-69 | GraphQL playground: true + introspection: true in production (not gated by IS_PRODUCTION). Public schema exposure + interactive query UI on prod endpoint. | Critical | index.js:68-69 — unconditional playground: true, introspection: true | In Go target, introspection is opt-in per environment via config. Playground disabled in prod. For interim Node: gate on IS_PRODUCTION. |
| SEC-3 | vtrc-api/api/src/config.js:41-148 | Production device keys committed in cleartext (7 prod + 10 UAT UUIDs). Anyone with repo read = full app impersonation. Keys are plain UUIDs sent as apiKey header on every request; never rotated; looked up by linear filter. | Critical | config.js:42-85 (prod), 86-148 (UAT); lookup at index.js:34 | Move to secret manager (Vault / GCP SM). Keys are Argon2id-hashed in DB, compared in constant time. App identity travels as a claim in a signed JWT, not a static header. Rotate immediately. |
| SEC-4 | vtrc-api/api/src/config.js:12 | JWT_SECRET defaults to 'secret' — if env is unset in any deployment, all JWTs are signed with a publicly known secret = total auth bypass. | Critical | config.js:12 — JWT_SECRET = 'secret' | Hard-fail on missing env at boot. Use golang-jwt/jwt/v5 with a key loaded from KMS; refuse to start without it. |
| SEC-5 | centralize-api/src/main.ts:31-35 | CORS origin: '*' + allowedHeaders: '*' on the HR-data service. Any website can call the central HR API cross-origin. | Critical | main.ts:31-35 | Explicit allow-list of origins (the two frontends' canonical domains). In Go, cors middleware with whitelist. |
| SEC-6 | vtrc-api/api/src/connector.js (implicit) + centralize-api/src/app.module.ts:36-38,55-57 | trustServerCertificate: true on MSSQL + DB credentials in env. MITM risk on internal network; DB traffic unverified. | High | app.module.ts:36-38,55-57 | Real TLS certs from internal CA; trustServerCertificate: false. |
| SEC-7 | cu-central-api/main-api/package.json:20 + lib/httpRequest.js:1 | axois typosquat declared; code imports axios which is absent from node_modules. httpRequest.js cannot run as written. Either dead code that crashes when invoked, or build is silently using a different resolution. | High | package.json:20, httpRequest.js:1,19 | Remove axois. Add axios@^1.x. In Go rewrite this becomes a stdlib net/http client (no dep needed). |
| SEC-8 | vtrc-api/api/src/routes.js:51,101,... | PII in URL paths (identityCard, empCode as :pid/:empcode). Lands in nginx access logs, browser history, CDN logs, referrer headers. | High | routes.js:51 — :pid/:empcode/:device/:timestamp/:hash | Move identifiers to signed POST body or short-lived opaque token. Logs scrubbed of PII via structured logger. |
| SEC-9 | vtrc-rc-backoffice/package.json | Typosquat / placeholder packages (undefined-service-web, i, npm, save) + cu-central-api error-ex declared as a git URL dependency. Supply-chain hazards. | High | vtrc-rc-backoffice/package.json:46,49,67,71; cu-central-api/.../package.json:25 | Purge. Regenerate lockfile. Run npm audit --production. Replace error-ex with a versioned npm package. |
| SEC-10 | vtrc-api/api/src/index.js:84 | allowedHeaders includes 'apiKey' — fine — but no rate limiting anywhere. Login/OTP/refresh/time-attendance endpoints unprotected against brute force / scraping. | High | index.js (no express-rate-limit) | Add token-bucket rate-limit middleware (Go: ulule/limiter or Redis-backed). Per-IP and per-token limits. |
| SEC-11 | vtrc-api/vtrc-gcp-sa-key.json (per workspace rule) | GCP service-account key committed to repo. Never log, never exfiltrate. | Critical | Workspace rule citation (file present) | Rotate key immediately. Use Workload Identity / short-lived tokens. Scan git history; rewrite if leaked. |
| PERF-1 | vtrc-api/api/src/routes.js:75,116,... | execSync in request handler blocks the Node event loop. One slow PDF generation starves all other requests on that replica. Combined with 4 replicas, throughput ceiling = 4 concurrent PDFs. | Critical | routes.js:73,75 (execSync), 115,117 | PDF generation moves to an async worker (outbound port adapter) or a dedicated renderer microservice. Go: chromedp in goroutines, or Asynq task queue. |
| PERF-2 | vtrc-web/src/config/apollo.js:173-181 | Apollo fetchPolicy: 'no-cache' on all watch + query operations. Every navigation hits network; Apollo's normalized cache is fully defeated. Largest single source of frontend latency. | High | apollo.js:173-181 | Nuxt 3 useFetch + stale-while-revalidate; or @nuxtjs/apollo with cache-and-network default. |
| PERF-3 | vtrc-api/api/src/lib/repositories/session/session.js:28-36 | Session.userSession JSON scanned with Op.like on every authenticated request to find the JWT id inside the JSON blob. Full-table scan per request (no index on JSON contents). | High | session.js:31-33 — userSession: { [Op.like]: \%${jwtid}%` }` | Promote jwtId to a real indexed column. Better: store sessionId in the JWT and look up by PK. |
| PERF-4 | vtrc-api/api/src/directives/auth.js + session.js:127-153 | N+1 RBAC query on every protected field. getCurrentRoleUser queries Role, then User, then filters in JS. checkMenuAuth queries User, then RoleAccess. 3+ DB round-trips per resolver call. | High | auth.js:43-44; session.js:127-153, 50-69 | Cache the user's effective permission set in Redis (TTL = session TTL). Go: permission set loaded once into context. |
| PERF-5 | vtrc-api/api/src/lib/controllers/hrmi/hrmi.js:65,122 | Approval-chain fetch crosses service boundary synchronously (hrmiGetApproverCD) on every leave/withdraw create. If centralize-api is slow, the entire create hangs. | High | hrmi.js:65,122 | Define an ApproverService port with a timeout. Pre-cache approver ladders per org; invalidate on MasterApprover change. |
| PERF-6 | vtrc-api/api/src/lib/controllers/withdraw/hospital.js (3,179 LOC) | Monolith controller — god object mixing creation, approval, bank export, reporting, status transitions, attachments. Unreachable test coverage; every change risks collateral damage. | High | wc -l hospital.js → 3,179 | Split into welfare domain aggregates: Claim (lifecycle), ApproverChain, BankExportBatch, Report. Each < 400 LOC. |
| PERF-7 | vtrc-api/docker-compose.yml:35-46 | phpMyAdmin exposed on :8093 with PMA_HOST=vtrc-database and PMA_ABSOLUTE_URI pointed at the public API host. DB admin surface publicly reachable. | Critical | docker-compose.yml:35-46 | Remove from prod compose. DB admin via bastion + SSH only. |
| PERF-8 | vtrc-api/docker-compose.yml:14-34 | Nginx network_mode: host — no port isolation; container sees host network. | High | docker-compose.yml:34 | Bridge network + explicit port mappings. |
| CORR-1 | vtrc-api/api/src/connector.js (cross-schema) | vtrc-api writes to two MariaDB schemas (db + db2) with no transactional boundary between them. Partial writes possible on failure. | Critical | connector.js:8-48; models/centralize/user writes to db2 | If cross-schema writes are unavoidable, use XA / 2-phase commit or saga pattern. In Go rewrite, each bounded context owns one schema; cross-context = event-driven. |
| CORR-2 | vtrc-api/api/src/lib/repositories/session/session.js:79-88,104-118 | Transaction swallowed. Models.db.transaction().then(...) — if Models.Session.create rejects, .catch runs t.rollback() then responError() (which throws), but the outer .then still resolves and t.commit() may run on a rolled-back txn. Race condition. | High | session.js:79-88 — create(); same pattern in update(), destroySession() | Use await + try/catch/finally, not .then. Go: Tx with deferred rollback; explicit commit. |
| CORR-3 | vtrc-api/api/src/lib/responErrors.js:3-29 + callers | responError() ALWAYS throws, yet callers across the codebase follow it with return / further statements (dead code). Readers may believe control returns. Worse: some try/catch blocks catch the thrown ApolloError and swallow it as "Something went wrong". | High | responErrors.js:3-29 (throw on every path); widespread responError(...); return result; | Make responError typed (returns never in TS). In Go, errors are values — no throw/catch asymmetry. |
| CORR-4 | vtrc-api/api/src/lib/repositories/session/session.js:50-69 | RBAC logic inconsistency. getCurrentRoleUser (line 130) matches Role.roleName, but checkMenuAuth (line 55) reads userResult[0].role.accessRole (a property that does not exist on the JSON role blob, which stores roleIds). RBAC may silently mis-authorize. | Critical | session.js:130 vs session.js:55 | Single source of truth for permissions. Casbin model + tests for every (role × menu × action) triple. |
| CORR-5 | centralize-api/src/app.module.ts:34,53 | logging: ['error','query'] in production — full SQL (potentially with PII) logged at query level in prod. Also perf cost. | High | app.module.ts:34,53 | Production logs = errors only. Debug queries via sampling/tracing. |
| CORR-6 | vtrc-api/api/src/index.js:104 + lib/model.js | syncModelToTable() runs on every boot — Sequelize auto-sync against production tables. A model change deployed by accident alters prod schema without migration review. | Critical | index.js:104 calls syncModelToTable() | Disable auto-sync in prod (IS_PRODUCTION). Explicit migrations only. Go: golang-migrate / goose / atlas, forward-only. |
| CORR-7 | vtrc-api/api/src/models/core/withdraw/hospital.js:14,138 + :159,189 | Duplicated column declarations in same model (coaCodeName declared twice; isJuristic declared twice). Sequelize silently keeps the last definition — the first is dead, and any consumer reading the source is misled. | High | hospital.js:14 & 138, 159 & 189 | ESLint no-dupe-keys (currently off). In Go, struct field duplication = compile error. |
| CORR-8 | vtrc-api/api/src/routes.js:79-82 | PDF deletion race: setTimeout(deletePDF, 5000) after res.sendFile. If download is slow (>5s), file is deleted mid-stream. | Med | routes.js:79-82 | Stream-then-delete via onFinish hook, not a timer. |
| CORR-9 | cu-central-api/main-api/src/lib/httpRequest.js | httpRequest.js is broken (see SEC-7). If any code path reaches it in prod, the service crashes. | High | httpRequest.js:1,19 | Fix the import. In Go rewrite, this is a typed HTTP client. |
| LEG-1 | vtrc-api/api/Dockerfile:1, cu-central-api/main-api/Dockerfile:1 | Node 12 runtime (EOL April 2022) — no security backports for ~4 years. centralize-api on Node 16 (EOL Sept 2023). | Critical | Dockerfiles | Go services eliminate the Node runtime tax entirely. Interim: Node 20 LTS minimum. |
| LEG-2 | vtrc-api/api/package.json:27 + cu-central-api/.../package.json:26 | esm shim locks both APIs to Node 12. esm unmaintained since 2019. This is the root cause of the Node-12 trap. | Critical | "esm": "^3.2.25"; start scripts node -r esm . | Convert to native ESM ("type":"module") on Node ≥14, or rewrite in Go. |
| LEG-3 | vtrc-api/api/package.json:18, cu-central-api/.../package.json:19 | apollo-server v2 EOL — replaced by @apollo/server v4. No security patches. | High | package.json:18 | Go target: gqlgen. Interim: Apollo Server v4. |
| LEG-4 | All frontend package.json | React 16 EOL + Apollo Client v2 (react-apollo) EOL + apollo-link-* EOL. The entire frontend Apollo stack was renamed/frozen in 2020. | High | react ^16, react-apollo ^3.1.5, apollo-client ^2.6.3 | Nuxt 3 + Vue 3 target. Interim: React 18 + @apollo/client v3. |
| LEG-5 | vtrc-web/package.json:21 + vtrc-rc-backoffice/package.json:21 | AntD split across major versions: web on v3 (EOL 2020), backoffice on v4 (maintenance). Two design systems in the same product family. | High | vtrc-web: "antd": "^3.26.20"; vtrc-rc-backoffice: "antd": "4.15.4" | Consolidate on one design system. Nuxt target: Nuxt UI or PrimeVue. |
| LEG-6 | vtrc-rc-backoffice/package.json:61 | react-scripts 3.4.3 (CRA) — CRA officially deprecated 2023. Ships Webpack 4, ESLint 6, Workbox 4, Babel 7.9 — all EOL. | High | "react-scripts": "3.4.3" | Migrate to Vite (interim) or Nuxt 3 (target). |
| LEG-7 | vtrc-web/package.json:106 | Webpack 4 EOL + html-webpack-plugin 4.0.0-beta.11 (beta in production). | High | "webpack": "4.42.0" | Webpack 5 (interim) → Vite → Nuxt 3. |
| LEG-8 | cu-central-api/main-api/package.json:30 | ldapjs unmaintained — fork ldapts is the active successor. CVE history. | High | "ldapjs": "^2.2.0" | Go: go-ldap/ldap/v3. Interim: ldapts. |
| LEG-9 | vtrc-api/api/package.json:41 + cu-central-api/.../package.json:42 | jsonwebtoken v8 — CVE-2022-23529 family. | High | "jsonwebtoken": "^8.5.1" | Upgrade to v9+. Go: golang-jwt/jwt/v5. |
| LEG-10 | vtrc-api/api/package.json:39,46 + cu-central-api/.../package.json:41 | html-pdf (PhantomJS, abandoned 2018) + node-qpdf (unmaintained 2017). PDF stack is end-of-life. | High | routes.js:41 require("html-pdf") | Playwright/Chromium (interim), chromedp (Go target), pdfcpu for encryption. |
| LEG-11 | vtrc-api/api/package.json:40,45 | mariadb v2 + mysql2 v2 both declared — ambiguous driver strategy. | Med | "mariadb": "^2.4.2", "mysql2": "^2.1.0" | Single driver (v3). Go: go-sql-driver/mysql. |
| LEG-12 | vtrc-api/api/package.json:50, cu-central-api/.../package.json:37 | redis v3 (callback API, CVE history). | High | "redis": "^3.1.2" | Upgrade to v4. Go: redis/go-redis/v9. |
| LEG-13 | vtrc-api/api/package.json:34,51,52,53 | s, stream, findit — never-imported / Node-core-shadowing packages. Supply-chain noise. | Med | package.json:52 (s), 55 (stream), 20 (findit); grep confirms 0 imports | Purge. |
| LEG-14 | vtrc-web (57 files) + vtrc-rc-backoffice (74 files) | 131 _bk/_copy/_removed/_pentest/_bak files committed to source. Massive noise, review burden, accidental-import risk. | Med | find ... -name "*_bk*" counts | Move to git history. Enforce .gitignore + pre-commit hook. |
| LEG-15 | centralize-api/ (no lockfile) | No yarn.lock committed, yet Dockerfile runs yarn install. Non-reproducible builds. | High | ls centralize-api/yarn.lock → not found | Commit lockfile. CI fails on drift. |
| LEG-16 | vtrc-web/ (both lockfiles) | Both yarn.lock and package-lock.json committed. Dual-resolver state. | Med | Both present | Pick one (yarn classic). Delete the other. |
| LEG-17 | centralize-api/tsconfig.json:15-19 | TypeScript safety disabled: strictNullChecks: false, noImplicitAny: false, forceConsistentCasingInFileNames: false. TS's safety net is off. | High | tsconfig.json:15-19 | Re-enable strict mode incrementally. Go is strict by default. |
| LEG-18 | All package.json | No engines field anywhere. Any Node version can install/run. | Med | rg '"engines"' → 0 matches | Add engines.node constraints; enforce via CI. |
| LEG-19 | vtrc-api/api/src/crons.js + cu-central-api/main-api/src/cron.js | In-process node-schedule — no HA, no retry, no visibility. If the single replica that holds the job restarts mid-run, the job is lost. With 4 replicas, jobs may run 4× unless guarded. | High | crons.js (72 LOC); cron.js | Distributed scheduler: BullMQ + Redis. Go: asynq or robfig/cron with distributed lock. |
| LEG-20 | vtrc-rc-backoffice/src/config/axios-*.js | 16+ axios instances (PMS, Recruitment, Meeting, WorkForce, Payments, Benefit, AI, etc.). Each is a separate config + interceptor + token-refresh path. | Med | ls src/config/axios-*.js | One typed API client per bounded context. Nuxt 3 $fetch + useFetch composable. |
| LEG-21 | vtrc-web/src/graphql/queries.js (4,493 LOC) + vtrc-rc-backoffice/src/graphql/queries.js (4,108 LOC) + mutations.js (2,208 LOC) | Monolith GraphQL operations files defeat code-splitting, review cognition, and tree-shaking. | Med | wc -l counts | Co-locate operations with features. FSD entities/<slice>/api/. |
| LEG-22 | vtrc-rc-backoffice/src/pages/ | Role-based route duplication: LeaveStudies{,Admin,Payments,PaymentsAdmin,Scholar,ScholarAdmin} — 6 copies of near-identical pages. | Med | ls pages/LeaveStudies* | Single feature with RBAC-driven UI variants. |
| LEG-23 | vtrc-api/api/src/resolvers/version.js (3,716 LOC) | Single resolver file at 3,716 LOC — almost certainly accumulated version-history logic. | Med | wc -l resolvers/version.js | Decompose by bounded context. |
| LEG-24 | vtrc-api/api/src/routes.js (77 console.logs) | 77 console.log calls in one file in production code. No log levels, no structured output. | Med | grep -c console.log routes.js → 77 | Structured logger (Go: slog / zerolog). The I3GatewayLogger in centralize-api/libs/log is the in-house template. |
| LEG-25 | vtrc-api/api/src/models/core/leave/leave.js:59, config.js:454,484 | Typos in production ENUMs/config: PEDDING_CANCLE (should be PENDING_CANCEL), DRAF (should be DRAFT), TRANSECTION_ERROR (should be TRANSACTION_ERROR). Permanent BC burden. | Med | leave.js:59; config.js:454,484; session.js:108 | Map legacy strings to canonical enums at the ACL boundary in the Go rewrite. Never carry typos forward. |
| LEG-26 | vtrc-rc-backoffice/package.json (primereact, @dnd-kit, react-sortable-hoc, array-move, 3 PDF viewers in web) | Duplicate / competing libraries: 4 UI libs (AntD + MUI + PrimeReact + styled-components), 3 icon sets, 2 DnD stacks, 3 PDF viewers, 2 date libs. Bundle bloat + cognitive load. | Med | Per-repo tables Phase 1 | Consolidate to one choice per category. |
| LEG-27 | cu-central-api/main-api/package.json:45 | xlsx (SheetJS community) with prototype-pollution CVE history. | High | "xlsx": "^0.16.9" | Move to SheetJS CDN build or exceljs. Go: xuri/excelize. |
| LEG-28 | cu-central-api/main-api/package.json:39 | sha512crypt-node — niche, unmaintained native-ish crypto. | Med | "sha512crypt-node": "^0.1.0" | Use bcrypt or Argon2. Go: crypto/sha512 + golang.org/x/crypto. |
| LEG-29 | vtrc-api/api/package.json:31 + cu-central-api/.../package.json:34 | firebase-admin v9 + googleapis v59 — massive API drift (current v12 / v140+). | High | Version pins | Upgrade. Go: firebase.google.com/go/v4, google.golang.org/api. |
| LEG-30 | vtrc-api/api/src/lib/responErrors.js (entire file) | Error catalogue mixes TH/EN ('Session ถูกขัดจังหวะ หรือไม่ถูกต้อง'). i18n lives in error strings, not a layer. | Med | responErrors.js:41,44,52,... | i18n at the presentation layer (Nuxt @nuxtjs/i18n). Errors carry codes; clients localize. |
5.2 Debt by Domain (Heat-map)
quadrantChart
title "VTRC Technical Debt — Severity × Effort"
x-axis "Low effort (days)" --> "High effort (months)"
y-axis "Medium severity" --> "Critical severity"
quadrant-1 {"Stop-the-bleed (do now)"}
quadrant-2 {"Strategic (modernization)"}
quadrant-3 {"Quick wins"}
quadrant-4 {"Defer / batch"}
"SEC-1 execSync injection": [0.15, 0.95]
"SEC-3 prod device keys in repo": [0.1, 0.95]
"SEC-4 JWT_SECRET default": [0.05, 0.95]
"SEC-11 GCP SA key": [0.1, 0.95]
"PERF-7 phpMyAdmin exposed": [0.05, 0.9]
"SEC-2 playground/introspection prod": [0.05, 0.85]
"SEC-5 centralize CORS *": [0.05, 0.85]
"CORR-6 syncModelToTable on boot": [0.1, 0.85]
"LEG-1 Node 12 EOL": [0.9, 0.95]
"LEG-2 esm shim lock-in": [0.85, 0.9]
"LEG-3/4 Apollo v2 + React 16 EOL": [0.95, 0.9]
"PERF-1 execSync blocking loop": [0.8, 0.9]
"CORR-1 cross-schema writes": [0.85, 0.85]
"CORR-4 RBAC inconsistency": [0.6, 0.85]
"PERF-2 no-cache Apollo policy": [0.3, 0.6]
"LEG-14 _bk files (131)": [0.2, 0.3]
"LEG-18 no engines field": [0.05, 0.3]
"LEG-25 typo ENUMs": [0.4, 0.4]
"LEG-26 duplicate UI libs": [0.5, 0.4]5.3 Recommended Modernization Sequencing
The order is engineered so each phase de-risks the next, and so the platform never has a "big bang" outage window.
Wave 0 — Stop-the-Bleed (Weeks 0-2, interim Node)
Quick, surgical fixes that remove active Critical security/availability risks without changing architecture.
| Action | Debt IDs | Effort |
|---|---|---|
| Rotate GCP SA key + device keys; move to env / secret manager | SEC-3, SEC-11 | 1-2 days |
Gate playground/introspection on !IS_PRODUCTION; hard-fail on missing JWT_SECRET | SEC-2, SEC-4 | 0.5 days |
Replace execSync(qpdf) with execFile/arg-array form (interim); plan renderer service | SEC-1, PERF-1 | 2-3 days |
Lock down CORS allow-list on centralize-api | SEC-5 | 0.5 days |
| Remove phpMyAdmin from prod compose (or IP-restrict) | PERF-7 | 0.5 days |
Disable syncModelToTable() in prod (IS_PRODUCTION guard) | CORR-6 | 0.5 days |
| Purge typosquat/unused packages; regenerate lockfiles | SEC-9, LEG-13 | 1 day |
Commit centralize-api/yarn.lock; add engines.node everywhere | LEG-15, LEG-18 | 0.5 days |
Wave 1 — Stabilize (Months 1-3, interim)
| Action | Debt IDs | Effort |
|---|---|---|
Bump Node 12 → 20 LTS; remove esm shim; convert to native ESM | LEG-1, LEG-2 | 2-3 weeks |
Upgrade jsonwebtoken v9, redis v4, multer v2, axios v1 | LEG-9, LEG-12, LEG-20 | 1-2 weeks |
| Add rate limiting + structured logging + Redis session-indexed lookup | SEC-10, LEG-24, PERF-3 | 1-2 weeks |
| Fix RBAC inconsistency + add (role × menu × action) test matrix | CORR-4 | 1-2 weeks |
Frontend: Apollo cache-and-network; consolidate duplicate libs | PERF-2, LEG-26 | 1-2 weeks |
Replace ldapjs → ldapts; fix cu-central-api/httpRequest.js | LEG-8, SEC-7, CORR-9 | 1 week |
Wave 2 — Domain Extraction → Go (Months 3-12)
Strategic rewrite, one bounded context at a time, starting with the most isolated.
| Order | Bounded Context | Why first | Target |
|---|---|---|---|
| 1 | document-renderer (PDF/Excel/qpdf) | Self-contained, removes SEC-1/PERF-1 root cause, highest blast radius | Go service + chromedp + pdfcpu |
| 2 | identity (auth, session, RBAC, device keys) | Unblocks all other contexts; fixes SEC-3/4, CORR-4 | Go + Casbin + Postgres |
| 3 | payroll (slip, tax) | Read-mostly against MSSQL; clean port boundary | Go + gqlgen, MSSQL ACL adapter |
| 4 | time-attendance | High-volume read; benefits most from caching | Go + Redis |
| 5 | leave | Multi-level workflow; well-bounded | Go + saga for approval chain |
| 6 | welfare (WDHospital) | Most complex (13-state machine, 3,179-LOC god controller) | Go + state-machine package |
| 7 | provident-fund, communication, organization, shared-kernel | Remaining modules | Go |
Each context follows Hexagonal / DDD:
cmd/— entrypoints (driving adapters: GraphQL viagqlgen, REST viachi)internal/<context>/domain/— entities, value objects, domain servicesinternal/<context>/port/— use-case interfacesinternal/<context>/adapter/— db (sqlc/sqlx), redis, sftp, pdf, smtpinternal/<context>/usecase/— application services
Wave 3 — Frontend → Nuxt 3 (Feature-Sliced Design) (Months 6-15)
Begins once the identity and payroll Go services are live (so the new frontend has stable APIs to target).
apps/web/ # end-user app (replaces vtrc-web)
└── src/
├── app/ # global shell, providers
├── processes/ # routing strategies (auth, onboarding)
├── pages/ # route-level composition
├── widgets/ # composite blocks (payslip-card, leave-form)
├── features/ # user actions (request-leave, download-slip)
├── entities/ # domain slices (employee, payslip, leave, fund, attendance)
│ └── <slice>/api/ # co-located gql operations (replaces queries.js)
└── shared/ # ui-kit, api, lib, config, i18nflowchart LR
subgraph NOW["Current (legacy)"]
A1[vtrc-web React16 WC4 AntD3]
A2[vtrc-rc-backoffice React16 CRA AntD4 MUI5 Recoil 16-axios]
end
subgraph INTERIM["Interim"]
B1[vtrc-web React18 Vite Apollo3 AntD5]
B2[vtrc-rc-backoffice React18 Vite Apollo3 AntD5]
end
subgraph TARGET["Target"]
C1[apps/web Nuxt3 FSD]
C2[apps/backoffice Nuxt3 FSD]
end
A1 --> B1 --> C1
A2 --> B2 --> C2
style NOW fill:#fde2e1,stroke:#c0392b
style INTERIM fill:#fcf3cf,stroke:#b7950b
style TARGET fill:#d5f5e3,stroke:#239b565.4 Modernization Guardrails (Non-Negotiables)
To prevent the new platform from accumulating the same debt:
| # | Guardrail | Prevents |
|---|---|---|
| G1 | No console.log in production code; structured logger only (slog/zerolog) | LEG-24 |
| G2 | No execSync / shell-out in request handlers; arg-array form only if unavoidable | SEC-1, PERF-1 |
| G3 | No secrets in source; secret-manager or KMS-enforced | SEC-3, SEC-4, SEC-11 |
| G4 | engines.node/go.mod go version enforced; CI fails on mismatch | LEG-18 |
| G5 | Lockfile committed; CI fails on drift | LEG-15 |
| G6 | Strict types: Go strict-by-default; TS strict: true for any surviving TS | LEG-17 |
| G7 | Migrations forward-only; no ORM auto-sync in prod | CORR-6 |
| G8 | One design system, one icon set, one DnD lib, one date lib per app | LEG-26 |
| G9 | No _bk/_copy files; .gitignore + pre-commit hook | LEG-14 |
| G10 | PII never in URLs | SEC-8 |
| G11 | CORS allow-list explicit; never '*' | SEC-5 |
| G12 | Rate limit on every public endpoint | SEC-10 |
| G13 | Casbin policy tests for every (role × menu × action) | CORR-4 |
| G14 | Bounded contexts < 400 LOC per file; god-files fail CI | PERF-6, LEG-21, LEG-23 |
| G15 | i18n at presentation layer, never in error strings | LEG-30 |
5.5 System Health Scorecard (Final)
| Dimension | Current State | After Wave 0-1 | After Wave 2-3 (target) |
|---|---|---|---|
| Security posture | 🔴 5 Critical (SEC-1,3,4,11 + PERF-7) | 🟡 Criticals closed | 🟢 Casbin + secret manager + no PII in URLs |
| Availability / HA | 🔴 In-process cron, single centralize-api, no rate limit | 🟡 BullMQ + replicas | 🟢 Asynq + distributed lock + autoscaling |
| Correctness | 🔴 RBAC bug (CORR-4), txn swallow (CORR-2), auto-sync (CORR-6) | 🟡 RBAC tested, txn fixed | 🟢 Strict types + saga + migrations |
| Performance | 🔴 execSync + no-cache + N+1 RBAC + JSON-scan session | 🟡 Renderer service + cache + indexed lookup | 🟢 Concurrent Go + Redis + cache-and-network |
| Maintainability | 🔴 3,179-LOC controllers, 4.5K-LOC queries.js, 131 _bk files, 4 UI libs | 🟡 Cleanup + consolidation | 🟢 FSD + bounded contexts + < 400 LOC/file |
| Reproducibility | 🔴 No engines, dual lockfile, no centralize lockfile, git-URL dep | 🟡 Engines + single lockfile | 🟢 Go modules + Docker pin + CI drift check |
| Modernization alignment | 🔴 Node 12 + Apollo 2 + React 16 + WC4 | 🟡 Node 20 + Apollo 4 + React 18 | 🟢 Go (Hex/DDD) + Nuxt 3 (FSD) |
Phase 5 complete. The i3 Gateway Knowledge Base for VTRC is now generated across docs/01 through docs/05. Each phase is evidence-anchored and maps directly to the Go (Hexagonal/DDD) + Nuxt 3 (Feature-Sliced Design) modernization target.