Architecture Decisions
The load-bearing choices behind the Uploz build. The recurring rules R1–R6 are the constraints these decisions encode — each backed by an accepted Architecture Decision Record — alongside the cross-cutting stack choices that frame them. ADR-backed cards link to the full record in docs/adr for the complete context and consequences.
Recurring rules
- R1DB is the enforcement boundary
- R2SQL-first migrations, Prisma types-only
- R3One shared Realtime hook
- R4Click-to-Move, not drag-and-drop
- R5Packages export TS source
- R6Self-hosted Supabase on Coolify
Decision records
ADR-backed decisions (R1–R6) first, then cross-cutting stack choices. Each card links to its source record where one exists.
Database as the enforcement boundary
Security & Integrity
PostgreSQL is the authoritative enforcement boundary. RLS policies enforce tenant isolation; integrity rides NOT NULL / CHECK / UNIQUE / FK / exclusion constraints; atomic transitions are guarded by constraints and transactional functions.
Why
Logic in the client or app layer is bypassable and drifts as call sites grow. Putting authorization and invariants in the database makes a violation structurally impossible regardless of caller, and lets tests assert security at the data layer (the T015 keystone gate runs against real Postgres).
ADR 0001 · docs/adr/0001-db-as-enforcement-boundary.md
Supabase CLI owns migrations; Prisma is types-only
Schema Lifecycle
The Supabase CLI is the single owner of schema migrations, authored as SQL. Prisma is used only for generated TypeScript types; we do not run prisma migrate or treat schema.prisma as authoritative.
Why
The schema leans on Postgres-native features (RLS, triggers, exclusion constraints) that Prisma Migrate models poorly. Two migration owners would diverge from what the database actually enforces; one SQL-first history keeps the source of truth singular and lossless.
ADR 0002 · docs/adr/0002-supabase-cli-owns-migrations.md
Internal packages export TypeScript source
Monorepo Build
Internal packages export their TypeScript source directly — package exports point at src/ rather than a built dist/. Consumers compile that source as part of their own build.
Why
For never-published internal packages, a per-package dist build adds latency and a class of stale-dist bugs. Source exports give no build-order graph, immediate cross-workspace changes, and "go to definition" that lands on real source without declaration maps.
ADR 0003 · docs/adr/0003-source-exporting-internal-packages.md
A single Realtime hook
Realtime
All Supabase Realtime access goes through one shared hook. Components consume live data through it and never open ad-hoc channels; channel lifecycle and event-to-state reconciliation live in one place.
Why
Per-component subscriptions accumulate duplicate channels, inconsistent reconnection logic, and races where the same event is applied differently. Centralizing gives one surface to instrument and test — the drift monitor and live E2E target a single, well-defined entry point.
ADR 0004 · docs/adr/0004-single-realtime-hook.md
Click-to-Move over drag-and-drop
Interaction
The move interaction is Click-to-Move: select an item, then select a destination, committing the move as a discrete "move X to Y" intent rather than a continuous drag gesture.
Why
Drag-and-drop carries poor touch support, accessibility gaps, and flaky hit-testing. A discrete two-step move is keyboard- and touch-operable by construction, maps cleanly to a server action and the single Realtime hook, and makes E2E tests deterministic.
ADR 0005 · docs/adr/0005-click-to-move-over-dnd.md
Self-hosted Supabase on Coolify
Deployment
Run a self-hosted Supabase stack (Postgres, Auth, Realtime, Storage) on our own infrastructure, orchestrated and deployed via Coolify with Git-driven deploys and managed secrets.
Why
Managed cloud cedes control of data residency, Postgres version/extensions, and cost at scale. Self-hosting keeps the full Postgres feature set the schema relies on (ADR 0001) and fits an existing self-managed footprint; the same SQL migrations give local/CI/prod parity.
ADR 0006 · docs/adr/0006-self-hosted-supabase-coolify.md
Next.js App Router + React Server Components
Stack
The web app and this dev-admin cockpit are built on Next.js with the App Router, defaulting to React Server Components and reaching for client components only where interactivity demands it.
Why
Server-first rendering keeps cockpit pages static and data-local (no network at render), pushes interactivity to the edges, and pairs with the single Realtime hook for the live surfaces that genuinely need it.
Shared validation-engine package
Stack
Cross-cutting readiness and gate validation lives in the @uploz/validation-engine package, consumed by the web app, the worker, and this cockpit alike.
Why
One engine keeps the gate semantics identical everywhere they are evaluated, mirroring the database-as-truth principle at the application layer while the database remains the final enforcement boundary (ADR 0001).
Strict TypeScript across the workspace
Stack
Every package and app compiles under strict TypeScript, with shared types generated from the database (via Prisma, per ADR 0002) feeding compile-time safety.
Why
Because internal packages export source (ADR 0003), strict settings must be compatible workspace-wide; uniform strictness catches integration errors at compile time rather than at a live event.
Decision log is illustrative cockpit project-tracking state authored in src/data/decisions, mirroring the accepted records in docs/adr. ADRs are immutable once accepted; revisit by adding a superseding record.