Profile
Back to NewsBack
Dev.to 11 min
Reader Mode
Building Climate Court: Evidence-First Climate Investigations with Next.js, Optional AI, and Verifiable Reports

Building Climate Court: Evidence-First Climate Investigations with Next.js, Optional AI, and Verifiable Reports

1 day ago

How we built a local-first investigation platform—and why the most valuable improvements came from testing what happens when things go wrong.

Corporate sustainability claims often sound precise:

“We reduced our carbon emissions by 40%.”

But precision in wording does not necessarily mean precision in evidence.

Reduced compared with which year? Does the figure describe absolute emissions or emissions intensity? Which operations are included? What happened to supply-chain emissions?

Climate Court was built to make those questions visible.

The application helps users organize a climate claim into assertions, inspect supporting material, identify evidence gaps, and produce a transparent report. A finalized report can then be fingerprinted and registered on a local blockchain for independent integrity verification.

The central design principle is simple:

Every finding should point to evidence—or explain what evidence is missing.

What Climate Court Actually Does

The core workflow is:

  1. Submit a climate-related claim.
  2. Add pasted source material or a text-based PDF.
  3. Extract atomic assertions and identify ambiguities.
  4. Inspect evidence classifications, passages, and provenance.
  5. Generate and finalize a versioned report.
  6. Register its snapshot hash on a local EVM.
  7. Recompute the hash and compare it with the registered record.

The application includes a dashboard, investigation workspace, printable reports, JSON exports, version history, audit explorer, and demo controls.

It runs locally without paid API keys or cloud accounts.

That independence matters. An investigation should remain usable when an optional model is unavailable, a quota is exhausted, or the local blockchain is offline.

There is also an important boundary: user-provided documents are not automatically authenticated. They remain inconclusive pending independent review. Clearly labeled synthetic fixtures demonstrate supporting and contradictory classifications without implying that the platform has independently verified real companies.

The Technology Stack

Layer Tools
Interface Next.js App Router, React, TypeScript, Tailwind CSS, Lucide
HTTP backend Next.js server-side route handlers
Runtime validation Zod
Local persistence Schema-validated JSON with atomic replacement and backups
Optional cloud persistence Supabase PostgreSQL
Optional AI Official Gemini SDK
Report integrity Canonical serialization and SHA-256
Blockchain Solidity, Hardhat, Ethers.js
Automated testing Node’s test runner, tsx, Playwright, Chromium
Release checks GitHub Actions, secret scanning, dependency auditing

We kept the application as a monolith. The challenge did not require independently deployed services; it required clear boundaries and predictable failure behavior.

Architecture: A Small Monolith with Explicit Responsibilities

The architecture separates presentation, orchestration, business rules, persistence, and external adapters.

flowchart TD
    User[Presenter or authenticated operator]
    Gateway[Access boundary]
    UI[React workspace]
    API[Validated API routes]
    Cases[Case services]
    Engine[Deterministic assessment engine]
    Store[Repository adapter]
    Audit[Audit service and reconciliation]
    PDF[Bounded PDF child process]
    AI[Optional Gemini adapter]

    User --> Gateway --> UI --> API
    API --> Cases
    Cases --> Engine
    Cases --> Store
    Cases -.-> AI
    API --> PDF
    API --> Audit
    Audit --> Store
    Audit --> EVM[Local Solidity registry]
    Store --> Local[(Local JSON)]
    Store -.-> Cloud[(Optional Supabase)]

The main modules reflect those responsibilities:

  • lib/engine.ts handles extraction, assessment, deduplication, and snapshots.
  • lib/services.ts orchestrates case operations and audit state transitions.
  • lib/store.ts handles persistence.
  • lib/chain.ts communicates with the registry.
  • lib/ai.ts provides optional interpretation.
  • lib/readiness.ts checks required dependencies.
  • Dedicated report, presentation, and API-client modules support the interface.

This structure made it possible to improve reliability without replacing the framework or introducing a message broker.

Deterministic Assessment Before AI Interpretation

Climate Court’s core assessment does not depend on an LLM.

Deterministic extraction identifies explicit quantities, units, reporting years, and qualifiers. It preserves unresolved questions rather than supplying missing context.

For example:

“Our emissions fell 40% last year.”

The platform can extract the percentage while leaving the reporting period unresolved. It should not silently decide which year “last year” refers to.

The assessment uses these verdict labels:

SUPPORTED
PARTIALLY_SUPPORTED
CONTRADICTED
INSUFFICIENT_EVIDENCE
UNRESOLVED

The current demo conservatively reserves SUPPORTED for independently complete reviewed evidence rather than issuing it automatically.

