HyprBox docs GitHub ↗

Development

Repo layout

hyprbox/
├── apps/
│   ├── web/                 # Next.js 16 — port 3000
│   │   ├── src/app/         # routes (App Router)
│   │   ├── src/components/  # nodes-dashboard, node-detail, studio-nav, …
│   │   ├── src/lib/         # tokens, auth, stream, hyprbox-api
│   │   └── src/proxy.ts     # edge proxy (gates /dashboard/*)
│   └── api/                 # Fastify 5 — port 4000
│       ├── src/routes/      # auth, nodes, tokens, presets, jobs, stream
│       ├── src/middleware/  # auth, nodeAuth
│       ├── src/lib/         # prisma, events, env, presets/{schema,loader,render}
│       ├── prisma/          # schema.prisma, seed.ts, seed-admin.ts
│       └── test/            # vitest specs (32 total)
├── agent/hyprnode/          # Go agent
├── cli/hyprbox/             # Go CLI
├── presets/                 # YAML recipes loaded at API boot
├── docs/                    # this directory
├── docker-compose.dev.yml   # Postgres + Redis for dev
├── docker-compose.prod.yml  # Postgres + API + Web
└── .github/workflows/ci.yml # node + go + docker jobs

Prerequisites

  • Node 20+, pnpm 9 (corepack enable && corepack prepare pnpm@9.15.4 --activate)
  • Go 1.23 for the agent + CLI
  • PostgreSQL 16 — either via Docker (pnpm db:up) or native install (winget install PostgreSQL.PostgreSQL.16 on Windows). The dev compose also spins up Redis but nothing currently uses it.
  • Optional: Docker Desktop for building the prod images locally.

First-time setup

git clone <repo> && cd hyprbox
pnpm install                   # installs all workspaces
pnpm db:up                     # OR run a native Postgres + create the DB
cp apps/api/.env.example apps/api/.env

# Schema + data — fresh clones land the baseline migration cleanly.
pnpm --filter @hyprbox/api exec prisma migrate deploy
pnpm db:seed                   # 6 fake nodes with 24h history
pnpm db:seed:admin             # admin@hyprbox.local / hyprbox-admin
pnpm db:seed:recommendations   # seeds the curated Recommendation catalogue

Upgrading from a pre-Phase-6.1 dev DB (one-time):

# Your DB was bootstrapped with `db push` and is already at the baseline
# shape — but Prisma's _prisma_migrations table doesn't know that yet.
pnpm --filter @hyprbox/api exec prisma migrate resolve --applied 20260531000000_init
# From here on, `prisma migrate deploy` will be a no-op until a new migration lands.

Dev loops

pnpm dev:api      # tsx watch — auto-reloads on src/** edits
pnpm dev:web      # next dev with Turbopack — HMR on src/app/**

Both processes are noisy on first run because of TLS cert generation and Prisma client compile. Subsequent reloads are sub-second.

Agent + CLI live separately:

cd agent/hyprnode && go run .   # heartbeats every 5m
cd cli/hyprbox && go run . status

The agent only runs the job loop on Linux (runtime.GOOS != "linux" short- circuits the polling). Heartbeats work everywhere.

Tests

pnpm test                                       # 66 specs vitest (API)
pnpm --filter @hyprbox/api test:watch           # watch mode
pnpm --filter @hyprbox/api exec tsc --noEmit    # API typecheck
pnpm --filter @hyprbox/web exec tsc --noEmit    # Web typecheck
cd agent/hyprnode && go test ./...              # 10 Go specs (sshd parser)

Tests assume:

  • A separate hyprbox_test database. Set it once:
    $env:PGPASSWORD = "postgres"
    & "C:\Program Files\PostgreSQL\16\bin\psql.exe" -U postgres \
      -c "CREATE DATABASE hyprbox_test OWNER hyprbox;"
    
  • HYPRBOX_TEST_DATABASE_URL=postgresql://hyprbox:hyprbox@localhost:5432/hyprbox_test in the shell that runs pnpm test.

test/global-setup.ts does a prisma db push once before any spec runs. Each spec's beforeEach calls resetDb() (heartbeats → jobs → tokens → nodes → users — in FK-respecting order).

db push (not migrate deploy) on the test DB is deliberate: the test DB is ephemeral and the migration replay history isn't worth the boot cost. CI compensates by running an explicit drift check that fails the build if schema.prisma diverges from prisma/migrations/.

Go side:

cd agent/hyprnode && go vet ./... && go test ./... && go build ./...
cd cli/hyprbox    && go vet ./... && go build ./...

The agent's internal/scanner/ssh_test.go covers the sshd_config parser edge cases (Include, Match, first-write-wins).

