Skip to content

Standards & Framework Metadata

Status: Final Version: 1.1 Last Updated: 2026-01-17


Purpose

Define the database schema and hierarchical structure for storing ESG reporting frameworks, standards, disclosures, and their versioning. This enables the platform to support multiple frameworks (GRI, ISSB, SASB, CSRD) and track framework evolution over time.


Scope

In Scope: - Database schema for framework hierarchy (framework → standard → disclosure → metric) - Version management for framework updates (GRI 2021 → GRI 2025) - Cross-framework mapping (metric appears in both GRI and TCFD) - Custom KPI traceability to standard disclosures

Out of Scope: - Complete GRI catalog data (see Metric Catalog) - Materiality assessment logic - Framework selection wizard (vNext)


Key Decisions

Decision Rationale
4-tier hierarchy (framework → standard → disclosure → metric) Aligns with GRI structure; flexible for other frameworks
Version as separate field (not separate records) Simplifies queries; historical versions archived separately
Cross-framework mapping via junction table Enables multi-framework reporting without duplication

1. Framework Hierarchy

erDiagram
    FRAMEWORKS ||--o{ GRI_STANDARDS : contains
    GRI_STANDARDS ||--o{ GRI_DISCLOSURES : contains
    GRI_DISCLOSURES ||--o{ METRIC_DEFINITIONS : defines
    FRAMEWORKS {
        id bigint PK
        code string "GRI_2021, ISSB_S1, TCFD"
        name string
        version string
        effective_date date
        status enum
    }
    GRI_STANDARDS {
        id bigint PK
        framework_id bigint FK
        code string "GRI_302, GRI_401"
        name string
        category enum
    }
    GRI_DISCLOSURES {
        id bigint PK
        standard_id bigint FK
        code string "302-1, 401-1"
        requirement text
        guidance_url string
    }
    METRIC_DEFINITIONS {
        id bigint PK
        disclosure_id bigint FK
        metric_id string
        name string
    }

2. Database Schemas

Frameworks Table

CREATE TABLE frameworks (
    id BIGSERIAL PRIMARY KEY,
    code VARCHAR(50) NOT NULL UNIQUE, -- e.g., 'GRI_2021', 'ISSB_S1_2024'
    name VARCHAR(255) NOT NULL, -- e.g., 'GRI Standards 2021'
    description TEXT,
    version VARCHAR(50), -- e.g., '2021', 'S1-2024'
    issuing_organization VARCHAR(255), -- e.g., 'Global Reporting Initiative'
    effective_date DATE, -- When framework came into effect
    sunset_date DATE, -- When framework deprecated (if applicable)
    status VARCHAR(50) DEFAULT 'active', -- ENUM: 'draft', 'active', 'deprecated'
    framework_url VARCHAR(500), -- Official documentation URL
    metadata JSONB, -- Extensible
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_frameworks_code ON frameworks(code);
CREATE INDEX idx_frameworks_status ON frameworks(status);

Example Records:

INSERT INTO frameworks VALUES
(1, 'GRI_2021', 'GRI Standards 2021', 'Global Reporting Initiative Standards', '2021', 'Global Reporting Initiative', '2021-01-01', NULL, 'active', 'https://www.globalreporting.org/standards/', '{}'),
(2, 'ISSB_S1_2024', 'IFRS S1 - General Requirements', 'ISSB Sustainability Disclosure Standard S1', 'S1-2024', 'IFRS Foundation', '2024-01-01', NULL, 'active', 'https://www.ifrs.org/issued-standards/issb-standards/', '{}');


GRI Standards Table

CREATE TABLE gri_standards (
    id BIGSERIAL PRIMARY KEY,
    framework_id BIGINT NOT NULL REFERENCES frameworks(id),
    code VARCHAR(50) NOT NULL, -- e.g., 'GRI_302', 'GRI_401'
    number INT, -- e.g., 302, 401 (for sorting)
    name VARCHAR(255) NOT NULL, -- e.g., 'Energy', 'Employment'
    category VARCHAR(50), -- ENUM: 'universal', 'environmental', 'social', 'governance'
    description TEXT,
    standard_url VARCHAR(500),
    metadata JSONB,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE (framework_id, code)
);

CREATE INDEX idx_gri_standards_framework ON gri_standards(framework_id);
CREATE INDEX idx_gri_standards_category ON gri_standards(category);

Example Records:

INSERT INTO gri_standards VALUES
(10, 1, 'GRI_302', 302, 'Energy', 'environmental', 'Energy consumption, production, and intensity', 'https://www.globalreporting.org/standards/gri-standards-download-center/gri-302-energy-2016/', '{}'),
(25, 1, 'GRI_401', 401, 'Employment', 'social', 'New employee hires and employee turnover', 'https://www.globalreporting.org/standards/gri-standards-download-center/gri-401-employment-2016/', '{}'),
(32, 1, 'GRI_405', 405, 'Diversity and Equal Opportunity', 'social', 'Diversity of governance bodies and employees', 'https://www.globalreporting.org/standards/gri-standards-download-center/gri-405-diversity-and-equal-opportunity-2016/', '{}');


GRI Disclosures Table

CREATE TABLE gri_disclosures (
    id BIGSERIAL PRIMARY KEY,
    standard_id BIGINT NOT NULL REFERENCES gri_standards(id),
    code VARCHAR(50) NOT NULL, -- e.g., '302-1', '401-1'
    name VARCHAR(255) NOT NULL, -- e.g., 'Energy consumption within the organization'
    disclosure_type VARCHAR(50), -- ENUM: 'management_approach', 'quantitative', 'qualitative'
    requirement TEXT NOT NULL, -- Full disclosure requirement text
    guidance TEXT, -- Implementation guidance
    disclosure_url VARCHAR(500),
    is_mandatory BOOLEAN DEFAULT false, -- Required for GRI compliance
    metadata JSONB,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE (standard_id, code)
);

CREATE INDEX idx_gri_disclosures_standard ON gri_disclosures(standard_id);
CREATE INDEX idx_gri_disclosures_code ON gri_disclosures(code);

Example Records:

INSERT INTO gri_disclosures VALUES
(50, 10, '302-1', 'Energy consumption within the organization', 'quantitative',
 'Report total fuel consumption, electricity consumption, heating consumption, cooling consumption, steam consumption, and total energy consumption within the organization.',
 'Break down by renewable and non-renewable sources. Convert all energy to joules or multiples.',
 'https://www.globalreporting.org/standards/gri-standards-download-center/gri-302-energy-2016/', true, '{}'),
(156, 25, '401-1-a', 'New employee hires and employee turnover', 'quantitative',
 'Report total number and rate of new employee hires during the reporting period, by age group, gender, and region. Report total number and rate of employee turnover during the reporting period, by age group, gender, and region.',
 'Break down new hires and turnover by employment type (permanent, fixed-term, casual). Calculate rates as (count / total employees) × 100.',
 'https://www.globalreporting.org/standards/gri-standards-download-center/gri-401-employment-2016/', true, '{}'),
(158, 32, '405-1', 'Diversity of governance bodies and employees', 'quantitative',
 'Report the percentage of individuals within the organization''s governance bodies in each category: gender, age group, and other indicators of diversity. Report the percentage of employees per employee category in each category: gender, age group, and other indicators of diversity.',
 'Disaggregate by employment level (executive, management, staff). Report as percentages derived from headcount. Include local community status where relevant.',
 'https://www.globalreporting.org/standards/gri-standards-download-center/gri-405-diversity-and-equal-opportunity-2016/', true, '{}');


Cross-Framework Mapping Table

CREATE TABLE framework_mappings (
    id BIGSERIAL PRIMARY KEY,
    source_disclosure_id BIGINT NOT NULL REFERENCES gri_disclosures(id),
    target_disclosure_id BIGINT NOT NULL REFERENCES gri_disclosures(id),
    mapping_type VARCHAR(50), -- ENUM: 'equivalent', 'subset', 'superset', 'related'
    mapping_notes TEXT,
    confidence_level VARCHAR(50), -- ENUM: 'exact', 'high', 'medium', 'low'
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE (source_disclosure_id, target_disclosure_id)
);

CREATE INDEX idx_framework_mappings_source ON framework_mappings(source_disclosure_id);
CREATE INDEX idx_framework_mappings_target ON framework_mappings(target_disclosure_id);

Example Use Case: Map GRI 305-1 (Scope 1 GHG) to TCFD and ISSB S2 climate disclosures.


3. Kotlin JPA Entities

Framework Model

@Entity
@Table(name = "frameworks")
data class Framework(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(unique = true, nullable = false, length = 50)
    val code: String,

    @Column(nullable = false, length = 255)
    val name: String,

    @Column(columnDefinition = "TEXT")
    val description: String? = null,

    @Column(length = 50)
    val version: String? = null,

    @Column(name = "issuing_organization", length = 255)
    val issuingOrganization: String? = null,

    @Column(name = "effective_date")
    val effectiveDate: LocalDate? = null,

    @Column(name = "sunset_date")
    val sunsetDate: LocalDate? = null,

    @Column(length = 50)
    val status: String = "active",

    @Column(name = "framework_url", length = 500)
    val frameworkUrl: String? = null,

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb")
    val metadata: Map<String, Any> = emptyMap(),

    @Column(name = "created_at", updatable = false)
    val createdAt: Instant = Instant.now(),

    @Column(name = "updated_at")
    var updatedAt: Instant = Instant.now(),

    @OneToMany(mappedBy = "framework", cascade = [CascadeType.ALL], orphanRemoval = true)
    val standards: List<GriStandard> = emptyList()
) {
    fun isActive(): Boolean {
        return status == "active" && (sunsetDate == null || sunsetDate.isAfter(LocalDate.now()))
    }
}

// Repository with custom query method for active frameworks
import io.quarkus.hibernate.orm.panache.kotlin.PanacheRepository
import jakarta.enterprise.context.ApplicationScoped

@ApplicationScoped
class FrameworkRepository : PanacheRepository<Framework> {
    fun findByStatus(status: String): List<Framework> =
        list("status", status)
}

GriStandard Model

@Entity
@Table(
    name = "gri_standards",
    uniqueConstraints = [UniqueConstraint(columnNames = ["framework_id", "code"])]
)
data class GriStandard(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "framework_id", nullable = false)
    val framework: Framework,

    @Column(nullable = false, length = 50)
    val code: String,

    val number: Int? = null,

    @Column(nullable = false, length = 255)
    val name: String,

    @Column(length = 50)
    val category: String? = null,

    @Column(columnDefinition = "TEXT")
    val description: String? = null,

    @Column(name = "standard_url", length = 500)
    val standardUrl: String? = null,

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb")
    val metadata: Map<String, Any> = emptyMap(),

    @Column(name = "created_at", updatable = false)
    val createdAt: Instant = Instant.now(),

    @Column(name = "updated_at")
    var updatedAt: Instant = Instant.now(),

    @OneToMany(mappedBy = "standard", cascade = [CascadeType.ALL], orphanRemoval = true)
    val disclosures: List<GriDisclosure> = emptyList()
)

// Repository with custom query method for category filtering
import io.quarkus.hibernate.orm.panache.kotlin.PanacheRepository
import jakarta.enterprise.context.ApplicationScoped

@ApplicationScoped
class GriStandardRepository : PanacheRepository<GriStandard> {
    fun findByCategory(category: String): List<GriStandard> =
        list("category", category)

    fun findByFrameworkId(frameworkId: Long): List<GriStandard> =
        list("framework.id", frameworkId)
}

GriDisclosure Model

@Entity
@Table(
    name = "gri_disclosures",
    uniqueConstraints = [UniqueConstraint(columnNames = ["standard_id", "code"])]
)
data class GriDisclosure(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "standard_id", nullable = false)
    val standard: GriStandard,

    @Column(nullable = false, length = 50)
    val code: String,

    @Column(nullable = false, length = 255)
    val name: String,

    @Column(name = "disclosure_type", length = 50)
    val disclosureType: String? = null,

    @Column(nullable = false, columnDefinition = "TEXT")
    val requirement: String,

    @Column(columnDefinition = "TEXT")
    val guidance: String? = null,

    @Column(name = "disclosure_url", length = 500)
    val disclosureUrl: String? = null,

    @Column(name = "is_mandatory")
    val isMandatory: Boolean = false,

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb")
    val metadata: Map<String, Any> = emptyMap(),

    @Column(name = "created_at", updatable = false)
    val createdAt: Instant = Instant.now(),

    @Column(name = "updated_at")
    var updatedAt: Instant = Instant.now(),

    @OneToMany(mappedBy = "disclosure", cascade = [CascadeType.ALL], orphanRemoval = true)
    val metrics: List<MetricDefinition> = emptyList(),

    // Cross-framework mappings as source
    @OneToMany(mappedBy = "sourceDisclosure", cascade = [CascadeType.ALL])
    val mappedTo: List<FrameworkMapping> = emptyList()
)

// Junction entity for many-to-many framework mappings
@Entity
@Table(
    name = "framework_mappings",
    uniqueConstraints = [UniqueConstraint(columnNames = ["source_disclosure_id", "target_disclosure_id"])]
)
data class FrameworkMapping(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "source_disclosure_id", nullable = false)
    val sourceDisclosure: GriDisclosure,

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "target_disclosure_id", nullable = false)
    val targetDisclosure: GriDisclosure,

    @Column(name = "mapping_type", length = 50)
    val mappingType: String? = null,

    @Column(name = "mapping_notes", columnDefinition = "TEXT")
    val mappingNotes: String? = null,

    @Column(name = "confidence_level", length = 50)
    val confidenceLevel: String? = null,

    @Column(name = "created_at", updatable = false)
    val createdAt: Instant = Instant.now()
)

