Self-hosted climate transparency for enterprises
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
Extend web/prisma/schema.prisma with new models:
Stores framework metadata and versioning:
id, code (tcfd, csrd, cdp, gri), name, versionschemaPath - path to JSON schema defining required disclosuresisActive, effectiveDateStores template definitions:
id, frameworkId, name, versiontemplatePath - path to React template componentsections (JSON) - ordered list of section definitionsbranding (JSON) - default styles, logo placementTracks async generation:
id, reportId, status (queued, processing, completed, failed)progress (0-100), currentStep, errorMessagestartedAt, completedAt, workerIdAdd to existing Report model:
frameworkId, templateId - link to framework/templatecontentHash - SHA-256 of generated contentverificationCode - short code for public lookuppublicUrl - shareable verification linkartifacts (JSON) - paths to all generated files (PDF, HTML, JSON, CSV)Create web/lib/reporting/frameworks/ directory:
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
}
tcfd.json - Task Force on Climate-related Financial Disclosurescsrd.json - Corporate Sustainability Reporting Directive (EU)cdp.json - Carbon Disclosure Projectgri.json - Global Reporting Initiative (GRI 305)Create web/lib/reporting/collectors/:
Gathers all data needed for a report:
collectEmissions(orgId, periodStart, periodEnd) - aggregated by scope/categorycollectActivityData(orgId, periodStart, periodEnd) - with evidence statuscollectOrganizationProfile(orgId) - boundaries, facilities, metadatacollectMethodology() - factors used, GWP sets, calculation versionsOutput: ReportDataBundle - normalized data structure ready for mapping.
Create web/lib/reporting/mappers/:
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: [...]
};
}
}
Create web/lib/reporting/templates/:
ReportShell.tsx - outer wrapper, header/footer, page numbersCoverPage.tsx - title, org logo, period, framework badgeTableOfContents.tsx - auto-generated from sectionsScopeBreakdown.tsx - pie chart + table for Scope 1/2/3EmissionsTrend.tsx - year-over-year line chartDisclosureSection.tsx - renders a single framework disclosureMethodologyAppendix.tsx - factors, sources, calculation notesVerificationFooter.tsx - hash, QR code, verification URLasync 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
}
Create web/lib/reporting/pdf/:
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;
}
}
deploy/compose.dev.ymlweb/lib/reporting/queue/:
reportQueue.ts - queue definitionreportWorker.ts - job processor// 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;
});
Create web/lib/reporting/verification/:
OE-2024-A7X9)qrcode npm packageGET /api/verify/[code] returns:
Create web/lib/reporting/storage/:
/reports/{orgId}/{reportId}/
- report.pdf
- report.html
- data.json
- activity-data.csv
- emissions.csv
- methodology.md
Extend web/app/api/reports/:
Create report and queue generation job
Get report with job status
Get generation progress (polling endpoint)
Download specific format (pdf, html, json, csv)
Public verification endpoint
Create web/components/reports/:
ReportBuilder.tsx - wizard for creating new reportsFrameworkSelector.tsx - choose framework, see required disclosuresReportProgress.tsx - real-time progress during generationReportCard.tsx - display report in list with statusReportViewer.tsx - preview HTML version in-appVerificationBadge.tsx - shows hash + QR codeweb/
├── 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
{
"bullmq": "^5.0.0",
"ioredis": "^5.3.0",
"playwright": "^1.40.0",
"qrcode": "^1.5.3",
"@aws-sdk/client-s3": "^3.0.0"
}