BusinessDecisions
42 days

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.

9 decisionsAccepted 9Proposed 0Superseded 0

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.

ADR 0001R1

Database as the enforcement boundary

Security & Integrity

Accepted

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

ADR 0002R2

Supabase CLI owns migrations; Prisma is types-only

Schema Lifecycle

Accepted

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

ADR 0003R5

Internal packages export TypeScript source

Monorepo Build

Accepted

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

ADR 0004R3

A single Realtime hook

Realtime

Accepted

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

ADR 0005R4

Click-to-Move over drag-and-drop

Interaction

Accepted

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

ADR 0006R6

Self-hosted Supabase on Coolify

Deployment

Accepted

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

Stack

Next.js App Router + React Server Components

Stack

Accepted

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.

Accepted
Stack

Shared validation-engine package

Stack

Accepted

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).

Accepted
Stack

Strict TypeScript across the workspace

Stack

Accepted

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.

Accepted

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.