Skip to content

[Tier 7] Executive reports + public Trust Center #63

@dcoln25-writer

Description

@dcoln25-writer

Problem

Aperio's UI is built for security analysts and operators. Two crucial audiences are unserved:

  1. CISO ↔ board / executive team — needs monthly/quarterly digestible posture reports with trends, narratives, and "what we did". The operator console is the wrong surface for this.
  2. Customer-facing security communications — every B2B buyer asks vendors for a Trust Center page ("show me your SOC 2 controls, your incident posture, your subprocessor list"). Today Aperio doesn't help its customers publish this; they hand-roll it elsewhere.

Both surfaces read from the same underlying posture data — they're presentation layers. Building both turns Aperio from a cost-center tool into a tool that executives can show off and sales teams can hand out.

Goals

  1. Executive reports — auto-generated, narrative-rich PDF (and HTML email) reports, scheduled or on-demand, with KPIs + trends + summarized findings.
  2. Custom KPI builder — operators define a few org-specific KPIs (mean time to remediate, MFA coverage, % findings in SLA, etc.) tracked over time.
  3. Public Trust Center pages — customer-facing posture summary publishable at trust.<customer-domain> or hosted by Aperio at trust.aperio.io/<slug>.
  4. Subprocessor + compliance list — first-class management of the artifacts buyers ask about.
  5. Scheduled report delivery — email (existing Resend integration) or Slack #channel posting.

Non-goals

  • Not building a full BI / dashboarding tool — KPIs are pre-defined templates with operator configurability, not a custom query builder.
  • Not white-labeling Aperio's product UI — the Trust Center is a separate, deliberately simplified surface.
  • Not handling customer security questionnaires (Vanta Trust, SafeBase territory) in v1 — Trust Center is read-only public posture, not response automation.

Proposed design

Executive report architecture

