Files
mrp-qrcode/docs/ARCHITECTURE.md
T
2026-04-20 15:49:01 -05:00

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.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 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/<kind>/<sha256>.<ext>, 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.