Backend Integration
Persist consent, DSR submissions, and breach reports to a database using @tantainnovative/ndpr-recipes
Overview of ndpr-recipes
@tantainnovative/ndpr-recipes is a reference implementation — not an installable library. Copy the files you need directly into your project 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.
| 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 |
| ROPA persistence | Prisma adapter |
| Next.js App Router | Consent, DSR, Breach, ROPA, Compliance route handlers |
| Express | Full NDPR router with all five compliance routes |
| Consent middleware | Next.js edge middleware + Express middleware |
Copy the files you need from the recipes package into your project:
# Clone the toolkit repo to access the recipes
git clone https://github.com/tantainnovative/ndpr-toolkit.git
# Copy the recipes source into your project
cp -r ndpr-toolkit/packages/ndpr-recipes/src ./ndprPrisma Schema
The recipes package ships a complete Prisma schema with five tables covering every NDPA compliance module. Copy packages/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 IV §29–36 |
| 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_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 ComplianceAuditLog {
id String @id @default(cuid())
module String // consent | dsr | 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 all five 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, PATCH | Log and update breach reports |
| api/ropa/route.ts | GET, POST, PATCH | Manage processing activity records |
| api/compliance/route.ts | GET | Return the current compliance score report |
Consent route — complete example
Below is the full consent route from the recipes package. It follows the immutable-audit pattern mandated by NDPA Section 25: revocation soft-deletes via revokedAt rather than hard-deleting rows, and each write is echoed to the audit log.
// 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 });
}Wire the front-end to these endpoints using the apiAdapter from the main toolkit:
import { apiAdapter } from '@tantainnovative/ndpr-toolkit';
const consentAdapter = apiAdapter({ baseUrl: '/api/consent' });
const dsrAdapter = apiAdapter({ baseUrl: '/api/dsr' });Express Routes
For non-Next.js Node.js backends, the recipes include an Express router factory that mounts all five 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/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 nextjsEnvironment 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=""