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

FolderPurpose
src/appEvery route: pages, layouts, and API route handlers (Next.js App Router - see Next.js & TypeScript).
src/componentsClient-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/libEverything 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

plaintext
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 details

The 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:

  1. src/middleware.ts runs first, before any page renders, for everything matched by its matcher (/dashboard, /orders, /minecraft, /network, /members, /finance, /api-keys - notably not /api/*). It checks the session cookie via auth.api.getSession(), redirects to /login if missing or the account is deactivated, and to /pending if not yet approved. That's the entire check - there's no further per-route or per-role gate after it.
  2. Individual API route handlers each call getSessionUser() and re-check the session/isActive themselves, 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 many route.ts files 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.tsx and layout.tsx files) query the database directly - db.select(...).from(...).get() - inside an async component. 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/status via the usePoll hook in src/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:

EndpointWhat it does
POST /api/orders/assignSets fund type, bucket and active quarter on the selected orders
POST /api/orders/bulk-actionApproves 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

ConcernPage
Next.js routing, server/client components, next.config.tsNext.js & TypeScript
Tailwind v4, shadcn/ui, icons, toastsStyling & UI Components
Forms and input validationForms & Validation
Schema, migrations, seedingDatabase
Sessions, account statusAuthentication & Authorization
Vault encryption, API keys, rate limitingSecurity
Unit testsTesting
Linting, formatting, git hooks, CICode Quality Tooling
Error tracking, uptime monitoringObservability
Minecraft/RCON, GitHub, Resend, OnShape, TailscaleExternal Integrations

On this page