# 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.db` at container start via `prisma 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 `:3000` with `HOSTNAME=0.0.0.0`. - Health probe: `GET /api/health` runs `SELECT 1` against 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 `Session` table. - Cookie `mrp_session` holds 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`, `secure` in production, path `/`, `expires` matched to DB TTL. - `lastSeenAt` is touched at most once per minute to avoid write amplification. - `purgeExpiredSessions()` is exposed for a future cron. ## Data model See [`prisma/schema.prisma`](../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) and `dedicated` (a QC operation type) via its `kind` field. - **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/v1` when step 10 formalizes the public API. - Every mutation writes an `AuditLog` row 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 just `LogoutButton`). 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//.`, and reference via `FileAsset.path`. Duplicate uploads collapse to one file on disk. ## Build-time vs. run-time - `npm run build` runs `prisma generate` and then `next build`. No database required. - `prisma migrate deploy` runs **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`, `secure` in 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_SECRET` is not yet used to sign QR tokens — that arrives in step 3. It is validated at boot so deploy-time misconfiguration fails loudly.