Skip to content

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-00docs/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)

#ComponentVulnerability / IssueSeverityEvidencei3 Gateway Modernization Strategy (stack focus)
SEC-1vtrc-api/api/src/routes.js PDF endpointsCommand 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.Criticalroutes.js:73,120,210,260,312let 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-2vtrc-api/api/src/index.js:68-69GraphQL playground: true + introspection: true in production (not gated by IS_PRODUCTION). Public schema exposure + interactive query UI on prod endpoint.Criticalindex.js:68-69 — unconditional playground: true, introspection: trueIn Go target, introspection is opt-in per environment via config. Playground disabled in prod. For interim Node: gate on IS_PRODUCTION.
SEC-3vtrc-api/api/src/config.js:41-148Production 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.Criticalconfig.js:42-85 (prod), 86-148 (UAT); lookup at index.js:34Move 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-4vtrc-api/api/src/config.js:12JWT_SECRET defaults to 'secret' — if env is unset in any deployment, all JWTs are signed with a publicly known secret = total auth bypass.Criticalconfig.js:12JWT_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-5centralize-api/src/main.ts:31-35CORS origin: '*' + allowedHeaders: '*' on the HR-data service. Any website can call the central HR API cross-origin.Criticalmain.ts:31-35Explicit allow-list of origins (the two frontends' canonical domains). In Go, cors middleware with whitelist.
SEC-6vtrc-api/api/src/connector.js (implicit) + centralize-api/src/app.module.ts:36-38,55-57trustServerCertificate: true on MSSQL + DB credentials in env. MITM risk on internal network; DB traffic unverified.Highapp.module.ts:36-38,55-57Real TLS certs from internal CA; trustServerCertificate: false.
SEC-7cu-central-api/main-api/package.json:20 + lib/httpRequest.js:1axois 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.Highpackage.json:20, httpRequest.js:1,19Remove axois. Add axios@^1.x. In Go rewrite this becomes a stdlib net/http client (no dep needed).
SEC-8vtrc-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.Highroutes.js:51:pid/:empcode/:device/:timestamp/:hashMove identifiers to signed POST body or short-lived opaque token. Logs scrubbed of PII via structured logger.
SEC-9vtrc-rc-backoffice/package.jsonTyposquat / placeholder packages (undefined-service-web, i, npm, save) + cu-central-api error-ex declared as a git URL dependency. Supply-chain hazards.Highvtrc-rc-backoffice/package.json:46,49,67,71; cu-central-api/.../package.json:25Purge. Regenerate lockfile. Run npm audit --production. Replace error-ex with a versioned npm package.
SEC-10vtrc-api/api/src/index.js:84allowedHeaders includes 'apiKey' — fine — but no rate limiting anywhere. Login/OTP/refresh/time-attendance endpoints unprotected against brute force / scraping.Highindex.js (no express-rate-limit)Add token-bucket rate-limit middleware (Go: ulule/limiter or Redis-backed). Per-IP and per-token limits.
SEC-11vtrc-api/vtrc-gcp-sa-key.json (per workspace rule)GCP service-account key committed to repo. Never log, never exfiltrate.CriticalWorkspace rule citation (file present)Rotate key immediately. Use Workload Identity / short-lived tokens. Scan git history; rewrite if leaked.
PERF-1vtrc-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.Criticalroutes.js:73,75 (execSync), 115,117PDF generation moves to an async worker (outbound port adapter) or a dedicated renderer microservice. Go: chromedp in goroutines, or Asynq task queue.
PERF-2vtrc-web/src/config/apollo.js:173-181Apollo 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.Highapollo.js:173-181Nuxt 3 useFetch + stale-while-revalidate; or @nuxtjs/apollo with cache-and-network default.
PERF-3vtrc-api/api/src/lib/repositories/session/session.js:28-36Session.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).Highsession.js:31-33userSession: { [Op.like]: \%${jwtid}%` }`Promote jwtId to a real indexed column. Better: store sessionId in the JWT and look up by PK.
PERF-4vtrc-api/api/src/directives/auth.js + session.js:127-153N+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.Highauth.js:43-44; session.js:127-153, 50-69Cache the user's effective permission set in Redis (TTL = session TTL). Go: permission set loaded once into context.
PERF-5vtrc-api/api/src/lib/controllers/hrmi/hrmi.js:65,122Approval-chain fetch crosses service boundary synchronously (hrmiGetApproverCD) on every leave/withdraw create. If centralize-api is slow, the entire create hangs.Highhrmi.js:65,122Define an ApproverService port with a timeout. Pre-cache approver ladders per org; invalidate on MasterApprover change.
PERF-6vtrc-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.Highwc -l hospital.js → 3,179Split into welfare domain aggregates: Claim (lifecycle), ApproverChain, BankExportBatch, Report. Each < 400 LOC.
PERF-7vtrc-api/docker-compose.yml:35-46phpMyAdmin exposed on :8093 with PMA_HOST=vtrc-database and PMA_ABSOLUTE_URI pointed at the public API host. DB admin surface publicly reachable.Criticaldocker-compose.yml:35-46Remove from prod compose. DB admin via bastion + SSH only.
PERF-8vtrc-api/docker-compose.yml:14-34Nginx network_mode: host — no port isolation; container sees host network.Highdocker-compose.yml:34Bridge network + explicit port mappings.
CORR-1vtrc-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.Criticalconnector.js:8-48; models/centralize/user writes to db2If 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-2vtrc-api/api/src/lib/repositories/session/session.js:79-88,104-118Transaction 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.Highsession.js:79-88create(); same pattern in update(), destroySession()Use await + try/catch/finally, not .then. Go: Tx with deferred rollback; explicit commit.
CORR-3vtrc-api/api/src/lib/responErrors.js:3-29 + callersresponError() 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".HighresponErrors.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-4vtrc-api/api/src/lib/repositories/session/session.js:50-69RBAC 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.Criticalsession.js:130 vs session.js:55Single source of truth for permissions. Casbin model + tests for every (role × menu × action) triple.
CORR-5centralize-api/src/app.module.ts:34,53logging: ['error','query'] in production — full SQL (potentially with PII) logged at query level in prod. Also perf cost.Highapp.module.ts:34,53Production logs = errors only. Debug queries via sampling/tracing.
CORR-6vtrc-api/api/src/index.js:104 + lib/model.jssyncModelToTable() runs on every boot — Sequelize auto-sync against production tables. A model change deployed by accident alters prod schema without migration review.Criticalindex.js:104 calls syncModelToTable()Disable auto-sync in prod (IS_PRODUCTION). Explicit migrations only. Go: golang-migrate / goose / atlas, forward-only.
CORR-7vtrc-api/api/src/models/core/withdraw/hospital.js:14,138 + :159,189Duplicated 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.Highhospital.js:14 & 138, 159 & 189ESLint no-dupe-keys (currently off). In Go, struct field duplication = compile error.
CORR-8vtrc-api/api/src/routes.js:79-82PDF deletion race: setTimeout(deletePDF, 5000) after res.sendFile. If download is slow (>5s), file is deleted mid-stream.Medroutes.js:79-82Stream-then-delete via onFinish hook, not a timer.
CORR-9cu-central-api/main-api/src/lib/httpRequest.jshttpRequest.js is broken (see SEC-7). If any code path reaches it in prod, the service crashes.HighhttpRequest.js:1,19Fix the import. In Go rewrite, this is a typed HTTP client.
LEG-1vtrc-api/api/Dockerfile:1, cu-central-api/main-api/Dockerfile:1Node 12 runtime (EOL April 2022) — no security backports for ~4 years. centralize-api on Node 16 (EOL Sept 2023).CriticalDockerfilesGo services eliminate the Node runtime tax entirely. Interim: Node 20 LTS minimum.
LEG-2vtrc-api/api/package.json:27 + cu-central-api/.../package.json:26esm 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-3vtrc-api/api/package.json:18, cu-central-api/.../package.json:19apollo-server v2 EOL — replaced by @apollo/server v4. No security patches.Highpackage.json:18Go target: gqlgen. Interim: Apollo Server v4.
LEG-4All frontend package.jsonReact 16 EOL + Apollo Client v2 (react-apollo) EOL + apollo-link-* EOL. The entire frontend Apollo stack was renamed/frozen in 2020.Highreact ^16, react-apollo ^3.1.5, apollo-client ^2.6.3Nuxt 3 + Vue 3 target. Interim: React 18 + @apollo/client v3.
LEG-5vtrc-web/package.json:21 + vtrc-rc-backoffice/package.json:21AntD split across major versions: web on v3 (EOL 2020), backoffice on v4 (maintenance). Two design systems in the same product family.Highvtrc-web: "antd": "^3.26.20"; vtrc-rc-backoffice: "antd": "4.15.4"Consolidate on one design system. Nuxt target: Nuxt UI or PrimeVue.
LEG-6vtrc-rc-backoffice/package.json:61react-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-7vtrc-web/package.json:106Webpack 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-8cu-central-api/main-api/package.json:30ldapjs unmaintained — fork ldapts is the active successor. CVE history.High"ldapjs": "^2.2.0"Go: go-ldap/ldap/v3. Interim: ldapts.
LEG-9vtrc-api/api/package.json:41 + cu-central-api/.../package.json:42jsonwebtoken v8 — CVE-2022-23529 family.High"jsonwebtoken": "^8.5.1"Upgrade to v9+. Go: golang-jwt/jwt/v5.
LEG-10vtrc-api/api/package.json:39,46 + cu-central-api/.../package.json:41html-pdf (PhantomJS, abandoned 2018) + node-qpdf (unmaintained 2017). PDF stack is end-of-life.Highroutes.js:41 require("html-pdf")Playwright/Chromium (interim), chromedp (Go target), pdfcpu for encryption.
LEG-11vtrc-api/api/package.json:40,45mariadb 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-12vtrc-api/api/package.json:50, cu-central-api/.../package.json:37redis v3 (callback API, CVE history).High"redis": "^3.1.2"Upgrade to v4. Go: redis/go-redis/v9.
LEG-13vtrc-api/api/package.json:34,51,52,53s, stream, findit — never-imported / Node-core-shadowing packages. Supply-chain noise.Medpackage.json:52 (s), 55 (stream), 20 (findit); grep confirms 0 importsPurge.
LEG-14vtrc-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.Medfind ... -name "*_bk*" countsMove to git history. Enforce .gitignore + pre-commit hook.
LEG-15centralize-api/ (no lockfile)No yarn.lock committed, yet Dockerfile runs yarn install. Non-reproducible builds.Highls centralize-api/yarn.lock → not foundCommit lockfile. CI fails on drift.
LEG-16vtrc-web/ (both lockfiles)Both yarn.lock and package-lock.json committed. Dual-resolver state.MedBoth presentPick one (yarn classic). Delete the other.
LEG-17centralize-api/tsconfig.json:15-19TypeScript safety disabled: strictNullChecks: false, noImplicitAny: false, forceConsistentCasingInFileNames: false. TS's safety net is off.Hightsconfig.json:15-19Re-enable strict mode incrementally. Go is strict by default.
LEG-18All package.jsonNo engines field anywhere. Any Node version can install/run.Medrg '"engines"' → 0 matchesAdd engines.node constraints; enforce via CI.
LEG-19vtrc-api/api/src/crons.js + cu-central-api/main-api/src/cron.jsIn-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.Highcrons.js (72 LOC); cron.jsDistributed scheduler: BullMQ + Redis. Go: asynq or robfig/cron with distributed lock.
LEG-20vtrc-rc-backoffice/src/config/axios-*.js16+ axios instances (PMS, Recruitment, Meeting, WorkForce, Payments, Benefit, AI, etc.). Each is a separate config + interceptor + token-refresh path.Medls src/config/axios-*.jsOne typed API client per bounded context. Nuxt 3 $fetch + useFetch composable.
LEG-21vtrc-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.Medwc -l countsCo-locate operations with features. FSD entities/<slice>/api/.
LEG-22vtrc-rc-backoffice/src/pages/Role-based route duplication: LeaveStudies{,Admin,Payments,PaymentsAdmin,Scholar,ScholarAdmin} — 6 copies of near-identical pages.Medls pages/LeaveStudies*Single feature with RBAC-driven UI variants.
LEG-23vtrc-api/api/src/resolvers/version.js (3,716 LOC)Single resolver file at 3,716 LOC — almost certainly accumulated version-history logic.Medwc -l resolvers/version.jsDecompose by bounded context.
LEG-24vtrc-api/api/src/routes.js (77 console.logs)77 console.log calls in one file in production code. No log levels, no structured output.Medgrep -c console.log routes.js → 77Structured logger (Go: slog / zerolog). The I3GatewayLogger in centralize-api/libs/log is the in-house template.
LEG-25vtrc-api/api/src/models/core/leave/leave.js:59, config.js:454,484Typos in production ENUMs/config: PEDDING_CANCLE (should be PENDING_CANCEL), DRAF (should be DRAFT), TRANSECTION_ERROR (should be TRANSACTION_ERROR). Permanent BC burden.Medleave.js:59; config.js:454,484; session.js:108Map legacy strings to canonical enums at the ACL boundary in the Go rewrite. Never carry typos forward.
LEG-26vtrc-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.MedPer-repo tables Phase 1Consolidate to one choice per category.
LEG-27cu-central-api/main-api/package.json:45xlsx (SheetJS community) with prototype-pollution CVE history.High"xlsx": "^0.16.9"Move to SheetJS CDN build or exceljs. Go: xuri/excelize.
LEG-28cu-central-api/main-api/package.json:39sha512crypt-node — niche, unmaintained native-ish crypto.Med"sha512crypt-node": "^0.1.0"Use bcrypt or Argon2. Go: crypto/sha512 + golang.org/x/crypto.
LEG-29vtrc-api/api/package.json:31 + cu-central-api/.../package.json:34firebase-admin v9 + googleapis v59 — massive API drift (current v12 / v140+).HighVersion pinsUpgrade. Go: firebase.google.com/go/v4, google.golang.org/api.
LEG-30vtrc-api/api/src/lib/responErrors.js (entire file)Error catalogue mixes TH/EN ('Session ถูกขัดจังหวะ หรือไม่ถูกต้อง'). i18n lives in error strings, not a layer.MedresponErrors.js:41,44,52,...i18n at the presentation layer (Nuxt @nuxtjs/i18n). Errors carry codes; clients localize.

5.2 Debt by Domain (Heat-map)

mermaid
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]

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.

