5.5 KiB
5.5 KiB
Architecture
Runtime
- Single container. Node 20 (alpine) running Next.js in production mode. One process serves both the UI and the REST API.
- Single volume (
/data). SQLite database (app.db), user uploads (uploads/), and backups (backups/) live side-by-side so the whole application state is one directory to back up. - Stateless app, stateful volume. Nothing in the image needs to survive a rebuild except the schema, which is applied to
/data/app.dbat container start viaprisma migrate deploy. - No background workers yet. All work happens inline on a request. PDF generation and STEP thumbnailing will stay request-scoped until throughput forces otherwise.
Request flow
Browser ──TLS── Reverse proxy ── HTTP ── Next.js (app+api) ── Prisma ── SQLite
└─► /data/uploads
- Reverse proxy terminates TLS (handled by Unraid SWAG / NPM / Caddy / Traefik etc.).
- The container listens on
:3000withHOSTNAME=0.0.0.0. - Health probe:
GET /api/healthrunsSELECT 1against SQLite.
Authentication
Two roles, no external identity providers.
Admin
- Email + password. Password hashed with bcrypt (cost 12).
- Longer session default (
ADMIN_SESSION_HOURS, default 8h).
Operator
- Name + 4-digit PIN. PIN hashed with bcrypt (cost 12).
- Device session default 12h (
OPERATOR_SESSION_HOURS). - 5 failed attempts lock the account for 15 minutes (configurable).
- The operator login UI is a tile grid of active operator names, followed by a numeric keypad; no email/username typing on a phone.
Sessions
- Server-side, backed by the
Sessiontable. - Cookie
mrp_sessionholds a random 32-byte token (base64url). Only the SHA-256 of the token is stored server-side, so a DB leak cannot hand out live sessions. httpOnly,sameSite=lax,securein production, path/,expiresmatched to DB TTL.lastSeenAtis touched at most once per minute to avoid write amplification.purgeExpiredSessions()is exposed for a future cron.
Data model
See prisma/schema.prisma for the canonical definition. Highlights:
- Project → Assembly → Part → Operation.
@@unique([assemblyId, code])and@@unique([partId, sequence])keep the hierarchy tidy. - Operation.claimedByUserId enforces single-claim at the app layer; the status/claim fields are indexed for dashboard queries.
- TimeLog is separate from Operation so a step can have multiple start/stop sessions over time.
- QCRecord supports both
inline(checkbox on the step card) anddedicated(a QC operation type) via itskindfield. - FileAsset is a single table for all uploads; parts and purchase orders reference by nullable foreign key.
- AuditLog captures mutations with before/after snapshots as JSON strings (SQLite does not have a native Json type).
Enums are stored as strings rather than Prisma enums because SQLite has no enum support; validation is done at the Zod layer.
API
- Planned surface:
/api/v1/*(REST + JSON). The auth, operator list, and health endpoints in the current commit are the internal surface and will move under/api/v1when step 10 formalizes the public API. - Every mutation writes an
AuditLogrow with actor, IP, and before/after payloads. - Zod schemas validate request bodies; they will also drive OpenAPI generation in step 10.
Front-end structure
app/login/*— public login pages (chooser, admin form, operator tile grid + keypad).app/admin/*— admin shell (requires admin session, top-nav layout).app/op/*— operator shell (requires operator session, mobile-friendly layout).app/api/*— route handlers.components/— shared client components (initially justLogoutButton).
Every protected layout calls requireAdmin() or requireOperator() in its server component, which redirects unauthenticated requests to /login. There is intentionally no edge middleware because Prisma does not run on the Edge runtime.
Files on disk
/data
├── app.db SQLite database
├── app.db-wal, app.db-shm SQLite WAL sidecars (when enabled)
├── backups/ point-in-time SQLite snapshots (future)
└── uploads/ user uploads, organized by content hash
├── step/
├── pdf/
├── dxf/
├── svg/
└── image/
Upload handling (step 2) will write content-addressed files: hash the incoming bytes, store at uploads/<kind>/<sha256>.<ext>, and reference via FileAsset.path. Duplicate uploads collapse to one file on disk.
Build-time vs. run-time
npm run buildrunsprisma generateand thennext build. No database required.prisma migrate deployruns at container start, not at build time, so the image is portable across environments.- The bootstrap admin seed runs at start too, idempotently (it does nothing if an admin already exists).
Security posture
- Passwords and PINs: bcrypt with cost 12.
- Session tokens: cryptographically random, never stored plaintext.
- Cookies:
httpOnly,sameSite=lax,securein prod. - Input validation: Zod on every route.
- Audit log for every mutation and every login attempt (success or failure).
- PIN lockout: 5 attempts / 15 min (configurable). Addresses the small 4-digit search space.
APP_SECRETis not yet used to sign QR tokens — that arrives in step 3. It is validated at boot so deploy-time misconfiguration fails loudly.