Skip to content

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:

npx prisma migrate deploy && npx prisma db seed && npm run db:reconcile-rbac

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 (SCOPES and ACTION_REQUIREMENTS in rbac-catalogue.ts) are typed against ENTITIES[number]['key'] via satisfies, so a typo fails at compile.
  • The __bootstrap__ year sentinel is a fixed string (canonical constant: BOOTSTRAP_YEAR_NAME in src/academic-years/academic-years.constants.ts, imported by the seed) — service code, the setup-wizard e2e, and tools/reset-tenant.sql filter on it explicitly. Don't rename without auditing every consumer. The academic-years API excludes it by name everywhere: it never appears in GET /academic-years, and GET/PATCH/DELETE by its id answer ACADEMIC_YEAR_NOT_FOUND (it is DRAFT, so without the exclusion it would be renameable/deletable).
  • Seed runs are intended to be idempotent in non-local modes. If a new addition needs delete-and-recreate semantics, it must run only under NODE_ENV === 'local'.

Drift guards

  • src/permissions/rbac-catalogue.drift.spec.ts — verifies every seed SCOPES[entity] entry has a matching *_SCOPES runtime constant (empty array allowed for descriptor-only scopes). See feedback memory feedback_rbac_drift_check.
  • src/common/rbac-deploy-reconciliation.drift.spec.ts — pins Railway's load-bearing migrate deploy → seed → db:reconcile-rbac ordering so hosted all-preset convergence cannot disappear during infrastructure edits.
  • The getActiveYear invariant ("at most one ACTIVE year per tenant") is enforced by the partial unique index uniq_academic_year_active_per_tenant (migration 20260525150000_active_year_unique_and_invitation_token_ttl), not the seed itself.