ActionDebt IDsEffort
Rotate GCP SA key + device keys; move to env / secret managerSEC-3, SEC-111-2 days
Gate playground/introspection on !IS_PRODUCTION; hard-fail on missing JWT_SECRETSEC-2, SEC-40.5 days
Replace execSync(qpdf) with execFile/arg-array form (interim); plan renderer serviceSEC-1, PERF-12-3 days
Lock down CORS allow-list on centralize-apiSEC-50.5 days
Remove phpMyAdmin from prod compose (or IP-restrict)PERF-70.5 days
Disable syncModelToTable() in prod (IS_PRODUCTION guard)CORR-60.5 days
Purge typosquat/unused packages; regenerate lockfilesSEC-9, LEG-131 day
Commit centralize-api/yarn.lock; add engines.node everywhereLEG-15, LEG-180.5 days

Wave 1 — Stabilize (Months 1-3, interim)

ActionDebt IDsEffort
Bump Node 12 → 20 LTS; remove esm shim; convert to native ESMLEG-1, LEG-22-3 weeks
Upgrade jsonwebtoken v9, redis v4, multer v2, axios v1LEG-9, LEG-12, LEG-201-2 weeks
Add rate limiting + structured logging + Redis session-indexed lookupSEC-10, LEG-24, PERF-31-2 weeks
Fix RBAC inconsistency + add (role × menu × action) test matrixCORR-41-2 weeks
Frontend: Apollo cache-and-network; consolidate duplicate libsPERF-2, LEG-261-2 weeks
Replace ldapjsldapts; fix cu-central-api/httpRequest.jsLEG-8, SEC-7, CORR-91 week

