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 areaImplementation
Database schemaPrisma + Drizzle ORM (PostgreSQL)
Consent persistencePrisma adapter, Drizzle adapter
DSR request persistencePrisma adapter, Drizzle adapter
Breach report persistencePrisma adapter, validated Next.js and Express routes, NDPC readiness metadata
ROPA persistencePrisma adapter, validated Next.js and Express routes
DPIA persistenceDrizzle adapter, validated Next.js and Express routes
Next.js App RouterConsent, DSR, DPIA, Breach, ROPA, Compliance route handlers
ExpressFull NDPR router with consent, DSR, DPIA, breach, ROPA, compliance, and registration routes
Consent middlewareNext.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.prisma

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

TableDescriptionNDPA reference
ndpr_consent_recordsImmutable consent audit trail. Revocation sets revokedAt — rows are never deleted.§25–26
ndpr_dsr_requestsData subject rights requests. Tracks type, status, and 30-day response deadline.Part VI §34–38
ndpr_breach_reportsBreach incident records with 72-hour NDPC notification tracking.§40
ndpr_processing_recordsRecord of Processing Activities (ROPA) — purpose, lawful basis, retention periods.Accountability principle
ndpr_dpia_recordsData Protection Impact Assessments with risk score, approval status, and full questionnaire data.§28
ndpr_audit_logAppend-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 generate

Prisma 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 migrate

Drizzle 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 fileHTTP methodsDescription
api/consent/route.tsGET, POST, DELETELoad, save, and revoke consent records
api/dsr/route.tsGET, POST, PATCHSubmit and manage data subject rights requests
api/breach/route.tsGET, POSTList and create validated breach reports with NDPC readiness
api/breach/[id]/route.tsGET, PATCHRead breach readiness and validate lifecycle updates
api/dpia/route.tsGET, POST, PUT, DELETEPersist validated DPIA records with audit logging
api/ropa/route.tsGET, POST, PATCH, DELETEManage processing activity records with server validation
api/compliance/route.tsGETReturn 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:

RouteModule
GET/POST/DELETE /api/ndpr/consentConsent management (NDPA §25, §26)
GET/POST/PATCH /api/ndpr/dsrData subject rights (NDPA §34–38)
GET/POST/PATCH /api/ndpr/breachBreach notification (NDPA §40)
GET/POST/PUT/DELETE /api/ndpr/dpiaData Protection Impact Assessments (NDPA §28)
GET/POST/PATCH /api/ndpr/ropaRecord of Processing Activities
GET /api/ndpr/complianceCompliance 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);

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@latest

The 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.ts

Adding 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 nextjs

Gate 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=""