Imported from Zerodayu/Obelisk (
apps/backend/AGENTS.md). Install upstream withnpx skills add Zerodayu/Obelisk --skill backend. Copyright stays with the author.
Obelisk Backend — Agent Guide
Product: Obelisk — Outcomes-Based Educational Learning and Intelligent System Kit for Jose Maria College Foundation, Inc. (JMCFI).
Stack: Bun runtime · Elysia (HTTP) · Prisma + PostgreSQL (pg driver adapter, Docker db service) · better-auth · Zod validation.
This backend digitizes the JMCFI WIN-OBE forms (the paper/manual OBE assessment forms) as structured records with computed attainment fields, an approval workflow, audit trail, and program-level rollups.
File Structure
Routes follow a feature-module pattern (see https://github.com/Zerodayu/bun-elysia-app):
src/
├── index.ts # Elysia app entry point
├── routes.ts # Aggregates all feature route plugins (api/v1 prefix)
└── v1/
├── academic/ # Academic reference data (programs, terms, class sections)
├── atrisk/ # At-risk watchlist + Action-Taken Record (clears flags on final approval)
├── audit/ # Audit-trail reads (GET /audit/logs — server-scoped waterfall)
├── auth/ # Auth (better-auth config, session guard macro, role requests)
├── car/ # Course Assessment Report (F13)
├── cqi/ # CQI/ACT loop (F22, F23, F25, F24 APAR)
├── check/ # CHECK-stage supporting instruments (F08/F10/F11/F12/F17/F18/F19)
├── forms/ # Generic form submission CRUD + approval workflow
├── ingest/ # Class-record upload + ETL pipeline
├── periodic/ # Periodic/institutional forms (F09/F20/F21/F26/F27/F28/F02)
├── plan/ # PLAN-phase setup forms (F01, F03, F04, F06)
└── rollup/ # Roll-up chain (F14, F15, F16)
Conventions
- controller.ts: Define routes as an Elysia plugin with
prefixandtags. Mount auth guard via.use(authPlugin), then guard individual routes with{ auth: true }. Usedetailfor OpenAPI descriptions. - service.ts: Class-based business logic. One class per feature. Throws on errors; controller handles status codes.
- model.ts: Validation schemas using Elysia's
tfromelysia. Export both schema objects andStatic<typeof ...>types. - routes.ts: Import and chain feature plugins:
.use(featureRoutes). Theapi/v1/prefix is set here, so feature routes use relative paths. - env vars: Always
import { env } from "@utils/env"instead of accessingprocess.envdirectly —utils/env.tsis a thin shim over@obelisk/env/server(packages/env/src/server.ts), which Zod-validates at import, so missing vars fail at startup with a clear error. - caching & limits: wrap read-only GET handlers with
cached(ttlSeconds, handler)from@lib/cache(Redis-backed, falls through on miss); rate limiting applies globally (100 requests / 15 min). - shared libs (
lib/):lib/role-access.ts(role → feature allow-lists + asserts — class-record capture, archive, PLO management, role requests, cluster confirm, AI insight generation; mirrored byapps/frontend/lib/role-access.tsand drift-guarded bytest/unit/role-access-sync.test.ts),lib/forms/approval-routes.ts(per-form-code approval chains + RBAC — see Domain Rules),lib/forms/state-machine.ts(submission status transitions),lib/forms/submit-gates.ts(pre-submit validation gates),lib/forms/approval-effects.ts(per-form-code final-approval side effects, run inside thedecidetransaction — e.g.action_takenclearingAtRiskFlags; same registration pattern as submit-gates),lib/validators(domain floors/enums),lib/ingest(python-server client),lib/prisma.ts,lib/cache.ts.
Environment & deployment (monorepo)
- Env files live at the repo root:
.env.local(dev) and.env.prod(production), both dotenvx-encrypted; private keys in the gitignored root.env.keys. Every package script wrapsdotenvx run -f ../../.env.local -- …(dev/build/test) or-f ../../.env.prod(build:prod). Edit viajust env-decrypt/just env-encrypt(repo root) — there are no per-packageenv:*scripts. - Runtime env fallback:
@obelisk/env/serverreadsprocess.env.X ?? runtimeEnv.X, whereruntimeEnvis the build-time snapshot frompackages/env/src/generated/runtime-env.ts— gitignored, written bypackages/env/scripts/gen-runtime-env.tsfrom the decrypted.env.prodduringbuild:prod(canonical key list:packages/env/env-keys.ts— add new vars there and mirror theprocess.env.Xreads inpackages/env/src/server.ts). The rootpostinstallwrites an empty stub so a fresh clone typechecks before any build. - Entry point:
src/index.tsdefault-exports the Elysia app;app.listen(8080)is guarded byimport.meta.main && !process.env.VERCEL, so Vercel (zero-config Elysia detection +bunVersion: "1.x"inapps/backend/vercel.json) serves the exported app whilebun run devstill listens locally. - Deploy: self-hosted Docker on a VPS behind Caddy — from the repo root:
just docker-deploy(image build runsbun run build:prod— prisma generate + runtime-env bake). Details:../../.github/DEPLOYMENT.md.
Domain Rules (canonical — do not violate)
These are institutional rules from the OBE manual and the forms implementation reference. Enforce them in services, not the client. See SYSTEM-DESIGN.md for the authoritative mapping.
- ≥70% hard floor — every attainment target/benchmark (program, cohort, CLO, PLO, KPI) validates to at least 70%. Shared validator lives in the service layer.
- Composite attainment formula —
Direct × 70% + Indirect × 30%. Mirrors defaultdirectWeight = 0.70/indirectWeight = 0.30onComputationRun. - At-risk auto-flag — a student is at-risk if any CLO score
< 70%. Computed, never manually entered (seeCloAttainment.isBelowThreshold,AtRiskFlag). - Loop Status is computed, not a free field — the Closing-the-Loop (CTL) report may only mark a CQI loop
CLOSEDwhen all five documented conditions are met; otherwise it isOPEN - Re-assessorOPEN - Not Implemented. Do not accept a manually-forced CLOSED value. - APAR validation gate — the Annual Program Report is blocked from submission if the Cohort Tracking Sheet is not attached.
- Approval chains descend by role:
faculty → program_chair → dean → aqau → vpaa, but the exact chain per form is server-derived —lib/forms/approval-routes.tsregisters each stable form code with its preparer roles and ordered approver chain, and the forms service enforces ownership + per-step role match on submit/approve/return/archive (system_adminoverride included). Never accept client-supplied steps or RBAC decisions;FormSubmission.currentApproverRole+ApprovalStepmirror the registry's state. Two institutional rules are coded there (useascendToVpaa(entry)when adding a form): every chain runs from its manual entry point up tovpaa— no rung skipped, so VPAA always gives the final review before archive — and the VPAA never appears inpreparerRoles(top of the hierarchy: it reviews, generates the AI report, and archives; it does not originate submissions). - Role → feature gates are centralized in
lib/role-access.ts(FEATURE_ACCESS) — class-record capture (all/ingest/*endpoints:faculty/program_chair/system_admin), archive (vpaa/system_admin), PLO management (deanonly — no admin bypass), role requests (system_admin), cluster confirm (aqau/system_admin), AI insight generation (vpaa/system_admin— guardsPOST /ai/recommendation/generate, the python-server LLM call). Changing role access = edit that file and its frontend mirrorapps/frontend/lib/role-access.tsin the same change —test/unit/role-access-sync.test.tsfails CI on drift. - PLO entity mutations are dean-only — create/update/delete on
/plan/plos*throws a 403PloForbiddenErrorfor any other role (faculty map the resulting PLOs to CLOs via the curriculum map); delete is further blocked when the PLO is mapped or carries attainment/gap/CQI history. - Retention classes — forms are either
5 yearsorPermanentretention. Permanent-retention records (longitudinal/audit: cohort tracking, APAR, CTL, system gap, CAPA, institutional review) also carry a strict audit-trail requirement. - Repeated 6-category root cause (used by gap/CQI/systemic analysis):
1-Curriculum Design | 2-Instruction & Pedagogy | 3-Assessment Design | 4-Student Factors | 5-Resources & Tools | 6-Industry & Field Alignment. Model as a type/enum constant, not free text.
Form Identification
The manual references forms by ephemeral IDs (F01, F02, ... F28). These IDs are provisional and may change when the manual is revised — never rely on them in code, DB, or documentation.
Refer to forms by either:
- the form title, or
- the stable snake_case code (maps directly to
FormType.code).
Stable codes (defined in SYSTEM-DESIGN.md):
curriculum_map -> CLO-PLO Curriculum Map
portfolio_roadmap -> Portfolio Roadmap and Rubric Standards
assessment_calendar -> Assessment Calendar with Cohort Tracking Milestones
target_setting_matrix -> Target-Setting Matrix
stakeholder_consultation -> Stakeholder Consultation Records
assessment_budget -> Approved Assessment Budget
clo_raw_data -> Per-Student CLO Raw Data Sheet
mid_cycle_attainment -> Mid-Cycle CLO Attainment Summary
resource_monitoring -> Resource Acquisition and Implementation Monitoring
peer_observation -> Peer Observation Record
exhibition_feedback -> Portfolio Exhibition Industry Feedback Record
clo_perception_survey -> CLO Achievement Perception Survey Tabulation
course_assessment_report -> Course Assessment Report (CAR)
clo_attainment_summary -> CLO Attainment Summary (Full Term)
plo_attainment_summary -> PLO Attainment Summary
cohort_tracking -> Cohort CLO/PLO Attainment Tracking Sheet
student_exit_survey -> Student Exit Survey Tabulation
portfolio_assessment_record -> Portfolio Assessment Record with CLO Evidence
capstone_panel_evaluation -> Capstone/Culminating Panel Evaluation Sheet
alumni_tracer -> Alumni Tracer Study Report
employer_satisfaction_survey -> Employer Satisfaction Survey Report
plo_gap_analysis -> PLO Attainment Report with Gap Analysis Matrix
cqi_action_plan -> CQI Action Plan
annual_program_report -> Annual Program Assessment Report (APAR)
closing_the_loop -> Closing-the-Loop (CTL) Report
systemic_gap_report -> Systemic Gap Report
capa_plan -> Corrective and Preventive Action (CAPA) Plan
institutional_review -> Institutional Management Review Records
action_taken -> Action-Taken Record (At-Risk Students)
Not-yet-developed forms (portfolio/consolidation/specialty schedules; no field structure defined yet) have no stable code — represent them as titles/topics only, and do not fabricate values.
Build Priority
Backend-first strategy: the whole backend is built and stabilized before any frontend work resumes (frontend work is consolidated in ../../system-docs/roadmap.md under a deferred section). When implementing forms incrementally, follow this order (rationale in SYSTEM-DESIGN.md):
- Foundation/stabilization —
tsconfigfix (moduleResolutionbundler),typecheck/lint/testscripts, bun:test harness, shared validators (≥70% floor,direct×0.70 + indirect×0.30, 6-category root-cause enum),formsmodule, python-server ingest client. clo_raw_data(per-student raw data entry — everything rolls up from here)course_assessment_report(CAR — the term-level hub)clo_attainment_summary→plo_attainment_summary→cohort_tracking(roll-up chain)plo_gap_analysis→cqi_action_plan→closing_the_loop(CQI/ACT loop)curriculum_map,assessment_calendar,target_setting_matrix,assessment_budget(PLAN-phase setup)- ✅
mid_cycle_attainment,resource_monitoring,peer_observation,exhibition_feedback,clo_perception_survey,student_exit_survey,portfolio_assessment_record,capstone_panel_evaluation(supporting DO/CHECK instruments) — DONE (Phase 6) - ✅
alumni_tracer,employer_satisfaction_survey,annual_program_report,systemic_gap_report,capa_plan,portfolio_roadmap,institutional_review(periodic/escalation/institutional) — DONE (Phase 6;annual_program_reportdone in Phase 4) - Graduation-cluster archival pipeline (after PEO attainment capture)
Each phase's Definition of Done includes unit tests (bun:test) for services/validators plus integration tests against the dev DB (gated on DB reachability), with lint and typecheck green.
⚠️
bun run testrunstest/helpers/run-tests.ts, which wipes the target database before and after every run — on this setup that isobelisk_dev(Docker Postgres,just db-up), never the deployedobeliskdatabase. Re-runbun run db:seed(orjust db-seed) afterwards or every seeded account and all reference data is gone.
Phase 6 Modules
Phases 6 forms are split across two feature plugins:
check-plugin(src/v1/check/, prefix/api/v1/check, PDCA stage CHECK): F08/F10/F11/F12/F17/F18/F19 — sequence nos 21–27.periodic-plugin(src/v1/periodic/, prefix/api/v1/periodic, PDCA stage ACT): F09/F20/F21/F26/F27/F28/F02 — sequence nos 28–34.
Schema: prisma/schema/13-phase6.prisma — 8 row models (MidCycleCohortRow, ResourceItemRow, CqiImplementRow, ExhibitionGuestRow, PortfolioCriterionRow, CapstonePanelistRow, PortfolioRoadmapRow, PortfolioRubricRow). 3 new enums in 01-enums.prisma: MidCycleStatus, AcquisitionStatus, CqiImplementationStatus. 8 back-relations on FormSubmission in 06-forms.prisma.
Submit gates (registered in lib/forms/submit-gates.ts):
- F11
exhibition_feedback: ≥3ExhibitionGuestRowrows. - F19
capstone_panel_evaluation: ≥2 faculty + ≥1 industryCapstonePanelistRowrows. - F20
alumni_tracer/ F21employer_satisfaction_survey: no approved submission within 18 months (biennial). - F26
systemic_gap_report:cohort_trackingwith 3+ consecutive NOT-MET cycles. - F27
capa_plan: referencedsystemic_gap_reportmust be approved. action_taken: non-emptyformData.studentIdsand a non-blankformData.actionTaken(seesrc/v1/atrisk/service.ts).
Tests: test/integration/check.test.ts (9 tests) + test/integration/periodic.test.ts (10 tests).
System Design Documentation
When adding a new feature (new endpoints, database tables, service methods, or auth changes) or modifying existing ones, update SYSTEM-DESIGN.md to reflect the changes. Keep the following sections in sync:
- API Endpoints — add/modify route table entries
- Database Schema — add/modify table definitions and ERD
- Roles Table / Authorization Matrix — update if role checks change
- Service Layer — document new/updated service methods
- System Architecture / Request Flow — update if middleware or request pipeline changes
