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.
| Item | Why it matters | Where to start |
|---|---|---|
Wire Biome into a real bun run lint | biome.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 config | apps/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 routes | The 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 git | docs/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 forms | Definitions 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 enums | admin.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 backups | The 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
- Add an embeddings table and actually use the vector service.
tenantScopedVectorSearchexists, 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 itstableparameter 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. - Finish the module engine's consumer half.
module_definitionsholds real metadata for M1–M20 and/modulesserves 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_entitlementsalso has no write path, so entitlements cannot be granted through the product. - 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). - 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.
- Turn the RLS test into a generated sweep. It uses
departmentsas a representative for all 19 RLS-covered tables because they share one policy shape. That is a reasonable trade, but a loop overTENANT_SCOPED_TABLESwould catch a table that got added but not policied — the exact failure mode the array exists to prevent. - 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_logtable 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
Personand 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_URLis 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
tenantsrow, 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.