Thanks for contributing. This doc covers the workflow, conventions, and gotchas you need to know before opening a PR.
If you haven't set up the project yet, start with the Local Development section in README.md.
dashboard/
├── src/
│ ├── app/ # Next.js App Router - pages and API routes
│ │ ├── (auth)/ # Login / register pages (unauthenticated layout)
│ │ ├── (dashboard)/ # Authenticated pages + shared layout
│ │ │ ├── layout.tsx # Auth gate + sidebar/topnav shell
│ │ │ ├── dashboard/ # Member overview page
│ │ │ ├── orders/ # Order submission + history
│ │ │ ├── api-keys/ # API Key Vault
│ │ │ ├── minecraft/ # Minecraft status, whitelist requests, leaderboard
│ │ │ ├── headscale/ # Network join requests
│ │ │ └── admin/ # Admin-only pages (order queue, users, etc.)
│ │ ├── api/ # Route handlers
│ │ │ ├── service/ # External service endpoints (e.g. API key verify)
│ │ │ └── minecraft/ # Internal endpoints (status, leaderboard)
│ │ └── pl3xmap/ # Pl3xMap reverse proxy (forwards to PL3XMAP_URL)
│ ├── components/
│ │ ├── layout/ # Sidebar, TopNav
│ │ ├── minecraft/ # Minecraft-specific components (status, leaderboard, map)
│ │ └── ui/ # shadcn/ui primitives (buttons, cards, dialogs, etc.)
│ └── lib/
│ ├── db/
│ │ ├── schema.ts # Application tables (edit this for schema changes)
│ │ └── auth-schema.ts # better-auth managed tables - do not edit
│ ├── auth.ts # better-auth server configuration
│ ├── auth-client.ts # better-auth browser client
│ ├── minecraft.ts # Minecraft server status ping + bot name resolution
│ └── minecraft-stats.ts # Playtime leaderboard from world stat files + Mojang API
├── drizzle/ # Auto-generated migration files - do not edit by hand
├── scripts/
│ └── seed.ts # Database seed (teams + admin)
└── public/ # Static assets served as-is
Caution
src/lib/db/auth-schema.ts is managed by better-auth. Do not edit it - your changes will be overwritten the next time better-auth regenerates it. Auth-related customisation belongs in src/lib/auth.ts.
| Table | Description |
|---|---|
user |
Registered members. role is "admin" or "member". isActive = false blocks access without deleting the account. |
session |
Active auth sessions - managed by better-auth, don't touch. |
account |
Auth provider records - managed by better-auth, don't touch. |
verification |
Email verification tokens - managed by better-auth, don't touch. |
team |
The 6 robot sub-teams. Seeded once; not user-editable through the UI. |
orders |
Part order requests. Status flows: pending → approved / rejected → ordered. |
api_key |
Hashed service API keys issued to members. Only the prefix and hash are stored. |
vault_entry |
API Key Vault entries (shared login / api_key credentials), AES-256-GCM encrypted at rest. |
vault_entry_access |
Per-person read grants for vault entries. A row (entry_id, user_id) lets that user read that secret. |
minecraft_whitelist |
Minecraft username whitelist requests. addedDirectly marks admin-added entries. |
headscale_join_request |
Requests to join the Tailscale network. Approved requests still require manual action in the Tailscale admin console. |
Note
All timestamps are stored as millisecond integers (timestamp_ms). Use datetime(col / 1000, 'unixepoch') when querying raw SQL.
sqlite3 db/dashboard.db.tables
.schema orders
SELECT * FROM orders WHERE status = 'pending';
.quit- Branch off
mainusing the naming convention below. - Make your changes.
- Verify lint and formatting pass locally.
- Open a pull request against
mainwith a short description of what changed and why.
| Type | Pattern | Example |
|---|---|---|
| Feature | feat/<short-description> |
feat/order-export |
| Bug fix | fix/<short-description> |
fix/session-expiry |
| Chore / infra | chore/<short-description> |
chore/update-deps |
Commit messages must follow Conventional Commits. A git hook enforces this automatically - bad commits are blocked before they land.
<type>: <short description>
| Type | When to use |
|---|---|
feat |
New feature or behaviour |
fix |
Bug fix |
chore |
Maintenance, deps, config - no behaviour change |
docs |
Documentation only |
style |
Formatting, whitespace - no logic change |
refactor |
Code restructure with no feature or fix |
perf |
Performance improvement |
ci |
CI/CD changes |
revert |
Reverts a previous commit |
The hook is installed automatically by pnpm install. Use git commit --no-verify only in genuine emergencies.
ESLint and Prettier are configured and run in CI.
pnpm lint # ESLint
pnpm format:check # Prettier check (no writes)
pnpm format # auto-fix formattingTip
In VS Code, install the ESLint and Prettier extensions and enable Format on Save - you won't need to run these manually.
- Keep PRs focused - one concern per PR. A PR that adds a feature and refactors an unrelated component is harder to review and harder to revert.
- Write a useful description - explain what changed and why, not just what the diff shows. Link to relevant issues or Slack threads.
- Schema changes need a migration - if your PR touches
src/lib/db/schema.ts, include the generated migration file (see Database Changes below).
Pages live under src/app/(dashboard)/. The (dashboard) route group applies the authenticated layout automatically.
- Create
src/app/(dashboard)/<your-route>/page.tsx - Add a nav entry in
src/components/layout/Sidebar.tsxif it should appear in the sidebar - Add a corresponding entry to the
routeLabelsmap insrc/components/layout/TopNav.tsxso the topnav shows the right page title
Admin-only pages go under src/app/(dashboard)/admin/. The auth gate in layout.tsx only checks authentication - admin-only access must be enforced inside the page or its API routes.
Always pair a schema edit with a generated migration and commit both together:
# 1. Edit src/lib/db/schema.ts
# 2. Generate the migration
pnpm db:generate
# 3. Apply it locally and verify the app still works
pnpm db:migrate
# 4. Commit schema.ts + the new file in drizzle/ in the same commitWarning
Never edit files inside drizzle/ by hand after they have been committed. Drizzle checksums each migration file and will refuse to run if it detects manual edits. To undo a migration, generate a new one that reverses the change - don't touch the existing file.
The external-facing REST API lives under src/app/api/service/. This is the endpoint called by the lab's simulation Python scripts to verify API keys.
Internal data fetching uses React Server Components and server actions - there is no separate internal REST layer. Server components call the database directly via Drizzle.
Internal route handlers (e.g. GET /api/minecraft/leaderboard) exist for data that must be fetched client-side (polled components, client-rendered cards). These require authentication - validate the session with auth.api.getSession before returning any data.
Note
POST /api/service/verify is IP-rate-limited in production via Cloudflare headers. There is no rate limiting in local dev - don't rely on that behaviour in tests.
Some internal services (currently Pl3xMap) are proxied through the Next.js app so they are accessible via the dashboard URL without exposing a second port. The proxy lives at src/app/pl3xmap/[[...path]]/route.ts.
Key points:
- Use a catch-all route handler (
[[...path]]) so all sub-paths forward correctly. - For SPA-based services, you may need to rewrite the HTML
<base href>tag so asset paths resolve through the proxy prefix. The Pl3xMap handler does this. - Catch
ECONNREFUSED/ENOTFOUNDand return a503so the client can show a graceful unavailable state instead of an unhandled error.
When two adjacent cards share a single data fetch (to avoid duplicate requests), wrap them in a container component that uses className="contents" (display: contents). This makes the wrapper invisible to CSS Grid, so the child cards participate in the outer grid as direct items while the fetch logic lives in one place. See ServerStatusSection.tsx for an example.
| Concern | Local dev | Production |
|---|---|---|
| Database | db/dashboard.db in repo root |
/home/trickfire/db/dashboard.db |
| Server | pnpm dev (Turbopack, hot reload) |
systemd + .next/standalone/server.js |
| HTTPS | None (HTTP on port 3000) | Cloudflare Tunnel provides TLS |
| Minecraft / Tailscale | Optional - app degrades gracefully | Required - configure in .env.production |
| LAN access | Set BETTER_AUTH_TRUSTED_ORIGINS to the LAN IP:port |
Not needed - all traffic goes via Tunnel |
Check inline comments first - they're sparse but mark non-obvious behaviour. If you're still stuck, ask in the team Slack.