Schema changes & migrations

Phase 6.1 switched the project from prisma db push to a real migration history in apps/api/prisma/migrations/. The workflow when you change the schema:

# 1. Edit prisma/schema.prisma.
# 2. Generate a migration. This runs against your *dev* DB AND creates
#    the migration.sql file in one shot.
cd apps/api
pnpm db:migrate -- --name <short_snake_case_name>
# 3. Commit both the schema change AND the new migration directory.

Drift check (locally) — confirm schema.prisma and migrations/ agree:

# One-time: create a shadow DB for Prisma to replay migrations into.
psql -U hyprbox -d postgres -c "CREATE DATABASE hyprbox_shadow OWNER hyprbox;"

# Then any time you want to verify:
pnpm --filter @hyprbox/api exec prisma migrate diff \
  --from-migrations ./prisma/migrations \
  --to-schema-datamodel ./prisma/schema.prisma \
  --shadow-database-url "postgresql://hyprbox:hyprbox@localhost:5432/hyprbox_shadow" \
  --exit-code

CI runs this on every PR. Exit code non-zero ⇒ you forgot to db:migrate.

Prod migration in the API containerprisma migrate deploy runs at boot (see apps/api/Dockerfile CMD). It applies any pending migrations in order; a no-op when the DB is up-to-date. Failure aborts the boot so the API never serves a request against an inconsistent schema.

Common debugging recipes

"Why doesn't my Bearer token authenticate?"

Both the cookie AND the Authorization header are accepted. The middleware extracts the token manually (see apps/api/src/middleware/auth.ts@fastify/jwt v9 has a known bug where the cookie config short-circuits the header path). If you've just edited that file, restart the API: tsx watch sometimes misses the change for module-level singletons.

"API responds 401 on /api/nodes but works fine on /api/auth/login"

Login is unauthenticated. Everything under /api/nodes/* (except /heartbeat) is requireAuth. Mint a token via /api/auth/login and pass it as Authorization: Bearer ….

"Agent heartbeats arrive but the dashboard doesn't update in realtime"

The dashboard SSE connection needs the hb_token cookie. The login flow sets it; if you cleared cookies, log out and back in. EventSource can't set custom headers, so the Bearer alone won't reach /api/stream/*.

"Preset YAML changes aren't picked up"

The loader caches at first call. Restart the API. There's no file-watcher yet because preset edits in prod should be code-reviewed PRs anyway.

"Hydration mismatch on the dashboard"

A browser extension is injecting attributes into <body> before React hydrates (Bitdefender, Honey, etc). The root layout has suppressHydrationWarning to silence the warning — your component tree hydration is still verified normally.

Editor config

VS Code: install ESLint, Prisma, Tailwind CSS IntelliSense. JetBrains: enable the same plugins. Both repos work fine with default Prettier settings (no .prettierrc — we use Prettier defaults).

TypeScript settings live in each workspace's tsconfig.json. The web app uses moduleResolution: 'bundler'; the API uses the same. Both target ES2022.

Conventions

  • No comments that just repeat code. Only explain non-obvious why (a hidden constraint, a workaround, a subtle invariant).
  • Trust internal callers. Validate at the system boundary (HTTP body via Zod, env vars at boot). Don't pepper internal helpers with defensive checks.
  • Idempotent writes. Anything an agent does should be safe to repeat: preset steps re-run cleanly, heartbeat upserts, token revoke is a no-op on already-revoked rows.
  • One step per concern in presets. Easier to read, easier to rerun a failed step manually.
  • Tests hit Postgres. We had mocked-DB tests bite us; everything that touches Prisma goes through a real hyprbox_test database.

Adding a new route

  1. New file in apps/api/src/routes/<area>.ts, exporting a FastifyPluginAsync.
  2. Wire it in apps/api/src/index.ts: await app.register(myRoutes, { prefix: '/api/<area>' }).
  3. Add a spec in apps/api/test/<area>.spec.ts. Don't forget to register the plugin in test/helpers.ts:buildTestApp() too — that's bit us before.
  4. Gate writes behind requireAuth (per-route preHandler, not plugin-wide — the plugin-wide hook interacts badly with @fastify/jwt cookie config).

Adding a new step type to presets

  1. Define the Zod schema in apps/api/src/lib/presets/schema.ts (extend baseStep, add to the discriminatedUnion).
  2. Add a renderXxx(step, vars) function in apps/api/src/lib/presets/render.ts.
  3. Wire it in the switch inside renderStep.
  4. Add an example in one of the built-in presets.
  5. Document it in docs/PRESETS.md under "Step types".