Evidence coverage is separate from the verdict. It counts completed checks, including source availability, reporting periods, comparable measurements, and independent scope verification.

It is not a probability that the claim is true.

The AI adapter has a narrow role

When configured, Gemini suggests ambiguities. It does not decide the verdict, invent sources, execute tools, or submit transactions.

The adapter uses:

  • A server-side API key.
  • A configured model rather than an assumed default.
  • Runtime validation of structured JSON.
  • Output and token limits.
  • Bounded attempts within an overall time budget.
  • Deterministic fallback.

The effective mode and fallback reason are recorded with the investigation. A “configured” integration flag alone cannot establish that a model successfully ran.

Live Gemini behavior was not part of the completed local validation, so the application does not claim otherwise.

Evidence Integrity Starts Before Blockchain

One of the most instructive defects appeared in deduplication.

An early normalization step removed punctuation. That made these passages appear identical:

Emissions were 1.2 tonnes.
Emissions were 12 tonnes.

Case folding introduced another scientific problem:

Energy use was 1 MWh.
Energy use was 1 mWh.

Those units are different.

The corrected implementation deliberately uses conservative, exact text deduplication. It normalizes Unicode and whitespace while preserving case, punctuation, signs, units, and negation.

const key = hash(
  excerpt
    .normalize("NFC")
    .replace(/\s+/g, " ")
    .trim()
);

Similar passages remain separate. The application no longer claims semantic near-duplicate detection.

This was a useful architectural lesson: normalization is a domain decision. A transformation that helps ordinary text matching can destroy scientific meaning.

We also corrected synthetic source passages so their classification explanations refer to details actually present in the excerpt. A synthetic label is disclosure, not a substitute for traceability.

Persistence: Simple Storage, Explicit Constraints

For the local demonstration, we chose a JSON repository rather than introducing a database server.

That keeps setup accessible, but it requires clear limits.

The local adapter now provides:

  • Runtime schema validation.
  • Aggregate and record-count limits.
  • Atomic file replacement.
  • File synchronization before replacement.
  • A previous validated backup.
  • A process-owner writer guard.
  • Checksum-verified backup and restore commands.

Finalized snapshots are copied separately from working case data. Later evidence additions do not rewrite those snapshots.

This is application-level immutability, not write-once storage. An actor with filesystem access can still alter files; independent hash verification is what makes changes detectable.

The optional Supabase adapter uses revision-based compare-and-swap updates to reject lost-update races. That implementation still requires separate validation with a live Supabase environment.

The lesson was not that JSON is universally sufficient. It was that a small demo benefits from a storage model whose constraints are explicit and tested.

Blockchain Auditing: Two Systems, One Difficult Failure Boundary

The Solidity registry stores minimal information:

  • An opaque case identifier.
  • A report version.
  • A 32-byte snapshot digest.
  • A registration timestamp.

Documents and full reports remain off-chain.

The contract restricts registration to its operator and rejects duplicate case versions and invalid inputs.

Before registration, the application serializes a finalized snapshot using application-defined canonicalization rules and calculates SHA-256 over the UTF-8 result.

Verification retrieves the actual registry record, recreates the snapshot digest, and compares them.

A matching result establishes integrity against that record. It does not establish that the evidence is authentic or the climate claim is correct.

The failure we initially missed

The difficult sequence was:

Transaction confirms on-chain
          ↓
Saving its local metadata fails
          ↓
User retries
          ↓
Contract rejects duplicate registration

Both individual systems behaved as designed. The combined workflow did not recover.

We fixed this with persisted operation states and reconciliation:

PENDING → BROADCAST → CONFIRMED
               ↘ UNAVAILABLE
               ↘ STALE
               ↘ CONFLICT

Before sending a transaction, the application checks the existing registry record.

  • An absent record can be registered.
  • A matching record can be reconciled with its confirmed receipt.
  • A conflicting digest stops the operation.
  • Historical metadata without a corresponding current registration is reported as stale or unavailable.

This also fixed another misleading behavior: after a chain reset, cached local metadata could previously produce a success response.

The corrected system checks the chain instead of treating its cache as proof.

Reliability: Keep External Waits Outside Storage Locks

The original mutation queue included network work.

That meant a stalled blockchain confirmation could delay unrelated case updates.

The revised approach keeps storage critical sections short:

Persist operation intent
          ↓
Release storage queue
          ↓
Perform bounded external work
          ↓
Re-enter queue
          ↓
Merge result into the intended version

Investigation updates use revision checks to avoid overwriting newer edits. Audit results are merged into the specific finalized version they belong to.

We tested this by pausing local mining. Anchoring reached its deadline while another case could still be finalized.

