[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Privacy Consent Management Platform

Build a Privacy Consent Management Platform

Build a GDPR/CCPA-compliant consent management platform that tracks user preferences, enforces data processing rules, and generates audit-ready compliance reports.

#postgresql#database#sql#relational#jsonb
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
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
>

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
>

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
>

nextjs

v1.0.0

Assists with building production-grade React applications using Next.js. Use when working with the App Router, Server Components, Server Actions, Middleware, or deploying to Vercel or self-hosted environments. Trigger words: nextjs, next.js, app router, server components, server actions, react framework, ssr, isr.

93/100 quality
1.16× impact
SAFE
View skill
$

The Problem

Suki leads compliance at a 45-person SaaS company operating in the EU and California. They process data for 50,000 users but track consent in a boolean marketing_opt_in column. When a GDPR subject access request arrives, the team manually searches 6 databases to compile a user's data — taking 3 days per request (regulation requires 30 days). They can't prove when consent was given, what version of the privacy policy users agreed to, or which third parties received data. A €200K fine from the Irish DPA for a similar-sized company lit a fire under leadership. They need programmatic consent tracking that can answer "what did this user consent to, when, and under which policy version?" in seconds.

Step 1: Design the Consent Data Model

Consent is modeled as versioned, purpose-specific records. Every consent change is an immutable event — nothing is ever deleted, only superseded.

typescript
// src/db/schema.ts — Consent management data model (append-only, audit-ready)
import { pgTable, uuid, varchar, text, boolean, timestamp, jsonb, pgEnum } from "drizzle-orm/pg-core";

export const consentStatus = pgEnum("consent_status", [
  "granted",
  "denied", 
  "withdrawn",
]);

export const legalBasis = pgEnum("legal_basis", [
  "consent",           // user explicitly opted in
  "legitimate_interest", // company has legitimate business reason
  "contract",          // necessary for contract performance
  "legal_obligation",  // required by law
]);

// Processing purposes — what we use the data for
export const processingPurposes = pgTable("processing_purposes", {
  id: uuid("id").primaryKey().defaultRandom(),
  slug: varchar("slug", { length: 50 }).unique().notNull(), // e.g., "marketing-email"
  name: varchar("name", { length: 200 }).notNull(),
  description: text("description").notNull(),
  legalBasis: legalBasis("legal_basis").notNull(),
  dataCategories: jsonb("data_categories").notNull(), // ["email", "name", "usage_data"]
  thirdParties: jsonb("third_parties"),               // ["Mailchimp", "Google Analytics"]
  retentionDays: varchar("retention_days", { length: 50 }), // "365 days" or "until withdrawal"
  isRequired: boolean("is_required").default(false),   // required for service operation
  active: boolean("active").default(true),
  createdAt: timestamp("created_at").defaultNow(),
});

// Consent records — immutable event log of every consent decision
export const consentRecords = pgTable("consent_records", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").notNull(),
  purposeId: uuid("purpose_id").notNull(),
  status: consentStatus("status").notNull(),
  
  // Provenance — exactly how and when consent was collected
  policyVersion: varchar("policy_version", { length: 20 }).notNull(), // "2.3.1"
  collectionMethod: varchar("collection_method", { length: 50 }).notNull(), // "cookie_banner", "settings_page", "signup_form"
  ipAddress: varchar("ip_address", { length: 45 }),
  userAgent: text("user_agent"),
  
  // For legitimate interest, document the balancing test
  legitimateInterestAssessment: text("lia_assessment"),
  
  createdAt: timestamp("created_at").defaultNow().notNull(),
  // No updated_at — records are immutable. New records supersede old ones.
});

// Data processing log — track every time user data is processed under consent
export const processingLog = pgTable("processing_log", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").notNull(),
  purposeId: uuid("purpose_id").notNull(),
  action: varchar("action", { length: 100 }).notNull(), // "email_sent", "data_exported_to_mailchimp"
  dataCategories: jsonb("data_categories").notNull(),    // what data was used
  thirdParty: varchar("third_party", { length: 100 }),   // who received it
  consentRecordId: uuid("consent_record_id").notNull(),  // which consent authorized this
  processedAt: timestamp("processed_at").defaultNow().notNull(),
});

// Subject access requests (DSAR)
export const dsarRequests = pgTable("dsar_requests", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").notNull(),
  type: varchar("type", { length: 30 }).notNull(), // "access", "deletion", "portability", "rectification"
  status: varchar("status", { length: 20 }).default("pending").notNull(),
  requestedAt: timestamp("requested_at").defaultNow().notNull(),
  verifiedAt: timestamp("verified_at"),
  completedAt: timestamp("completed_at"),
  responseData: jsonb("response_data"),
  dueDate: timestamp("due_date").notNull(), // 30 days from request
});

