Initial scaffold: Turborepo monorepo + design wiki
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.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// Schema notes:
|
||||
// - `events` is APPEND-ONLY. Never expose UPDATE/DELETE on it. A correction or
|
||||
// void is a new row of type 'void'. Each row chains to the previous via
|
||||
// `prevHash` and is signed by the ATECC608 (`signature`). This is the core
|
||||
// anti-fraud integrity mechanism. See wiki/concepts/append-only-event-chain.md.
|
||||
// - `users` holds bcrypt hashes + a role; auth is fully local (offline-first).
|
||||
// See wiki/entities/local-jwt-auth.md.
|
||||
|
||||
export const users = sqliteTable("users", {
|
||||
id: text("id").primaryKey(),
|
||||
username: text("username").notNull().unique(),
|
||||
passwordHash: text("password_hash").notNull(),
|
||||
role: text("role", {
|
||||
enum: ["admin", "operator", "cashier", "readonly"],
|
||||
}).notNull(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
export const events = sqliteTable("events", {
|
||||
id: text("id").primaryKey(),
|
||||
// Monotonic chain index. Gaps are alarms (see event-log-ingestion).
|
||||
index: integer("index").notNull().unique(),
|
||||
type: text("type").notNull(),
|
||||
direction: text("direction", { enum: ["entry", "exit"] }),
|
||||
lane: integer("lane").notNull(),
|
||||
source: text("source"),
|
||||
identity: text("identity"),
|
||||
occurredAt: text("occurred_at").notNull(),
|
||||
// Hash of the previous event (hex). Null only for the genesis event.
|
||||
prevHash: text("prev_hash"),
|
||||
// ATECC608 signature over the canonical event payload (hex).
|
||||
signature: text("signature").notNull(),
|
||||
});
|
||||
|
||||
export type UserRow = typeof users.$inferSelect;
|
||||
export type EventRow = typeof events.$inferSelect;
|
||||
Reference in New Issue
Block a user