Technical appendix
Engineering behind rodado
The public-facing site is a sociological reading of Brazilian public data. This page is the engineering appendix for readers interested in how the platform behind it is built — ingestion, ontology, semantic layer, MCP query interface, and a direct comparison to how the same architecture maps onto Palantir Foundry.
Engineering competencies demonstrated
Each capability below is backed by a concrete, deployed piece of this project — not a claim, a running system.
Scraping and mirroring pipelines + semantic layer + 17-tool MCP interface, in daily use as a single-engineer project.
1,403 tables · 256 datasets · 103 sources · 743 GB (36.33 billion rows) — 904 mirrored from Base dos Dados, 499 scraped independently — with a canonical join-key graph and partition-aware query patterns.
8 business object types (Municipality, Company, Person, Contract…) with typed relationships, 60 documented join concepts and 21 catalogued false friends.
mcp_server.py — 17 MCP tools over stdio for schema browsing, join resolution, named metrics and read-only SQL.
Table discovery by a compact dataset catalog placed in the model's prompt — 88% of questions land on the right dataset, against ~53% for the embedding search it replaced.
Statement guard in mcp_server.py before any SSH call, and every DuckDB session opened with -readonly.
Resumable, checkpointed scrapers. A metadata catalog records rows, source and provenance for every table, and schemas are regenerated after each sync.
CPF (individual) and CNPJ (company) tax IDs — read-only access, no PII export endpoints, masking documented source by source.
Delivered case studies
Five shipped applications over Brazilian public data — each with a stakeholder problem, a data model, an application built, and a measurable outcome.
Investigators needed to trace beneficial ownership chains and detect undisclosed conflicts of interest across thousands of interconnected companies and individuals — previously weeks of manual registry lookups.
Data model: Company (CNPJ) ↔ Person (CPF) via br_me_cnpj.socios — multi-hop graph traversal across ownership layers. Application: interactive force-directed network, nodes as entities, edges as relationships.
A public administration team needed a real-time operational picture of formal economic activity across Rio's 96 neighborhoods. Live map ↗
Data model: 1M+ CNPJ establishments joined to IBGE geocodes → spatial index → tile-rendered point layer per CNAE sector. Rendered client-side with sub-100ms pan/zoom.
Regulators and investors needed to map Swiss corporate presence across Brazil — subsidiary structures, holding layers, undisclosed connections. Live network ↗
Data model: CNPJ country-of-origin codes → Swiss-linked Company objects → director Person nodes → subsidiary chains via cnpj_basico.
A regional economic development agency needed to know which sectors closed first and which survived longest during COVID-19 lockdowns.
Data model: CNPJ open/close event timestamps × CNAE codes → temporal event stream partitioned by sector and municipality. Application: sequential closure heatmap.
A governance team needed to map a senator's full corporate network to identify conflicts between business interests and public budget votes, regulatory decisions, and contract awards.
Data model: TSE candidatos (CPF) → CNPJ socios → CNPJ empresas → CGU contratos — four-entity join across electoral, corporate, and procurement registries.
User workflows
Three analyst scenarios showing the full data → insight → decision arc.
Compliance — contracts signed with sanctioned suppliers
A compliance team checks which federal contracts were signed with companies while they were listed in CGU's sanctions register (CEIS).
SELECT c.cpf_cnpj_contratado,
any_value(c.nome_contratado) AS contractor,
COUNT(DISTINCT c.id_contrato) AS contracts,
SUM(c.valor_final_compra) AS total_brl,
any_value(s.categoria_sancao) AS sanction
FROM br_cgu_licitacao_contrato.contrato_compra c
JOIN br_cgu_sancoes.ceis s
ON s.cpf_cnpj_sancionado = c.cpf_cnpj_contratado
AND c.data_assinatura_contrato >= s.data_inicio_sancao
AND (s.data_final_sancao IS NULL OR c.data_assinatura_contrato <= s.data_final_sancao)
WHERE c.ano = 2024
GROUP BY 1
ORDER BY total_brl DESC
LIMIT 20
Decision: check each sanction's scope (abrangencia_sancao) — a suspension may bind only the sanctioning agency — then route the remaining cases to procurement governance.
Policy — infrastructure gap prioritization
A state health secretariat identifies municipalities with critically low SUS hospital bed coverage to prioritize federal budget allocation.
SELECT m.nome, m.sigla_uf, pop.populacao,
ROUND(cnes.leitos_sus * 1000.0 / NULLIF(pop.populacao, 0), 2) AS leitos_sus_por_mil,
ideb.ideb AS ideb_anos_iniciais
FROM br_bd_diretorios_brasil.municipio m
JOIN br_ibge_populacao.municipio pop
ON m.id_municipio = pop.id_municipio AND pop.ano = 2024
JOIN ( SELECT id_municipio,
SUM(quantidade_leito_cirurgico + quantidade_leito_clinico
+ quantidade_leito_complementar) AS leitos_sus
FROM br_ms_cnes.estabelecimento
WHERE ano = 2024 AND mes = 12 AND indicador_vinculo_sus = 1
GROUP BY id_municipio ) cnes ON m.id_municipio = cnes.id_municipio
JOIN br_inep_ideb.municipio ideb
ON m.id_municipio = ideb.id_municipio AND ideb.ano = 2023
AND ideb.rede = 'publica' AND ideb.ensino = 'fundamental'
AND ideb.anos_escolares = 'iniciais (1-5)'
WHERE pop.populacao > 50000
ORDER BY leitos_sus_por_mil ASC, ideb_anos_iniciais ASC
LIMIT 50
Decision: ranked shortlist delivered to the budget committee; the top 10 flagged for emergency transfer.
Investigation — campaign suppliers that later won federal contracts
An investigative journalist checks which 2022 campaign suppliers went on to win federal contracts. The CPF in the company-partner register is masked, so the link goes through the supplier's CNPJ rather than the candidate's CPF.
WITH fornecedores AS (
SELECT d.cpf_cnpj_fornecedor, c.nome, c.sigla_partido, c.cargo,
SUM(d.valor_despesa) AS pago_na_campanha
FROM br_tse_eleicoes.despesas_candidato d
JOIN br_tse_eleicoes.candidatos c
ON c.ano = d.ano AND c.sigla_uf = d.sigla_uf
AND c.sequencial = d.sequencial_candidato
WHERE d.ano = 2022 AND d.sigla_uf = 'SP'
AND length(d.cpf_cnpj_fornecedor) = 14
GROUP BY 1, 2, 3, 4
HAVING SUM(d.valor_despesa) >= 50000
)
SELECT f.nome, f.sigla_partido, f.cargo,
COUNT(DISTINCT f.cpf_cnpj_fornecedor) AS fornecedores_contratados,
SUM(f.pago_na_campanha) AS pago_na_campanha,
SUM(k.valor_federal) AS contratos_federais_2023_2025
FROM fornecedores f
JOIN ( SELECT cpf_cnpj_contratado, SUM(valor_final_compra) AS valor_federal
FROM br_cgu_licitacao_contrato.contrato_compra
WHERE ano BETWEEN 2023 AND 2025
GROUP BY 1 ) k ON k.cpf_cnpj_contratado = f.cpf_cnpj_fornecedor
GROUP BY 1, 2, 3
HAVING contratos_federais_2023_2025 > 1000000
ORDER BY contratos_federais_2023_2025 DESC
Decision: the minimum-payment filter matters — without it, national telecom and utility suppliers paid a few reais by hundreds of campaigns dominate the ranking. The shortlist goes to the editorial team with source data for verification.
Domain ontology
Brazilian public data modeled as typed entities with explicit join keys.
| Join key | Tables | Object |
|---|---|---|
id_municipio | 271 | Municipality |
sigla_uf | 336 | State |
cnpj / cnpj_basico | 23 | Company |
cpf | 26 | Person |
id_setor_censitario | 27 | CensusSector |
id_municipio_tse | 23 | ElectoralZone |
cbo_2002 | 8 | OccupationClass |
cnae_2_subclasse | 6 | EconomicActivity |
ano | 423 | Temporal partition |
Architecture
Everything is local: partitioned Parquet on one machine, queried on demand by DuckDB over SSH. No live web service, no cloud object storage.
Analysts · policy teams · researchers · journalists
Claude Desktop / Claude Code · mcp/mcp_server.py over stdio · 17 tools: schema browsing, join resolution, named metrics, read-only SQL, per-theme lookups
rodado-schema.json (1,403 tables) · bridges.yaml + join_keys.md · 12 named metrics · 3 rollup hierarchies · 47 domain overviews
ssh beelink '~/bin/duckdb -readonly' · no persistent server process · statement guard before every call
Local disk · Parquet + zstd · 1,403 tables · 743 GB · DuckDB reads the files directly, no import
_rodado_metadata view: rows, source, status and provenance for every table, rebuilt after each sync
Table selection
With 1,403 tables, the hard step is picking the handful worth reading before any SQL is written — the step that keeps a large schema from producing hallucinated joins. The local harness puts the whole dataset catalog in the prompt (~2k tokens, cached after the first question) and lands on the right dataset 88% of the time. An embedding index over ~8 synthetic questions per table scored ~53% on the same measure and was removed.
Palantir Foundry mapping
Not a Foundry deployment. rodado is an open-source system built around the same idea: datasets alone are not enough. You need a typed semantic layer that says what the data means, and tools that act on that meaning instead of on table names. The table translates each layer for readers who know Foundry.
| Foundry layer | Foundry primitive | rodado counterpart |
|---|---|---|
| Data integration | Data Connection sources & syncs | Base dos Dados mirror sync + independent scrapers (309 tables), resumable and checkpointed |
| Datasets | One Parquet+zstd directory per table, 1,403 tables. No dataset versions or transactions | |
| Data Lineage & metadata | _rodado_metadata catalog: rows, size, source URL, scrape date, status, provenance | |
| Pipeline schedules | An ordered regeneration chain, run by hand after each sync | |
| Ontology | Object types | 61 hub concepts in bridges.yaml: municipality, state, company, person, station… Objects stay virtual (rows, not an indexed object store) |
| Link types | resolve_join(a, b) returns a ready ON clause, including the key normalization (e.g. un-padded CNPJ) that Foundry would push into a transform | |
| Interfaces | Anything carrying id_municipio rolls up to state and region, a natural HasMunicipality interface | |
| Shared property / value types | 13 coded_differently concepts (sex, race codes change by dataset and year), column code books and per-dataset decode dictionaries | |
| — | 21 catalogued false friends: same column name, different meaning (valor in 91 tables). A guard against links that only look valid | |
| Functions | 15 named metrics and CNAE / CID-10 / territorial rollups, each with a dated verified measurement | |
| Object Views | describe_table: columns, decodable codes, gotchas and coded-value warnings in one call, built for an agent reader | |
| Data quality | Data Expectations & Health Checks | Per-dataset gotchas (26 datasets), each measured on real data. Returned to the agent, not enforced at build time |
| AIP | Ontology MCP / AIP Agent tools | mcp_server.py: 17 read-only MCP tools. An external agent reads through the semantic layer, not raw tables |
| AIP Evals | Local evaluation harness (Gemma 4): 88% dataset hit rate vs ~53% for the embedding search it replaced. Each failure becomes a bridge or a note, not a prompt rule | |
| Governance | Markings / restricted views | Read-only sessions, statement guard, allow-listed directories, locked configuration. Applies to the whole session, not per row |
| Applications | Vertex | Atlas: bipartite table↔key graph (2,151 edges instead of 89,598 table pairs) |
| Workshop / Notepad / Contour | Published analyses and FDR-corrected research runs. Reports, not operational apps |
Solved by hand here, by construction in Foundry
Sources are mirrored as they come, so key cleaning lives in the bridge: resolve_join replaces the naive a.cnpj = b.cnpj instead of competing with it.
The same code means different things in different datasets or years. rodado warns about this at query time; Foundry would fix it once, in a transform.
Bridges, metrics, hierarchies and gotchas each record what matched on the real data, with a date. This plays the role of Data Expectations, but a failure shows up as a warning, not a failed build.
What a Foundry deployment would add
rodado is read-only by design. A compliance workflow (flag a supplier for review) needs write-back, an inbox and an audit trail.
Indexed object storage, so a single company or municipality can be fetched without scanning tables.
Scheduled, incremental builds with staleness tracking, instead of a manual regeneration chain.
Markings and row-level policies for CPF-bearing data, plus branching for ontology changes.
Porting sketch
- Sync the Parquet tables as datasets through Data Connection.
- Write transforms that apply each bridge normalization and emit clean keys. Turn each gotcha into a Data Expectation.
- Ontology: object types for the hub concepts (Municipality, Company, Person [marked], Establishment, PublicContract, HealthEvent), link types from the bridges, and an interface for anything located in a municipality.
- Turn the named metrics into functions, each tested against its recorded measurement. Move the harness cases into AIP Evals.
- Build a Workshop app for the supplier integrity check, with an action type that routes a flagged company to review.
Data quality & governance
Large tables (100M+ rows) require ano, sigla_uf, or mes filters. ano appears in 423 tables, sigla_uf in 336, mes in 133.
cpf (individual), cnpj (company) — read-only, no PII export endpoints. Masking varies by source, so masked CPFs from different sources cannot be joined to each other.
Data freshness varies (CNPJ monthly, census 2010/2022, health 12–18 month lag). Codes for the same concept (sex, race) differ across datasets and years.
State expected order of magnitude, flag outliers, verify counts two independent ways before reporting.
Stack
An earlier version served live queries from db.xn--2dk.xyz: a BigQuery → GCS → Hetzner Object Storage pipeline with httpfs reads, a Caddy + HMAC auth layer, a browser SQL shell and a Rust NL→SQL TUI. All of it has been retired in favor of the local setup described above.
Full engineering reference (environment, MCP setup, regeneration order): see docs/tecnico/TECHNICAL.md in the repository.