Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/nextjs-deletion-webhook/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Shared signing secret configured for your DSAR outbound webhook endpoint
DSAR_WEBHOOK_SECRET=whsec_demo_secret_key_12345

# Optional path to demo user data store file (defaults to .demo-users.json)
DEMO_DATABASE_PATH=.demo-users.json
6 changes: 6 additions & 0 deletions examples/nextjs-deletion-webhook/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.next/
.env
.demo-users.json
*.db
*.db-shm
*.db-wal
63 changes: 63 additions & 0 deletions examples/nextjs-deletion-webhook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Next.js Deletion Webhook Quickstart

A production-ready Next.js App Router quickstart demonstrating how to receive outbound DSAR deletion webhooks using `@dsar/node-sdk/webhooks/next`.

## Quickstart in 5 Steps

### 1. Clone & Install

```bash
bun install
```

### 2. Configure Environment

Copy `.env.example` to `.env`:

```bash
cp .env.example .env
```

### 3. Inspect Route Handler

The Next.js App Router POST route handler lives in [`app/api/webhooks/dsar/route.ts`](app/api/webhooks/dsar/route.ts):

```typescript
import { createWebhookReceiver } from "@dsar/node-sdk/webhooks";
import { nextWebhookHandler } from "@dsar/node-sdk/webhooks/next";
import { deleteDemoUserByEmail } from "../../../../lib/db";

const receiver = createWebhookReceiver({
signingSecret: process.env.DSAR_WEBHOOK_SECRET!,
});

receiver.on("request_captured", (event) => {
deleteDemoUserByEmail(event.payload.email as string);
});

export const POST = nextWebhookHandler(receiver);
```

### 4. Start Dev Server

```bash
bun run dev
```

### 5. Run Smoke Test

Run the automated end-to-end verification script:

```bash
bun run smoke
```

---

## Deploying to Vercel

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Finthhq%2Fdsar%2Ftree%2Fmain%2Fexamples%2Fnextjs-deletion-webhook&env=DSAR_WEBHOOK_SECRET)

1. Click the button above to clone and deploy to Vercel.
2. Set `DSAR_WEBHOOK_SECRET` in your Vercel Environment Variables.
3. Configure your DSAR Webhook Endpoint URL to point at `https://your-domain.vercel.app/api/webhooks/dsar`.
42 changes: 42 additions & 0 deletions examples/nextjs-deletion-webhook/app/api/webhooks/dsar/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createWebhookReceiver } from "@dsar/node-sdk/webhooks";
import { nextWebhookHandler } from "@dsar/node-sdk/webhooks/next";

import { deleteDemoUserByEmail } from "../../../../lib/db";

const signingSecret =
process.env.DSAR_WEBHOOK_SECRET ?? "whsec_demo_secret_key_12345";

const receiver = createWebhookReceiver({ signingSecret });

receiver.on("request_captured", (event) => {
const email =
typeof event.payload.email === "string"
? event.payload.email
: undefined;
if (email) {
const deleted = deleteDemoUserByEmail(email);
console.log(
`[DSAR Webhook] Processed request_captured event (record_deleted: ${deleted})`
);
}
});

receiver.on("request_fulfilled", (event) => {
const email =
typeof event.payload.email === "string"
? event.payload.email
: undefined;
if (email) {
const deleted = deleteDemoUserByEmail(email);
console.log(
`[DSAR Webhook] Processed request_fulfilled event (record_deleted: ${deleted})`
);
}
});

/**
* Next.js App Router route handler for DSAR webhooks.
*
* Verifies HMAC signature, extracts subject payload, and executes data erasure.
*/
export const POST = nextWebhookHandler(receiver);
118 changes: 118 additions & 0 deletions examples/nextjs-deletion-webhook/lib/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import fs from "node:fs";

/** Demo user record stored in the application database. */
export interface DemoUser {
readonly id: string;
readonly email: string;
readonly name: string;
readonly status: string;
}

/**
* Lightweight, zero-dependency demo database manager for quickstarts
* and serverless deployments (Vercel, Node, Bun).
*/
export class DemoDatabase {
private readonly filePath: string;
private users: Map<string, DemoUser> = new Map();

constructor(
filePath = process.env.DEMO_DATABASE_PATH ?? ".demo-users.json"
) {
this.filePath = filePath;
this.load();
}

private load(): void {
if (fs.existsSync(this.filePath)) {
try {
const data = fs.readFileSync(this.filePath, "utf8");
const list: readonly DemoUser[] = JSON.parse(data);
this.users = new Map(list.map((user) => [user.email, user]));
} catch {
this.users = new Map();
}
}
}

private save(): void {
try {
const list = Array.from(this.users.values());
fs.writeFileSync(
this.filePath,
JSON.stringify(list, null, 2),
"utf8"
);
} catch {
// Ignore filesystem write errors in read-only serverless environments
}
}

/** Seeds initial user records into the demo database. */
seed(): readonly DemoUser[] {
const demoUsers: readonly DemoUser[] = [
{
email: "alex.subject@example.com",
id: "usr_alex_123",
name: "Alex Subject",
status: "active",
},
{
email: "jordan.user@example.com",
id: "usr_jordan_456",
name: "Jordan User",
status: "active",
},
];
for (const user of demoUsers) {
this.users.set(user.email, user);
}
this.save();
return demoUsers;
}

/** Deletes a demo user record by email. */
deleteByEmail(email: string): boolean {
const existed = this.users.has(email);
if (existed) {
this.users.delete(email);
this.save();
}
return existed;
}

/** Looks up a demo user record by email. */
findByEmail(email: string): DemoUser | undefined {
return this.users.get(email);
}
}

let dbInstance: DemoDatabase | undefined;

/**
* Returns a shared demo database instance.
*
* @param dbPath - Optional path to user data store file.
*/
export const getDb = (dbPath?: string): DemoDatabase => {
if (!dbInstance) {
dbInstance = new DemoDatabase(dbPath);
}
return dbInstance;
};

/** Seeds demo users. */
export const seedDemoUsers = (dbPath?: string): readonly DemoUser[] =>
getDb(dbPath).seed();

/** Deletes demo user by email. */
export const deleteDemoUserByEmail = (
email: string,
dbPath?: string
): boolean => getDb(dbPath).deleteByEmail(email);

/** Finds demo user by email. */
export const findDemoUserByEmail = (
email: string,
dbPath?: string
): DemoUser | undefined => getDb(dbPath).findByEmail(email);
27 changes: 27 additions & 0 deletions examples/nextjs-deletion-webhook/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "dsar-nextjs-deletion-webhook-example",
"private": true,
"description": "Quickstart example demonstrating DSAR deletion webhooks with Next.js App Router and @dsar/node-sdk.",
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"smoke": "bun ./scripts/smoke.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@dsar/node-sdk": "workspace:*",
"dotenv": "17.4.2",
"next": "^16.2.11",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@dsar/typescript-config": "workspace:*",
"@types/node": "catalog:types",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"typescript": "catalog:utils"
}
}
Loading