The important distinction is that a per-request RPC timeout is not an end-to-end operation deadline. The complete operation needs its own budget.

Security: Protect the Authority Behind the API

Keeping keys off the frontend was necessary, but insufficient.

The API can invoke an authorized registry operator. Without access control, a reachable client could exercise that authority indirectly.

Climate Court now has two access modes.

Local presenter mode

The custom Node server binds to loopback and checks the actual peer, Host header, and original forwarding headers before Next.js handles the request.

It adds a random process-private gateway proof. Local API access requires that proof and fails closed without it.

Shared operator mode

Shared mode requires HTTPS configuration and securely injected operator credentials. Sessions use signed, expiring cookies with:

  • HttpOnly
  • Secure
  • SameSite=Strict

Case reads, reports, mutations, reset controls, and blockchain actions require authorization.

Additional protections include server-only credential modules, sanitized API errors, secret scanning, staged-content checks, request budgets, and browser security headers.

PDF extraction runs in a separate process with a bounded heap, page/text limits, a deadline, and no inherited application credentials. This provides resource isolation; it is not an OS security sandbox.

Readiness, Observability, and Recovery

A health endpoint that always says “ok” is not a readiness check.

We separated:

  • Liveness: the process can answer.
  • Readiness: required configuration, storage, and dependencies are usable.

The demo preflight also performs a functional case read and checks the local registry and operator.

Operational events contain an operation name, request ID, outcome, allowlisted diagnostic code, and duration. Raw SDK errors, credentials, request bodies, and source documents are excluded.

Recovery is documented and executable. Backup restoration validates the manifest, stored structure, and finalized snapshot digests, while preserving replaced data for inspection.

Local report backups do not restore ephemeral blockchain history. The runbook makes that distinction explicit.

How Architectural Best Practices Helped

We used established frameworks as review lenses rather than claiming formal certification.

Principle Concrete application
AWS Well-Architected reliability Bounded operations, dependency readiness, reconciliation, tested restoration
OWASP access control Authorization around report data and operator-backed actions
OWASP secrets and error handling Server-only modules, secret checks, sanitized public errors
12-Factor configuration Runtime configuration and optional backing-service adapters
SOLID separation of concerns Independent services, repository boundary, extracted presentation modules
Google SRE principles Failure injection, truthful status, operational budgets, recovery rehearsals
ISO/IEC 25010 Correctness, recoverability, maintainability, and measured resource behavior
NIST SSDF Repeatable CI gates, dependency review, security regression tests
AI risk management Narrow model authority, explicit uncertainty, provenance, bounded fallback

The most useful outcome was a prioritized list of small changes. None required an enterprise-scale rewrite.

What Went Well

The final local validation included:

  • 34 passing automated tests.
  • HTTP and desktop/mobile browser workflows.
  • Browser upload, retry, and refresh failure paths.
  • Real transaction confirmation followed by local persistence failure.
  • Chain stalls, resets, and conflicting digests.
  • Backup, restore, restart, and operator-access checks.
  • Ten consecutive complete claim-to-verification rehearsals.

A bounded local resource rehearsal used 20 load cases, five 1,000-word sources per case, and three snapshots each.

After warm-up, across 20 measured iterations:

Measurement Observed
Read latency p95 24 ms
Write latency p95 54 ms
Peak RSS 288 MiB

These are local observations, not hosted availability guarantees.

The production dependency audit reported zero vulnerabilities after patching Zod and PostCSS. A separate full-tree audit still identified development-tool advisories; their applicability and residual risks are documented rather than hidden.

What We Learned

The happy path is weak evidence of reliability. The most valuable tests interrupted the workflow between successful steps.

Retries are part of the architecture. An error message cannot compensate for an operation that is unsafe to repeat.

Truthful status is a correctness requirement. “Configured,” “registered,” “confirmed,” and “verified” describe different facts.

Scientific text needs conservative processing. Decimal points, signs, scope, units, and casing can change the meaning of evidence.

AI availability should not determine application availability. A narrow optional adapter made fallback straightforward.

A monolith can have strong boundaries. Separating responsibilities and shortening critical sections solved the demonstrated problems without multiplying infrastructure.

What Comes Next

The next useful capability is a human-reviewed evidence classification workflow, with stronger provenance and richer comparisons across reporting periods and emissions scopes.

Other planned work includes live validation of the optional Gemini and Supabase adapters, authenticated collaboration, and public-testnet integration.

Those are separate deployment and product scopes. The current evidence supports a controlled local single-operator demonstration—not a claim of public production readiness.

Climate Court’s goal remains consistent: help users follow the evidence, preserve uncertainty, and make the resulting investigation independently inspectable.

Repository: github.com/scha54/Climate-Court

Chat with me