Compliance Dashboard Design

Production engineering guide for compliance dashboard design covering patterns, implementation strategies, and operational best practices.

Compliance dashboards are the operational nerve centers for governance, risk, and compliance (GRC) teams. They surface real-time status, highlight drift from policy, and enable proactive response to regulatory demands. A well-designed dashboard matters when auditors arrive unannounced, when a data breach triggers a compliance trigger, or when a new regulation like GDPR or HIPAA requires immediate visibility across systems. This page details how to build and maintain dashboards that answer the right questions at the right time, using only proven, widely adopted tools and patterns.

Defining the Dashboard's Purpose

A compliance dashboard must answer three core questions: *What is compliant? What is failing? What needs attention?* The design begins not with UI mockups, but with a mapping of these questions to data sources, transformations, and alerting logic.

Each question drives a distinct data pipeline:

The dashboard is not a static report. It is a live system, updated at known intervals, with changes visible within minutes of data ingestion.

Designing for the Three Core Questions

1. What is compliant?

This view shows the current state of compliance across domains (e.g., data privacy, security, financial controls). It answers: *Are we meeting the requirements of Regulation X?*

Each row represents a control or requirement. Each column is a compliance metric, such as:

This table is built from a compliance_status table in a PostgreSQL database:

CREATE TABLE compliance_status (
    control_id      TEXT PRIMARY KEY,
    regulation        TEXT NOT NULL,
    description       TEXT,
    policy_id         TEXT,
    evidence_count    INTEGER,
    expected_evidence INTEGER,
    last_scanned      TIMESTAMPTZ,
    status            TEXT CHECK (status IN ('compliant', 'non-compliant', 'partial', 'out-of-scope')),
    score             DECIMAL(5,2),
    updated_at        TIMESTAMPTZ DEFAULT NOW()
);

The dashboard updates this table via a daily cron job using pg_dump and psql:


#!/bin/bash
set -euo pipefail

# Export current compliance status to CSV
psql -U compliance -d compliance_db -c "
    SELECT control_id, regulation, status, score, last_scanned
    FROM compliance_status
    ORDER BY regulation, control_id
" | csvformat -T > /tmp/compliance-status.csv

# Upload to S3 as part of a sync pipeline
aws s3 cp /tmp/compliance-status.csv s3://compliance-data/dashboards/status.csv

# Trigger dashboard refresh via API
curl -X POST https://dashboards.example.com/api/refresh \
     -H "Authorization: Bearer $DASHBOARD_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "source": "s3://compliance-data/dashboards/status.csv" }'

A failure mode: if the last_scanned field is null for a control, the dashboard shows "No scan in 30 days" — but this column is not updated if the cron job fails silently. Use systemctl status cron and journalctl -u cron to debug. The most common oversight: forgetting to run psql -c "VACUUM ANALYZE compliance_status" after bulk inserts, leading to slow queries during peak usage.

2. What is failing?

This view tracks deviations from expected compliance states. It answers: *What has changed? What is drifting? What is out of policy?*

Failing controls are identified through comparisons between expected and actual states.

For example, a control requires that all user accounts are encrypted in storage. The expected state is defined in a JSON schema stored in a Git repository:

// controls/user-encryption.json
{
  "control_id": "user-encryption-2024",
  "description": "All user data must be encrypted at rest.",
  "expected": {
    "encryption_enabled": true,
    "key_rotation_interval_days": 90,
    "default_algorithm": "AES-256"
  },
  "severity": "high",
  "last_updated": "2024-01-15T10:00:00Z"
}

The dashboard pulls this schema and compares it with actual system state via curl calls to internal APIs:

# Get current state of user encryption from auth service
curl -s -H "Authorization: Bearer $AUTH_API_KEY" \
     https://auth.example.com/v1/users/encryption-config \
     | jq -c '{algorithm: .algorithm, enabled: .enabled, rotation_days: .rotation_days}'

The response might be:

{
  "algorithm": "AES-128",
  "enabled": true,
  "rotation_days": 60
}

A discrepancy is flagged when:

The dashboard computes a drift score per control:

def compute_drift_score(expected, actual):
    score = 1.0
    if expected["encryption_enabled"] != actual["enabled"]:
        score -= 0.2
    if expected["default_algorithm"] != actual["algorithm"]:
        score -= 0.3
    if actual["rotation_days"] < expected["key_rotation_interval_days"]:
        score -= 0.5
    return round(score, 2)

A score below 0.8 is marked as "failing." The dashboard visualizes this as a heatmap with color gradients, where red indicates severe drift.

The most subtle failure: the control expects rotation_days to be *at least* 90, but the dashboard computes actual["rotation_days"] < expected["key_rotation_interval_days"], which fails when rotation is 89 days. The expected value is 90, but the actual is 90 — and the control is still compliant. The fix: use <= instead of <, or add a tolerance_days field in the control definition.

