Skip to main content

Overview

Risk Legion uses PostgreSQL (via Supabase) with a normalized schema design. The database implements Row-Level Security (RLS) for multi-tenant data isolation and comprehensive audit logging.

Schema Design Principles

PrincipleImplementation
NormalizationThird Normal Form (3NF) for data integrity
UUID KeysUUID primary keys for distributed compatibility
Soft Deletesarchived flags preserve audit trail
Timestampscreated_at, updated_at on all tables
Multi-Tenancyenterprise_id for tenant isolation

Core Tables

Enterprise & User Management

enterprises

Top-level tenant organization.
ColumnTypeDescription
idUUIDPrimary key
nameVARCHAREnterprise display name
countryVARCHARCountry code
registration_numberVARCHARBusiness registration
statusVARCHARactive, inactive, suspended
subscription_tierVARCHARstarter, professional, enterprise
mrr_centsINTEGERMonthly recurring revenue (cents)

enterprise_users

Links users to enterprises with roles.
ColumnTypeDescription
enterprise_idUUID FKReference to enterprises
user_idUUID FKReference to profiles
roleVARCHARadmin, assessor, reviewer

Organization Structure

Legal organizational units with hierarchical structure.
ColumnTypeDescription
parent_idUUID FKSelf-reference for hierarchy
entity_typeVARCHARBank, Insurance, Investment Firm
country_of_incorporationVARCHARJurisdiction

business_units

Operational divisions within legal entities.

products

Financial products with JSONB arrays for flexible attributes.

Risk Library

risk_scenarios

Risk scenario definitions.
ColumnTypeDescription
categoryVARCHARCredit, Operational, Compliance, etc.
risk_groupVARCHAROptional grouping

sub_controls

Control instances with effectiveness ratings.
ColumnTypeDescription
todVARCHARTest of Design (A-E)
toeVARCHARTest of Effectiveness (1-5)
total_effectivenessVARCHARCalculated effectiveness
testing_dateDATEWhen control was tested

Business Risk Assessments

business_risk_assessments

Master BRA record.
ColumnTypeDescription
statusVARCHARdraft, in_progress, under_review, approved, cancelled
approved_byUUID FKUser who approved
approved_atTIMESTAMPApproval timestamp

bra_risk_ratings

Risk ratings (inherent and residual).
ColumnTypeDescription
rating_typeVARCHARinherent, residual
impact_scoreINTEGER1-5
likelihood_scoreINTEGER1-5
risk_scoreINTEGERCalculated (1-25)
risk_levelVARCHARLow, Medium, High, Very High

Mitigation Actions

CREATE TABLE mitigation_actions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    enterprise_id UUID NOT NULL REFERENCES enterprises(id),
    bra_id UUID REFERENCES business_risk_assessments(id),
    bra_risk_scenario_id UUID REFERENCES bra_risk_scenarios(id),
    action TEXT NOT NULL,
    owner VARCHAR(255) NOT NULL,
    status VARCHAR(50) DEFAULT 'created',
    priority VARCHAR(50) NOT NULL,
    due_date DATE NOT NULL,
    action_type VARCHAR(100),
    archived BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

Audit Log

CREATE TABLE audit_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    enterprise_id UUID REFERENCES enterprises(id),
    user_id UUID NOT NULL,
    action_type VARCHAR(50) NOT NULL,
    entity_type VARCHAR(100) NOT NULL,
    entity_id UUID,
    action_details JSONB,
    ip_address INET,
    user_agent TEXT,
    status VARCHAR(50) DEFAULT 'success',
    created_at TIMESTAMPTZ DEFAULT NOW()
);
The audit_log table is immutable. No UPDATE or DELETE operations are permitted.

BRA Snapshots

CREATE TABLE bra_snapshots (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    bra_id UUID NOT NULL REFERENCES business_risk_assessments(id),
    snapshot_type VARCHAR(50) NOT NULL,
    snapshot_data JSONB NOT NULL,
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);
snapshot_typeDescription
creationInitial snapshot when BRA created
approvalImmutable snapshot when BRA approved

