Imported from widyaonelabs/widya-go (
AGENTS.md). Install upstream withnpx skills add widyaonelabs/widya-go. Copyright stays with the author.
AGENTS.md — widyaGO
What this repo is
WhatsApp-first workforce-readiness product for deskless workers. Two parts:
- Frontend (
public/): vanilla HTML/CSS/JS prototype served as static files. No build step. - Backend (
server/): Node.js + Express + TypeScript + PostgreSQL (Drizzle ORM).
Key commands
docker compose up -d # start PostgreSQL (widyago/widyago@localhost:5432/widyago)
cp .env.example .env # ALL vars required at boot (see gotchas)
npm install
npm run dev # tsx watch, Express + frontend on http://localhost:3000
npm run db:push # push schema to database
npm run db:seed # seed sample learners
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build && npm start # compile to dist/ then run
Tests
tests/unit/ and tests/integration/ are currently empty — npm test exits "no test files found". Vitest runs from stdin with no config file. When a test exists, run one file with:
npx vitest run tests/unit/foo.test.ts
Note: tests/ is excluded from tsconfig.json, so npm run typecheck never covers test files.
Architecture
Browser → Express (same-origin)
├── GET / → public/index.html (SPA, also catch-all *)
├── GET /api/health → health check
├── POST /api/auth/* → dummy auth (MVP)
├── /api/learners/* → HR: register, list workers
├── /api/learners/* → invitations (create, resend, send magic link)
├── /api/notifications/* + /api/dashboard/* → notifications + KPI summary
├── /onboarding/t/:token → magic link exchange → redirect /#session=<id>
├── /api/onboarding/* → worker: session, course, quiz attempts
└── /api/webhooks/* → Kirim.dev webhook (raw body, HMAC verified)
Magic link flow: GET /onboarding/t/:token exchanges the token server-side, then 302-redirects to /#session=<sessionId>; the frontend then calls GET /api/onboarding/session?sessionId=....
Directory layout
server/
├── app.ts Express app setup, middleware, routes
├── server.ts Entry point
├── config/env.ts Zod-validated environment
├── db/
│ ├── schema.ts Drizzle ORM schema (all tables)
│ ├── client.ts PostgreSQL pool + Drizzle instance
│ ├── seed.ts Sample data
│ └── migrations/ SQL migrations (db:generate output)
├── middleware/ auth, validate, rate-limit, error-handler
├── providers/ WhatsApp provider interface + Kirim.dev impl
├── routes/ Express routers per domain
├── services/ Business logic (learner, invitation, onboarding, notification)
└── types/domain.ts Zod schemas + response envelope helpers
Backend conventions
- Env validation:
server/config/env.tsparses all env vars with Zod at startup (getEnv()caches). Every var in.env.exampleis required to boot — missingDATABASE_URL,SESSION_SECRET,TOKEN_PEPPER, orKIRIMDEV_*crashes the app even in dummy-auth mode.SESSION_SECRETis currently unused (auth is stub) but still mandatory. - ESM imports: package is
"type": "module"(moduleResolution: "bundler"). Relative imports MUST carry the.jsextension (from "../db/client.js"). Omit it and runtime import fails even though typecheck passes. - Path alias:
@/*→server/*available in tsconfig (code currently uses relative paths). - Response envelope: always
{ data, error, meta }. UsesuccessResponse()/errorResponse()fromtypes/domain.ts. - Validation: Zod schemas in
types/domain.ts, applied viavalidate()middleware. - Auth: MVP dummy auth —
authMiddlewaresetsreq.userto a fixed HR admin regardless of headers. Replace before production. - Phone normalization:
normalizePhone()inlearner.service.ts— handles08xxx→+62xxx. - Magic link token:
randomBytes(32).toString('base64url')→ stored as SHA-256 hash (with pepper). Raw token never stored. Expiry fromMAGIC_LINK_EXPIRY_HOURS(default 24h). - Webhook: mounted at
/api/webhooks/kirimdevwithexpress.raw()body parser. Signature verified via HMAC before JSON parse. Deduplication byevent_id(single check, no unique constraint). - Course content: hardcoded in
onboarding.service.tsasONBOARDING_COURSE(4 modules + questions). Seed data, not CMS. - Database: Drizzle ORM + PostgreSQL. Push schema with
npm run db:push; regenerate SQL withnpm run db:generate. Migration SQL inserver/db/migrations/.
Gotchas
- Stale root frontend copies:
index.html,app.js,styles.cssat the repo ROOT are old git-tracked duplicates that differ frompublic/. The server serves onlypublic/. Always editpublic/— treat root copies as dead. - Express version mismatch:
expressis v4 but@types/expressis v5 — typecheck is looser than runtime behavior; don't rely on types for Express APIs. - Rate limiting is in-memory (
Map<ip:path>), resets on restart — no shared store.
Frontend conventions
- Vanilla JS, no build step.
api()helper at top ofpublic/app.jswrapsfetchwithcredentials: 'include'and returnsbody.data. - Admin navigation:
.nav-item[data-page]→showPage(p). Groups via.nav-group-toggle[data-group]. - Click handling: delegated
document.addEventListener("click", ...)viadata-action. Check all listener blocks before changing interactions. - DOM updates: call
icons()after Lucide markup,mk(id, cfg)for Chart.js,openForm()/closeAllForms()for modals. - Design tokens: CSS custom properties in
:rootinstyles.css. Do not invent new tokens. - UI copy: Bahasa Indonesia with English product terms.
Product invariants
- Placement produces only a
suggested_level; level changes happen solely through human HR approval. Never add auto-promotion. - AI-generated course content is always a draft pending human review; never show autonomous publishing.
- WhatsApp delivery/read receipts are channel telemetry, never learning state.
- Derive KPIs and counts from the database, not hard-coded numbers.
- Magic link: one-time use, configurable expiry (default 24h), token hash stored (never raw).
Kirim.dev integration
- Provider interface:
server/providers/whatsapp.provider.ts— swap implementations without changing routes. Provider instance lives onapp.locals.whatsappProvider. - Template: configured via
KIRIMDEV_ONBOARDING_TEMPLATEenv var. Template approval is done in Meta Business Manager, not code. - Webhook secret:
KIRIMDEV_WEBHOOK_SECRET— HMAC-SHA256 signing. Verification uses raw body bytes, never parsed JSON. - SDK: using raw
fetch(not@kirimdev/sdk) for minimal dependencies. Can migrate later.
Documentation to consult
Before changing product flows, roles, progression, assessment, messaging, or adding pages, read these docs in the repo root:
design.md— WidyaOne design system (colors, typography, rules)disprz-inspired-prd.md— product requirementswidyaGo-business-schema.md— data model and business ruleswidyago-product.md— product overview and positioningwidyago-express-backend-plan.md— full backend implementation planclarifications.md— decisions made before implementation