// Repository with custom query methods
import io.quarkus.hibernate.orm.panache.kotlin.PanacheRepository
import jakarta.enterprise.context.ApplicationScoped

@ApplicationScoped
class GriDisclosureRepository : PanacheRepository<GriDisclosure> {
    fun findByStandardId(standardId: Long): List<GriDisclosure> =
        list("standard.id", standardId)

    fun findByCode(code: String): List<GriDisclosure> =
        list("code", code)
}

4. Version Management

Approach: Framework Versioning

  • New Major Version: Create new framework record (e.g., GRI_2025 alongside GRI_2021)
  • Metric Definitions: Link to specific framework version via framework_id
  • Migration Path: Admin maps old metrics to new framework disclosures
  • Historical Data: Always tied to framework version in effect during reporting period

Version Migration Workflow

@ApplicationScoped
class FrameworkMigrationService(
    private val metricDefinitionRepository: MetricDefinitionRepository,
    private val frameworkMappingRepository: FrameworkMappingRepository
) {
    @Transactional
    fun migrateMetrics(oldFramework: Framework, newFramework: Framework) {
        // Get all metrics tied to old framework
        val oldMetrics = metricDefinitionRepository.findByFrameworkId(oldFramework.id)

        oldMetrics.forEach { metric ->
            // Check if disclosure exists in new framework
            val newDisclosure = findMappedDisclosure(metric.disclosure, newFramework)

            if (newDisclosure != null) {
                // Create new metric version
                val newMetric = MetricDefinition(
                    frameworkId = newFramework.id,
                    disclosureId = newDisclosure.id,
                    metricId = metric.metricId,
                    name = metric.name,
                    version = metric.version + 1,
                    // ... copy other fields
                )
                metricDefinitionRepository.save(newMetric)

                // Mark old metric as deprecated
                metric.apply {
                    deprecatedAt = Instant.now()
                    replacedByMetricId = newMetric.metricId
                }
                metricDefinitionRepository.save(metric)
            }
        }
    }

    private fun findMappedDisclosure(oldDisclosure: GriDisclosure, newFramework: Framework): GriDisclosure? {
        // Find mapped disclosure in the new framework using framework_mappings table
        return frameworkMappingRepository.findBySourceDisclosureId(oldDisclosure.id)
            .mapNotNull { it.targetDisclosure }
            .firstOrNull { it.standard.framework.id == newFramework.id }
    }
}

Acceptance Criteria

Done When: - [ ] All 4 tables created (frameworks, gri_standards, gri_disclosures, framework_mappings) - [ ] Spring Data JPA entities with relationships implemented - [ ] GRI 2021 framework seeded with Universal + Topic Standards - [ ] Version migration service can upgrade framework versions - [ ] Cross-framework mapping table supports GRI ↔ ISSB/TCFD mappings (vNext) - [ ] API endpoints expose framework hierarchy (see Admin API)


Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-03 Senior Product Architect Initial framework metadata structure
1.1 2026-01-17 Ralph Agent (TASK-008) Added GRI 405 standard and GRI 405-1, 401-1-a disclosure entries for human capital reporting