Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 5 min read

Using SHA256 to Build Trustworthy Data Portals in Brazil

Data portals in emerging markets face a specific credibility problem: how do you prove that the data you display was not silently altered after publication? This matters more than it sounds. Editorial portals covering l

Data portals in emerging markets face a specific credibility problem: how do you prove that the data you display was not silently altered after publication?

This matters more than it sounds. Editorial portals covering lottery results, election counts, or public dataset feeds in Brazil are frequently accused (rightly or not) of tampering. The typical defense β€” "trust us, we did not change anything" β€” is not defensible in 2026.

I run deunobicho.online, a portal covering Brazilian lottery results, and I want to share how we solved this with a very old, very boring cryptographic primitive: SHA256 content hashing with public evidence pages.

The problem in concrete terms

Every day, the portal publishes ~7 lottery result batches. Each batch has:

  • A publication timestamp
  • A source (usually a screenshot of the official state lottery broadcast)
  • A tabular payload (numbers, prizes, groups)

If a user visits at 15:30, sees result X, and later returns at 20:00 to find result Y at the same URL, they have a legitimate reason to distrust the portal.

The naive fix is "just don't change results." Real editorial life is messier: typos happen, source corrections happen, timezones are hard. The correct fix is: make every change publicly auditable.

The SHA256 evidence pattern

Here is the pattern we ship:

  1. Every published resource has a canonical JSON representation.
  2. At publish time, we compute sha256(canonical_json) and store it alongside the resource.
  3. We publish a public evidence page at /prova/<result-id> that displays:
    • The raw canonical JSON
    • The SHA256 hash
    • The timestamp
    • A link to the source (screenshot / official broadcast)
  4. The evidence page is served with Cache-Control: public, immutable. It cannot be modified without changing the URL.
  5. If we ever correct a result, we publish a new evidence page with a new hash and link both old and new from an audit log.

The user experience: any reader can, at any time, verify that the number they saw on Twitter matches the hash on the portal.

Implementation in Next.js 16 (App Router)

Here is the minimal shape of the API route:

// src/app/api/prova/[id]/route.ts
import { createHash } from 'node:crypto';
import { NextResponse } from 'next/server';

export const dynamic = 'force-static';
export const revalidate = false; // never revalidate β€” evidence is immutable

interface Evidence {
  id: string;
  published_at: string;   // ISO 8601 UTC
  source_url: string;     // screenshot / broadcast
  payload: Record<string, unknown>;
  hash: string;           // sha256 hex
}

function canonicalize(obj: unknown): string {
  // stable JSON: sorted keys, no whitespace
  if (obj === null || typeof obj !== 'object') {
    return JSON.stringify(obj);
  }
  if (Array.isArray(obj)) {
    return '[' + obj.map(canonicalize).join(',') + ']';
  }
  const keys = Object.keys(obj as Record<string, unknown>).sort();
  return '{' + keys.map(k =>
    JSON.stringify(k) + ':' + canonicalize((obj as Record<string, unknown>)[k])
  ).join(',') + '}';
}

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const record = await loadRecord(id); // from DB
  const canonical = canonicalize(record.payload);
  const hash = createHash('sha256').update(canonical).digest('hex');

  const evidence: Evidence = {
    id: record.id,
    published_at: record.published_at,
    source_url: record.source_url,
    payload: record.payload,
    hash,
  };

  return NextResponse.json(evidence, {
    headers: {
      'Content-Type': 'application/json; charset=utf-8',
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  });
}

The key detail is canonical JSON. JSON.stringify on an object with keys in different orders yields different bytes, hence different SHA256. If you skip canonicalization, your hash is unreliable.

Verifying from the client (curl or browser)

A reader can verify independently:

curl -s https://deunobicho.online/api/prova/2026-09-18-federal-1 \
  | jq -c '.payload' \
  | shasum -a 256

The output should match the hash field in the same JSON. If it doesn't, someone (us or an intermediate cache) tampered with the payload.

This is trivially scriptable. We publish a verify snippet on the portal itself, so non-technical journalists can copy-paste and confirm.

Why not blockchain?

We get this question every week. The short answer: you do not need a blockchain to prove content integrity. A blockchain proves ordering and consensus β€” properties we do not need. SHA256 + public URL + Wayback Machine snapshots gives:

  • Content integrity: SHA256 on canonical JSON
  • Temporal proof: Wayback Machine crawls (https://web.archive.org/web/*/deunobicho.online/prova/*) β€” free, no gas fees, higher trust than most L2s
  • Public verifiability: any reader can curl | shasum in 3 seconds

A blockchain adds cost, latency, and complexity without adding trust value for this use case. We looked at OpenTimestamps for a while (it is free and adds a real timestamping anchor to Bitcoin), and we may add it as a secondary anchor later. But the primary evidence is the SHA256 + immutable URL.

What we learned in production

Six months in, three lessons:

1. Immutable URLs are contagious. Once we published /prova/<id> pages, journalists started linking directly to them from their articles. This is now our #1 referrer source. Reader trust compounded.

2. Canonical JSON is harder than it looks. We had a bug where dates were serialized as "2026-09-18T00:00:00.000Z" in one code path and "2026-09-18T00:00:00Z" in another. Same date, different bytes, different hash. Fix: normalize all dates to millisecond-precision UTC ISO 8601 at write time.

3. Corrections happen. Plan for them. We had to correct one result in six months. The audit trail worked: we published /prova/<id>-v2 with a new hash, added a superseded_by field to the old evidence page, and posted a public correction note. Zero credibility damage β€” the transparency actually increased trust.

Reading the source

The full implementation of /prova/<id> is open on the portal itself β€” visit any recent result and scroll down to the "Prova SHA256" footer. The API is documented at deunobicho.online/api-publica.

For portal maintainers building similar infra in other regulated / high-scrutiny domains (elections, public health data, financial disclosures), the same pattern generalizes:

  1. Canonical JSON of the payload
  2. SHA256 at publish time
  3. Immutable URL for the evidence
  4. Wayback Machine as temporal anchor
  5. Public audit log for corrections

That is it. No blockchain, no proprietary vendor, no trust in the portal itself β€” just cryptographic integrity readers can verify with shasum -a 256.

Athos Alexandre is the founder of deunobicho.online, an editorial portal covering Brazilian lottery data. He writes about data transparency, Next.js, and building trustworthy public interfaces in high-scrutiny domains.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.