OpenEco Documentation

Self-hosted climate transparency for enterprises

View the Project on GitHub Open-Eco/oe-core

Scalable Reporting Engine

Architecture Overview

flowchart TB
    subgraph client [Client Layer]
        UI[Report UI]
        API[REST API]
    end

    subgraph queue [Job Queue]
        BullMQ[BullMQ]
        Redis[(Redis)]
    end

    subgraph engine [Report Engine]
        Collector[Data Collector]
        Mapper[Framework Mapper]
        Renderer[Template Renderer]
        PDFGen[PDF Generator]
        Verifier[Verification Layer]
    end

    subgraph storage [Storage]
        Postgres[(PostgreSQL)]
        S3[(S3/MinIO)]
    end

    subgraph frameworks [Framework Definitions]
        TCFD[TCFD Schema]
        CSRD[CSRD Schema]
        CDP[CDP Schema]
        GRI[GRI Schema]
    end

    UI --> API
    API --> BullMQ
    BullMQ --> Redis
    BullMQ --> Collector
    Collector --> Postgres
    Collector --> Mapper
    Mapper --> frameworks
    Mapper --> Renderer
    Renderer --> PDFGen
    PDFGen --> Verifier
    Verifier --> S3
    Verifier --> Postgres

1. Database Schema Extensions

Extend web/prisma/schema.prisma with new models:

ReportFramework

Stores framework metadata and versioning:

ReportTemplate

Stores template definitions:

ReportJob

Tracks async generation:

Report Model Enhancements

Add to existing Report model:


2. Framework Definition System

Create web/lib/reporting/frameworks/ directory:

Framework Schema Structure

Each framework gets a JSON schema file defining:

interface FrameworkSchema {
  code: string;           // "tcfd"
  version: string;        // "2023"
  disclosures: Disclosure[];
}

interface Disclosure {
  id: string;             // "governance-a"
  category: string;       // "Governance"
  requirement: string;    // "Board oversight of climate risks"
  dataMapping: DataMapping;
  required: boolean;
}

interface DataMapping {
  source: 'emissions' | 'activity' | 'organization' | 'computed';
  query?: string;         // How to extract from OpenEco data
  computation?: string;   // Formula for computed fields
}

Initial Frameworks


3. Data Collection Layer

Create web/lib/reporting/collectors/:

ReportDataCollector

Gathers all data needed for a report:

Output: ReportDataBundle - normalized data structure ready for mapping.


4. Framework Mapping Layer

Create web/lib/reporting/mappers/:

FrameworkMapper

Maps OpenEco data to framework-specific disclosures:

class FrameworkMapper {
  constructor(schema: FrameworkSchema) {}
  
  map(data: ReportDataBundle): MappedReport {
    // For each disclosure in schema:
    // - Extract relevant data from bundle
    // - Apply computations
    // - Flag missing/incomplete data
    return {
      framework: this.schema.code,
      disclosures: [...],
      completeness: { filled: 12, total: 15, percent: 80 },
      warnings: [...]
    };
  }
}

5. Template Rendering System

Create web/lib/reporting/templates/:

Template Components (React)

HTML Renderer

async function renderReportHTML(
  mappedReport: MappedReport,
  template: ReportTemplate,
  branding: BrandingConfig
): Promise<string> {
  // Server-side render React components to HTML string
  // Include Tailwind CSS inline for PDF compatibility
}

6. PDF Generation Service

Create web/lib/reporting/pdf/:

PlaywrightPDFGenerator

class PlaywrightPDFGenerator {
  async generate(html: string, options: PDFOptions): Promise<Buffer> {
    const browser = await playwright.chromium.launch();
    const page = await browser.newPage();
    await page.setContent(html, { waitUntil: 'networkidle' });
    const pdf = await page.pdf({
      format: 'A4',
      printBackground: true,
      margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' }
    });
    await browser.close();
    return pdf;
  }
}

7. Job Queue System

Redis + BullMQ Setup

Job Flow

