an entry is validated, it becomes append-only. Corrections are never performed via UPDATE or DELETE. Instead, the system uses financial-style reversal entries. If a user logs 500 liters of diesel but meant 50, the system creates a new entry that subtracts 450 liters. This preserves the complete audit trail while allowing data correction.
Step 2: The Versioned Factor Registry (Temporal Library)
Emission factors must be treated as time-bound constants. The registry stores every published version of a factor set, each with explicit validity windows. Queries against this table must resolve factors based on the timestamp of the activity, not the current date. This prevents future updates from contaminating historical calculations.
Step 3: The Calculation Archive (Point-in-Time Receipts)
When a calculation executes, the engine does not return a scalar value. It generates a calculation receipt that embeds every variable used in the computation. This includes the raw activity value, the exact emission factor applied, the Global Warming Potential (GWP) baseline, the methodology version, and a cryptographic hash of the calculation logic. The receipt is stored in an archive table that is never modified after insertion.
Step 4: Query & Frontend Strategy
The frontend never triggers fresh calculations when displaying historical reports. It queries the calculation archive directly. If a regulatory body updates methodology guidance (e.g., ESRS E1 revisions), the system flags affected receipts as "methodology stale" rather than auto-recalculating them. Sustainability officers must explicitly authorize a re-statement, which generates a new receipt while preserving the original for audit comparison.
Implementation Code (TypeScript)
The following implementation demonstrates the snapshot engine, temporal factor resolution, and receipt generation. Note the deliberate separation of concerns and the embedding of values rather than foreign keys.
import { createHash } from 'crypto';
// Domain interfaces
interface ActivityRecord {
id: string;
resourceType: 'diesel' | 'grid_electricity' | 'natural_gas';
quantity: number;
unit: string;
occurredAt: Date;
metadata: Record<string, unknown>;
}
interface EmissionFactor {
id: string;
label: string;
value: number;
source: string;
validFrom: Date;
validUntil: Date | null;
}
interface CalculationReceipt {
id: string;
activityId: string;
calculatedAt: Date;
inputs: {
quantity: number;
unit: string;
};
snapshot: {
factorValue: number;
factorLabel: string;
factorSource: string;
gwpBaseline: string;
methodologyVersion: string;
logicChecksum: string;
};
totalEmissions: number;
status: 'frozen' | 'superseded';
}
// Factor repository with temporal resolution
class FactorRepository {
async resolveForDate(
resourceType: string,
targetDate: Date
): Promise<EmissionFactor> {
// In production, this uses a temporal query:
// WHERE resource_type = $1 AND valid_from <= $2 AND (valid_until IS NULL OR valid_until > $2)
const factors = await this.db.query<EmissionFactor>(
`SELECT * FROM emission_factors
WHERE resource_type = $1
AND valid_from <= $2
AND (valid_until IS NULL OR valid_until > $2)
ORDER BY valid_from DESC LIMIT 1`,
[resourceType, targetDate]
);
if (!factors.length) {
throw new Error(`No valid emission factor found for ${resourceType} on ${targetDate.toISOString()}`);
}
return factors[0];
}
}
// Core calculation engine
class EmissionsEngine {
private readonly factorRepo: FactorRepository;
private readonly GWP_BASELINE = 'AR6';
private readonly METHODOLOGY_VERSION = 'ESRS_E1_v2024';
constructor(factorRepo: FactorRepository) {
this.factorRepo = factorRepo;
}
async generateReceipt(activity: ActivityRecord): Promise<CalculationReceipt> {
// 1. Resolve factor based on activity timestamp, not current time
const factor = await this.factorRepo.resolveForDate(
activity.resourceType,
activity.occurredAt
);
// 2. Perform calculation
const totalEmissions = activity.quantity * factor.value;
// 3. Generate cryptographic provenance hash
const logicPayload = JSON.stringify({
gwp: this.GWP_BASELINE,
method: this.METHODOLOGY_VERSION,
factorSource: factor.source,
timestamp: new Date().toISOString()
});
const logicChecksum = createHash('sha256').update(logicPayload).digest('hex');
// 4. Construct immutable receipt
const receipt: CalculationReceipt = {
id: `receipt_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
activityId: activity.id,
calculatedAt: new Date(),
inputs: {
quantity: activity.quantity,
unit: activity.unit
},
snapshot: {
factorValue: factor.value,
factorLabel: factor.label,
factorSource: factor.source,
gwpBaseline: this.GWP_BASELINE,
methodologyVersion: this.METHODOLOGY_VERSION,
logicChecksum
},
totalEmissions,
status: 'frozen'
};
// 5. Persist to archive (append-only)
await this.db.insert('calculation_archive', receipt);
return receipt;
}
}
Architecture Rationale
Why embed values instead of foreign keys? Foreign keys create indirection. If the factors table updates, the FK still points to the same row, but the row's value changes. Embedding the exact numeric value at calculation time breaks the dependency chain and guarantees mathematical reproducibility.
Why temporal resolution in the factor repository? Emission factors are not static. A diesel factor from 2023 differs from 2024 due to updated biofuel blend ratios and grid decarbonization metrics. Resolving by occurredAt ensures historical accuracy while allowing the registry to evolve.
Why cryptographic checksums? Auditors require proof that the calculation logic hasn't been silently altered. Hashing the methodology version, GWP baseline, and factor source creates a tamper-evident seal. If the checksum doesn't match the expected payload, the receipt is flagged for manual review.
Pitfall Guide
1. Dynamic Factor Resolution at Query Time
Explanation: Fetching emission factors when rendering a dashboard instead of when the activity occurred. This causes historical totals to shift whenever factor databases are updated.
Fix: Resolve factors strictly at calculation time and embed the resolved values in the receipt. Never perform runtime lookups for historical reporting.
2. Overwriting Archived Receipts
Explanation: Using UPDATE on the calculation archive when a user corrects an activity value. This destroys the original mathematical context.
Fix: Treat the archive as append-only. Generate a new receipt for the corrected activity and mark the original as superseded. Maintain both for audit lineage.
3. Ignoring GWP and Methodology Versioning
Explanation: Storing only the emission value without recording whether AR5, AR6, or AR7 Global Warming Potentials were used. Different baselines produce different tCO₂e totals for the same gas.
Fix: Explicitly record the GWP baseline and methodology version in every receipt. Validate that all receipts in a reporting period use the same baseline, or segment reports by methodology.
4. Soft-Deletes in the Factor Registry
Explanation: Marking old emission factors as is_active = false instead of using temporal validity windows. This breaks historical lookups because the factor disappears from queries.
Fix: Use valid_from and valid_until columns. Never delete or soft-delete historical factors. Temporal queries naturally filter to the correct version based on the activity timestamp.
5. Client-Side Recalculation
Explanation: Sending raw activity data to the frontend and letting the browser compute emissions on render. This exposes calculation logic, bypasses server-side validation, and creates drift if factor sets update.
Fix: Perform all calculations server-side. The frontend should only display pre-computed receipts. If a methodology change occurs, trigger a server-side re-statement workflow, not a client-side recalculation.
6. Missing Reversal Entries for Corrections
Explanation: Allowing users to edit activity quantities directly. This creates a gap in the audit trail and makes it impossible to reconstruct what was originally reported.
Fix: Implement a reversal pattern. Corrections must be logged as new entries with negative quantities or explicit reversal flags. The ledger remains append-only, and totals are derived by summing all entries.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| New reporting period | Generate fresh receipts using current factor versions | Ensures accurate baseline for upcoming disclosures | Low (standard calculation flow) |
| Factor set update (e.g., DEFRA 2025 → 2026) | Leave historical receipts frozen; apply new factors only to future activities | Prevents silent drift and preserves audit lineage | Medium (requires versioned registry queries) |
| Audit request for Q3 2024 | Query calculation archive directly; export receipts with checksums | Provides tamper-evident, point-in-time proof | Low (direct read, no recalculation) |
| ESRS methodology revision | Trigger re-statement workflow; generate new receipts, mark old as superseded | Maintains historical truth while complying with new standards | High (requires manual review and approval) |
Configuration Template
-- Factor Registry (Temporal, Immutable)
CREATE TABLE emission_factors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_type TEXT NOT NULL,
label TEXT NOT NULL,
value NUMERIC(12, 6) NOT NULL,
source TEXT NOT NULL,
valid_from TIMESTAMPTZ NOT NULL,
valid_until TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT chk_validity CHECK (valid_until IS NULL OR valid_until > valid_from)
);
CREATE INDEX idx_factors_temporal ON emission_factors(resource_type, valid_from, valid_until);
-- Calculation Archive (Append-Only)
CREATE TABLE calculation_archive (
id TEXT PRIMARY KEY,
activity_id UUID NOT NULL REFERENCES activity_ledger(id),
calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
inputs JSONB NOT NULL,
snapshot JSONB NOT NULL,
total_emissions NUMERIC(12, 6) NOT NULL,
status TEXT NOT NULL DEFAULT 'frozen' CHECK (status IN ('frozen', 'superseded')),
CONSTRAINT chk_no_updates CHECK (status = 'frozen') -- Prevents accidental overwrites
);
-- Enforce append-only via database trigger
CREATE OR REPLACE FUNCTION prevent_archive_updates()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'Calculation archive is append-only. Use reversal entries instead.';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_prevent_archive_modification
BEFORE UPDATE OR DELETE ON calculation_archive
FOR EACH ROW EXECUTE FUNCTION prevent_archive_updates();
Quick Start Guide
- Initialize the Factor Registry: Import your baseline emission factor set (DEFRA, IEA, or EEA) with explicit
valid_from dates. Ensure no factors are marked as deleted; use valid_until for superseded entries.
- Deploy the Append-Only Ledger: Create the activity and archive tables with the provided schema. Attach the database trigger to block
UPDATE/DELETE operations on historical data.
- Wire the Calculation Engine: Integrate the
EmissionsEngine into your ingestion pipeline. Ensure every activity submission triggers generateReceipt(), which resolves the temporal factor and embeds the snapshot.
- Configure the Query Layer: Point your reporting dashboards to the
calculation_archive table. Disable any frontend calculation logic. Implement a discrepancy alert that checks for methodology version mismatches across reporting periods.