Seeding¶
The seed script (prisma/seed.ts) bootstraps the platform baseline (RBAC
catalogue, room-type defaults, global role presets) and — outside production —
tenants, role clones, test users, and E2E fixtures.
The two-tier model¶
The seed is split into two tiers. Tier 1 runs in every environment on every
deploy; Tier 2 is gated by NODE_ENV. This split is the answer to "how do the
preseeded platform tables stay consistent across prod and dev": they are
code-versioned app metadata, not tenant data, so they ship with the code
everywhere.
Tier 1 — Platform baseline (always, including production)¶
The platform baseline plus its cross-tenant role-maintenance steps:
| Seeder | Rows | Notes |
|---|---|---|
applyRbacCatalogue |
entities × scopes × actions × scope-field mappings | Every permission guard resolves against these. Must track the deployed code. |
applyDefaultRoomTypes |
global room-type defaults (incl. TO_BE_DEFINED placeholder) |
Shared, FK'd directly by tenant rooms. Create-only, so backoffice rename/delete of these globals survives a re-seed; only CANTEEN is name-protected. |
seedGlobalRolePresets |
the 11 global role presets + their grants | Authors the code baseline and the global rows required by the final deployment reconciler. |
backfillMissingPresetRoles |
tenant clones of presets a tenant lacks entirely | Seed-local prerequisite: existing tenants receive presets authored after provisioning (for example administrative_assistant in 2026-08-28). The helper is additive, but the final deployment reconciler subsequently makes every recognized preset exact. |
reconcileFrozenPresetRolesIntoTenants |
tenant clones of the frozen presets | Seed-local safety retained from 2026-07-22 for direct seed runs. The final deployment reconciler covers these and every management preset transactionally. |
backfillStaffProfileRoleAssignments |
missing user-only staff UserRole bindings |
Additive/idempotent (2026-08-21): deduplicates academic-year Staff snapshots by (tenantId, userId) and binds the tenant's frozen staff role to users activated before that role existed. |
These seed steps are idempotent. Within a direct seed invocation, catalogue rows and management presets retain their older additive semantics, while frozen presets receive the 2026-07-22 add/update/prune safety pass. Hosted deployment does not stop at that intermediate state.
Every preset is code-authoritative after deployment (2026-08-28). Railway
runs tools/reconcile-preset-roles.ts after migrations and seed. In one
privileged transaction it creates missing tenant clones, aligns preset metadata,
adds or updates the expected scope/action grants, deletes every unexpected grant
from each recognized global or tenant preset, and verifies the complete matrix
before commit. A mismatch or prerequisite failure rolls back and blocks the
deployment. Historical tenant edits and platform-admin global-preset edits are
therefore intentionally overwritten; custom roles and role assignments are not
touched. See the
authoritative-preset reconciliation iteration spec.
Permission catalogue rows remain additive because deleting a removed/renamed scope or action may affect custom roles or structural dependants. The all-preset reconciler still removes grants to such a row from recognized presets; deleting the catalogue row itself or retiring a preset role key requires an explicit product-aware migration.
Tier 2 — Environment data (gated by NODE_ENV)¶
NODE_ENV |
Tier-2 behaviour | Safe on |
|---|---|---|
local |
Destructive. Truncates everything first, then (after Tier 1) recreates 4 tenants + role clones + the platform preset catalogs (eval-scale + subject-level + curriculum) + every dev/E2E user + the E2E fixture school. | Dev DB only. Used by npm run docker:reset and db:setup. NEVER against a hosted DB. |
development or stage |
Idempotent. Upserts 4 tenants + role clones + a single gaetano.perna@universidata.it user per tenant. On development ONLY (not stage), additionally seeds the sadmin@e2e.dev platform-admin (same god-account credentials as local/E2E) so platform-admin-gated backoffice surfaces are reachable on hosted dev. No destructive deletes; no E2E fixtures; no preset catalogs — the eval-scale, subject-level, and curriculum presets are human-curated on hosted and preserved across deploys. |
Hosted dev / stage. Re-runs on every deploy. |
production |
Tier 1 only, then exits. Creates no tenants, users, or fixtures. It prepares existing tenants' missing/frozen role clones and missing Staff profile bindings; the following deployment reconciler makes every preset exact. Tenant creation itself runs through the backoffice provisioning transaction. | Production. Re-runs on every deploy. |
Deploy wiring¶
railway.toml runs the following uniformly in every environment — there is
no prod-specific override:
The reconciler reads the privileged deployment DATABASE_URL. Ordering is
load-bearing: migrations establish schema/data transitions, the seed authors new
catalogue/global/missing-preset prerequisites, and the final transaction makes
every recognized preset matrix exact. A non-zero exit at any step blocks the
deployment.
For a manual authoritative repair, run the same tool from the exact code revision deployed to the target environment:
npm run db:reconcile-rbac -- "<PUBLIC_DATABASE_URL>"
# Execute every check and mutation, then roll the transaction back:
npm run db:reconcile-rbac -- --dry-run "<PUBLIC_DATABASE_URL>"
tools/reconcile-preset-roles.ts calculates the role matrix directly from
expectedPresetGrants, reconciles both global presets and every tenant clone in
one transaction, creates missing tenant clones, and verifies the result before
commit. It intentionally replaces tenant customization on preset roles; custom
roles and role assignments are untouched. The URL must be a privileged public
database URL capable of bypassing FORCE RLS, like the migration/seed credential.
The script never prints the URL. When no positional URL is supplied it reads
DATABASE_URL, which is how Railway invokes it. Manual URLs still target one
environment at a time.
The 2026-08-28 role rollout originally followed both paths deliberately. The
data migration
20260828120000_realign_management_role_grants reconciles the changed
Director/Curriculum Coordinator grants on existing editable preset clones;
the following Tier-1 seed creates the missing Administrative Assistant preset
and clones it into every tenant. The new final all-preset step then verifies and
repairs the complete matrix, making later role changes independent of bespoke
grant migrations for steady-state convergence.
Role lifecycle across the tiers¶
Role presets are cloned, not referenced: seedGlobalRolePresets (Tier 1)
authors the tenantId IS NULL baseline; provisioning copies it into a tenant.
Those copies are independent runtime rows, but no longer independent ownership
boundaries. After every hosted seed, db:reconcile-rbac aligns the global rows
and every tenant clone to expectedPresetGrants: additions, removals, and access
changes all propagate. The older frozen-only in-seed reconciler remains as
direct-seed safety, not as the hosted convergence boundary. Custom roles remain
the supported customization seam.
The Staff base role adds one account-binding step after that lifecycle:
backfillStaffProfileRoleAssignments runs only after the tenant clone exists,
creates user-only assignments (recipientType/recipientId = null), and skips a
tenant rather than ever falling back to a global/cross-tenant role. It runs in
Tier 1 for real existing users and repeats after Tier-2 fixture/admin creation so
a fresh local/hosted seed reaches the same end state. Backoffice tenant
provisioning binds staff alongside admin inside the creation transaction, so
new production tenants do not wait for the next deploy.
Local tenants and credentials¶
Authoritative role-matrix v3 rollout¶
Migration 20260820120000_realign_management_preset_v3_grants_and_labels
is the deliberate exception to clone-not-propagate for the 2026-08-20 product
realignment. It updates or inserts only the listed v3 scope/action pairs on the
global presets and every is_preset tenant clone, revokes Front Office's four
curriculum-authoring/window actions, and overwrites the three stored English
labels (HR Manager, Front Office, Department Principal). Custom roles and
unlisted preset grants are untouched. Normal seed runs retain their usual
clone/backfill behavior and are not the rollout mechanism.
Management communications grant rollout¶
Migration 20260826121000_backfill_management_communications_grants is the
same kind of deliberate exception for communications. It adds or corrects
communications.mailing_groups: WRITE and adds communications.send on every
global and tenant management preset (admin, principal, hr, secretary,
department_head, curriculum_coordinator). This closes the upgrade gap for
tenant clones created before communications existed. It does not touch custom
or profile roles, and it does not grant the admin-only
communications.management scope.
Management attendance grant rollout¶
Migration 20260826122000_realign_management_attendance_grants makes the
attendance matrix authoritative across global presets and existing tenant
clones. At that migration's point in history, Admin, Director, Front Office,
Department Principal, and Curriculum Coordinator received
attendance.register: WRITE plus attendance.take; HR was
forced to READ and any stale attendance.take or
attendance.manage_communications row is removed. The attendance policy keeps
only Admin and Front Office school-wide: other non-admin writers remained
parameter- or related-Teacher-scoped when taking the register. Director's
then-existing school-wide follow-up management was unchanged. Custom and profile
roles are untouched.
Migration 20260826123000_grant_management_attendance_followup_actions then
adds attendance.manage_communications and attendance.justify to Admin,
Director, Front Office, Department Principal, and Curriculum Coordinator in
both global presets and existing tenant clones. It removes all attendance
actions from HR defensively. This changes action grants only: attendance record
filters and the family-only justification policy remain unchanged.
Migration 20260828120000_realign_management_role_grants supersedes that
attendance baseline for the current matrix: Director is downgraded to
attendance.register: READ and loses all three attendance actions. HR remains
READ-only. Front Office and the new Administrative Assistant are school-wide
writers; Department Principal and Curriculum Coordinator remain qualified
writers. The same migration grants Curriculum Coordinator
students.health: READ; Administrative Assistant itself is created for all
tenants by the immediately following Tier-1 missing-preset backfill.
Admin-only roles-surface repair¶
Migration 20260826120000_revoke_non_admin_roles_surface_grants removes the
historical roles.management:READ rows from teacher and the five editable
management presets, across both global presets and tenant clones. The corrected
seed grants this descriptor scope only to admin, matching the admin-only
GET /roles and /role-assignments routes. Teacher would converge through the
frozen-preset reconciler; at that point the data migration was required for
additive management presets. The final all-preset deployment reconciler now
maintains the corrected state automatically. Custom roles remain untouched.
Documented in the header of prisma/seed.ts. Test-suite credentials live in prisma/seed/users.ts → E2E_CREDENTIALS and are imported by test/helpers/auth.helper.ts (single source of truth).
Admin-only department catalog writes¶
On 2026-09-03 departments.configuration WRITE and the departments.create/
delete actions were removed from every non-admin preset: Front Office and
Department Principal (and, by derivation, Administrative Assistant) now hold
READ, like every other role. This is a seed-only change in
prisma/seed/roles.ts (SECRETARY_* and DEPT_HEAD_* rule sets) with no data
migration: the transactional deployment reconciler makes every preset
code-authoritative, so existing global presets and tenant clones converge on
the next deploy. Custom roles are untouched. The change also removes those
roles' access to the per-department calendar (dates + period set), which rides
the same scope.
Module files¶
| File | Purpose |
|---|---|
prisma/seed/rbac-catalogue.ts |
Declarative catalogue of entities × scopes × actions × scope-fields. The rbac-catalogue.drift.spec.ts test guards against drift between this file and the runtime SCOPE_FIELDS / ENTITY_KEYS constants. |
prisma/seed/tenants.ts |
The 4 local tenants + the __bootstrap__ sentinel academic year (a DRAFT row, no calendar dates, used as a placeholder anchor during the setup wizard). |
prisma/seed/roles.ts |
The 11 preset roles (admin, six editable management presets, and frozen teacher/staff/referent/student) with their scope and action grants. |
prisma/seed/users.ts |
Test users + the E2E_CREDENTIALS map consumed by test/helpers/auth.helper.ts. |
prisma/seed/evaluation-scale-presets.ts |
Three platform-owned read-only EvaluationScale rows (tenantId IS NULL). Seeded in local mode only — on hosted dev/stage these are human-curated and the seed never touches them. Locally they are recreated fresh each run (post-truncate), so value IDs are NOT stable; never FK-reference them. |
prisma/seed/subject-level-presets.ts |
Platform-owned (tenantId IS NULL) IB subject-level catalog (Higher Level / Standard Level). Seeded in local mode only — hosted copies are human-curated via backoffice and preserved across deploys. |
prisma/seed/curriculum-presets.ts |
Global CurriculumPreset catalog (DB-backed runtime source), derived from the legacy positional CURRICULUM_PRESET_MAP. Seeded in local mode only — hosted copies are human-curated and preserved across deploys. |
prisma/seed/e2e-fixtures.ts |
The "E2E Test School" — a fully-populated tenant with departments, grades, students, teachers, staff, referents, curricula, etc., used by *.e2e-spec.ts. |
Conventions¶
- RBAC catalogue entries (
SCOPESandACTION_REQUIREMENTSinrbac-catalogue.ts) are typed againstENTITIES[number]['key']viasatisfies, so a typo fails at compile. - The
__bootstrap__year sentinel is a fixed string (canonical constant:BOOTSTRAP_YEAR_NAMEinsrc/academic-years/academic-years.constants.ts, imported by the seed) — service code, the setup-wizard e2e, andtools/reset-tenant.sqlfilter on it explicitly. Don't rename without auditing every consumer. The academic-years API excludes it by name everywhere: it never appears inGET /academic-years, and GET/PATCH/DELETE by its id answerACADEMIC_YEAR_NOT_FOUND(it is DRAFT, so without the exclusion it would be renameable/deletable). - Seed runs are intended to be idempotent in non-
localmodes. If a new addition needs delete-and-recreate semantics, it must run only underNODE_ENV === 'local'.
Drift guards¶
src/permissions/rbac-catalogue.drift.spec.ts— verifies every seedSCOPES[entity]entry has a matching*_SCOPESruntime constant (empty array allowed for descriptor-only scopes). See feedback memoryfeedback_rbac_drift_check.src/common/rbac-deploy-reconciliation.drift.spec.ts— pins Railway's load-bearingmigrate deploy → seed → db:reconcile-rbacordering so hosted all-preset convergence cannot disappear during infrastructure edits.- The
getActiveYearinvariant ("at most one ACTIVE year per tenant") is enforced by the partial unique indexuniq_academic_year_active_per_tenant(migration20260525150000_active_year_unique_and_invitation_token_ttl), not the seed itself.