The destination of the wayfinder map: directory and module structure, the domain model as real schema files, permission boundaries expressed as code, and typed seams where the undecided parts land. Satisfies the four structural requirements earlier resolutions handed to a layout that did not exist: - a single database entry point (#11) — the pool is not exported - a scheduler entry point with actor context and no request (#13) - an entitlement assertion helper (#20, #34) — its call sites are the paid-feature list - an enumerated list of privileged RLS bypasses (#33) — three of them Seams carry real types and throw with the ticket that owns them, so the skeleton wires up and fails only where a decision is genuinely missing. Verified: tsc --noEmit clean; drizzle-kit generate produces RLS on 18 tables and 6 policies calling the release-level functions. Resolves #35 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49 lines
2.0 KiB
Markdown
49 lines
2.0 KiB
Markdown
# Migrations
|
|
|
|
**Generated SQL is never hand-edited.** Hand-editing poisons Drizzle's diff
|
|
baseline, and the baseline is what tells you the database still matches the
|
|
schema that the types are inferred from.
|
|
|
|
The split follows what Drizzle can actually diff:
|
|
|
|
| Owned by Drizzle | Owned by hand |
|
|
|---|---|
|
|
| Tables, columns, indexes | SQL functions |
|
|
| RLS policies, via `pgPolicy` beside the table | Data seeds |
|
|
| Enums | The release matrix and its versions |
|
|
|
|
```
|
|
yarn generate # schema diff -> migrations/
|
|
yarn generate:custom # empty file -> migrations/, for functions and data
|
|
yarn migrate
|
|
```
|
|
|
|
## Why policies sit beside their tables
|
|
|
|
A new table shipping with no policy is the failure mode that matters, and RLS
|
|
default-deny only helps if it was enabled on that table. Declaring the policy in
|
|
the same file as the table makes the omission visible at review time.
|
|
|
|
## Why the matrix is a data migration
|
|
|
|
The release matrix is **versioned, never updated** — a past release has to be
|
|
reconstructable when an award is protested. A rule change is an INSERT at a new
|
|
version. That is exactly what `--custom` is for.
|
|
|
|
## Ordering: why the functions are migration 0000, and why they are plpgsql
|
|
|
|
Drizzle emits an RLS policy in the **same migration as the table it guards**, so
|
|
the functions those policies call must already exist. But the functions read
|
|
`solicitation`, `participation` and `release_matrix` — which that same later
|
|
migration creates. A straightforward cycle.
|
|
|
|
`LANGUAGE sql` validates a function body at `CREATE` time and would fail. plpgsql
|
|
defers validation to first call, which breaks the cycle. For the same reason the
|
|
signatures use `text` rather than the `release_level` enum: the enum does not
|
|
exist yet either.
|
|
|
|
The cost is real and worth knowing: **a typo inside a function body is not caught
|
|
at migration time.** It surfaces the first time a policy runs — which, because
|
|
policy failures present as empty results, is the failure mode #11 already flagged
|
|
as hard to diagnose. Database-level tests over these functions are not optional.
|