Layer 2: Historical Data

le_assessed_risk_scenarios

Stores historical assessment data for efficiency:
CREATE TABLE le_assessed_risk_scenarios (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    enterprise_id UUID NOT NULL,
    legal_entity_id UUID NOT NULL,
    business_unit_id UUID,
    risk_scenario_id UUID NOT NULL,
    last_inherent_impact INTEGER,
    last_inherent_likelihood INTEGER,
    last_inherent_score INTEGER,
    last_residual_impact INTEGER,
    last_residual_likelihood INTEGER,
    last_residual_score INTEGER,
    last_assessed_at TIMESTAMPTZ,
    last_bra_id UUID,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);
Purpose:
  • Pre-fills new BRAs with previous assessment data
  • Enables trend analysis without querying all historical BRAs
  • Updated automatically when BRAs are approved

Indexes

Risk Legion uses strategic indexes for performance:
-- Enterprise isolation
CREATE INDEX idx_bras_enterprise ON business_risk_assessments(enterprise_id);
CREATE INDEX idx_legal_entities_enterprise ON legal_entities(enterprise_id);
CREATE INDEX idx_products_enterprise ON products(enterprise_id);

-- BRA queries
CREATE INDEX idx_bra_risk_scenarios_bra ON bra_risk_scenarios(bra_id);
CREATE INDEX idx_bra_risk_ratings_bra ON bra_risk_ratings(bra_id);
CREATE INDEX idx_bra_risk_ratings_scenario ON bra_risk_ratings(bra_risk_scenario_id);

-- Control queries
CREATE INDEX idx_sub_controls_key_control ON sub_controls(key_control_id);

-- Action queries
CREATE INDEX idx_mitigation_actions_status ON mitigation_actions(status);
CREATE INDEX idx_mitigation_actions_due_date ON mitigation_actions(due_date);

-- Audit queries
CREATE INDEX idx_audit_log_enterprise ON audit_log(enterprise_id);
CREATE INDEX idx_audit_log_created ON audit_log(created_at);

Row-Level Security

RLS Policies

Every table has RLS policies enforcing tenant isolation:
-- Example: BRA access policy
CREATE POLICY "Users can access their enterprise BRAs"
ON business_risk_assessments
FOR ALL
USING (
    enterprise_id IN (
        SELECT enterprise_id
        FROM enterprise_users
        WHERE user_id = auth.uid()
    )
);

-- Example: Entity-scoped access for Assessors
CREATE POLICY "Assessors see only assigned entities"
ON business_risk_assessments
FOR SELECT
USING (
    legal_entity_id IN (
        SELECT legal_entity_id
        FROM user_entity_roles
        WHERE user_id = auth.uid()
    )
    OR
    EXISTS (
        SELECT 1 FROM enterprise_users
        WHERE user_id = auth.uid()
        AND role = 'admin'
    )
);

RLS Hierarchy

Super Admin → All enterprises (via separate table)

Client Admin → All entities within enterprise

Assessor/Reviewer → Assigned entities only

Database Triggers

Automatic Timestamps

CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_bras_updated_at
BEFORE UPDATE ON business_risk_assessments
FOR EACH ROW EXECUTE FUNCTION update_updated_at();

Risk Score Calculation

CREATE OR REPLACE FUNCTION calculate_risk_score()
RETURNS TRIGGER AS $$
BEGIN
    NEW.risk_score = NEW.impact_score * NEW.likelihood_score;
    NEW.risk_level = CASE
        WHEN NEW.risk_score <= 4 THEN 'Low'
        WHEN NEW.risk_score <= 9 THEN 'Medium'
        WHEN NEW.risk_score <= 15 THEN 'High'
        ELSE 'Very High'
    END;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Data Retention

Data TypeRetention Period
Active BRAsIndefinite
Approved BRA Snapshots7 years minimum
Audit Logs7 years minimum
Archived Records7 years from archive date
Session Data24 hours