Skip to main content

Roadmap and known gaps

Ordered by cost-to-value, not by ambition. The first group is small work with real return; the second is the sequence that turns the scaffolds into the architecture the other documents describe.

Quick wins​

These are all small, self-contained, and address something currently misleading or broken.

Done since this page was written: the two internal-only services (apps/agents, apps/credentials) are now authenticated by a shared INTERNAL_SERVICE_TOKEN and fail closed without it, wired through a new packages/internal-auth package with tests; and bun run test actually works from the repo root, which it previously did not — the task scripts never loaded .env, so the suite only passed when the variables happened to be exported already. See Agents.

The repo is also deployable now: a single app image, a production compose file where only Caddy publishes ports, and a runbook in Deploying to a VPS. Containerising it surfaced a real bug rather than only packaging work — server-rendered routes resolved the API URL from a build-time Vite substitution, so /admin returned 500 inside a container instead of redirecting to /login. apps/web/src/lib/api-base-url.ts now resolves that per runtime.

ItemWhy it mattersWhere to start
Wire Biome into a real bun run lintbiome.json is configured and nothing runs it; only apps/site has a lint script, so 9 of 10 packages are unlinted. bun run lint currently does almost nothing.Root package.json scripts + add lint to each package
Fix or remove the agent's memory configapps/agents passes a plain object as memory to agent.generate; Mastra needs a Memory instance on the Agent, so tenant-scoped conversation history is not persisting. The code claims otherwise in comments. See Agents.apps/agents/src/agents/iqac-agent.ts
Apply requirePermission to read routesThe RBAC matrix declares student/staff are view-only and restricted to their own records; in practice every authenticated member of a tenant can list all people, org structure, and attendance. Tenant * isolation* is solid — this is intra-tenant scoping. See API Surface.Add permission middleware + own-record filtering to the GET routes
Get the legacy ERP backup out of gitdocs/cs-04Aug2026-02 (1)/ is 75 MB across 2,207 files of a live college's .sql.gz dumps — real student and staff PII, and permanent repo bloat once history is shared. It is a deliberate input to the importer, so the fix is to relocate it (object storage, or a git-LFS/ignored path) and keep load-legacy-tables.sh pointed at a configurable location rather than deleting the workflow..gitignore, infra/import/load-legacy-tables.sh, README
Render custom fields on formsDefinitions can be created in settings and their values belong in the target row's attributes JSONB, but no person/enrollment/employment form renders inputs for existing definitions — so the no-migration extension mechanism is half-usable.admin.people.$personId.tsx, admin.people.tsx
De-duplicate the custom-field enumsadmin.settings.tsx re-declares ENTITY_TYPES/FIELD_TYPES/VISIBILITIES as literals with a comment pointing at the schema. They can drift silently.Export the const arrays from @campus/db and import them
Schedule database backupsThe deployment runbook has a manual pg_dump and nothing automates it. With a single node and no replica, one disk failure loses every tenant's data.cron/systemd timer plus a restore that has actually been rehearsed

Next, in the order that keeps the foundation honest​

  1. Add an embeddings table and actually use the vector service. tenantScopedVectorSearch exists, enforces pre-filtering, and has zero callers; there is no embeddings table in the schema. Either build the table and wire it into one real retrieval path, or remove the service until it is needed — an unused isolation-enforcing abstraction reads as coverage that does not exist. Note also that its table parameter is interpolated into raw SQL and its comment says the value is "validated against an allowlist upstream": that allowlist should exist before anything passes non-literal values in.
  2. Finish the module engine's consumer half. module_definitions holds real metadata for M1–M20 and /modules serves it, but nothing renders a route or a form from a definition. Until a generic consumer exists, "a new module is configuration, not code" is true at the storage layer only. module_entitlements also has no write path, so entitlements cannot be granted through the product.
  3. Finish the workflow service — verify the DBOS API, make the step persist real state through mutation-service, add a trigger from the API, and prove one SLA timer survives a crash. See Workflow. Also decide deliberately whether DBOS continues to connect as the table-owning role (it currently does, which puts its own state outside the RLS boundary).
  4. Route-level HTTP tests. The three existing test files cover the infrastructure — RLS, mutation/audit atomicity, and permission mapping. Nothing tests a route end to end, so auth-before-authorization ordering, tenant resolution, and validation error shapes are unverified. The read-side permission work above is the natural moment to add them.
  5. Turn the RLS test into a generated sweep. It uses departments as a representative for all 19 RLS-covered tables because they share one policy shape. That is a reasonable trade, but a loop over TENANT_SCOPED_TABLES would catch a table that got added but not policied — the exact failure mode the array exists to prevent.
  6. Credentials: keys, DID lifecycle, and persistence before anything real is issued. The in-memory Veramo stores mean a signed credential cannot be verified later, which makes issuance pointless today. See Credentials.

Then: the pillars that are design-only​

  • Pillar 2 — always-on compliance. The largest gap between the docs and the code. Requires policies-as-code over the graph, evaluation triggered by relevant writes, and an evidence record emitted per evaluation. The audit_log table is an audit trail, not this. The architecture doc's own first step is the right one: encode one NAAC/NBA criterion against real Phase 1–2 data and prove it produces an always-current evidence trail with zero manual report-building.
  • Pillar 3 — digital twin. No simulation layer. It also depends on Pillar 1 having enough graph to make cross-module what-if questions meaningful — today the graph holds Person and org vertices with two edge types, and there is no hostel, fee, vehicle, or room data at all.
  • Pillar 4 — agent-native operations. Beyond fixing the one agent's memory: A2A delegation, capability cards, and — most importantly — the governance gate that routes every agent output through human approval before it has institutional standing. Autonomy without that gate is the thing the architecture explicitly says not to ship.
  • Real-time / IoT (M13, M18). NATS_URL is declared and nothing uses it. The architecture's own guidance is to prove one ElectricSQL + TanStack DB live view end to end before committing to it as the pattern for every live dashboard.
  • /portal/* surfaces for students, faculty, and parents, and then mobile. Note that faculty cannot mark attendance through the UI today even though the API supports it and the authorization logic is implemented — the portal tree is the missing piece, not the backend.
  • Self-service institution onboarding. Onboarding is documented as a data operation (a tenants row, a Better Auth organization, entitlements, an admin user). Nothing automates it yet.

A note on the two architecture documents​

architecture/strategy still recommends Python/Django and Temporal. architecture/technical-architecture supersedes both with Bun and the composed DBOS/pg-boss/XState layer, for reasons it documents (Temporal's TS SDK worker does not run on Bun; there was no strong preference for Python). The code follows the technical architecture. The strategy document is kept for the product thinking — the competitor analysis, the five pillars, and the wedge — and its stack section should be read as superseded rather than current. It is worth a callout at the top of that page so nobody implements against it.