Wave 2 — Domain Extraction → Go (Months 3-12)

Strategic rewrite, one bounded context at a time, starting with the most isolated.

OrderBounded ContextWhy firstTarget
1document-renderer (PDF/Excel/qpdf)Self-contained, removes SEC-1/PERF-1 root cause, highest blast radiusGo service + chromedp + pdfcpu
2identity (auth, session, RBAC, device keys)Unblocks all other contexts; fixes SEC-3/4, CORR-4Go + Casbin + Postgres
3payroll (slip, tax)Read-mostly against MSSQL; clean port boundaryGo + gqlgen, MSSQL ACL adapter
4time-attendanceHigh-volume read; benefits most from cachingGo + Redis
5leaveMulti-level workflow; well-boundedGo + saga for approval chain
6welfare (WDHospital)Most complex (13-state machine, 3,179-LOC god controller)Go + state-machine package
7provident-fund, communication, organization, shared-kernelRemaining modulesGo

Each context follows Hexagonal / DDD:

  • cmd/ — entrypoints (driving adapters: GraphQL via gqlgen, REST via chi)
  • internal/<context>/domain/ — entities, value objects, domain services
  • internal/<context>/port/ — use-case interfaces
  • internal/<context>/adapter/ — db (sqlc/sqlx), redis, sftp, pdf, smtp
  • internal/<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, i18n
