Benchflow is a local makerspace coordinator desk: one workspace for the certified members, machine
bookings, maintenance blackouts, and material reservations that keep a shared workshop running. It
is a Preact 10 + Vite 7 single-page client on a Hapi 21 server, with SQLite storage through
better-sqlite3, written end to end in TypeScript.
Connected workflows:
- Dashboard and analytics. Live metrics (active members, available machines, upcoming bookings, low-stock materials, utilization), the booking pipeline, the next bookings, and warning/info alerts, all computed from stored rows.
- Member roster. Create members with their certifications selected at creation time, search and filter the roster, and suspend or reactivate a member.
- Conflict-aware machine booking. A new booking is checked against member status, machine status, the machine's required certification (valid at the booking end), member overlap, machine overlap, and scheduled maintenance blackouts before materials are reserved.
- Inventory reservations. Active bookings hold
reservedstock, signed adjustments changeon_hand, and completing or cancelling a booking settles stock atomically. - Machines, maintenance, and audit. Take machines in and out of service, schedule maintenance blackouts, and read the append-only activity trail written by every mutation. Bookings download as CSV.
The full contract, seed data, and acceptance criteria live in SPEC.md.
Not implemented in this release: there is no authentication or session handling (the desk is meant
for one trusted local operator, and roles are descriptive); certifications can only be chosen when
a member is created — editing or removing them afterwards is not exposed by the API or the UI;
there is no JSON export bundle and no activity CSV export (the only export is
GET /api/export/bookings.csv); maintenance blocks are created as scheduled and cannot be
completed or cancelled through an endpoint; and booking edits are limited to status transitions
(cancel and rebook to change a window or materials).
Three commands from a fresh clone:
npm ci
npm run build
npm startThen open http://127.0.0.1:4173. The health probe is
http://127.0.0.1:4173/api/health, and npm start serves the compiled client and the API from one
process, seeding the deterministic demo workshop the first time it opens a database file.
Requirements: Node.js 22.12 or newer (CI uses Node 24 LTS) and npm 10+. Only the browser tests
need Chromium, installed once with npx playwright install --with-deps chromium.
| Variable | Default | Notes |
|---|---|---|
BENCHFLOW_DB_PATH |
./benchflow.sqlite |
SQLite file, resolved against the working directory; parent directories are created automatically |
BENCHFLOW_RESET_DB |
unset | 1 removes the target file and its -journal/-wal/-shm siblings before opening a file database; ignored for :memory: |
PORT |
4173 |
HTTP port for the API and the production client |
HOST |
0.0.0.0 |
Bind address |
BENCHFLOW_CLIENT_DIR |
dist/client |
Optional override for the built client directory that is served as static assets |
The database is a single file, so a backup is a file copy — stop the server first, or use
sqlite3 benchflow.sqlite ".backup backup.sqlite" while it runs.
| Command | What it does |
|---|---|
npm run dev |
Vite dev server on http://127.0.0.1:5173, proxying /api to http://127.0.0.1:4173 |
npm run dev:server |
Build the server and run it so /api is available while developing |
npm run build |
Compile the server to dist/server and bundle the client to dist/client |
npm start |
Run node dist/server/index.js (requires npm run build first) |
npm run lint |
ESLint over src, tests, and config files |
npm run format:check |
Prettier check |
npm run typecheck |
tsc --noEmit for the client, shared contracts, and tests |
npm test |
Vitest unit and integration suites |
npm run test:unit |
Vitest over tests/unit |
npm run test:integration |
Vitest over tests/integration (real Hapi server and a real SQLite file) |
npm run test:e2e |
Playwright Chromium against the built app on port 4173 |
All routes live under /api. Every JSON response uses one envelope — { "data": ... } on
success — except GET /api/health, which returns a raw status object, and
GET /api/export/bookings.csv, which returns text/csv. Failures always answer with the same JSON
shape, including unknown /api routes:
{
"error": {
"code": "CONFLICT",
"message": "ShopBot CNC Router is already booked by booking-ava-cnc overlapping 2026-10-15 from 09:30 to 10:30.",
"fields": {
"bookingId": "booking-ava-cnc",
"startsAt": "2026-10-15T09:00:00.000Z",
"endsAt": "2026-10-15T10:30:00.000Z"
},
"details": {
"bookingId": "booking-ava-cnc",
"startsAt": "2026-10-15T09:00:00.000Z",
"endsAt": "2026-10-15T10:30:00.000Z"
}
}
}fields and details are aliases that carry structured context when the failure has any.
| Method | Route | Input | Success | Failure codes |
|---|---|---|---|---|
| GET | /api/health |
— | 200 raw { status, service, storage, version, uptimeSeconds } |
— |
| GET | /api/dashboard |
— | 200 { data: DashboardData } |
— |
| GET | /api/analytics |
— | 200 { data: Analytics } |
— |
| GET | /api/certifications |
— | 200 { data: Certification[] } |
— |
| GET | /api/members |
?q= (name/email search), ?status=active|suspended |
200 { data: Member[] } |
VALIDATION_ERROR |
| POST | /api/members |
{ name, email, role?, certificationIds? } — up to 5 certifications selected at creation |
201 { data: Member } |
VALIDATION_ERROR, CONFLICT (email already registered) |
| PATCH | /api/members/{id}/status |
{ status: "active" | "suspended" } |
200 { data: Member } |
VALIDATION_ERROR, NOT_FOUND, ACTIVE_BOOKING |
| GET | /api/machines |
— | 200 { data: Machine[] } |
— |
| PATCH | /api/machines/{id}/status |
{ status: "available" | "maintenance" | "retired" } |
200 { data: Machine } |
VALIDATION_ERROR, NOT_FOUND, ACTIVE_BOOKING |
| POST | /api/machines/{id}/maintenance |
{ startsAt, endsAt, reason } |
201 { data: MaintenanceBlock } |
VALIDATION_ERROR, NOT_FOUND, CONFLICT (booking/blackout overlap), MACHINE_UNAVAILABLE (retired) |
| GET | /api/maintenance |
— | 200 { data: MaintenanceBlock[] } |
— |
| GET | /api/materials |
— | 200 { data: Material[] } |
— |
| PATCH | /api/materials/{id}/stock |
{ delta, reason } — signed adjustment |
200 { data: Material } |
VALIDATION_ERROR, NOT_FOUND, CONFLICT (below zero or below reserved stock) |
| GET | /api/bookings |
?status=, ?from=, ?to= (ISO date or date-time, half-open window overlap) |
200 { data: Booking[] } |
VALIDATION_ERROR |
| POST | /api/bookings |
{ memberId, machineId, startsAt, endsAt, purpose, materials? } |
201 { data: Booking } (status requested) |
VALIDATION_ERROR, NOT_FOUND, MEMBER_SUSPENDED, MACHINE_UNAVAILABLE, CERTIFICATION_REQUIRED, CONFLICT, INSUFFICIENT_STOCK |
| PATCH | /api/bookings/{id}/status |
{ status } — allowed transitions only |
200 { data: Booking } |
VALIDATION_ERROR, NOT_FOUND, INVALID_TRANSITION, CONFLICT (stock settlement) |
| GET | /api/activity |
?limit= (1–100, default 50) |
200 { data: Activity[] } newest first |
VALIDATION_ERROR |
| GET | /api/export/bookings.csv |
— | 200 text/csv attachment benchflow-bookings.csv |
— |
Stable machine-readable error codes: VALIDATION_ERROR (400), NOT_FOUND (404),
CONFLICT, CERTIFICATION_REQUIRED, INSUFFICIENT_STOCK, INVALID_TRANSITION,
MEMBER_SUSPENDED, MACHINE_UNAVAILABLE, and ACTIVE_BOOKING (all 409); unexpected faults become
INTERNAL_ERROR (500) without leaking details.
SQLite tables: certifications, members, member_certifications, machines, materials,
maintenance_blocks, bookings, booking_materials, and activity. reserved on materials is a
stored column kept transactionally consistent with non-terminal bookings, and
available = onHand - reserved.
The deterministic seed runs only when the corresponding tables are empty, uses fixed ids and the reference day 2026-10-13, and contains:
- 5 certifications:
cert-wood,cert-cnc,cert-laser,cert-metal,cert-print. - 5 members:
member-ava(Ava Chen, manager, active),member-ben(Ben Ortiz, active),member-cora(Cora Malik, suspended),member-dee(Dee Nakamura, active),member-eli(Eli Thompson, suspended). - 5 machines:
machine-cnc-01ShopBot CNC Router (available, needscert-cnc, 45/h),machine-laser-01Epilog Laser Cutter (available,cert-laser, 30/h),machine-lathe-01Jet Metal Lathe (maintenance,cert-metal, 40/h),machine-printer-01Prusa 3D Printer (retired,cert-print, 25/h),machine-saw-01Panel Saw (available,cert-wood, 20/h). - 5 materials:
material-plywood,material-mdf,material-filament,material-aluminium,material-acrylic— including PLA filament below its reorder point, which drives the low-stock alert. - 4 bookings across 2026-10-09 to 2026-10-20: two completed, one confirmed, one requested. The active ones reserve 6 sheets of plywood and 3 sheets of acrylic.
- 1 maintenance block:
block-lathe-serviceonmachine-lathe-01, 2026-10-18 08:00–12:00 UTC. - 13 activity entries, plus one new row for every later mutation.
npm testruns the Node-based Vitest suites.tests/unitcovers contracts, transition tables, conflict and cost math, and store rules;tests/integrationboots the real Hapi server against a temporary.tmp/*.sqlitefile and verifies persistence, transactionality, and error envelopes.npm run test:e2eruns the Playwright journey suite against the production build; the config startsnpm starton port 4173 withBENCHFLOW_DB_PATH=.tmp/e2e.sqliteandBENCHFLOW_RESET_DB=1, so every run begins from the same seeded workshop.- CI (
.github/workflows/ci.yml) runs lint,format:check, typecheck, the unit and integration suites, the production build, the Playwright suite, and a packaged-server smoke test that fails with the server log when/api/healthdoes not answer within 30 seconds.
MIT — see LICENSE.