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:
- Submit a climate-related claim.
- Add pasted source material or a text-based PDF.
- Extract atomic assertions and identify ambiguities.
- Inspect evidence classifications, passages, and provenance.
- Generate and finalize a versioned report.
- Register its snapshot hash on a local EVM.
- 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.tshandles extraction, assessment, deduplication, and snapshots. -
lib/services.tsorchestrates case operations and audit state transitions. -
lib/store.tshandles persistence. -
lib/chain.tscommunicates with the registry. -
lib/ai.tsprovides optional interpretation. -
lib/readiness.tschecks 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:
HttpOnlySecureSameSite=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