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 areaImplementation
Database schemaPrisma + Drizzle ORM (PostgreSQL)
Consent persistencePrisma adapter, Drizzle adapter
DSR request persistencePrisma adapter, Drizzle adapter
Breach report persistencePrisma adapter
ROPA persistencePrisma adapter
Next.js App RouterConsent, DSR, Breach, ROPA, Compliance route handlers
ExpressFull NDPR router with all five compliance routes
Consent middlewareNext.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 ./ndpr

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

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 IV §29–36
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_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 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 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 all five 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, POST, PATCHLog and update breach reports
api/ropa/route.tsGET, POST, PATCHManage processing activity records
api/compliance/route.tsGETReturn 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:

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/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

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