mermaid
flowchart 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:#239b56

5.4 Modernization Guardrails (Non-Negotiables)

To prevent the new platform from accumulating the same debt:

#GuardrailPrevents
G1No console.log in production code; structured logger only (slog/zerolog)LEG-24
G2No execSync / shell-out in request handlers; arg-array form only if unavoidableSEC-1, PERF-1
G3No secrets in source; secret-manager or KMS-enforcedSEC-3, SEC-4, SEC-11
G4engines.node/go.mod go version enforced; CI fails on mismatchLEG-18
G5Lockfile committed; CI fails on driftLEG-15
G6Strict types: Go strict-by-default; TS strict: true for any surviving TSLEG-17
G7Migrations forward-only; no ORM auto-sync in prodCORR-6
G8One design system, one icon set, one DnD lib, one date lib per appLEG-26
G9No _bk/_copy files; .gitignore + pre-commit hookLEG-14
G10PII never in URLsSEC-8
G11CORS allow-list explicit; never '*'SEC-5
G12Rate limit on every public endpointSEC-10
G13Casbin policy tests for every (role × menu × action)CORR-4
G14Bounded contexts < 400 LOC per file; god-files fail CIPERF-6, LEG-21, LEG-23
G15i18n at presentation layer, never in error stringsLEG-30

5.5 System Health Scorecard (Final)

DimensionCurrent StateAfter Wave 0-1After 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.