Architecture Overview
How the codebase is organized, how a request flows through it, and how access control is layered.
This page is a map of the codebase. It assumes you've read Getting Started and know how to run the app - here we look at how it's built.
The shape of the app in one sentence
It's a single Next.js app (no separate backend service) that renders pages on the server, talks directly to a local SQLite file through Drizzle ORM, and exposes a set of JSON API routes for anything that needs to be called from the browser after the page has loaded.
Top-level src/ layout
| Folder | Purpose |
|---|---|
src/app | Every route: pages, layouts, and API route handlers (Next.js App Router - see Next.js & TypeScript). |
src/components | Client-facing UI, one subfolder per feature area (admin, auth, dashboard, finance, github, minecraft, network, onshape, orders, settings, vault, layout) plus ui for the shadcn/ui primitives. |
src/lib | Everything that isn't UI: auth/ (better-auth setup + session helpers), db/ (Drizzle schema + SQLite client), finance/, integrations/ (GitHub, OnShape, Minecraft/Azalea, email), security/ (vault crypto, API-key hashing, rate limiting), plus small root-level modules (use-poll.ts, utils.ts, validation.ts). |
There's no src/hooks or src/types folder - the one meaningful custom hook (usePoll) lives in src/lib, and types are defined next to the schema/integration code they belong to rather than centralized.
Note
The admin subfolder under src/components (and admin/* under src/app/api) is a naming
holdover, not a live access tier - there's no admin role anymore (see below). It groups
components/routes that happen to manage shared/global data (users, whitelist, server control,
vault), as opposed to a member's own data.
src/app structure
src/app/
├── (auth)/ # /login, /pending - unauthenticated flow, own layout
├── (dashboard)/ # everything behind auth - own layout, flat routes
│ ├── dashboard/
│ ├── orders/[id]/edit
│ ├── api-keys/ # the vault UI
│ ├── minecraft/ # status, whitelist, server control, playtime, map
│ ├── network/
│ ├── members/ # users, GitHub org, OnShape company - tabbed
│ ├── finance/
│ └── settings/
├── pl3xmap/[[...path]] # Minecraft map proxy - relaxed CSP, no auth group
└── api/ # route handlers - see the tech-stack pages for detailsThe two parenthesized folders - (auth) and (dashboard) - are Next.js route groups: they organize routes and give each area its own layout.tsx without adding a /auth or /dashboard segment to the URL.
Every (dashboard) route is a flat top-level page - there used to be a nested admin/ section with its own layout, but that's gone along with the admin role (see below). What were once separate admin pages (users, GitHub, OnShape, server control) are now tabs or sections on the ordinary top-level pages (/members, /minecraft) instead of a separate route tree.
How a page request is authorized (two layers)
Access control isn't layered by role anymore - every approved, active user reaches the same pages and the same data. What's still layered is where the session/status check happens, because /api/* isn't covered by the middleware:
src/middleware.tsruns first, before any page renders, for everything matched by itsmatcher(/dashboard,/orders,/minecraft,/network,/members,/finance,/api-keys- notably not/api/*). It checks the session cookie viaauth.api.getSession(), redirects to/loginif missing or the account is deactivated, and to/pendingif not yet approved. That's the entire check - there's no further per-route or per-role gate after it.- Individual API route handlers each call
getSessionUser()and re-check the session/isActivethemselves, because/api/*isn't covered by the middleware at all. This is why you'll see the same-looking auth check copy-pasted at the top of manyroute.tsfiles rather than factored into one shared function - see Authentication & Authorization for why, and where the vault endpoints add an extra per-entry check on top.
Data fetching pattern
- Server components (most
page.tsxandlayout.tsxfiles) query the database directly -db.select(...).from(...).get()- inside anasynccomponent. No API call, no client-side loading state, no waterfall. - Client components that need live or interactive data call an API route (e.g. a Minecraft status tile polling
/api/minecraft/statusvia theusePollhook insrc/lib/use-poll.ts), because a server component can't re-fetch on an interval without a full page reload.
Only two files in the entire src/app tree are "use client" at the page/layout level (global-error.tsx and the dashboard error.tsx) - both are boundaries Next.js requires to be client components. Everything else that needs interactivity is a small client component imported into a server page, not the page itself.
The ordering workflow
Parts ordering is the largest feature, and it splits deliberately between
submitting an item and triaging it - not between two different roles. Both
halves live on the single /orders page; anyone can do either.
Submission carries items only. The submission form asks for vendor, link,
item name, part number, quantity, cost and notes - no fund type and no STF
bucket. One form can carry many items (see the repeatable-rows section in
Forms & Validation), and POST /api/orders
writes them all in one request, stamping a shared batchId when there is more
than one. Orders therefore land with fundType, stfBucketId and quarterId
all null.
The order queue triages in batches. The "Order queue" section of /orders
groups pending orders into "Needs triage" (no fund yet) and "Ready to review"
(assigned). Whoever's triaging multi-selects rows and acts on the whole
selection:
| Endpoint | What it does |
|---|---|
POST /api/orders/assign | Sets fund type, bucket and active quarter on the selected orders |
POST /api/orders/bulk-action | Approves or denies the selected orders |
Balance checks run at assignment and approval, never at submission. A member has no bucket to check against, and a balance checked at submit time is stale by the time an officer reviews it. Two consequences worth knowing:
- Approving a selection checks each fund/bucket group's total at once
(
validateBatchBalance). Checking orders one at a time would let a batch overdraw a bucket that each individual order fits inside. - Assigning checks the selection plus everything already queued against that
bucket (
validateAssignmentBalance). Pending orders don't reduce a bucket's remaining balance - only approved and ordered ones do - so without this an officer could quietly park more in a bucket than could ever be approved.
Where each concern is documented
| Concern | Page |
|---|---|
Next.js routing, server/client components, next.config.ts | Next.js & TypeScript |
| Tailwind v4, shadcn/ui, icons, toasts | Styling & UI Components |
| Forms and input validation | Forms & Validation |
| Schema, migrations, seeding | Database |
| Sessions, account status | Authentication & Authorization |
| Vault encryption, API keys, rate limiting | Security |
| Unit tests | Testing |
| Linting, formatting, git hooks, CI | Code Quality Tooling |
| Error tracking, uptime monitoring | Observability |
| Minecraft/RCON, GitHub, Resend, OnShape, Tailscale | External Integrations |