Turborepo (pnpm workspaces) with all dependencies pinned to latest mutually-compatible versions: turbo 2.9, TypeScript 6, Fastify 5, React 19, Vite 8, better-sqlite3 12 + Drizzle ORM 0.45. Layout: - apps/server Fastify backend (local JWT auth + role guard, /health) - apps/web React 19 + Vite 8 operator SPA - packages/db Drizzle schema on SQLite/WAL; append-only events + users - packages/devices reader/printer/relay adapter interfaces (intent-only relay) - packages/shared shared domain types Architecture constraints from the design wiki are encoded in the scaffold: append-only hash-chained + signed event log, device-agnostic adapters, "a barrier is not a door" (relay expresses intent only), fully-local offline-first auth. wiki/ is an LLM-maintained Obsidian knowledge base (28 pages) ingested from the architecture & design notes, with its own maintenance schema. Verified: pnpm install, full turbo build (5/5), server boots and serves /health, drizzle-kit generates the initial migration.
4.5 KiB
Parking System — Project Guide
A parking-management system: a web app on a dedicated, hardened Linux appliance, deployed on-site at a parking facility. Two forces shape almost every decision: offline-first operation and a threat model whose primary adversary is the legitimate operator at the booth (not an outsider). Keep both front of mind.
Repository layout
This directory is a Turborepo monorepo. App code lives here; the knowledge base lives in
wiki/.
parking-system/
├── CLAUDE.md # this file — app development guide
├── package.json # turborepo root
├── turbo.json
├── apps/
│ ├── server/ # Fastify backend (device drivers, API, auth); serves the SPA
│ └── web/ # React + Vite SPA (operator UI)
├── packages/
│ ├── db/ # Drizzle ORM schema + migrations (SQLite local; PostgreSQL sync target)
│ ├── devices/ # device adapters behind shared interfaces (reader/printer/relay)
│ └── shared/ # shared types/utils
└── wiki/ # LLM-maintained knowledge base (Obsidian vault) — see wiki/CLAUDE.md
Code layout above is the intended target; scaffold packages as the work reaches them rather than all up front.
The wiki is the knowledge base — consult it first
wiki/ is an LLM-maintained design knowledge base (the "LLM Wiki" pattern). It is not app
code and has its own schema at wiki/CLAUDE.md. Before making architectural decisions or
implementing a subsystem, read the relevant wiki pages for the rationale, rejected
alternatives, and open questions:
- Start at
wiki/overview.md; catalog inwiki/index.md. - Settled decisions:
wiki/decisions/standing-decisions.md. - Unsettled, procurement-driving items:
wiki/decisions/open-questions.md— do not hard-code around these without flagging them.
When app work surfaces a new design fact, decision, or contradiction, update the wiki
following wiki/CLAUDE.md (ingest/query/lint workflows). Source documents go in wiki/raw/.
Stack (settled)
All dependencies are MIT / Apache / BSD — a hard constraint to avoid vendor lock-in and
license rug-pulls. See wiki/entities/technology-stack.md for the full table and rationale.
| Layer | Choice |
|---|---|
| Monorepo | Turborepo |
| Backend | Node.js + Fastify |
| Frontend | React (SPA, Vite), served by Fastify |
| Local DB | SQLite (better-sqlite3) + Drizzle ORM (Drizzle Kit) |
| Remote sync target | PostgreSQL (deferred — not a runtime dependency) |
| Auth | Local JWT (@fastify/jwt) + bcrypt + role guard (admin/operator/cashier/readonly) |
Architecture constraints that bind the code
These are not negotiable defaults — they come from the threat model and safety analysis:
- Offline-first. Nothing in core operation may depend on a network. Auth, DB, and device decisions must work air-gapped. No external identity provider; no cloud runtime dependency.
- Append-only, signed event log. Entry/exit events are never edited or deleted — a "void" is itself an appended event. Events are hash-chained (each stores the prior event's hash) and signed by an ATECC608 secure element. This is the core anti-fraud mechanism; don't add update/delete paths to event records.
- Device-agnostic adapters. Business logic talks only to interfaces (reader/printer/relay),
never to a device SDK. Hardware swaps = a new adapter in
packages/devices, nothing else. - A barrier is not a door. Never drive a barrier as a timed "open for N ms" auto-close.
Physical safety lives in the barrier operator's firmware; the app only ever expresses intent
("open"). Relay interfaces are
pulseOpen, never timed close. - Fail-state. On power/network/host loss: entry fails closed, exit fails open (never trap a vehicle — often a legal egress requirement).
- Network isolation for access controllers. The UHPPOTE controller speaks unauthenticated UDP; it must sit on an isolated VLAN reachable only by the host. Treat its event log as tamper-evident (host-side index tracking), not tamper-proof.
- Keep PCI scope out of the app. Payments go through a standalone bank-certified P2PE terminal — the application must not handle card data.
For the full reasoning behind each, follow the links from wiki/overview.md.
Conventions
- TypeScript throughout. Match the style of surrounding code.
- Confirm before destructive or outward-facing actions. Commit/push only when asked.