Step 2: Build the Consent API

The API records consent decisions, checks consent before data processing, and handles subject access requests. Every operation creates an audit trail.

typescript
// src/routes/consent.ts — Consent management API
import { Hono } from "hono";
import { z } from "zod";
import { eq, and, desc } from "drizzle-orm";
import { db } from "../db";
import { consentRecords, processingPurposes, processingLog, dsarRequests } from "../db/schema";
import { Redis } from "ioredis";

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

const RecordConsentSchema = z.object({
  purposes: z.array(z.object({
    purposeSlug: z.string(),
    granted: z.boolean(),
  })),
  policyVersion: z.string(),
  collectionMethod: z.string(),
});

// Record consent decisions (e.g., from cookie banner or settings page)
app.post("/consent", async (c) => {
  const userId = c.get("userId");
  const ip = c.req.header("x-forwarded-for") || "unknown";
  const userAgent = c.req.header("user-agent") || "unknown";
  const body = RecordConsentSchema.parse(await c.req.json());

  const results = [];

  for (const decision of body.purposes) {
    // Look up purpose by slug
    const [purpose] = await db.select().from(processingPurposes)
      .where(eq(processingPurposes.slug, decision.purposeSlug));

    if (!purpose) continue;

    // Create immutable consent record
    const [record] = await db.insert(consentRecords).values({
      userId,
      purposeId: purpose.id,
      status: decision.granted ? "granted" : "denied",
      policyVersion: body.policyVersion,
      collectionMethod: body.collectionMethod,
      ipAddress: ip,
      userAgent: userAgent,
    }).returning();

    results.push(record);

    // Update cached consent state for fast lookups
    await redis.hset(
      `consent:${userId}`,
      purpose.slug,
      decision.granted ? "granted" : "denied"
    );
    await redis.expire(`consent:${userId}`, 3600); // 1h cache
  }

  return c.json({ recorded: results.length, records: results }, 201);
});

// Withdraw consent for a specific purpose
app.post("/consent/withdraw", async (c) => {
  const userId = c.get("userId");
  const { purposeSlug, reason } = await c.req.json();

  const [purpose] = await db.select().from(processingPurposes)
    .where(eq(processingPurposes.slug, purposeSlug));

  if (!purpose) return c.json({ error: "Unknown purpose" }, 404);
  if (purpose.isRequired) {
    return c.json({ error: "Cannot withdraw consent for required processing. To stop this processing, you must delete your account." }, 400);
  }

  await db.insert(consentRecords).values({
    userId,
    purposeId: purpose.id,
    status: "withdrawn",
    policyVersion: "current", // withdrawal isn't tied to a specific policy version
    collectionMethod: "user_settings",
    ipAddress: c.req.header("x-forwarded-for") || "unknown",
    userAgent: c.req.header("user-agent") || "unknown",
  });

  // Invalidate cache
  await redis.hdel(`consent:${userId}`, purposeSlug);

  // Trigger downstream cleanup (stop sending emails, remove from third-party lists)
  await redis.lpush("consent:withdrawal:queue", JSON.stringify({
    userId, purposeSlug, withdrawnAt: new Date().toISOString(),
  }));

  return c.json({ success: true, message: "Consent withdrawn. Processing will stop within 24 hours." });
});

// Check consent before processing — called by other services
app.get("/consent/check", async (c) => {
  const userId = c.req.query("userId")!;
  const purposeSlug = c.req.query("purpose")!;

  // Fast path: check Redis cache
  const cached = await redis.hget(`consent:${userId}`, purposeSlug);
  if (cached) {
    return c.json({ allowed: cached === "granted", source: "cache" });
  }

  // Slow path: query latest consent record from database
  const [purpose] = await db.select().from(processingPurposes)
    .where(eq(processingPurposes.slug, purposeSlug));

  if (!purpose) return c.json({ allowed: false, reason: "Unknown purpose" });

  // Legitimate interest doesn't require explicit consent
  if (purpose.legalBasis === "legitimate_interest") {
    return c.json({ allowed: true, basis: "legitimate_interest" });
  }

  const [latest] = await db.select().from(consentRecords)
    .where(and(
      eq(consentRecords.userId, userId),
      eq(consentRecords.purposeId, purpose.id)
    ))
    .orderBy(desc(consentRecords.createdAt))
    .limit(1);

  const allowed = latest?.status === "granted";

  // Cache the result
  await redis.hset(`consent:${userId}`, purposeSlug, allowed ? "granted" : "denied");
  await redis.expire(`consent:${userId}`, 3600);

  return c.json({ allowed, latestRecord: latest?.id, basis: purpose.legalBasis });
});

