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
- Form — the client page mounts
<NDPRSubjectRights submitTo="/api/dsr" onSubmitSuccess={ … } />. The success handler reads the typedbodyand navigates to a confirmation page with the reference number in the URL. - Validation — the route handler runs
validateDsrSubmissionStructuredfrom/server. Bad payloads return400with{ 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. - Persistence (Prisma) — a
DSRRequestrow is created with a server-generatedreferenceId, the submitter's details, and anestimatedCompletionAt30 days out (per NDPA Part VI §34-38). - 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 devVisit 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
- Provision a database. The example ships
prisma/schema.prismawithdatasource = sqlitefor local-dev convenience — change it topostgresqlfor production, thennpx prisma migrate dev. - Sign up at resend.com, verify a sending domain, and create an API key.
- Set
DATABASE_URL,RESEND_API_KEY, andDSR_FROM_EMAILin.env. The shims detect those env vars and switch to real clients with no code changes. - 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
DSRFormSubmissionand read it back byreferenceId. 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.