[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Privacy Dashboard for Data Subject Requests

Build a Privacy Dashboard for Data Subject Requests

Build a privacy dashboard that handles GDPR/CCPA data subject requests — data export, deletion, access logs, consent management, and automated compliance workflows with audit trails.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Lukas leads compliance at a 35-person SaaS with 50K users. GDPR gives users the right to access, export, and delete their data within 30 days. CCPA requires a "Do Not Sell" option. Currently, data requests come via email; an engineer manually queries 12 database tables, exports to CSV, and emails back. It takes 8 hours per request and they get 15/month — 120 hours of engineering time. They need a self-service privacy dashboard where users manage their data rights, and automated workflows that handle requests without engineering involvement.

Step 1: Build the Privacy Request System

typescript
// src/privacy/dashboard.ts — Data subject request handling with automated workflows
import { pool } from "../db";
import { Redis } from "ioredis";
import { createWriteStream } from "node:fs";
import { mkdir } from "node:fs/promises";

const redis = new Redis(process.env.REDIS_URL!);

type RequestType = "access" | "export" | "deletion" | "rectification" | "portability" | "opt_out";
type RequestStatus = "pending" | "processing" | "awaiting_verification" | "completed" | "rejected";

interface DataSubjectRequest {
  id: string;
  userId: string;
  type: RequestType;
  status: RequestStatus;
  details: string;
  processedData?: Record<string, any>;
  downloadUrl?: string;
  expiresAt?: string;
  auditTrail: Array<{
    action: string;
    actor: string;
    timestamp: string;
    details?: string;
  }>;
  createdAt: string;
  completedAt: string | null;
  dueDate: string;             // 30 days from creation (GDPR requirement)
}

// All tables containing user data
const USER_DATA_TABLES = [
  { table: "users", idColumn: "id", personalColumns: ["email", "name", "phone", "avatar_url", "address"] },
  { table: "profiles", idColumn: "user_id", personalColumns: ["bio", "website", "location", "birthday"] },
  { table: "orders", idColumn: "customer_id", personalColumns: ["shipping_address", "billing_address", "phone"] },
  { table: "comments", idColumn: "author_id", personalColumns: ["body"] },
  { table: "messages", idColumn: "sender_id", personalColumns: ["content"] },
  { table: "login_events", idColumn: "user_id", personalColumns: ["ip_address", "user_agent"] },
  { table: "payment_methods", idColumn: "user_id", personalColumns: ["last_four", "card_brand", "billing_name"] },
  { table: "support_tickets", idColumn: "user_id", personalColumns: ["subject", "description"] },
  { table: "newsletter_subscriptions", idColumn: "user_id", personalColumns: ["email"] },
  { table: "consent_records", idColumn: "visitor_id", personalColumns: ["preferences", "ip_address"] },
  { table: "analytics_events", idColumn: "user_id", personalColumns: ["ip_address", "user_agent", "page_url"] },
  { table: "file_uploads", idColumn: "uploaded_by", personalColumns: ["file_name"] },
];

// Submit a data subject request
export async function submitRequest(
  userId: string,
  type: RequestType,
  details?: string
): Promise<DataSubjectRequest> {
  // Verify identity (require recent login or email confirmation)
  const isVerified = await verifyIdentity(userId);
  const status: RequestStatus = isVerified ? "pending" : "awaiting_verification";

  const id = `dsr-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
  const dueDate = new Date(Date.now() + 30 * 86400000).toISOString();

  const request: DataSubjectRequest = {
    id, userId, type, status,
    details: details || "",
    auditTrail: [{
      action: "request_submitted", actor: userId,
      timestamp: new Date().toISOString(),
      details: `${type} request submitted`,
    }],
    createdAt: new Date().toISOString(),
    completedAt: null,
    dueDate,
  };

  await pool.query(
    `INSERT INTO data_subject_requests (id, user_id, type, status, details, audit_trail, due_date, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
    [id, userId, type, status, details, JSON.stringify(request.auditTrail), dueDate]
  );

  // Auto-process certain request types
  if (isVerified) {
    switch (type) {
      case "access":
      case "export":
      case "portability":
        await processExportRequest(id, userId);
        break;
      case "opt_out":
        await processOptOut(id, userId);
        break;
      case "deletion":
        // Deletion requires manual review for legal holds
        await notifyDPO(id, userId, type);
        break;
    }
  }

  return request;
}

// Process data export request
async function processExportRequest(requestId: string, userId: string): Promise<void> {
  await updateRequestStatus(requestId, "processing");

  const exportData: Record<string, any[]> = {};

  for (const tableDef of USER_DATA_TABLES) {
    try {
      const { rows } = await pool.query(
        `SELECT * FROM ${tableDef.table} WHERE ${tableDef.idColumn} = $1`,
        [userId]
      );
      if (rows.length > 0) {
        exportData[tableDef.table] = rows;
      }
    } catch {
      // Table might not exist in all environments
    }
  }

  // Generate export file
  const exportDir = `/tmp/privacy-exports`;
  await mkdir(exportDir, { recursive: true });
  const filePath = `${exportDir}/${requestId}.json`;

  const exportContent = {
    requestId,
    exportDate: new Date().toISOString(),
    userId,
    dataCategories: Object.keys(exportData),
    data: exportData,
  };

  await require("node:fs/promises").writeFile(filePath, JSON.stringify(exportContent, null, 2));

  // Generate download URL (expires in 7 days)
  const downloadUrl = `${process.env.APP_URL}/api/privacy/download/${requestId}`;
  const expiresAt = new Date(Date.now() + 7 * 86400000).toISOString();

  await pool.query(
    `UPDATE data_subject_requests SET
       status = 'completed', download_url = $2, expires_at = $3,
       completed_at = NOW(), file_path = $4
     WHERE id = $1`,
    [requestId, downloadUrl, expiresAt, filePath]
  );

  await addAuditEntry(requestId, "system", "export_completed", `${Object.keys(exportData).length} data categories exported`);

  // Notify user
  await redis.rpush("email:queue", JSON.stringify({
    type: "privacy_export_ready",
    userId,
    downloadUrl,
    expiresIn: "7 days",
  }));
}

// Process deletion request
export async function processDeletion(requestId: string, approvedBy: string): Promise<{
  tablesProcessed: number;
  rowsDeleted: number;
  rowsAnonymized: number;
}> {
  const { rows: [request] } = await pool.query(
    "SELECT user_id FROM data_subject_requests WHERE id = $1", [requestId]
  );
  const userId = request.user_id;

  await updateRequestStatus(requestId, "processing");

  let rowsDeleted = 0;
  let rowsAnonymized = 0;
  let tablesProcessed = 0;

  for (const tableDef of USER_DATA_TABLES) {
    try {
      // Some tables need anonymization instead of deletion (legal requirements)
      if (["orders", "payment_methods"].includes(tableDef.table)) {
        // Anonymize: replace personal data with placeholder
        const setClauses = tableDef.personalColumns
          .map((col) => `${col} = '[DELETED]'`)
          .join(", ");

        const { rowCount } = await pool.query(
          `UPDATE ${tableDef.table} SET ${setClauses} WHERE ${tableDef.idColumn} = $1`,
          [userId]
        );
        rowsAnonymized += rowCount || 0;
      } else {
        // Hard delete
        const { rowCount } = await pool.query(
          `DELETE FROM ${tableDef.table} WHERE ${tableDef.idColumn} = $1`,
          [userId]
        );
        rowsDeleted += rowCount || 0;
      }
      tablesProcessed++;
    } catch {}
  }

  // Deactivate user account
  await pool.query(
    "UPDATE users SET status = 'deleted', email = $2, name = '[DELETED]', deleted_at = NOW() WHERE id = $1",
    [userId, `deleted-${userId}@redacted.local`]
  );

  await pool.query(
    "UPDATE data_subject_requests SET status = 'completed', completed_at = NOW() WHERE id = $1",
    [requestId]
  );

  await addAuditEntry(requestId, approvedBy, "deletion_completed",
    `${rowsDeleted} rows deleted, ${rowsAnonymized} rows anonymized across ${tablesProcessed} tables`);

  return { tablesProcessed, rowsDeleted, rowsAnonymized };
}

// Process opt-out (CCPA "Do Not Sell")
async function processOptOut(requestId: string, userId: string): Promise<void> {
  await pool.query("UPDATE users SET do_not_sell = true, do_not_track = true WHERE id = $1", [userId]);
  await pool.query("DELETE FROM marketing_consents WHERE user_id = $1", [userId]);
  await pool.query("DELETE FROM analytics_events WHERE user_id = $1 AND created_at > NOW() - interval '90 days'", [userId]);

  await updateRequestStatus(requestId, "completed");
  await addAuditEntry(requestId, "system", "opt_out_completed", "Marketing data cleared, tracking disabled");
}

async function verifyIdentity(userId: string): Promise<boolean> {
  const { rows: [session] } = await pool.query(
    "SELECT 1 FROM sessions WHERE user_id = $1 AND last_active_at > NOW() - interval '1 hour'",
    [userId]
  );
  return !!session;
}

async function updateRequestStatus(requestId: string, status: RequestStatus) {
  await pool.query("UPDATE data_subject_requests SET status = $2 WHERE id = $1", [requestId, status]);
}

async function addAuditEntry(requestId: string, actor: string, action: string, details: string) {
  await pool.query(
    `UPDATE data_subject_requests SET audit_trail = audit_trail || $2::jsonb WHERE id = $1`,
    [requestId, JSON.stringify([{ action, actor, timestamp: new Date().toISOString(), details }])]
  );
}

async function notifyDPO(requestId: string, userId: string, type: string) {
  await redis.rpush("email:queue", JSON.stringify({
    type: "dpo_review_needed", requestId, userId, requestType: type,
  }));
}

Results

  • DSR processing: 8 hours → 5 minutes — automated export scans 12 tables and generates a downloadable JSON file; no engineering involvement
  • 120 hours/month engineering time freed — 15 requests × 8 hours = 120 hours; now handled automatically with manual review only for deletions
  • 30-day GDPR deadline never missed — due date tracking with alerts at 7 and 3 days remaining; compliance team reviews dashboard instead of tracking emails
  • Deletion handles legal holds — orders and payments anonymized instead of deleted; financial audit trail preserved while personal data is removed
  • Full audit trail — every action logged; "when was this data exported, by whom, and when did it expire?" — single query