Browse documentation

Frontend Specs

Section 10 — Data Governance (`/admin/data-governance`)

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard.

docs/specs/frontend/admin-spec-03d-data-governance.md
On this page

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard. Layout: 3-tab interface: Retention, Exports, Deletion Requests. 1200px max-width. Sidebar: carbon (#12161E) background. Notes: All data governance actions are logged in the audit trail. GDPR/CCPA compliance features are centralized here. Deletion is permanent and irreversible after 7-day grace period.


10.1 Data Retention Dashboard#

1. Purpose#

Platform-wide overview of data retention policies and current storage footprint. Admins see how much data is stored, what's approaching retention limits, and which clients have data retention overrides.

2. Visual Layout#

Section A: Storage Overview Cards (top, full width)

  • 4 stat cards in a row:
    • Total Storage Used: {N} GB (e.g., 47.3 GB). Progress bar: used / limit. Green if < 50%, amber if < 80%, red if >= 80%.
    • Clients: {N} active clients with data.
    • Content Items: {N} total (blog posts, social posts, GBP posts, citations).
    • Audit Events: {N} (last 90 days). "Auto-purged in {N} days."
  • Each card: white background, shadow-sm, 8px radius. Value in text-2xl text-primary. Label in text-sm text-muted.

Section B: Retention Policy Table (middle, full width)

  • Table columns: Data Type | Default Retention | Auto-Purge | Current Volume | Next Purge | Actions
  • Data types:
    • Audit Logs: 90 days
    • System Logs: 30 days
    • Content Versions: 30 days (keep latest + last 5 versions)
    • Session Data: 30 days
    • Failed Login Attempts: 7 days
    • Exported Reports: 30 days
    • Deleted Content (Soft): 30 days (then hard delete)
    • Analytics Events: 365 days
    • Backup Snapshots: 30 days (keep last 10)
  • Default Retention: Number + unit. Editable inline for ADMIN.
  • Auto-Purge: Toggle ON/OFF. ON = system automatically deletes after retention period. OFF = data kept indefinitely (manual purge only).
  • Current Volume: {N} MB or {N} GB for that data type.
  • Next Purge: "In 3 days" or "{date} {time}" or "Manual only" (if auto-purge OFF).
  • Actions: "Purge Now" (secondary, danger style) → confirmation modal → immediate purge. "Edit Policy" (pencil) → inline edit retention days + toggle auto-purge.

Section C: Client Overrides (bottom, full width)

  • Table: Client Name | Plan | Default Retention | Override | Status | Actions
  • Override: If client has custom retention (e.g., enterprise client requests 180-day audit retention), shows override value. Otherwise "—".
  • Status: COMPLIANT (green) or OVERRIDE (amber).
  • Actions: "Edit Override" → modal with retention days + justification field. "Remove Override" → reset to default.
  • Pagination: 20 per page.

Section D: Storage Trend Chart (above table, right side)

  • Area chart (Recharts): storage GB over last 30 days. 1 line per data type (stacked). Toggle data types on/off via legend.
  • Y-axis: GB. X-axis: date. Tooltip: total GB on that date + breakdown by type.

3. Data Source (tRPC endpoint)#

admin.getRetentionDashboard.useQuery(undefined, { refetchInterval: 300000 }); // 5min static
admin.updateRetentionPolicy.useMutation({
  dataType: z.string(),
  retentionDays: z.number().min(1).max(3650),
  autoPurge: z.boolean(),
});
admin.purgeDataNow.useMutation({
  dataType: z.string(),
  confirmText: z.literal("PERMANENTLY DELETE"), // safety confirmation
});
admin.updateClientRetentionOverride.useMutation({
  clientId: z.string().uuid(),
  retentionDays: z.number().min(1).max(3650),
  justification: z.string().min(10).max(500),
});

4. Zod Schema#

const DataTypeSchema = z.enum([
  "AUDIT_LOGS", "SYSTEM_LOGS", "CONTENT_VERSIONS", "SESSION_DATA",
  "FAILED_LOGINS", "EXPORTED_REPORTS", "DELETED_CONTENT_SOFT",
  "ANALYTICS_EVENTS", "BACKUP_SNAPSHOTS",
]);

const RetentionPolicySchema = z.object({
  dataType: DataTypeSchema,
  displayName: z.string(),
  defaultRetentionDays: z.number().min(1).max(3650),
  autoPurge: z.boolean().default(true),
  currentVolumeMb: z.number(),
  nextPurgeAt: z.date().optional(),
  isConfigurable: z.boolean().default(true), // some types are fixed by compliance
  complianceReason: z.string().optional(), // e.g., "GDPR Article 5(1)(e)"
});

const ClientRetentionOverrideSchema = z.object({
  clientId: z.string().uuid(),
  clientName: z.string(),
  plan: z.enum(["STARTER", "GROWTH", "PRO"]),
  defaultRetentionDays: z.number(),
  overrideDays: z.number().optional(),
  overrideJustification: z.string().optional(),
  status: z.enum(["COMPLIANT", "OVERRIDE"]),
  overriddenAt: z.date().optional(),
  overriddenBy: z.string().optional(),
});

const RetentionDashboardSchema = z.object({
  totalStorageGb: z.number(),
  storageLimitGb: z.number().default(100),
  activeClients: z.number(),
  totalContentItems: z.number(),
  auditEventsCount: z.number(),
  auditEventsAutoPurgeInDays: z.number(),
  policies: z.array(RetentionPolicySchema),
  clientOverrides: z.array(ClientRetentionOverrideSchema),
  storageTrend: z.array(z.object({
    date: z.date(),
    totalGb: z.number(),
    byType: z.record(z.number()), // { AUDIT_LOGS: 1.2, SYSTEM_LOGS: 0.8, ... }
  })),
});

5. Fetch Frequency#

  • Static (300000ms / 5min): Storage and retention data changes slowly. 5min polling.
  • On-demand: After policy update, purge, or override change.
  • SSE: When auto-purge job runs, push update to dashboard (next purge dates update).

6. Data Manipulations#

  • Storage formatting: < 1024 MB → "{N} MB"; >= 1024 MB → "{N} GB" (1 decimal). < 1 MB → "{N} KB".
  • Progress bar: currentVolume / totalStorageLimit → colored bar. Green < 50%, amber < 80%, red >= 80%.
  • Retention formatting: 1 → "1 day"; > 1 → "{N} days"; >= 365 → "{N} years" (e.g., "2 years" for 730 days).
  • Next purge: "In {N} days" if > 1 day; "Tomorrow" if 1 day; "Today at {time}" if 0 days; "Manual only" if auto-purge OFF.
  • Trend chart: Stacked area chart. Each data type gets a color from the palette (no Ember orange for non-CTA). Legend toggleable.
  • Purge confirmation: Modal requires typing "PERMANENTLY DELETE" in uppercase. This is a destructive action. 7-day grace period for deleted content.
  • Compliance tagging: Some policies show compliance reason (e.g., "GDPR Article 5(1)(e) — kept no longer than necessary"). Non-editable.

7. Rationale#

  • Storage visibility: Admins need to know if the platform is approaching storage limits. If a client uploads 1000 high-res images, storage spikes. Visibility prevents surprises.
  • Retention policies: Legal requirement (GDPR/CCPA). Data can't be kept indefinitely. Automated purging ensures compliance without manual work.
  • Client overrides: Enterprise clients may negotiate longer retention. Centralized override management prevents ad-hoc exceptions scattered in DB.
  • 5min polling: Storage changes slowly (GB-scale). No need for real-time. Purge job runs daily, so daily updates are sufficient.
  • Trend chart: Visualizes storage growth. If storage grows 10% week-over-week, admin knows to investigate (likely a client bulk-uploading or logs not rotating).
  • Purge confirmation: "PERMANENTLY DELETE" typing requirement is a safety pattern. Prevents accidental clicks. 7-day grace period allows recovery if mistake.
  • Compliance tagging: Some retention periods are legally mandated, not arbitrary. Showing the compliance reason educates admins and prevents them from shortening retention illegally.
  • Auto-purge toggle: Some data types (e.g., audit logs) might need to be kept longer for an active investigation. Toggle OFF temporarily, then back ON.

8. Interaction Flows#

  • Edit policy: Click pencil on row → inline edit: retention days number input + auto-purge toggle → "Save" → mutation → toast "Policy updated. Next purge: {date}." → audit logged.
  • Purge now: Click "Purge Now" → modal: "This will permanently delete {N} {data type} items. Type PERMANENTLY DELETE to confirm." → input field → type correctly → "Purge" → mutation → toast "Purge complete. {N} items deleted." → row updates (volume drops to 0, next purge shows "—").
  • Add override: Click "Add Override" on client row → modal: retention days + justification textarea → "Save" → toast "Override applied for {clientName}." → row updates to OVERRIDE status.
  • Remove override: Click "Remove" → confirmation → reset to default → toast "Override removed."
  • Trend chart interaction: Hover → tooltip shows total + breakdown. Click legend item → toggle that data type on/off. Reset button restores all.
  • Impersonation: Read-only. All edit, purge, override buttons disabled. Dashboard viewable for context.

9. Error States#

  • Loading: Skeleton cards + table (8 rows).
  • Empty policies: "No retention policies configured. Set up policies to ensure compliance."
  • Storage limit approaching: If > 80%, red banner: "Storage at {N}% of limit. Purge old data or increase limit."
  • Purge fail: Toast "Purge failed. {N} items could not be deleted. Check logs." → some items may be locked (referenced by active processes).
  • Policy update fail: Toast "Failed to update policy. Retention cannot exceed {max} days for {data type}." (compliance-enforced max).
  • Override fail: Toast "Override failed. Enterprise plan required for custom retention." (if client not on eligible plan).
  • No permission: Non-ADMIN redirected.

10. Role-Based Variations#

  • ADMIN: Full access — edit policies, purge, manage overrides, view trends.
  • Other roles: No access.
  • Impersonation: Read-only. All action buttons disabled. Can view storage, policies, overrides. Cannot modify or purge.

10.2 Client Data Export#

1. Purpose#

GDPR Article 20 compliance — "right to data portability." Admins can export all data for a specific client in a structured, machine-readable format. Also used for client offboarding and backup.

2. Visual Layout#

Section A: Export Form (top, full width)

  • Client selector: Searchable dropdown. Type client name → autocomplete. Shows name, email, plan, data size estimate.
  • Data scope: Multi-select checkboxes:
    • Profile data (name, email, phone, company info)
    • Content (all blog posts, social posts, GBP posts)
    • Leads (all lead data, forms, submissions)
    • Analytics (traffic, rankings, engagement data)
    • Billing (invoices, payments, subscriptions)
    • System data (audit events, login history, sessions)
  • Format: Radio group — JSON (structured, machine-readable) | CSV (tabular, human-readable) | ZIP (all files, attachments included).
  • Anonymize: Toggle "Anonymize third-party data" (remove PII of leads/customers if not owned by client). GDPR best practice.
  • Estimated size: "Estimated: {N} MB" — updates live as checkboxes change.
  • "Generate Export" button: Ember Orange CTA. Disabled until client selected and at least one scope checked.

Section B: Export History (below, full width)

  • Table: Client | Requested By | Scope | Format | Size | Status | Created | Download | Actions
  • Status:
    • QUEUED: Gray badge, spinner icon. "In queue..."
    • PROCESSING: Amber badge, spinner icon. "Processing..."
    • READY: Green badge, checkmark. "Ready for download"
    • EXPIRED: Gray badge. "Expired (7 days old)"
    • FAILED: Red badge. "Failed — retry available"
  • Download: "Download" link (if READY) or "—" (if not). File expires after 7 days.
  • Actions: "Retry" (if FAILED), "Delete" (if EXPIRED or FAILED), "View Details" (modal with scope breakdown and any errors).
  • Pagination: 20 per page.

Section C: Export Detail Modal

  • Click "View Details" → modal:
    • Client info, requested by, requested at.
    • Scope checklist with item counts: "Blog posts: 47", "Social posts: 123", "Leads: 89", etc.
    • Processing log: timestamps of each phase (queued → processing → ready).
    • Error log (if failed): specific error message and stack trace.
    • "Download" button (if ready).

3. Data Source (tRPC endpoint)#

admin.requestClientDataExport.useMutation({
  clientId: z.string().uuid(),
  scopes: z.array(z.enum(["PROFILE", "CONTENT", "LEADS", "ANALYTICS", "BILLING", "SYSTEM"])),
  format: z.enum(["JSON", "CSV", "ZIP"]),
  anonymize: z.boolean().default(false),
});

admin.getExportHistory.useQuery({
  page: z.number().min(1).default(1),
  pageSize: z.number().min(1).max(50).default(20),
}, { refetchInterval: 30000 }); // 30s frequent

admin.downloadExport.useQuery({
  exportId: z.string().uuid(),
}, { enabled: false }); // manual trigger

admin.retryExport.useMutation({ exportId: z.string().uuid() });
admin.deleteExport.useMutation({ exportId: z.string().uuid() });

4. Zod Schema#

const ExportScopeSchema = z.enum(["PROFILE", "CONTENT", "LEADS", "ANALYTICS", "BILLING", "SYSTEM"]);

const ExportStatusSchema = z.enum(["QUEUED", "PROCESSING", "READY", "EXPIRED", "FAILED"]);

const ExportHistoryItemSchema = z.object({
  id: z.string().uuid(),
  clientId: z.string().uuid(),
  clientName: z.string(),
  requestedBy: z.string(),
  scopes: z.array(ExportScopeSchema),
  format: z.enum(["JSON", "CSV", "ZIP"]),
  estimatedSizeMb: z.number(),
  actualSizeMb: z.number().optional(),
  status: ExportStatusSchema,
  itemCounts: z.record(z.number()).optional(), // { "CONTENT": 47, "LEADS": 89 }
  createdAt: z.date(),
  completedAt: z.date().optional(),
  expiresAt: z.date().optional(),
  downloadUrl: z.string().url().optional(),
  errorMessage: z.string().optional(),
  anonymized: z.boolean().default(false),
});

const ExportHistoryResponseSchema = z.object({
  items: z.array(ExportHistoryItemSchema),
  total: z.number(),
  hasMore: z.boolean(),
});

5. Fetch Frequency#

  • Frequent (30000ms / 30s): Export status changes from QUEUED → PROCESSING → READY. 30s polling catches transitions.
  • On-demand: Initial request, retry, delete, download.
  • SSE: When export status changes (processing complete, failed), push to history table.

6. Data Manipulations#

  • Size formatting: < 1 MB → "{N} KB"; < 1024 MB → "{N} MB"; >= 1024 MB → "{N} GB".
  • Estimated size: Computed server-side based on row counts and average row size per scope. Updates as user checks/unchecks scopes.
  • Status badge colors: QUEUED/EXPIRED = gray; PROCESSING = amber; READY = green; FAILED = red.
  • Time remaining: For READY exports, "Expires in {N} days" (7-day default). Expired exports show "Expired" and download disabled.
  • Scope labels: "PROFILE" → "Profile Data"; "CONTENT" → "Content (Blog, Social, GBP)"; etc. Human-readable mapping.
  • Format icon: JSON = { } icon; CSV = spreadsheet icon; ZIP = archive icon.
  • Anonymize: If toggled, export excludes PII fields (names, emails, phones) from lead data and third-party contacts. Replaced with hashes or "REDACTED".
  • Progress tracking: For PROCESSING exports, progress bar in history table: "Processing: 45%" with progress bar.

7. Rationale#

  • GDPR compliance: Article 20 requires data portability. This is the tool to fulfill it. Every client can request their data, and admin can generate it in minutes.
  • Structured format: JSON is machine-readable — client can import into another system. CSV is human-readable for review. ZIP includes attachments (images, PDFs).
  • Anonymize toggle: GDPR says you can only export data the client has a legal basis to process. If leads didn't consent to data portability, their PII is anonymized. Protects admin from liability.
  • 30s polling: Export generation is async (could take 1–10 minutes for large clients). Polling shows progress. SSE pushes completion event.
  • 7-day expiry: Security. Export files contain sensitive data. They shouldn't sit on a server forever. Auto-expire after 7 days. Admin can delete earlier.
  • Scope selection: Not all clients want everything. "Just my blog posts and leads, please." Scope checkboxes let them choose. Also reduces server load.
  • Estimated size: Prevents shock. If a client has 2GB of data, admin knows before clicking generate. Prevents timeouts and browser crashes.
  • Retry on fail: If export fails (e.g., DB timeout), retry button tries again with same parameters. No need to re-fill the form.
  • Audit trail: Every export request is logged with who, what, when. Compliance auditors love this.

8. Interaction Flows#

  • Select client: Type in dropdown → autocomplete → click client → form populates with estimated size → scope checkboxes enabled.
  • Check scope: Click checkbox → estimated size recalculates → "Estimated: 47 MB" updates live.
  • Generate: Click "Generate Export" → mutation → history table updates with new row (QUEUED) → toast "Export queued. Check history for progress."
  • Watch progress: History row shows spinner → status changes to PROCESSING → progress bar fills → status changes to READY → download link appears → green toast "Export ready for {clientName}."
  • Download: Click "Download" → browser downloads file. File name: {clientName}_export_{YYYYMMDD}_{format}.zip.
  • Retry fail: Click "Retry" on FAILED row → status resets to QUEUED → processing begins again → toast "Retry started."
  • Delete: Click "Delete" → confirmation → row removed → file deleted from server → toast "Export deleted."
  • Expired: Download link disabled, grayed out. "Expired on {date}. Request a new export."
  • Impersonation: Read-only. Cannot request new exports. Can view history (for context). Download, retry, delete disabled.

9. Error States#

  • Loading: Skeleton form + history table (5 rows).
  • No clients: "No clients to export. Add clients first."
  • Export fail: Row shows FAILED + error message. "Database timeout during content export. Retry or contact engineering."
  • Large export: If estimated > 500 MB, amber warning: "Large export ({N} GB). May take 10+ minutes. Continue?" → confirmation.
  • Download fail: Toast "Download failed. File may have expired. Request a new export."
  • Anonymize warning: If anonymize ON, info banner: "Third-party PII will be redacted. Client's own data remains intact."
  • Concurrent export: If another export for same client is QUEUED/PROCESSING, toast "An export for {clientName} is already in progress. Wait for completion."
  • No permission: Non-ADMIN redirected.

10. Role-Based Variations#

  • ADMIN: Full access — request, download, retry, delete, view history.
  • Other roles: No access.
  • Impersonation: Read-only. History viewable. Cannot request, download, retry, or delete exports.

10.3 Data Deletion Requests#

1. Purpose#

GDPR Article 17 compliance — "right to erasure." Centralized queue of all data deletion requests (client-initiated or admin-initiated). Admins review, approve, and execute deletion with full audit trail.

2. Visual Layout#

Section A: Stats Bar (top, full width)

  • 4 stat cards:
    • "Pending Requests: {N}" (amber if > 0, green if 0)
    • "Approved Today: {N}"
    • "Completed Today: {N}"
    • "Avg Processing Time: {N} min"
  • Pulsing amber indicator on "Pending" if > 0.

Section B: Request Queue (below, full width)

  • Table: Request ID | Client | Type | Reason | Requested | Status | Actions
  • Request ID: Short UUID (first 8 chars) + copy icon.
  • Client: Name + email.
  • Type:
    • FULL_ACCOUNT: Delete entire client account + all data.
    • CONTENT_ONLY: Delete all content but keep account/billing.
    • LEADS_ONLY: Delete all lead data.
    • ANALYTICS_ONLY: Delete all analytics data.
    • CUSTOM: Specific scope listed in tooltip.
  • Reason: "Client request via email", "GDPR Article 17", "Account closure", "Data breach cleanup", "Custom: {text}"
  • Requested: Relative time. "2 hr ago" by "Admin John" or "Client via portal".
  • Status:
    • PENDING: Amber badge. "Pending review"
    • APPROVED: Blue badge. "Approved by {name} at {time}"
    • PROCESSING: Amber badge + spinner. "Deleting..."
    • COMPLETED: Green badge. "Completed at {time}"
    • REJECTED: Red badge. "Rejected: {reason}"
    • CANCELLED: Gray badge. "Cancelled by {name}"
  • Actions:
    • PENDING: "Approve" (green) + "Reject" (red) + "View Details"
    • APPROVED: "Execute Now" (ember) + "Cancel"
    • PROCESSING: "—" (no actions, in progress)
    • COMPLETED: "View Details" + "Download Certificate" (PDF proof of deletion)
    • REJECTED/CANCELLED: "View Details" + "Reopen" (if appropriate)

Section C: Request Detail Panel (slide-in from right)

  • Click row → panel:
    • Full request details: ID, client, type, reason, scope, requested by, requested at.
    • Data impact summary: "This will delete: {N} blog posts, {N} social posts, {N} leads, {N} analytics records. Estimated size: {N} MB."
    • Deletion checklist: 10+ items (delete from DB, delete from S3, delete from cache, delete from search index, etc.) with status per item.
    • Audit trail: log of all actions on this request (requested → approved → executed → completed).
    • For COMPLETED: "Certificate of Deletion" section with download link and verification hash.
    • Notes textarea: admin can add internal notes (not visible to client).

Section D: New Request Button

  • "+ New Deletion Request" button (ember, top right) → opens modal:
    • Client selector (searchable)
    • Type radio group (FULL_ACCOUNT, CONTENT_ONLY, etc.)
    • Custom scope (if CUSTOM selected): multi-select checkboxes
    • Reason textarea (required, min 10 chars)
    • "Request Deletion" button
    • Warning banner: "This action is irreversible after 7-day grace period. All data will be permanently deleted."

3. Data Source (tRPC endpoint)#

admin.getDeletionRequests.useQuery({
  status: z.enum(["PENDING", "APPROVED", "PROCESSING", "COMPLETED", "REJECTED", "CANCELLED", "ALL"]).default("ALL"),
  page: z.number().min(1).default(1),
  pageSize: z.number().min(1).max(50).default(20),
}, { refetchInterval: 30000 }); // 30s frequent

admin.approveDeletionRequest.useMutation({ requestId: z.string().uuid(), reason: z.string().optional() });
admin.rejectDeletionRequest.useMutation({ requestId: z.string().uuid(), reason: z.string().min(1) });
admin.executeDeletion.useMutation({ requestId: z.string().uuid(), confirmText: z.literal("PERMANENTLY DELETE") });
admin.cancelDeletionRequest.useMutation({ requestId: z.string().uuid() });
admin.createDeletionRequest.useMutation({
  clientId: z.string().uuid(),
  type: z.enum(["FULL_ACCOUNT", "CONTENT_ONLY", "LEADS_ONLY", "ANALYTICS_ONLY", "CUSTOM"]),
  customScopes: z.array(z.string()).optional(),
  reason: z.string().min(10).max(1000),
});
admin.downloadDeletionCertificate.useQuery({ requestId: z.string().uuid() }, { enabled: false }); // manual

4. Zod Schema#

const DeletionTypeSchema = z.enum(["FULL_ACCOUNT", "CONTENT_ONLY", "LEADS_ONLY", "ANALYTICS_ONLY", "CUSTOM"]);

const DeletionStatusSchema = z.enum(["PENDING", "APPROVED", "PROCESSING", "COMPLETED", "REJECTED", "CANCELLED"]);

const DeletionRequestSchema = z.object({
  id: z.string().uuid(),
  clientId: z.string().uuid(),
  clientName: z.string(),
  clientEmail: z.string().email(),
  type: DeletionTypeSchema,
  customScopes: z.array(z.string()).optional(),
  reason: z.string(),
  requestedBy: z.string(), // user name or "CLIENT_PORTAL"
  requestedByRole: z.enum(["ADMIN", "CLIENT", "SYSTEM"]).optional(),
  requestedAt: z.date(),
  approvedBy: z.string().optional(),
  approvedAt: z.date().optional(),
  approvedReason: z.string().optional(),
  rejectedBy: z.string().optional(),
  rejectedAt: z.date().optional(),
  rejectedReason: z.string().optional(),
  executedBy: z.string().optional(),
  executedAt: z.date().optional(),
  completedAt: z.date().optional(),
  status: DeletionStatusSchema,
  dataImpact: z.object({
    blogPosts: z.number(),
    socialPosts: z.number(),
    gbpPosts: z.number(),
    leads: z.number(),
    analyticsRecords: z.number(),
    totalSizeMb: z.number(),
  }).optional(),
  deletionChecklist: z.array(z.object({
    step: z.string(),
    status: z.enum(["PENDING", "DONE", "FAILED"]),
    completedAt: z.date().optional(),
  })).optional(),
  certificateHash: z.string().optional(), // SHA-256 hash of deletion proof
  notes: z.array(z.object({
    text: z.string(),
    createdAt: z.date(),
    createdBy: z.string(),
  })).optional(),
});

const DeletionRequestListResponseSchema = z.object({
  items: z.array(DeletionRequestSchema),
  total: z.number(),
  pendingCount: z.number(),
  approvedToday: z.number(),
  completedToday: z.number(),
  avgProcessingMinutes: z.number().optional(),
  hasMore: z.boolean(),
});

5. Fetch Frequency#

  • Frequent (30000ms / 30s): Deletion request status changes (PENDING → APPROVED → PROCESSING → COMPLETED). 30s polling.
  • On-demand: After approval, rejection, execution, cancellation, creation.
  • SSE: When status changes on any request, push to queue. When new request submitted, push to top of queue.

6. Data Manipulations#

  • Status badge colors: PENDING = amber; APPROVED = blue; PROCESSING = amber + spinner; COMPLETED = green; REJECTED = red; CANCELLED = gray.
  • Relative timestamps: Same as other screens. "2 hr ago", "Yesterday", etc.
  • Data impact: Formatted counts. "1,247 blog posts" with commas. "47 MB" total size.
  • Deletion checklist: Visual checklist with checkmarks for DONE, spinners for PENDING, X for FAILED. Shows real-time progress during PROCESSING.
  • Certificate hash: SHA-256 hash shown in detail panel. Copy button. "This hash proves the deletion was executed and verified."
  • 7-day grace period: For PENDING requests, banner shows: "7-day grace period: data will be deleted on {date} if approved." For APPROVED but not executed: "Execute before {date} or request will expire."
  • Type labels: Human-readable. FULL_ACCOUNT → "Full Account Deletion"; CONTENT_ONLY → "Content Only"; etc.
  • Requester: "By admin" or "By client via portal" or "By system (auto-cleanup)".
  • Notes: Admin-only notes (not visible to client). Used for internal justification.

7. Rationale#

  • Approval workflow: Deletion is destructive. Two-person rule (requester + approver) prevents accidents. Even if admin creates the request, another admin should approve it. Or same admin can approve with explicit justification.
  • GDPR compliance: Article 17 requires erasure within 30 days of request. This queue tracks deadlines, ensures nothing falls through cracks, and provides audit proof.
  • Certificate of deletion: Legal proof. If a client claims "you still have my data," the certificate with hash is cryptographic proof of deletion. PDF format for legal admissibility.
  • Data impact preview: Before approving, admin sees exactly what will be deleted. Prevents "oops, I deleted 2 years of blog posts." Informed consent.
  • Deletion checklist: Technical deletion has 10+ steps (DB, S3, cache, search index, CDN, backup, etc.). Checklist shows progress. If one step fails, admin knows exactly what wasn't deleted.
  • 30s polling: Request status changes during the approval workflow. Real-time awareness prevents delays.
  • 7-day grace period: GDPR says erasure must be prompt, but a short grace period allows for accidental request recovery. After 7 days, irreversible.
  • Rejection with reason: If a deletion request is invalid (e.g., client still owes money, or legal hold applies), admin rejects with reason. Client sees the reason in their portal.
  • Notes: Internal documentation. "Legal hold applied until 2027-01-01 due to litigation. Do not delete." — critical for legal teams.
  • Impersonation: During impersonation, an admin sees a client's data but cannot delete it. Deletion request would be created as the admin, not the client. Prevents accidental deletion while in client context.

8. Interaction Flows#

  • Create request: Click "+ New Deletion Request" → modal → select client → select type → enter reason → "Request Deletion" → confirmation modal: "This will queue a deletion request. Data will not be deleted until approved and executed." → mutation → toast "Request created. Pending approval." → row appears in queue.
  • Approve: Click "Approve" on PENDING row → optional reason textarea → "Approve" → status changes to APPROVED → toast "Request approved. Execute when ready." → audit logged.
  • Reject: Click "Reject" → mandatory reason textarea → "Reject" → status changes to REJECTED → toast "Request rejected. Reason: {reason}" → client notified (if client-initiated).
  • Execute: Click "Execute Now" on APPROVED row → modal: "This is IRREVERSIBLE. Type PERMANENTLY DELETE." → input → type correctly → "Execute" → status changes to PROCESSING → checklist appears in detail panel → steps complete one by one → status changes to COMPLETED → green toast "Deletion complete. Certificate generated."
  • Cancel: Click "Cancel" on PENDING or APPROVED → confirmation → status changes to CANCELLED → toast "Request cancelled."
  • View certificate: On COMPLETED row, click "Download Certificate" → PDF downloads. Filename: deletion_certificate_{clientName}_{requestId}.pdf.
  • Reopen: On REJECTED/CANCELLED, click "Reopen" → status resets to PENDING → toast "Request reopened for review."
  • Detail panel: Click row → panel slides in → full checklist, audit trail, notes, impact summary.
  • Add note: In detail panel, type note → "Save Note" → appended to notes list.
  • Impersonation: Cannot create deletion requests. "New Deletion Request" button hidden. Approve, reject, execute all disabled. Can view queue and details for context.

9. Error States#

  • Loading: Skeleton stats + table (5 rows).
  • Empty queue: "No deletion requests. When clients or admins request data deletion, they will appear here."
  • Execution fail: If checklist step fails, status stays at PROCESSING but step shows FAILED. "Step 7 (Search index cleanup) failed. Retry or contact engineering." → "Retry Step" button on failed step.
  • Certificate generation fail: Toast "Certificate generation failed. Deletion completed but proof unavailable. Contact engineering."
  • Pending overflow: If > 10 pending requests, amber banner: "{N} pending deletion requests. Review and process to meet GDPR deadlines."
  • Overdue: If PENDING > 30 days since request, red banner: "⚠️ Overdue deletion request ({N} days). GDPR violation risk." with direct link to request.
  • Concurrent execution: If another deletion for same client is PROCESSING, toast "A deletion for {clientName} is already running. Wait for completion."
  • No permission: Non-ADMIN redirected.

10. Role-Based Variations#

  • ADMIN: Full access — create, approve, reject, execute, cancel, reopen, view certificates, add notes.
  • Other roles: No access.
  • Impersonation: Read-only. Queue viewable. All action buttons disabled/hidden. Cannot create, approve, or execute deletions.