Backend Integration
Persist consent, DSR submissions, and breach reports to a database using @tantainnovative/ndpr-recipes
Overview of ndpr-recipes
@tantainnovative/ndpr-recipes is the published npm package for backend recipe templates. Install it when you want a pinned recipe version, then copy the source files you need into your app and adapt them to your architecture. Each recipe is self-contained, fully documented, and covers two ORM families, two server frameworks, and complete wiring examples.
The repo also includes runnable backend examples for the first production handoff paths: examples/nextjs-app/app/api/consent/route.ts validates consent payloads with validateConsentStructured, stores the current consent snapshot, and appends audit events; examples/dsr-backend-reference covers DSR intake, Prisma persistence, reference IDs, target dates, and confirmation email.
| Coverage area | Implementation |
|---|---|
| Database schema | Prisma + Drizzle ORM (PostgreSQL) |
| Consent persistence | Prisma adapter, Drizzle adapter |
| DSR request persistence | Prisma adapter, Drizzle adapter |
| Breach report persistence | Prisma adapter, validated Next.js and Express routes, NDPC readiness metadata |
| ROPA persistence | Prisma adapter, validated Next.js and Express routes |
| DPIA persistence | Drizzle adapter, validated Next.js and Express routes |
| Next.js App Router | Consent, DSR, DPIA, Breach, ROPA, Compliance route handlers |
| Express | Full NDPR router with consent, DSR, DPIA, breach, ROPA, compliance, and registration routes |
| Consent middleware | Next.js edge middleware + Express middleware |
Install the recipes package, then copy the files you need into your project:
pnpm add -D @tantainnovative/ndpr-recipes
pnpm add @tantainnovative/ndpr-toolkit
# Copy the recipes source into your app so you can own and modify it
cp -r node_modules/@tantainnovative/ndpr-recipes/src ./ndpr
cp node_modules/@tantainnovative/ndpr-recipes/prisma/schema.prisma ./prisma/schema.prismaPrefer browsing first? The same files live in the repository. The npm package is the safer handoff for production teams because it gives you a versioned source snapshot.
Prisma Schema
The recipes package ships a complete Prisma schema covering every NDPA compliance module and the audit log. Copy node_modules/@tantainnovative/ndpr-recipes/prisma/schema.prisma into your project and run a migration.
| Table | Description | NDPA reference |
|---|---|---|
| ndpr_consent_records | Immutable consent audit trail. Revocation sets revokedAt — rows are never deleted. | §25–26 |
| ndpr_dsr_requests | Data subject rights requests. Tracks type, status, and 30-day response deadline. | Part VI §34–38 |
| ndpr_breach_reports | Breach incident records with 72-hour NDPC notification tracking. | §40 |
| ndpr_processing_records | Record of Processing Activities (ROPA) — purpose, lawful basis, retention periods. | Accountability principle |
| ndpr_dpia_records | Data Protection Impact Assessments with risk score, approval status, and full questionnaire data. | §28 |
| ndpr_audit_log | Append-only compliance event log written automatically by each route handler. | §44 |
Full schema
// prisma/schema.prisma
model ConsentRecord {
id String @id @default(cuid())
subjectId String // stable user/session identifier
consents Json // { analytics: true, marketing: false, ... }
version String // consent policy version (e.g. "1.2")
method String // how consent was captured (e.g. "banner", "api")
lawfulBasis String? // NDPA lawful basis, e.g. "consent"
ipAddress String? // for audit evidence
userAgent String? // for audit evidence
createdAt DateTime @default(now())
revokedAt DateTime? // null = active; non-null = withdrawn
@@index([subjectId])
@@map("ndpr_consent_records")
}
model DSRRequest {
id String @id @default(cuid())
type String // access | erasure | portability | rectification | objection | restriction
status String @default("pending") // pending | in_progress | completed | rejected
subjectName String
subjectEmail String
subjectPhone String?
identifierType String // how the subject identified themselves
identifierValue String // the actual identifier value
description String?
internalNotes String?
assignedTo String?
submittedAt DateTime @default(now())
acknowledgedAt DateTime?
completedAt DateTime?
dueAt DateTime // 30-day statutory deadline
@@index([status])
@@index([subjectEmail])
@@map("ndpr_dsr_requests")
}
model BreachReport {
id String @id @default(cuid())
title String
description String
category String // unauthorized_access | data_loss | ransomware | ...
severity String // low | medium | high | critical
status String @default("ongoing") // ongoing | contained | resolved
discoveredAt DateTime
occurredAt DateTime?
reportedAt DateTime @default(now())
ndpcNotifiedAt DateTime? // when 72-hour notification was sent
reporterName String
reporterEmail String
reporterDepartment String?
affectedSystems Json // string[]
dataTypes Json // string[] — categories of data involved
estimatedAffected Int? // number of data subjects affected
initialActions String? // immediate containment steps taken
ndpcNotificationSent Boolean @default(false)
@@index([status])
@@index([severity])
@@map("ndpr_breach_reports")
}
model ProcessingRecord {
id String @id @default(cuid())
purpose String // why the data is being processed
lawfulBasis String // NDPA lawful basis for processing
dataCategories Json // string[] — types of personal data
dataSubjects Json // string[] — categories of data subjects
recipients Json // string[] — who receives the data
retentionPeriod String // human-readable (e.g. "7 years")
securityMeasures Json // string[] — technical/org measures in place
transferCountries Json? // string[] — countries data is transferred to
transferMechanism String? // SCCs | adequacy decision | consent | ...
dpiaConducted Boolean @default(false)
status String @default("active") // active | inactive | archived
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("ndpr_processing_records")
}
model DPIARecord {
id String @id @default(cuid())
projectName String
description String
dpiaData Json // assessor, answers, risks, conclusion, version
overallRisk String // low | medium | high | critical
score Int
status String @default("draft") // draft | in_progress | completed | approved | rejected
conductedBy String
approvedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@map("ndpr_dpia_records")
}
model ComplianceAuditLog {
id String @id @default(cuid())
module String // consent | dsr | dpia | breach | ropa
action String // created | updated | revoked | completed | ...
entityId String // ID of the affected record
entityType String // ConsentRecord | DSRRequest | BreachReport | ...
changes Json? // before/after snapshot of changed fields
performedBy String? // user ID who triggered the action
createdAt DateTime @default(now())
@@index([module, entityId])
@@map("ndpr_audit_log")
}Run the migration
npx prisma migrate dev --name init-ndpr-tables
npx prisma generatePrisma Adapters
The adapters in src/adapters/prisma-*.ts implement the StorageAdapter<T> interface from the toolkit. Copy them alongside your Prisma client, then pass them to the corresponding toolkit hook.
Consent adapter
Follows the immutable-audit pattern required by NDPA Section 25: records are never deleted, and revocation sets revokedAt on the existing row.
import { PrismaClient } from '@prisma/client';
import { prismaConsentAdapter } from '@/ndpr/adapters/prisma-consent';
const prisma = new PrismaClient();
// In a React component or server action — pass the data subject's identifier
const adapter = prismaConsentAdapter(prisma, session.userId);
// Then pass the adapter to useConsent
const { settings, updateConsent } = useConsent({ adapter });DSR adapter
import { prismaDSRAdapter } from '@/ndpr/adapters/prisma-dsr';
// Scoped to the authenticated user's email address
const adapter = prismaDSRAdapter(prisma, session.user.email);
// Pass to useDSR, or call adapter.save(requests) directly in a route handler
const { requests, submitRequest } = useDSR({ adapter });Breach adapter
import { prismaBreachAdapter } from '@/ndpr/adapters/prisma-breach';
const adapter = prismaBreachAdapter(prisma);
// Pass to useBreach
const { reports, submitReport } = useBreach({ adapter });ROPA adapter
Organisation metadata (name, DPO contact, address) is not stored in the database — supply it when constructing the adapter.
import { prismaROPAAdapter } from '@/ndpr/adapters/prisma-ropa';
const adapter = prismaROPAAdapter(prisma, {
organizationName: process.env.ORG_NAME!,
organizationContact: process.env.DPO_EMAIL!,
organizationAddress: process.env.ORG_ADDRESS!,
ndpcRegistrationNumber: process.env.NDPC_REG_NUMBER, // optional
});
// Pass to useROPA
const { activities, addActivity } = useROPA({ adapter });All four adapters are exported from the central index so you can import from one place:
import {
prismaConsentAdapter,
prismaDSRAdapter,
prismaBreachAdapter,
prismaROPAAdapter,
} from '@/ndpr/adapters';Drizzle Support
If your project uses Drizzle ORM instead of Prisma, equivalent adapters are available in src/adapters/drizzle-consent.ts and src/adapters/drizzle-dsr.ts. The Drizzle schema that mirrors the Prisma schema lives in src/drizzle/schema.ts.
Setup
pnpm add drizzle-orm pg @paralleldrive/cuid2
pnpm add -D drizzle-kit @types/pg// src/db.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from '@/ndpr/drizzle/schema';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });// Push schema to DB (no migration files) or generate migration files:
npx drizzle-kit push
# or
npx drizzle-kit generate && npx drizzle-kit migrateDrizzle consent adapter
import { drizzleConsentAdapter } from '@/ndpr/adapters/drizzle-consent';
import { db } from '@/db';
const adapter = drizzleConsentAdapter(db, session.userId);
const { settings, updateConsent } = useConsent({ adapter });Drizzle DSR adapter
import { drizzleDSRAdapter } from '@/ndpr/adapters/drizzle-dsr';
import { db } from '@/db';
const adapter = drizzleDSRAdapter(db, session.user.email);
const { requests, submitRequest } = useDSR({ adapter });Next.js API Routes
The recipes include ready-made Next.js App Router handlers for the compliance modules. Copy each route file from src/nextjs/app-router/api/ into your project's app/api/ directory:
| Route file | HTTP methods | Description |
|---|---|---|
| api/consent/route.ts | GET, POST, DELETE | Load, save, and revoke consent records |
| api/dsr/route.ts | GET, POST, PATCH | Submit and manage data subject rights requests |
| api/breach/route.ts | GET, POST | List and create validated breach reports with NDPC readiness |
| api/breach/[id]/route.ts | GET, PATCH | Read breach readiness and validate lifecycle updates |
| api/dpia/route.ts | GET, POST, PUT, DELETE | Persist validated DPIA records with audit logging |
| api/ropa/route.ts | GET, POST, PATCH, DELETE | Manage processing activity records with server validation |
| api/compliance/route.ts | GET | Return the current compliance score report |
Consent route — database pattern
The route in the recipes package follows the immutable-audit pattern mandated by NDPA Section 25: revocation soft-deletes via revokedAt rather than hard-deleting rows, each write is echoed to the audit log, and the request body is validated with validateConsentStructured before Prisma writes. The excerpt below shows the persistence shape; copy the maintained source from packages/ndpr-recipes/src/nextjs/app-router/api/consent/route.ts.
// app/api/consent/route.ts
// Copy from: src/nextjs/app-router/api/consent/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// GET /api/consent?subjectId=xxx
// Returns the most recent active consent record, or null if none exists.
export async function GET(req: NextRequest) {
const subjectId = req.nextUrl.searchParams.get('subjectId');
if (!subjectId) {
return NextResponse.json({ error: 'subjectId required' }, { status: 400 });
}
const record = await prisma.consentRecord.findFirst({
where: { subjectId, revokedAt: null },
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(record);
}
// POST /api/consent
// Body: { subjectId, consents, version, method?, lawfulBasis? }
// Soft-revokes any existing active record, then inserts the new one.
export async function POST(req: NextRequest) {
const { subjectId, consents, version, method, lawfulBasis } = await req.json();
if (!subjectId || !consents || !version) {
return NextResponse.json(
{ error: 'subjectId, consents, and version are required' },
{ status: 400 },
);
}
// Revoke previous active record (immutable-audit pattern — NDPA §25)
await prisma.consentRecord.updateMany({
where: { subjectId, revokedAt: null },
data: { revokedAt: new Date() },
});
const record = await prisma.consentRecord.create({
data: {
subjectId,
consents,
version,
method: method ?? 'api',
lawfulBasis: lawfulBasis ?? null,
ipAddress: req.headers.get('x-forwarded-for'),
userAgent: req.headers.get('user-agent'),
},
});
// Audit log — accountability under NDPA §44
await prisma.complianceAuditLog.create({
data: {
module: 'consent',
action: 'created',
entityId: record.id,
entityType: 'ConsentRecord',
changes: { subjectId, version, consents },
},
});
return NextResponse.json(record, { status: 201 });
}
// DELETE /api/consent?subjectId=xxx
// Revokes all active consent records for the given subject.
export async function DELETE(req: NextRequest) {
const subjectId = req.nextUrl.searchParams.get('subjectId');
if (!subjectId) {
return NextResponse.json({ error: 'subjectId required' }, { status: 400 });
}
await prisma.consentRecord.updateMany({
where: { subjectId, revokedAt: null },
data: { revokedAt: new Date() },
});
await prisma.complianceAuditLog.create({
data: {
module: 'consent',
action: 'revoked',
entityId: subjectId,
entityType: 'ConsentRecord',
},
});
return NextResponse.json({ success: true });
}Breach route — intake validation and readiness
The maintained Next.js and Express breach routes validate incident intake before Prisma writes: valid ISO discovery dates, reporter email, non-empty affected systems, non-empty data types, and non-negative affected-subject counts. Create and detail responses include ndpcReadiness from assessBreachNotification so DPO workflows can see missing GAID 2025 Article 33 notification content and the 72-hour deadline state. The update routes also reject invalid status or severity values before persistence.
// Copy the maintained routes:
// packages/ndpr-recipes/src/nextjs/app-router/api/breach/route.ts
// packages/ndpr-recipes/src/nextjs/app-router/api/breach/[id]/route.ts
// packages/ndpr-recipes/src/express/routes/breach.ts
{
"title": "Customer export exposed",
"description": "A customer export was uploaded to a public bucket.",
"category": "unauthorized_access",
"discoveredAt": "2026-06-26T10:00:00.000Z",
"occurredAt": "2026-06-26T09:00:00.000Z",
"reporterName": "Ada DPO",
"reporterEmail": "ada@example.com",
"reporterDepartment": "Privacy",
"affectedSystems": ["object-storage"],
"dataTypes": ["name", "email"],
"estimatedAffected": 220,
"initialActions": "Bucket access was disabled."
}DPIA route — Section 28 persistence
The maintained Next.js and Express DPIA routes persist complete assessment records to DPIARecord and write each create, update, and delete to the compliance audit log. The routes validate project metadata, full dpiaData, risk level, non-negative score, and lifecycle status before Prisma writes.
// Copy the maintained routes:
// packages/ndpr-recipes/src/nextjs/app-router/api/dpia/route.ts
// packages/ndpr-recipes/src/express/routes/dpia.ts
{
"projectName": "Customer analytics model",
"description": "Profiling customer activity for product recommendations.",
"dpiaData": {
"assessor": {
"name": "Ada DPO",
"role": "DPO",
"email": "ada@example.com"
},
"answers": { "highRiskProcessing": true },
"risks": [{ "id": "risk_1", "title": "Profiling risk", "score": 12 }],
"conclusion": "Proceed with mitigation plan.",
"recommendations": ["Run quarterly review."],
"version": "1.0",
"ndpcConsultationRequired": true
},
"overallRisk": "high",
"score": 12,
"conductedBy": "ada@example.com"
}ROPA route — production validation
The maintained Next.js and Express ROPA routes validate each processing record with validateProcessingRecord before Prisma writes. A valid payload must include controller details, lawful-basis justification, data categories, data-subject categories, recipients, retention period, security measures, and a dpiaReference whenever dpiaRequired is true. Use public_interest for public-interest processing.
// Copy the maintained routes:
// packages/ndpr-recipes/src/nextjs/app-router/api/ropa/route.ts
// packages/ndpr-recipes/src/express/routes/ropa.ts
{
"purpose": "Customer order fulfilment",
"description": "Processes customer identity and delivery details to fulfil orders.",
"controllerDetails": {
"name": "Example Controller Ltd",
"contact": "privacy@example.com",
"address": "1 Compliance Way, Lagos"
},
"lawfulBasis": "contract",
"lawfulBasisJustification": "Processing is necessary to fulfil customer purchase contracts.",
"dataCategories": ["name", "email", "delivery address"],
"dataSubjects": ["customers"],
"recipients": ["payment processor", "delivery partner"],
"retentionPeriod": "7 years after order completion",
"securityMeasures": ["role-based access", "encryption at rest"],
"dataSource": "data_subject",
"dpiaRequired": false,
"automatedDecisionMaking": false
}Wire the front-end to these endpoints using the apiAdapter from the main toolkit:
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
const consentAdapter = apiAdapter('/api/consent');
const dsrAdapter = apiAdapter('/api/dsr');Express Routes
For non-Next.js Node.js backends, the recipes include an Express router factory that mounts the compliance routes in one call. Copy the src/express/ directory into your project, then mount it:
// src/app.ts
import express from 'express';
import cookieParser from 'cookie-parser';
import { createNDPRRouter } from './ndpr/express';
const app = express();
app.use(express.json());
app.use(cookieParser()); // required for consent cookie fallback
// Mount all NDPR compliance routes under /api/ndpr
app.use('/api/ndpr', createNDPRRouter());
app.listen(3001);This mounts the following routes:
| Route | Module |
|---|---|
| GET/POST/DELETE /api/ndpr/consent | Consent management (NDPA §25, §26) |
| GET/POST/PATCH /api/ndpr/dsr | Data subject rights (NDPA §34–38) |
| GET/POST/PATCH /api/ndpr/breach | Breach notification (NDPA §40) |
| GET/POST/PUT/DELETE /api/ndpr/dpia | Data Protection Impact Assessments (NDPA §28) |
| GET/POST/PATCH /api/ndpr/ropa | Record of Processing Activities |
| GET /api/ndpr/compliance | Compliance score dashboard |
Granular mounting
If you only need a subset of routes, import the individual routers directly:
import { consentRouter, dsrRouter } from './ndpr/express';
// Mount only the routes you need
app.use('/api/consent', consentRouter);
app.use('/api/dsr', dsrRouter);Consent Middleware
Both Next.js and Express recipes include consent-gate middleware. Use it to block any route that requires a specific consent type before processing begins — enforcing NDPA Section 25 at the HTTP layer.
Next.js — inline guard
The middleware looks for a subject identifier in the x-subject-id header first, then falls back to the subject_id cookie. Returns null to allow through, or a 403 response to block.
// app/api/email/marketing/route.ts
import { NextRequest } from 'next/server';
import { consentMiddleware } from '@/ndpr/middleware';
export async function POST(req: NextRequest) {
const guard = await consentMiddleware(req, 'marketing');
if (guard) return guard; // 403 — consent not granted
// Subject has consented to marketing — proceed
}Next.js — higher-order wrapper
import { withConsent } from '@/ndpr/middleware';
// Wraps the handler — consent check runs before your logic
export const POST = withConsent('marketing', async (req) => {
// marketing consent is guaranteed here
});Express — single consent
import { requireConsent } from './ndpr/express/middleware/consent-check';
// Require marketing consent before sending a marketing email
app.post('/email/marketing', requireConsent('marketing'), sendEmailHandler);Express — multiple consents
import { requireAllConsents } from './ndpr/express/middleware/consent-check';
// All listed consents must be granted
app.post(
'/profile/analytics',
requireAllConsents(['analytics', 'functional']),
handler,
);The create-ndpr CLI Scaffolder
Run the scaffolder to bootstrap an entire NDPA-compliant Next.js app — including routes, pages, Prisma schema, environment variable templates, and example tests — in a single command:
npx create-ndpr@latestThe interactive CLI will ask a few questions and then scaffold the chosen features:
? Project name: my-app
? Framework: Next.js (App Router)
? Database: PostgreSQL (Prisma)
? Which modules would you like? (space to toggle)
✓ Consent management
✓ Data subject rights (DSR)
✓ Breach notification
○ DPIA questionnaire
✓ Privacy policy generator
○ ROPA
? Include example tests? Yes
? Install dependencies now? Yes
✓ Scaffold complete!
→ src/app/api/consent/route.ts
→ src/app/api/dsr/route.ts
→ src/app/api/breach/route.ts
→ src/app/privacy-policy/page.tsx
→ src/app/data-rights/page.tsx
→ prisma/schema.prisma (NDPR tables added)
→ .env.example
→ __tests__/consent.test.ts
→ __tests__/dsr.test.tsAdding modules to an existing project
Use the --add flag to scaffold just the files you need into an existing codebase:
npx create-ndpr@latest --add dsr --add breach --framework nextjsGate compliance in CI with ndpr audit
The toolkit also ships an ndpr binary. Once your backend is wired up, add ndpr audit to CI: it scores a compliance config against the toolkit engine (compliance score + GAID 2025 DCPMI, CAR, and breach checks) and exits non-zero when the audit fails, so a regression blocks the build.
npx ndpr audit --init # scaffold ndpr.audit.json
npx ndpr audit --min-score 80 # run in CI (exit 1 on failure)See the Compliance Audit CLI guide for the config shape, all flags, and a GitHub Actions workflow.
Environment Variables
The scaffolder generates a .env.example file. Copy it to .env.local and fill in your values:
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/myapp_dev"
# Optional: email notifications for DSR acknowledgements
NDPR_EMAIL_FROM="privacy@myapp.ng"
NDPR_EMAIL_PROVIDER="resend" # resend | sendgrid | nodemailer
RESEND_API_KEY="re_..."
# Optional: NDPC submission endpoint (future)
NDPC_API_KEY=""