// Get user's current consent status across all purposes
app.get("/consent/status", async (c) => {
  const userId = c.get("userId");

  const purposes = await db.select().from(processingPurposes)
    .where(eq(processingPurposes.active, true));

  const status = await Promise.all(purposes.map(async (purpose) => {
    const [latest] = await db.select().from(consentRecords)
      .where(and(
        eq(consentRecords.userId, userId),
        eq(consentRecords.purposeId, purpose.id)
      ))
      .orderBy(desc(consentRecords.createdAt))
      .limit(1);

    return {
      purpose: purpose.slug,
      name: purpose.name,
      description: purpose.description,
      legalBasis: purpose.legalBasis,
      status: latest?.status || "not_recorded",
      isRequired: purpose.isRequired,
      lastUpdated: latest?.createdAt,
      thirdParties: purpose.thirdParties,
    };
  }));

  return c.json({ userId, purposes: status });
});

// Submit a Data Subject Access Request (DSAR)
app.post("/dsar", async (c) => {
  const userId = c.get("userId");
  const { type } = await c.req.json(); // "access", "deletion", "portability"

  const dueDate = new Date();
  dueDate.setDate(dueDate.getDate() + 30); // GDPR: 30 days

  const [request] = await db.insert(dsarRequests).values({
    userId,
    type,
    dueDate,
  }).returning();

  // Queue for processing
  await redis.lpush("dsar:queue", JSON.stringify({
    requestId: request.id, userId, type,
  }));

  return c.json({
    requestId: request.id,
    type,
    dueDate: dueDate.toISOString(),
    message: "Your request has been received. We will respond within 30 days.",
  }, 201);
});

// Generate compliance report
app.get("/compliance/report", async (c) => {
  const fromDate = c.req.query("from") || new Date(Date.now() - 90 * 86400000).toISOString();
  const toDate = c.req.query("to") || new Date().toISOString();

  const consentStats = await db.execute(`
    SELECT 
      pp.slug as purpose,
      cr.status,
      COUNT(*) as count
    FROM consent_records cr
    JOIN processing_purposes pp ON cr.purpose_id = pp.id
    WHERE cr.created_at BETWEEN '${fromDate}' AND '${toDate}'
    GROUP BY pp.slug, cr.status
    ORDER BY pp.slug, cr.status
  `);

  const dsarStats = await db.execute(`
    SELECT type, status, COUNT(*) as count,
           AVG(EXTRACT(EPOCH FROM (completed_at - requested_at)) / 86400) as avg_days
    FROM dsar_requests
    WHERE requested_at BETWEEN '${fromDate}' AND '${toDate}'
    GROUP BY type, status
  `);

  const processingStats = await db.execute(`
    SELECT pp.slug as purpose, COUNT(*) as operations, COUNT(DISTINCT pl.user_id) as users
    FROM processing_log pl
    JOIN processing_purposes pp ON pl.purpose_id = pp.id
    WHERE pl.processed_at BETWEEN '${fromDate}' AND '${toDate}'
    GROUP BY pp.slug
  `);

  return c.json({
    period: { from: fromDate, to: toDate },
    consentDecisions: consentStats.rows,
    dsarRequests: dsarStats.rows,
    dataProcessing: processingStats.rows,
    generatedAt: new Date().toISOString(),
  });
});

export default app;

Results

After deploying the consent management platform:

  • DSAR response time: from 3 days to 4 minutes — automated data compilation across all systems replaces manual database searching; well within GDPR's 30-day requirement
  • Audit readiness: instant — every consent decision has a complete provenance chain (when, how, which policy version, IP address); the compliance team generates reports in one click
  • Consent check latency: 2ms — Redis cache means services can verify consent before processing without adding noticeable latency; 99.7% cache hit rate
  • Third-party data sharing compliance improved — withdrawal triggers automatically propagate to Mailchimp, analytics, and ad platforms within 24 hours; previously took weeks of manual work
  • Fine risk reduced to near-zero — the DPA assessment praised the granular, purpose-specific consent model with full audit trail; estimated €200K+ in avoided penalty risk