Admissions & Recruitment
The inbound pipelines — student applications, job vacancies and applicants, and website inquiries with AI answers.
Three different kinds of stranger want something from a school: a family applying for a place, a candidate applying for a job, and a website visitor asking a question. The Education Hub treats all three as inbound pipelines — and they share one shape. Each has a guest side where the outsider fills in a form and tracks their status, and a staff side where someone inside the school reviews what came in and pushes it forward.
Once you see that mirror — guest submits, staff reviews — the three features stop looking like three apps and start looking like one pattern wearing three hats. This page is the map: what each pipeline does, and where its code lives.
Guests are a real user type
The guest area lives under src/routes/_authenticated/guest, behind a requirePermission({ userType: 'guest' }) guard. An applicant is a logged-in user — just a different user type from staff. See User Types for how that split works.
The guest landing page sets the tone — it greets the applicant by name and offers three doors:
const actionButtons = [
{ title: 'Admission Applications', path: '/guest/admissions', icon: TaskEdit01Icon },
{ title: 'Job Applications', path: '/guest/jobs', icon: WorkIcon },
{ title: 'My Letters', path: '/guest/letters', icon: Mail02Icon },
]Admissions
The first pipeline is student admissions. A family applies for a place; the school reviews the application, tracks the application fee, and records a decision.
Guest side — families apply and track status at /guest/admissions. The multi-step form and the per-application detail view live in src/routes/_authenticated/guest/admissions/ (the -partials/ folder holds each step).
Staff side — staff review every application at /staff-hub/school/admissions. The route itself is tiny: it asks one hook for the count, shows an empty state when nobody has applied, and otherwise hands off to a data table.
const { applications, loadingApplications } = useAdmissionApplications()
const showEmptyState = applications?.totalCount === 0 && !loadingApplications
// ...
{!showEmptyState && <AdmissionApplicationDataTable />}The real work is in the data table at src/components/data-tables/admission-applications/. It's a server-paginated table — the columns.tsx file declares the columns (reference, name, campus, class, status, started-at, plus a row-actions cell), and filter.tsx drives the query params sent to the API.
columnHelper.accessor('reference', {
header: 'Ref',
cell: (info) => (
<Link to="/staff-hub/school/admissions/$applicationId"
params={{ applicationId: info.row.original.id }}>
{info.getValue()}
</Link>
),
})Clicking a reference opens the detail route admissions_.$applicationId.tsx, where staff see the full application and record a decision.
Two data sources, one screen
Admissions reads from two query modules: queries/application for the applications themselves, and queries/transaction for the application-fee payment attached to each one. When you trace a value on the screen, check which of the two it came from.
Recruitment
The second pipeline is jobs. Same mirror — but now the school is the one posting, and the outsider is a candidate.
Staff side — at /staff-hub/school/jobs, staff create vacancies and review applicants. The list route opens a surface to create a new vacancy and renders the vacancies in a data table:
const { openSurface } = useSurface()
const { vacancies, loadingVacancies } = useStaffJobVacancies()
// New vacancy → openSurface(CreateJobVacancy)
// Otherwise → <JobVacanciesDataTable data={vacancies ?? []} />Drilling into a single vacancy (jobs/$vacancyId) lists the people who applied to that listing.
Guest side — candidates browse open roles and apply at /guest/jobs. The browse page (guest/jobs/index.tsx) gives them search, department and employment-type filters, and a grid-or-list view; submitting a role runs the multi-step application form in the -partials/ folder.
Three query modules for one feature
Jobs spreads across queries/job (the public vacancy listing a guest browses), queries/staff-jobs (the staff-side management view), and queries/job-application (an individual candidate's submission). Reach for the one that matches the side of the mirror you're on.
The application form itself is the textbook case for Forms — TanStack Form for state, Zod for validation, one schema per step. If you're adding a field, that's the page to read.
Inquiries
The third pipeline is the lightest — website inquiries. When a visitor fills in the contact form on the public marketing site, the submission lands here for staff to triage at /staff-hub/school/inquiries.
The route is a three-tab surface — Overview, Inquiries, and AI enquiries — with the active tab kept in the URL via nuqs so a link can deep-link straight to a tab:
const [currentTab, setCurrentTab] = useQueryState(
'currentTab', parseAsString.withDefault('overview'),
)
// tabs: overview | inquiries | aiThe Inquiries tab is plain human work — read a message, assign it, set a priority, leave notes. All of that lives in queries/inquiry, whose methods read like the verbs staff perform: updateStatus, updatePriority, assignInquiry, addInquiryNotes.
The AI enquiries tab is the interesting one. Questions that arrived through automated channels are answered — or drafted — by the assistant, and staff approve or override. That data comes from queries/social-agent-question, which exposes just two methods: list and answer.
readonly answer = async (id: string, response: string) =>
this.exec(this.op.answerSocialAgentQuestion({ id }, { response }))See The AI Assistant for where these answers come from and how the model fits into the platform.
Enquiry vs inquiry
The UI says inquiries, but the generated API contracts spell it enquiries (listEnquiries, ListEnquiriesQueryParams). Both spellings refer to the same thing — don't let the swap throw you when you're searching the codebase.
The shared shape
Step back and the three pipelines rhyme. Every one of them is the same handful of moving parts:
| Pipeline | Guest route | Staff route | Query modules |
|---|---|---|---|
| Admissions | /guest/admissions | /staff-hub/school/admissions | application, transaction |
| Recruitment | /guest/jobs | /staff-hub/school/jobs | job, staff-jobs, job-application |
| Inquiries | website form | /staff-hub/school/inquiries | inquiry, social-agent-question |
Learn one and you've mostly learned all three — a guest-side form, a staff-side table or tabs, and a query module per concern.
Where to go next
Data Tables
The server-paginated table that powers the admissions and jobs review screens.
Forms
TanStack Form plus Zod — the engine behind every multi-step application.
The AI Assistant
Where the AI-answered inquiry replies actually come from.
Surfaces
The create-vacancy flow and other launch-from-anywhere overlays.