// API creates job
const job = await reportQueue.add('generate', {
  reportId: report.id,
  organizationId: org.id,
  frameworkCode: 'tcfd',
  periodStart,
  periodEnd,
});

// Worker processes
reportWorker.process('generate', async (job) => {
  await updateProgress(job, 10, 'Collecting data...');
  const data = await collector.collect(...);
  
  await updateProgress(job, 30, 'Mapping to framework...');
  const mapped = await mapper.map(data);
  
  await updateProgress(job, 50, 'Rendering template...');
  const html = await renderer.render(mapped);
  
  await updateProgress(job, 70, 'Generating PDF...');
  const pdf = await pdfGenerator.generate(html);
  
  await updateProgress(job, 90, 'Uploading artifacts...');
  const urls = await storage.upload([
    { name: 'report.pdf', buffer: pdf },
    { name: 'report.html', buffer: html },
    { name: 'data.json', buffer: JSON.stringify(mapped) },
  ]);
  
  await updateProgress(job, 100, 'Complete');
  return urls;
});

8. Verification Layer

Create web/lib/reporting/verification/:

Content Hashing

Verification Code

QR Code Generation

Public Verification Endpoint

GET /api/verify/[code] returns:


9. Storage Layer

Create web/lib/reporting/storage/:

S3-Compatible Storage

File Organization

/reports/{orgId}/{reportId}/
  - report.pdf
  - report.html
  - data.json
  - activity-data.csv
  - emissions.csv
  - methodology.md

10. API Endpoints

Extend web/app/api/reports/:

POST /api/reports

Create report and queue generation job

GET /api/reports/[id]

Get report with job status

GET /api/reports/[id]/status

Get generation progress (polling endpoint)

GET /api/reports/[id]/download/[format]

Download specific format (pdf, html, json, csv)

GET /api/verify/[code]

Public verification endpoint


11. UI Components

Create web/components/reports/:


File Structure Summary

web/
├── lib/
│   └── reporting/
│       ├── index.ts              # Public exports
│       ├── types.ts              # Shared types
│       ├── frameworks/
│       │   ├── index.ts
│       │   ├── tcfd.json
│       │   ├── csrd.json
│       │   ├── cdp.json
│       │   └── gri.json
│       ├── collectors/
│       │   └── ReportDataCollector.ts
│       ├── mappers/
│       │   └── FrameworkMapper.ts
│       ├── templates/
│       │   ├── components/
│       │   │   ├── ReportShell.tsx
│       │   │   ├── CoverPage.tsx
│       │   │   └── ...
│       │   └── HTMLRenderer.ts
│       ├── pdf/
│       │   └── PlaywrightPDFGenerator.ts
│       ├── queue/
│       │   ├── reportQueue.ts
│       │   └── reportWorker.ts
│       ├── verification/
│       │   ├── hash.ts
│       │   ├── qrcode.ts
│       │   └── verificationCode.ts
│       └── storage/
│           └── S3Storage.ts
├── app/
│   └── api/
│       └── reports/
│           ├── route.ts
│           ├── [id]/
│           │   ├── route.ts
│           │   ├── status/route.ts
│           │   └── download/[format]/route.ts
│       └── verify/
│           └── [code]/route.ts
├── components/
│   └── reports/
│       ├── ReportBuilder.tsx
│       ├── FrameworkSelector.tsx
│       ├── ReportProgress.tsx
│       └── ...
└── prisma/
    └── schema.prisma  # Extended with new models

Dependencies to Add

{
  "bullmq": "^5.0.0",
  "ioredis": "^5.3.0",
  "playwright": "^1.40.0",
  "qrcode": "^1.5.3",
  "@aws-sdk/client-s3": "^3.0.0"
}

Implementation Order

  1. Schema extensions + migrations
  2. Framework definitions (start with TCFD)
  3. Data collector
  4. Framework mapper
  5. Template components
  6. HTML renderer
  7. Redis + BullMQ setup
  8. PDF generator
  9. Storage layer
  10. Verification layer
  11. API endpoints
  12. UI components