Migrating from 5.7.x to 6.0.0
A production-boundary hardening release. Frontend-only users get a one-line bump; anyone calling the breach assessor or running @tantainnovative/ndpr-recipes has real work to do — including a reviewed database migration.
TL;DR — find your upgrade path
6.0.0 is not a deprecation-window release. Nothing was renamed and nothing was removed, so your build will very likely keep compiling. What changed is what the toolkit is willing to conclude: compliance duties now stay switched on until complete, correlated evidence establishes otherwise, and backend identity comes from the server instead of the request. That means a green build does not mean you are done.
| If you… | Effort | Read |
|---|---|---|
| Use components, hooks, presets, or policy generation only | One-line bump | § 5 (re-run audits), then done |
Call assessBreachNotification or the JSON audit with breach input | Code review | § 1 |
Ran create-ndpr and copied the generated API routes | Wire two seams | § 2, § 4 |
Have a populated @tantainnovative/ndpr-recipes 0.1.x / 0.2.0 database | Reviewed DB migration | All of it — start at § 3 |
pnpm up @tantainnovative/ndpr-toolkit@^6.0.0
# backend users also:
pnpm up @tantainnovative/ndpr-recipes@^0.3.0Do not run prisma db push or drizzle-kit push against an existing recipes database. The 0.3.0 schema is tenant-scoped and those commands cannot infer tenant ownership, verified DSR subjects, consent deduplication, or lossless ROPA facts. Use the reviewed migrations/0.3.0 procedure in § 3.
1. Breach duties now fail closed
In 5.x you could tell the assessor that a breach did not require notification by passing a bare boolean. A missing, malformed, or mismatched assessment also quietly resolved to “no duty”. Under NDPA 2023 S. 40 that is the wrong default — absence of evidence is not evidence of low risk. In 6.0 the two override options are force-on only:
export interface BreachNotificationOptions {
assessment?: RiskAssessment;
notification?: RegulatoryNotification;
asOf?: number;
deadlineHours?: number;
/** Force the high-risk data-subject duty on; a complete assessment is required to establish false. */
highRisk?: true;
/** Force Commission notification on; a complete assessment is required to establish false. */
notificationRequired?: true;
}The literal type true means TypeScript rejects false at compile time. If the value reaches the assessor at runtime anyway — from JSON, a database column, or an any cast — it is recorded in validationErrors, which sets valid: false and therefore ready: false. It does not silently waive the duty.
// Before (5.x) — this waived both duties
const assessment = assessBreachNotification(report, {
notificationRequired: false,
highRisk: false,
});
// After (6.0) — compile error; supply the evidence instead
const assessment = assessBreachNotification(report, {
assessment: riskAssessment, // must correlate to report.id and validate completely
});How each duty now resolves
Both duties default to true and can only be lowered by a correlated assessment — one that is present, whose breachId matches the report's id, and which passes every field check:
notificationRequired = options.notificationRequired === true
|| (correlatedAssessment?.risksToRightsAndFreedoms ?? true)
dataSubjectCommunicationRequired = options.highRisk === true
|| (correlatedAssessment?.highRisksToRightsAndFreedoms ?? true)An assessment only counts as correlated when all of the following hold. Any single failure drops it, and the duty reverts to on:
idandbreachIdare non-empty strings, andbreachIdequalsreport.id.assessedAtis a finite epoch timestamp betweenreport.discoveredAtandasOf.assessoris an object with non-emptyname,role, andemail.- All five scores —
confidentialityImpact,integrityImpact,availabilityImpact,harmLikelihood,harmSeverity— are numbers from 1 to 5. overallRiskScoreis non-negative andriskLevelis one oflow,medium,high,critical.risksToRightsAndFreedomsandhighRisksToRightsAndFreedomsare real booleans — and high risk requires plain risk to also be true, because “high risk but no risk” is impossible.justificationis a non-empty string.
Regulatory notification evidence is checked the same way: matching breachId, a sentAt between discovery and asOf, a method of email/portal/letter/other, and non-empty content. Incident chronology is enforced too: occurredAt ≤ discoveredAt ≤ reportedAt ≤ asOf.
What to do
- Grep for
notificationRequired:andhighRisk:at your assessor callsites. Anyfalseis now a type error — delete it. - Wherever you relied on that
false, supply a completeRiskAssessmentwhosebreachIdmatches the report. - Re-run every stored breach-readiness assessment. Reports that read “ready” under 5.x on thin evidence will now read not-ready — that is the correction, not a regression.
- The same force-on-only options apply to the breach input of the JSON audit surface and the
ndpr auditCLI.
Full field-by-field reference: Breach Notification Completeness.
3. Tenant-scoped recipes schema (database migration)
Consent, DSR, breach, ROPA, DPIA, lawful-basis, cross-border, and audit persistence are all tenant-scoped in 0.3.0. Global primary keys became tenant-scoped composite keys. If your recipes database is empty, generate fresh and skip to § 4. If it holds data, follow the reviewed procedure — it ships inside the package:
node_modules/@tantainnovative/ndpr-recipes/migrations/0.3.0/
README.md # the authoritative procedure — read it in full
postgresql.sql # standalone psql + Prisma (owns its BEGIN;/COMMIT;)
drizzle.sql # Drizzle-managed migration BODY (no transaction control)Pick exactly one artifact for your runner. Never concatenate them and never add BEGIN;/COMMIT; to the Drizzle body — drizzle-kit migrate supplies the outer transaction so the migration and its ledger entry commit or roll back together.
Each artifact carries one guarded placeholder. The migration aborts if it is left in place or set empty:
'__REPLACE_WITH_TENANT_ID__' -- must equal the deployed NDPR_TENANT_IDPrisma
mkdir -p prisma/migrations/202607180001_ndpr_recipes_0_3_hardening
cp node_modules/@tantainnovative/ndpr-recipes/migrations/0.3.0/postgresql.sql \
prisma/migrations/202607180001_ndpr_recipes_0_3_hardening/migration.sql
# Edit and review this exact migration.sql — especially the tenant ID.
npx prisma migrate deploy
npx prisma generateDrizzle
npx drizzle-kit generate --custom --name ndpr-recipes-0-3-hardening
DRIZZLE_MIGRATION_SQL="drizzle/0007_ndpr-recipes-0-3-hardening.sql" # exact generated path
test -f "$DRIZZLE_MIGRATION_SQL"
cp node_modules/@tantainnovative/ndpr-recipes/migrations/0.3.0/drizzle.sql \
"$DRIZZLE_MIGRATION_SQL"
# Edit and review this exact generated SQL file — especially the tenant ID.
npx drizzle-kit migrateWhat the backfill will not guess
The script assigns the tenant, normalizes column names, dedupes active consent, converts JSON to JSONB, and runs hard-invariant checks. Three things it deliberately refuses to fabricate, because inventing compliance evidence is worse than having none:
| Module | Left fail-closed | Your remediation |
|---|---|---|
| DSR | subject_id = legacy_dsr_<id> placeholders, which grant no account access | Load a reviewed request-id → account-subject-id map before enabling subject-facing DSR routes |
| ROPA | record_data stays NULL; routes return 409 | Stage snapshots and validate each with validateProcessingRecord before backfilling |
| Breach | No RegulatoryNotification is invented; readiness stays not-ready | Attach the original filing content and reference, with sentAt between discovery and asOf |
Lawful-basis activity_data and cross-border transfer_data stay nullable for the same reason. Legacy columns (internal_notes_legacy, acknowledged_at, safeguards_legacy) are retained through your rollback window — drop them only in a later reviewed migration.
Rollback reality. Before the transaction commits, any failed check rolls everything back safely. After commit but before app cutover, restore your tested snapshot — do not run 0.2.x code against renamed 0.3.0 columns. Once 0.3.0 writes begin there is no lossy down migration: freeze writes, export new audit and consent rows, and forward-reconcile under an incident plan. Take and restore-test a backup first.
4. Canonical consent responses and evidence archival
Generated consent endpoints used to return raw persistence rows — database column names, internal ids, and Date objects leaked into API responses and coupled every client to your schema. They now return the canonical ConsentSettingsshape the toolkit's own hooks and adapters consume:
// GET /api/consent → ConsentSettings | null
{
consents: Record<string, boolean>,
timestamp: number, // epoch ms, safe integer
version: string,
method: string,
hasInteracted: boolean,
lawfulBasis?: string
}If a client read record.createdAt, record.id, or any other row field, update it to the shape above. The subject is always the verified one from context — subject ids in the query string or body are ignored, not honoured.
Replay is idempotent, conflicts are explicit
A repeat POST carrying the same timestamp + version + method returns the original record instead of writing a duplicate. If that same triple arrives with a different payload it is an idempotency collision and returns 409 rather than picking a winner. Two new failure modes worth handling client-side:
- 409 — concurrent update conflict under serializable isolation. Safe to retry the identical request.
- 400 — fractional or unsafe-integer
timestamp, rejected before any database access.
DELETE archives, it no longer destroys
Accountability records are the evidence you would show the Commission, so nothing is physically deleted any more:
- Consent — sets
revokedAt, clears the active key, writes arevokedaudit entry, and responds{ success, revoked }with the number of rows revoked. History is preserved. - ROPA — archives the activity, retaining its complete evidence snapshot.
- DPIA — soft-removes, auditing the transition in the same transaction.
If any test or client asserted that a record disappeared after DELETE, update it to assert revoked/archived state instead. Business writes and their accountability entries now commit in a single transaction, so you can no longer end up with a consent change that has no audit trail.
5. Compliance rule changes — re-run your audits
Two changes alter output for unchanged input. Everyone upgrading is affected, including frontend-only users, and stored results computed under 5.x are stale:
- 2026 CAR filing deadline extended to 30 May, bundled with explicit ruleset provenance. Non-canonical, inherited, accessor-backed, out-of-range, or cross-year deadline overrides are now rejected. Re-run CAR and aggregate audit output, and confirm current NDPC guidance before you file.
- Breach duties are treated as applicable until complete correlated evidence establishes otherwise (§ 1). Re-run every stored breach-readiness assessment.
npx ndpr audit --json > audit-after-6.0.json
# diff against your pre-upgrade output and explain every change before filingScores dropping after this upgrade is the expected outcome where evidence was incomplete. Treat the delta as a work list, not a bug. See Legal Sources & Update Governance for how regulatory changes are reviewed and released.
What hasn't changed
- No component, hook, or preset was renamed or removed.
- All 22 subpath exports still resolve in ESM, CJS, and TypeScript.
- Peer range is still
react ^18 || ^19. - Same seven locales (en, yo, ig, ha, pcm, ar, fr).
NDPRProvider,NDPRThemeProvider, and the adapter contracts are unchanged.- The 5.x
@deprecatedaliases still work — includingnitdaNotificationRequiredand thestorageKey/useLocalStoragehook options. They were not removed in 6.0. - The unscoped
create-ndpr@1.0.0alias is unchanged and delegates to the latest scoped CLI.
Upgrade checklist
- Bump to
^6.0.0and runtsc. Fix anyhighRisk: false/notificationRequired: falsetype errors by supplying real assessments. - Re-run stored breach-readiness assessments and CAR/aggregate audits. Diff against pre-upgrade output.
- Backend only: set server-side
NDPR_TENANT_ID. - Backend only: replace the
resolveVerifiedNDPRActorstub and map your roles tondpr:staff/ndpr:admin. - Populated database only: back up, restore-test, freeze writes, then run the single correct
migrations/0.3.0artifact. - Apply reviewed DSR subject mappings and ROPA/breach evidence backfills. Drive the review-queue counts in the migration README to zero for every workflow you intend to enable.
- Update clients that read raw consent rows to the canonical
ConsentSettingsshape, and handle 400/409. - Update tests asserting hard deletion to assert revoked/archived state.
- Smoke-test under two tenant fixtures (or a deliberately foreign tenant id) and verify subject vs staff authorization, consent create/replay/replace/revoke, and atomic business+audit commits.
Stuck? Open an issue at github.com/mr-tanta/ndpr-toolkit/issues. Related reading: Production Recipes, CLI Scaffolder, and Migrating 4.1 → 5.0.