Seeding¶
prisma/seed.ts prepares platform metadata and, according to NODE_ENV, development data. Seed is a database mutation, not an application startup check. Establish the target and mode before running it.
1. Two tiers¶
Platform baseline¶
These steps run in every environment, including production:
| Seeder | Purpose |
|---|---|
applyRbacCatalogue |
Permission entities, scopes, actions, action requirements and field mappings |
applyDefaultRoomTypes |
Create missing shared room-type defaults |
seedGlobalRolePresets |
Global preset roles and grant prerequisites |
backfillMissingPresetRoles |
Missing tenant clones of recognized presets |
reconcileFrozenPresetRolesIntoTenants |
Exact frozen-preset grants during a direct seed run |
backfillStaffProfileRoleAssignments |
Missing user-only Staff profile-role bindings |
Room-type defaults are create-only; human-managed global edits survive reseeding. CANTEEN is name-protected because application behavior depends on it.
Catalogue row creation and preset convergence are distinct. Existing catalogue entities/scopes use empty update arms; do not assume a label or description edit in the seed updates retained rows. Removed catalogue keys may still have custom-role or structural dependants. Plan explicit metadata changes or retirement rather than deleting them indiscriminately.
Environment data¶
NODE_ENV |
Behavior |
|---|---|
local |
Destructive reset, platform baseline, development tenants, role clones, local preset catalogues, users and E2E school fixtures |
development / stage |
Idempotent tenant/role/admin fixtures; no full fixture school or catalogue reset |
production |
Exit after platform baseline; no fixture tenants or users |
The entry point explicitly distinguishes local and production; other values enter its hosted branch. Set NODE_ENV deliberately. E2E database provisioning runs the full seed with local mode on disposable clones; merely setting test mode does not select the full seed.
The local reset has a host guard, but a localhost connection may still point to valuable data or a tunnel. Never use the environment label or guard as a substitute for target confirmation and authorization.
Hosted development also creates the platform-admin fixture; stage does not. Hosted admin passwords come from SEED_ADMIN_PASSWORD; the password helper refuses its known local fallback outside local/test when invoked. Production baseline does not create those admins.
Evaluation-scale, subject-level and curriculum preset fixtures are local-only. Hosted copies are curated through the backoffice and preserved across deploys. Room-type defaults and RBAC catalogue metadata have the separate always-on behavior above.
2. Deployment convergence¶
railway.toml runs:
npm run db:provision-app-role && npx prisma migrate deploy && npm run db:provision-app-role && npx prisma db seed && npm run db:reconcile-rbac
The final command is the authoritative preset-grant boundary. tools/reconcile-preset-roles.ts derives expected grants from prisma/seed/helpers/expected-preset-grants.ts and reconciles recognized global and tenant presets in one privileged transaction.
It:
- Checks catalogue/global-role prerequisites and refuses unknown preset keys or custom-role collisions.
- Creates missing tenant clones.
- Aligns parameter dimensions and tenant metadata with the global preset.
- Adds/updates expected scope and action grants.
- Removes unexpected grants from recognized presets.
- Checks the result before commit.
Preset grant edits made through the platform API or directly in a tenant are overwritten by this code-authoritative convergence. Existing custom roles, assignments and assignment parameters are outside the preset matrix rewrite. This does not imply a public tenant custom-role creation API.
The reconciler has a targeted cleanup for communications.groups: it refuses structural dependants and removes that descriptor when its replacement exists. Do not generalize that exception into automatic deletion of every catalogue key absent from code.
Direct prisma db seed is not equivalent to the full deployment chain. The frozen-preset pass inside seed does not perform the final all-preset reconciliation.
Manual reconciliation¶
Use the code corresponding to the target application and an authorized privileged DATABASE_URL. With the URL already supplied securely through the environment:
Dry run executes the same checks and mutations inside a transaction, then rolls it back. It is not a read-only operation: it can acquire locks. A positional URL is supported, but avoid putting credentials in shell history/process arguments.
Failure aborts that reconciliation transaction. It does not undo earlier migration or seed commits. See infrastructure and migrations.
3. Account and role fixtures¶
Shared credentials and membership classification¶
seedUser canonicalizes email and upserts credential data create-only. Existing credential hashes and versions are preserved. It creates or claims the school-local User.profileType in a transaction; an incompatible established type in that school fails.
A LoginIdentity shared across schools does not impose one global person type. Preserve User/person identity and explicit bindings rather than deleting accounts to make a fixture fit.
Management anchors¶
Management assignments need an explicit same-tenant Teacher or Staff anchor. prisma/seed/helpers/management-assignment.ts validates the role, user and recipient, protects uniqueness, and uses a serializable transaction.
Existing complete or partial anchors are preserved. A both-null anchor is filled only when the pre-existing employee evidence is unambiguous and collision-free. A person row created during a seed run is not proof of an older assignment's intended anchor. Do not choose an arbitrary latest-year person or merge assignments to suppress an error.
Profile carrier roles are user-only. backfillStaffProfileRoleAssignments deduplicates Staff snapshots by tenant/user and binds the tenant's Staff role only after that clone exists. It runs again after environment fixtures so newly created people receive the carrier too.
Runtime tenant provisioning creates the selected employee profile and its base role alongside the anchored admin assignment. The bootstrap year is a structural anchor, not a real visible school year.
Hosted admin preservation¶
prisma/seed/helpers/hosted-admin.ts checks the bootstrap email/year Staff tuple before creating a credential or User. An occupied row must already belong to the expected User; otherwise the fixture is skipped with an ID-based warning for manual review.
Seeding is not account recovery. It does not reattach orphaned people or recreate an intentionally deleted/renamed login over an occupied person. Ambiguous Staff snapshots and assignment anchors are preserved.
New hosted Staff identification codes are allocated from persisted data under the code-generation advisory lock. The helper rechecks the email/year tuple in that transaction. Unexpected constraint conflicts fail instead of being swallowed.
4. Fixture ownership and source map¶
Use prisma/seed/users.ts → E2E_CREDENTIALS through the test authentication helpers. Do not copy credentials or a changing role matrix into this chapter.
| File | Responsibility |
|---|---|
prisma/seed.ts |
Mode selection, reset and ordered tier execution |
prisma/seed/rbac-catalogue.ts |
Code catalogue and action requirements |
prisma/seed/roles.ts |
Preset role rules |
prisma/seed/helpers/expected-preset-grants.ts |
Expected scope/action matrices |
src/permissions/role-clone.ts |
Clone, missing-preset and frozen/profile helpers |
prisma/seed/tenants.ts |
Development tenants and bootstrap year |
prisma/seed/users.ts |
User fixtures and test credential constants |
prisma/seed/helpers/seed-user.ts |
Shared identity and school membership |
prisma/seed/helpers/hosted-admin.ts |
Conservative hosted admin fixtures |
prisma/seed/helpers/management-assignment.ts |
Employee-anchored management grants |
prisma/seed/e2e-fixtures.ts |
Complete development/E2E domain graph |
prisma/seed/evaluation-scale-presets.ts |
Local grading presets |
prisma/seed/subject-level-presets.ts |
Local subject-level presets |
prisma/seed/curriculum-presets.ts |
Local curriculum presets |
tools/reconcile-preset-roles.ts |
Transactional global/tenant preset convergence |
The bootstrap sentinel name comes from BOOTSTRAP_YEAR_NAME in src/academic-years/academic-years.constants.ts. Academic-year reads and mutations exclude it. Audit seed, provisioning, reset and API consumers together before changing it.
Local reset recreates rows; fixture IDs are not durable external references. Tests should resolve intended fixtures through shared helpers or own complete graphs. See testing.
5. Changing seed behavior¶
- Keep destructive fixture reset restricted to explicitly authorized disposable local databases.
- Preserve existing hosted person/account bindings and human-curated catalogues.
- Derive grants from the shared role rules; do not maintain an independent copied matrix.
- Keep catalogue prerequisites before preset reconciliation.
- Update focused tests for semantics, including repeat-run behavior and rejected ambiguity.
- Document the resulting contract here; put environment-specific execution records elsewhere.
Relevant drift guards are src/permissions/rbac-catalogue.drift.spec.ts and src/common/rbac-deploy-reconciliation.drift.spec.ts. The one-active-year invariant is a database partial unique, not a seed convention.