Backend Integration
Deploy the tenant-scoped, fail-closed Next.js, Express, Prisma, and Drizzle source recipes from @tantainnovative/ndpr-recipes.
Use the recipes as owned source
@tantainnovative/ndpr-recipes is a versioned source package, not a hosted persistence service. Pin a version, copy the schema, request-context seam, routes, and adapters that you need, then connect them to your authentication and deployment conventions.
pnpm add @tantainnovative/ndpr-toolkit
pnpm add -D @tantainnovative/ndpr-recipes
# Prisma schema
cp node_modules/@tantainnovative/ndpr-recipes/prisma/schema.prisma prisma/schema.prisma
# Copy source before adapting imports and auth integration
cp -r node_modules/@tantainnovative/ndpr-recipes/src src/ndpr-recipesKeep copied recipe files in your own source control. Future package updates should be reviewed and merged like application code rather than copied over local authentication, retention, or audit changes.
Connect the security context first
Every maintained route resolves identity through request-context.ts before accessing Prisma. The default actor resolver returns no actor, so staff routes fail closed until you connect a verified server session.
# Server-only configuration
DATABASE_URL="postgresql://user:password@localhost:5432/myapp_dev"
NDPR_TENANT_ID="tenant-production-id"NDPR_TENANT_IDis the only tenant authority used by the recipes.- Map verified application roles to
ndpr:stafforndpr:admin. - Resolve actor ID, profile fields, roles, and account subject ID from a verified server session, never a body, query string, cookie, or arbitrary identity header.
- Validate and normalize authoritative profile fields, especially staff email, in your authentication integration; server derivation prevents client impersonation but does not certify arbitrary profile strings.
- Consent and subject DSR operations require a verified account subject or an
anon_<UUID>capability inX-NDPR-Subject-Id. - DPIA, breach, ROPA, compliance, registration, and DSR administration require a verified staff actor.
// Replace the fail-closed default in request-context.ts.
export async function resolveVerifiedNDPRActor(request: NextRequest) {
const session = await resolveYourVerifiedSession(request);
if (!session?.user) return null;
return {
id: session.user.id,
displayName: session.user.name,
email: session.user.email,
subjectId: session.user.dataSubjectId,
roles: session.user.canManagePrivacy ? ['ndpr:staff'] : [],
};
}Do not restore the old ?subjectId=... pattern. Request bodies and query parameters cannot choose a tenant, subject, actor, role, reporter, assessor, or audit principal.
Install the canonical database schema
Copy the schema file instead of recreating it from a shortened documentation model. The maintained Prisma and Drizzle schemas include tenant keys and indexes, active-consent uniqueness, soft-removal fields, lossless JSON snapshots, and tenant/actor-scoped audit indexes.
# Fresh databases only — review before applying
# Prisma
cp node_modules/@tantainnovative/ndpr-recipes/prisma/schema.prisma prisma/schema.prisma
pnpm exec prisma migrate dev --name add-ndpr-compliance
pnpm exec prisma generate
# Drizzle
cp node_modules/@tantainnovative/ndpr-recipes/src/drizzle/schema.ts src/db/ndpr-schema.ts
pnpm exec drizzle-kit generate
pnpm exec drizzle-kit migrateThose generation commands are for a fresh database. For populated 0.1.x or 0.2.0 recipe tables, do not use a blind db push or an auto-generated replacement migration. Use the packaged, reviewed migrations/0.3.0/postgresql.sql for standalone psql/Prisma, or the transaction-control-free migrations/0.3.0/drizzle.sqlinside Drizzle's managed transaction so schema changes and its ledger insert remain atomic. Follow the mapping, rehearsal, evidence-backfill, verification, and rollback steps in the Production Recipes guide.
Review every fresh-install or upgrade migration before applying it. Preserve compound tenant keys and tenant predicates if you merge the models into an existing schema.
Construct database adapters on the server
Prisma and Drizzle adapters require explicit trusted context objects. They are server-only persistence code; do not import a database client into a React component. A browser hook or preset should call your route throughapiAdapter instead.
Prisma contexts
import {
prismaBreachAdapter,
prismaConsentAdapter,
prismaDSRAdapter,
prismaROPAAdapter,
} from '@/ndpr-recipes/adapters';
// Resolve this context and enforce its requirement before construction.
const subject = {
tenantId: context.tenantId,
subjectId: context.subjectId!,
};
const consent = prismaConsentAdapter(prisma, {
...subject,
ipAddress: request.headers.get('x-forwarded-for') ?? undefined,
userAgent: request.headers.get('user-agent') ?? undefined,
});
const dsr = prismaDSRAdapter(prisma, subject);
// Staff-only workflows still receive tenant scope explicitly.
const breach = prismaBreachAdapter(prisma, { tenantId: context.tenantId });
const ropa = prismaROPAAdapter(
prisma,
{ tenantId: context.tenantId },
{
organizationName: process.env.ORG_NAME!,
organizationContact: process.env.DPO_EMAIL!,
organizationAddress: process.env.ORG_ADDRESS!,
},
);Drizzle contexts
const subject = {
tenantId: context.tenantId,
subjectId: context.subjectId!,
};
const consent = drizzleConsentAdapter(db, {
...subject,
ipAddress: request.ip,
userAgent: request.get('user-agent'),
});
const dsr = drizzleDSRAdapter(db, subject);The adapters advertise server-acknowledged storage capabilities, but your application remains responsible for database availability, backups, retention, encryption, concurrency policy, and evidentiary controls.
Next.js App Router recipes
Copy the API routes together with request-context.ts, shared-contracts.ts, and operational-indicators.ts. Adapt import paths after placing them in your application.
| Route | Methods | Requirement | Behavior |
|---|---|---|---|
| Consent | GET, POST, DELETE | Verified subject | Tenant/subject-scoped snapshots and revocation |
| DSR | GET, POST, PATCH | Subject intake; staff administration | Tenant/subject intake and staff workflow |
| DPIA | GET, POST, PUT, DELETE | NDPR staff | Lossless assessment snapshots and derived risk evidence |
| Breach | GET, POST, PATCH | NDPR staff | Incident evidence and NDPC readiness metadata |
| ROPA | GET, POST, PATCH, DELETE | NDPR staff | Complete processing records and lossless snapshots |
| Compliance | GET | NDPR staff | Server-calculated implementation-readiness indicators; not legal certification |
| Registration | GET | NDPR staff | DCPMI classification and CAR schedule |
Every lookup and mutation includes the server tenant. Consent is additionally subject-scoped. Mutating routes write the business record and accountability event in one transaction; breach reporter details, DPIA assessor and conductor details, risk evidence, and audit principals are derived on the server.
Anonymous consent client
import { apiAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
// Generate once and retain it in first-party storage. It is a scoped capability,
// not an authenticated account or a staff credential.
const subjectId = `anon_${crypto.randomUUID()}`;
const consentAdapter = apiAdapter('/api/consent', {
credentials: 'same-origin',
headers: { 'X-NDPR-Subject-Id': subjectId },
loadFailureMode: 'throw',
mutationFailureMode: 'throw',
retry: { attempts: 2, baseDelayMs: 250 },
});The maintained layout-example.tsx shows stable capability storage, cookie fallback, idempotency keys, hydration without a false success state, and observable persistence failures.
Express recipes
Copy the complete src/express directory so routes and the shared request-context seam stay together. Connect verified auth middleware state inresolveVerifiedNDPRActor before exposing staff routes.
import express from 'express';
import { createNDPRRouter } from './ndpr/express';
const app = express();
app.use(express.json());
app.use(yourVerifiedSessionMiddleware());
app.use('/api/ndpr', createNDPRRouter());The default actor resolver is intentionally empty. Mounting the router without replacing it leaves staff routes unavailable rather than trusting request data.
Consent guards use the same identity boundary
Next.js consentMiddleware and Express requireConsent resolve the tenant and subject through the shared context. They do not accept query parameters or ordinary cookies as identity authority, and the Next.js helper requires a Node route-handler runtime because it queries Prisma.
// Next.js route handler
const guard = await consentMiddleware(request, 'marketing');
if (guard) return guard;
// Express
app.post(
'/email/marketing',
requireConsent('marketing'),
sendEmailHandler,
);Production checklist
- Pin toolkit and recipe versions and review copied source in your repository.
- Configure a server-only tenant ID and test cross-tenant isolation.
- Connect verified actor resolution and test anonymous, authenticated subject, staff, and forbidden paths.
- Keep record and audit writes atomic and test rollback behavior.
- Exercise consent revocation, DSR intake, breach updates, DPIA lifecycle changes, and ROPA completeness in staging.
- Review retention, backups, access controls, incident escalation, and current NDPC requirements with your DPO or counsel.
Scaffold or audit
npm create ndpr@latest
npx ndpr audit --init
npx ndpr audit --min-score 80Generated request-context files also fail closed until connected to your authentication provider. See the Compliance Audit CLI guide for CI configuration.