Getting Started
Clone, install with Bun, point at the API, and boot the Education Hub locally with the schema watcher running.
Getting the Education Hub running locally takes about five minutes — but there's one twist that catches most newcomers. This frontend doesn't define its own API types. They're generated from the backend's live OpenAPI spec. So "running the app" really means running two things side by side: the dev server, and a watcher that keeps the typed API client in sync with the API.
Get that mental model first and the rest is just commands.
Stack at a glance
Before you touch a command, here's the shape of what you're booting. You don't need to know any of these deeply yet — this is just a map so the package names stop looking like noise.
| Concern | Tool |
|---|---|
| Framework | TanStack Start (React 19 + SSR) on Vite 7 |
| Routing | TanStack Router (file-based, src/routes/) |
| Data fetching | TanStack React Query |
| Client state | Zustand (auth tokens, theme, panels) |
| API client | Axios, with types generated from OpenAPI |
| Package manager | Bun |
| Testing | Vitest + React Testing Library |
The one row that drives this whole page is the API client. Let's set it up.
Step 1: Clone and install
The project uses Bun as its package manager — not npm, not pnpm. Install Bun first if you don't have it, then install dependencies. Bun reads the same package.json and is dramatically faster on a cold install.
git clone <repo-url> education-hub
cd education-hub
bun installThat bun install also runs the prepare script, which wires up Husky git hooks — so your commits get auto-formatted and linted on the way out. Nothing for you to configure.
Step 2: Point at the API
The app needs to know where the backend lives. That address comes from a single environment variable, VITE_API_URL, which you set in a .env.local file at the project root.
VITE_API_URL=http://localhost:8000This one variable does double duty. At runtime it's the base URL every Axios request is sent to. At codegen time it's how the generated client knows which server to talk to — api.config.toml declares base_url_env = "VITE_API_URL", so the two stay in lockstep.
The API has to be running
The schema watcher generates types by polling the API's live spec at http://localhost:8000/docs/swagger/json. If the backend isn't up, the watcher has nothing to read and your types won't refresh. Start the backend API before — or alongside — the frontend. This is the single most common "why won't it work" for new devs.
Step 3: Boot with dev:all
You could run bun dev on its own — it starts Vite on port 3000 with --host (so it's reachable from other devices on your network). But that only runs the app. It does nothing to keep your API types fresh.
The recommended way to run the project is dev:all, which uses concurrently to run the schema watcher and the dev server together in one terminal:
bun run dev:allUnder the hood that's just two scripts glued together:
"dev": "vite dev --port 3000 --host",
"api:watch": "chowbea-axios watch",
"dev:all": "concurrently --names 'api,vite' \"bun api:watch\" \"bun dev\"",The api process polls the OpenAPI spec every 20 seconds (poll_interval_ms = 20000 in api.config.toml). When a backend dev changes an endpoint, your src/services/api client regenerates automatically — and TypeScript starts flagging the mismatch in your editor seconds later. That tight loop is the whole point of dev:all.
Where the generated client lives
The watcher writes into src/services/api (the [output] folder in api.config.toml). Treat that directory as generated, not hand-written — never edit it by hand, because the next poll will overwrite your changes. The generated client page explains how it's structured.
Step 4: Open it and log in
With dev:all running, open the app:
http://localhost:3000You'll land on a guest view. Authentication is OAuth2 via Google — tokens come back from the backend and are stored in Zustand, persisted to localStorage under the key auth-token (the same token_key the generated Axios instance reads to attach your bearer token to every request).
After you sign in, who you are shapes what you see. The platform recognizes four user types — guest, student, guardian, and staff — and the UI and permissions branch on them. The user types page covers that; for now, just know that logging in is what turns the guest shell into a real session.
Keeping types in sync
Most of the time the watcher in dev:all handles this for you. But if you started with plain bun dev, or you pulled a branch where the backend changed, you can regenerate the client by hand in a one-shot:
bun run api:generateThat hits the same spec and rewrites src/services/api once, then exits. Run it any time your types look stale and you don't have the watcher running.
Style and conventions
You don't have to memorize the style rules — they're enforced for you. The project runs Prettier + ESLint (TanStack's config) with TypeScript in strict mode, and the git hooks format staged files on commit. The house style is no semicolons, single quotes, trailing commas.
To format and lint the whole project on demand:
bun run checkOne alias is worth committing to memory: @/* maps to src/*. So @/services/api is the generated client, @/routes is the route tree, and you'll see that prefix everywhere instead of long ../../ chains.
A quick sanity check
If the app loads at localhost:3000, the api process in your dev:all terminal isn't throwing errors, and your editor shows no red squiggles in src/services/api, you're set. Run the tests once to confirm the toolchain is healthy:
bun testThat runs the Vitest suite once and exits. A green run means Bun, Vite, and the generated client are all wired up correctly — time to go read how the pieces fit together.