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-global rows (tenantId IS NULL):

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 9 global role presets + their grants Code authors the baseline; platform-admin edits them via backoffice PATCH /roles/presets.
backfillMissingPresetRoles tenant clones of presets a tenant lacks entirely Additive-only (2026-07-02): existing tenants receive presets authored after provisioning (e.g. secretary / curriculum_coordinator); never touches roles a tenant already has, so per-tenant grant edits survive.

All three are idempotent and additive — re-running never deletes rows, so platform-admin edits to presets survive every redeploy. One refinement (audit #4, 2026-06-29): seedGlobalRolePresets / cloneRolesIntoTenant now re-apply an existing preset grant's access levelrolePermission.upsert carries update: { access } — so a code-side READ↔WRITE change converges on reseed; action grants stay presence-only (update: {}). Caveat (still additive for presence): grants/rows removed from code are not pruned — an orphaned scope/action row is inert because guards look up by key. A grant removal that must reach the DB is an explicit DELETE migration, never the seed. The test/rbac-grants.db-sync.e2e-spec.ts drift guard turns the build red when DB preset grants diverge from the code definition (expectedPresetGrants) in either direction, so a forgotten removal is caught.

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. No tenants, role clones, users, or fixtures — there is no tenant-provisioning workflow yet, so nothing tenant-scoped is safe to author from a seed. Production. Re-runs on every deploy.

Deploy wiring

railway.toml runs npx prisma migrate deploy && npx prisma db seed uniformly in every environment — there is no prod-specific override. Safety lives in the seed itself (NODE_ENV gate + resetDatabase's hard refusal of any non-localhost DATABASE_URL), not in the deploy command.

Role lifecycle across the tiers

Role presets are cloned, not referenced: seedGlobalRolePresets (Tier 1) authors the tenantId IS NULL baseline; applyRolescloneRolesIntoTenant (Tier 2) copies them into each tenant. Editing a global preset — in code or via backoffice — does not retroactively update existing tenants' cloned roles (clone-not-propagate); only newly-provisioned tenants pick up the current presets. Pushing a preset change to live tenants is a deliberate one-off migration, never a seed side-effect.

Local tenants and credentials

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

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 preset roles (admin, principal, hr, department_head, teacher, referent, student) per tenant, 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 ('__bootstrap__') — service code and the setup-wizard filter on it explicitly. Don't rename without auditing every consumer.
  • 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.
  • 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.