3. What needs attention?

This view surfaces actions that require human intervention. It answers: *What tasks are overdue? What decisions are pending?*

Tasks are modeled as a compliance_tasks table:

CREATE TABLE compliance_tasks (
    task_id         UUID PRIMARY KEY,
    control_id      TEXT REFERENCES compliance_status(control_id),
    title             TEXT NOT NULL,
    description       TEXT,
    assigned_to       TEXT,
    due_date          DATE,
    status            TEXT CHECK (status IN ('pending', 'in-progress', 'review', 'completed', 'overdue')),
    created_at        TIMESTAMPTZ DEFAULT NOW(),
    updated_at        TIMESTAMPTZ,
    metadata          JSONB
);

Each task has a metadata field that captures context:

{
  "evidence_file": "/evidence/consent/2024-01-15-001.pdf",
  "reviewer": "[email protected]",
  "approval_required": true,
  "related_scans": [
    "scan-2024-01-14",
    "scan-2024-01-12"
  ]
}

The dashboard refreshes this list from a task-scheduler service that runs every 15 minutes:

# /etc/cron.d/compliance-tasks
*/15 * * * * root /opt/compliance/task-scheduler --from 2024-01-15T00:00:00Z

The task scheduler fetches new tasks from multiple sources:

Tasks are grouped by:

A critical edge case: a task is marked in-progress but no updated_at timestamp is updated when a reviewer submits a comment. The dashboard shows the task as stale, even though it's being worked on. The fix: use UPDATE compliance_tasks SET updated_at = NOW() WHERE task_id = $1 after every comment save.

Integrating Data Sources

Data for the dashboard comes from multiple sources, each with distinct patterns and failure modes.

From Automated Scans

Scans are run by tools like nmap, ansible, or osquery. The output is a JSON file that conforms to a standard schema:

{
  "scan_id": "2024-01-15-001",
  "timestamp": "2024-01-15T12:34:56Z",
  "target": "all-users",
  "results": [
    {
      "control_id": "user-encryption-2024",
      "status": "passed",
      "details": {
        "encryption_enabled": true,
        "algorithm": "AES-256",
        "rotation_days": 90
      },
      "metrics": {
        "scan_duration_seconds": 32.4,
        "files_scanned": 1247
      }
    }
  ]
}

The dashboard ingests this via a scan-importer service that:

  1. Listens on s3://compliance-scans/ for new files.
  2. Parses JSON, validates schema with jq:
   jq -e 'has("scan_id") and .results | all(.[] | has("control_id"))' /tmp/scan.json
  1. Loads into the compliance_status table using psql:
   psql -U compliance -d compliance_db -c "
       INSERT INTO compliance_status (control_id, status, score, last_scanned)
       SELECT r.control_id, r.status, 1.0, s.timestamp
       FROM json_populate_recordset(NULL::compliance_status, $1) r
       JOIN (SELECT scan_id, timestamp FROM scans WHERE scan_id = $2) s ON r.scan_id = s.scan_id
       ON CONFLICT (control_id) DO UPDATE SET status = EXCLUDED.status, last_scanned = EXCLUDED.last_scanned
   " --param scan_data="$(cat /tmp/scan.json)" --param scan_id="2024-01-15-001"

Failures: missing scan_id field, duplicate scans, and timestamp in invalid format (e.g., 2024-01-15 12:34:56 instead of ISO 8601). The dashboard fails silently when scan_duration_seconds is a string instead of a number.

Real-time Updates and Visual Feedback

The dashboard uses WebSockets to push updates in real time.

A dashboard-sync service subscribes to changes in the database via LISTEN and NOTIFY:

-- In the database
CREATE OR REPLACE FUNCTION notify_compliance_change()
RETURNS TRIGGER AS $$
BEGIN
    PERFORM pg_notify('compliance_change', json_build_object(
        'table', TG_TABLE_NAME,
        'action', TG_OP,
        'row_id', NEW.control_id
    )::text);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER compliance_change_trigger
    AFTER INSERT OR UPDATE OR DELETE ON compliance_status
    FOR EACH ROW EXECUTE FUNCTION notify_compliance_change();

Clients connect via wscat or a simple WebSocket client:

const ws = new WebSocket('wss://dashboards.example.com/ws');

ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.table === 'compliance_status') {
        updateDashboardRow(data.row_id, data.action);
    }
};

The most common failure: pg_notify not being triggered due to missing LISTEN command in the client connection, or the NOTIFY event being sent but not delivered because the WebSocket connection was dropped.

Common Pitfalls in Dashboard Design

Summary of Core Patterns

The dashboard is not a product. It is a process, a data flow, and a set of expectations. When it breaks, the answer is always: *What is the last updated timestamp? Is the data in the right format? Has the pipeline been notified?*

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.