Production DSR backend

A reference implementation of a production-grade Data Subject Rights submission backend: validate, persist (Prisma), email-confirm (Resend), and return the typed shape NDPRSubjectRights.onSubmitSuccess expects.

Overview

The quickstart route handler stores DSR submissions in an in-memory let store = null — fine for demos, useless for production. This guide describes the reference implementation under examples/dsr-backend-reference/in the repo: a Next.js 15 App Router app that wires the toolkit's validator, a Prisma model, and Resend email confirmations into a single endpoint.

Both Prisma and Resend are shipped behind dual-mode shims: the example runs out of the box with no env vars (mocks log to stdout); set DATABASE_URL and RESEND_API_KEY to flip to real services without touching the route handler. Useful for evaluating the pattern before provisioning infrastructure.

The four-layer pipeline

  1. Form — the client page mounts <NDPRSubjectRights submitTo="/api/dsr" onSubmitSuccess={ … } />. The success handler reads the typed body and navigates to a confirmation page with the reference number in the URL.
  2. Validation — the route handler runs validateDsrSubmissionStructured from /server. Bad payloads return 400 with { error, fields }; the preset surfaces those messages to the user. Then a defense-in-depth check rejects e-mails on a configurable disposable-domain blocklist.
  3. Persistence (Prisma) — a DSRRequest row is created with a server-generated referenceId, the submitter's details, and an estimatedCompletionAt 30 days out (per NDPA Part VI §34-38).
  4. Confirmation (Resend) — a confirmation e-mail with the reference number and deadline is sent best-effort. If sending fails, the request still succeeds (the DSR is already persisted) — the failure is logged for manual follow-up.

The 201 response contract

On success, the handler returns exactly:

{
  "referenceId": "DSR-LXTYZ-7K4P",
  "status": "received",
  "estimatedCompletionAt": "2026-06-24T10:14:00.000Z"
}

That matches the shape the toolkit's DSR docs document for NDPRSubjectRights.onSubmitSuccess. The client reads body.referenceId and body.estimatedCompletionAt directly, with no transformation layer.

Run the example locally

git clone https://github.com/mr-tanta/ndpr-toolkit.git
cd ndpr-toolkit/examples/dsr-backend-reference
bun install   # or pnpm / npm
bun dev

Visit http://localhost:3000/dsr and submit a request. With no env vars set, the request is held in an in-memory Map and the confirmation e-mail is printed to your terminal.

Swapping in real services

  1. Provision a database. The example ships prisma/schema.prisma with datasource = sqlite for local-dev convenience — change it to postgresql for production, then npx prisma migrate dev.
  2. Sign up at resend.com, verify a sending domain, and create an API key.
  3. Set DATABASE_URL, RESEND_API_KEY, and DSR_FROM_EMAIL in .env. The shims detect those env vars and switch to real clients with no code changes.
  4. Optional: set DSR_BLOCKED_EMAIL_DOMAINS="mailinator.com,tempmail.com" to enable the disposable-email rejection.

Adapting it to your stack

The Prisma + Resend choices are illustrative, not prescriptive. The two interfaces you actually need to honour are:

  • Persist — anything that can store the validated DSRFormSubmission and read it back by referenceId. Drizzle, Kysely, raw SQL, Firestore, DynamoDB — anything.
  • Notify — anything that can send transactional e-mail. Resend, Postmark, SES, your existing SMTP — the route handler just awaits a function.

The validator and the response contract are the contract; everything between them is yours.