A new background job runs on the configured cadence per ExecutiveReportSubscription row:

  1. Gather inputs for the period:
  2. Generate narrative via the AI provider (AI-native investigation, triage & posture narratives #46): "what changed, what's getting better, what's getting worse, what we recommend."
  3. Render to HTML (Tailwind-styled, branded) and PDF (via chromium headless / weasyprint).
  4. Persist as an ExecutiveReport artifact + deliver via Resend email or Slack.
enum ReportPeriod {
  WEEK
  MONTH
  QUARTER
}

model ExecutiveReportSubscription {
  id              String   @id @default(cuid())
  organizationId  String   @map("organization_id")
  name            String   @db.VarChar(160)
  period          ReportPeriod
  recipientEmails String[] @default([]) @map("recipient_emails")
  slackChannel    String?  @map("slack_channel") @db.VarChar(120)
  kpiSelection    String[] @default([]) @map("kpi_selection")    // refs to KPI template ids
  enabled         Boolean  @default(true)
  lastRunAt       DateTime? @map("last_run_at")
  nextRunAt       DateTime  @map("next_run_at")
  organization    Organization @relation(...)
  @@index([organizationId, enabled, nextRunAt])
  @@map("executive_report_subscriptions")
}

model ExecutiveReport {
  id              String   @id @default(cuid())
  organizationId  String   @map("organization_id")
  subscriptionId  String?  @map("subscription_id")
  period          ReportPeriod
  periodStart     DateTime @map("period_start")
  periodEnd       DateTime @map("period_end")
  htmlStorageUrl  String?  @map("html_storage_url") @db.VarChar(500)
  pdfStorageUrl   String?  @map("pdf_storage_url") @db.VarChar(500)
  summary         String?  @db.Text
  kpiSnapshot     Json     @map("kpi_snapshot")
  status          String   @db.VarChar(20)   // "generating" | "ready" | "delivered" | "failed"
  generatedAt     DateTime? @map("generated_at")
  organization    Organization @relation(...)
  @@index([organizationId, periodEnd])
  @@map("executive_reports")
}

Trust Center

enum TrustCenterVisibility {
  DRAFT
  PRIVATE_LINK
  PUBLIC
}

model TrustCenter {
  id                String   @id @default(cuid())
  organizationId    String   @unique @map("organization_id")
  slug              String   @unique @db.VarChar(120)
  customDomain      String?  @map("custom_domain") @db.VarChar(255)
  brand             Json     // logo url, colors, contact, tagline
  visibility        TrustCenterVisibility @default(DRAFT)
  publishedSections Json     // operator chooses which sections to publish
  privateLinkSecret String?  @map("private_link_secret") @db.VarChar(64)
  createdAt         DateTime @default(now()) @map("created_at")
  updatedAt         DateTime @updatedAt @map("updated_at")
  organization      Organization @relation(...)
  documents         TrustCenterDocument[]
  subprocessors     Subprocessor[]
  @@map("trust_centers")
}

model TrustCenterDocument {
  id              String   @id @default(cuid())
  trustCenterId   String   @map("trust_center_id")
  title           String   @db.VarChar(220)
  description     String?  @db.Text
  storageUrl      String   @map("storage_url") @db.VarChar(500)
  documentKind    String   @map("document_kind") @db.VarChar(60)  // "soc2_report", "iso27001_cert", "pen_test_summary", ...
  expiresAt       DateTime? @map("expires_at")
  requiresNda     Boolean  @default(false) @map("requires_nda")
  createdAt       DateTime @default(now()) @map("created_at")
  trustCenter     TrustCenter @relation(fields: [trustCenterId], references: [id], onDelete: Cascade)
  @@index([trustCenterId, documentKind])
  @@map("trust_center_documents")
}

model Subprocessor {
  id              String   @id @default(cuid())
  trustCenterId   String   @map("trust_center_id")
  name            String   @db.VarChar(160)
  purpose         String   @db.VarChar(255)
  hostingLocation String?  @map("hosting_location") @db.VarChar(120)
  websiteUrl      String?  @map("website_url") @db.VarChar(500)
  privacyUrl      String?  @map("privacy_url") @db.VarChar(500)
  dpaUrl          String?  @map("dpa_url") @db.VarChar(500)
  isActive        Boolean  @default(true) @map("is_active")
  trustCenter     TrustCenter @relation(fields: [trustCenterId], references: [id], onDelete: Cascade)
  @@index([trustCenterId, isActive])
  @@map("subprocessors")
}

The published Trust Center surface is its own Next.js route group (apps/web/app/trust/[slug]/) served separately from the operator console (no auth required for PUBLIC; signed URL for PRIVATE_LINK). Sections operators can toggle:

  • Compliance certifications (with downloadable evidence reports per Handle disallowed CORS origins explicitly #5)
  • Security controls summary (auto-generated from #16 baseline scores + #5 framework scorecards)
  • Subprocessors list
  • Incident history (operator-curated; not auto-published from cases)
  • Contact + responsible disclosure policy
  • Last-updated stamp

KPI templates

Ship built-in templates the operator can enable:

Each KPI is a small SQL/RPC + formatter that produces {value, trend, baseline_label}.

Phasing

Phase Scope
P1 KPI templates; executive report subscription + scheduled cron; HTML/PDF rendering; email delivery via Resend; /admin/reports UI
P2 Trust Center editor + publish flow; public route group; document storage; subprocessors management
P3 AI-generated narrative summaries in reports (depends on #46); custom domain support for Trust Center
P4 Optional integration with vendor risk-management platforms (push compliance posture into ProcessUnity, OneTrust, etc.)

Open questions

  • PDF rendering — bundle headless Chromium (large dep) or accept the WeasyPrint feature gap?
  • Trust Center hosting — Aperio-hosted with subdomain (easy, branding limit) vs. customer CNAME (more work, better look)?
  • Public Trust Center caching — CDN in front so a customer's marketing site doesn't fall over when the page goes viral.
  • Subprocessor auto-discovery from connected vendors (Aperio's own integration list could seed it, but it's the customer's call).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    complianceCompliance frameworks, controls, evidence packsexecutive-reportingExecutive reports + Trust Centertier-7-audience-expansionTier 7: new audiences (execs, customers, ecosystem)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions