Admin app
apps/web is the internal, authenticated Campus OS surface today — and it is admin-only, by
deliberate scoping rather than accident. The architecture docs describe admin, faculty, HoD, IQAC,
warden, and placement portals; no /portal/* route tree exists yet.
Built on TanStack Start (Vite dev server on port 3000) with TanStack Router, TanStack Query, and
TanStack Table. API access goes through a single typed Hono RPC client
(apps/web/src/lib/api-client.ts) — hc<AppType> importing the API's route type, with
credentials: "include" so the Better Auth session cookie rides along. That is what gives
end-to-end type safety without hand-written per-endpoint wrappers.
Routes
| Route file | Path | What it does |
|---|---|---|
__root.tsx | — | Root layout, Header/Footer, providers |
index.tsx | / | Landing / redirect |
login.tsx | /login | Email + password sign-in against apps/api |
about.tsx | /about | Static |
admin.tsx | /admin | Admin shell layout |
admin.index.tsx | /admin | Dashboard |
admin.departments.tsx | /admin/departments | Org structure — departments and related reference lists |
admin.people.tsx | /admin/people | People list with search + "Add Person" flow |
admin.people.index.tsx | /admin/people | List index |
admin.people.$personId.tsx | /admin/people/:id | Person detail: addresses, sensitive identifiers, employment, enrollment, attendance history, plus edit forms |
admin.curriculum.tsx | /admin/curriculum | Subjects and class sections |
admin.attendance.tsx | /admin/attendance | Attendance: assignments, delegations, roster marking, records |
admin.settings.tsx | /admin/settings | Modules, custom fields, reference data, audit log |
routeTree.gen.ts is generated — run bun --filter '@campus/web' generate-routes (tsr generate)
after adding a route.
What the settings screen actually contains
Four sections in one page, each backed by a real endpoint:
- Modules — a badge per module the tenant is entitled to, from
GET /modules. Read-only: there is no UI or endpoint to enable or disable an entitlement yet. - Custom Fields — a table of this tenant's
custom_field_definitionsplus a create form (entity, key, label, type, enum options when type isenum, required flag, visibility). This is the no-migration extension mechanism exposed end to end. The option lists are duplicated as literals in this file with a comment pointing atpackages/db/src/schema/tenancy.tsas the source of truth — a candidate for a shared export, since the two can drift. - Reference Data — all six global lookup tables as badge groups with counts. Read-only; the reference tables are platform-managed and intentionally not tenant-writable.
- Audit Log — the paginated audit trail (50 per page) with a Previous/Next pager, action badges
coloured by create/delete/update, and the acting person resolved by name where available. Note
this is
requirePermission({ auditLog: ["view"] }), so a non-admin gets an error here.
Components
components/ui/ holds the primitives — badge, button, card, input, label, select,
table — written as local components rather than pulled from a UI library, styled with Tailwind CSS
v4 via @tailwindcss/vite. There is a Header, Footer, ThemeToggle, and a person-picker
used wherever a route needs to choose a person.
There is no shared design system package between web and mobile yet; mobile does not exist yet, so the token-drift problem the architecture doc anticipates has not arrived.
Known limitations
- No self-service surface. Students, faculty, and parents have no UI. Faculty cannot mark
attendance through the app today even though
POST /attendance/recordssupports them — the authorization logic for it is built and tested at the API layer. - No permission-aware navigation. The shell renders the same nav regardless of role. Combined
with the read-side authorization gap in API Surface,
a low-privilege user logging in will see screens whose writes fail with
403rather than being hidden. - No real-time updates. Everything is request/response through TanStack Query. The TanStack DB +
ElectricSQL live-view architecture in
TECHNICAL_ARCHITECTURE.md§7 is intentionally a later concern. - No custom-field rendering on forms. Custom fields can be defined in settings, and the schema
stores their values in the target row's
attributesJSONB — but the person/enrollment/employment forms do not yet render inputs for the definitions that exist.