Plataforma de análise de despesa pública em Portugal, baseada em dados abertos de contratos públicos (Portal BASE/IMPIC via dados.gov.pt).
| Componente | Tecnologia |
|---|---|
| Backend API | Node.js + TypeScript + Fastify |
| Query Builder | Kysely (type-safe, zero-overhead) |
| Base de Dados | PostgreSQL (partições por ano, materialized views) |
| Queue System | Redis + BullMQ |
| Frontend | Next.js 15 (App Router) + Tailwind CSS v4 |
| Monorepo | npm workspaces + Turborepo |
Antes de começar, certifica-te que tens instalado:
- Node.js >= 20 (
node -v) - PostgreSQL >= 15 (
psql --version) - Redis >= 7 (
redis-server --version)
brew install node@20 postgresql@15 redis# Iniciar PostgreSQL
brew services start postgresql@15
# Iniciar Redis
brew services start rediscd ~/Desktop/gov
npm install# Criar o utilizador e a base de dados
psql postgres -c "CREATE USER gov WITH PASSWORD 'gov';"
psql postgres -c "CREATE DATABASE gov OWNER gov;"
psql postgres -c "ALTER USER gov CREATEDB;"cp .env.example .envEditar .env se necessário (os valores por defeito devem funcionar para desenvolvimento local):
DATABASE_URL=postgresql://gov:gov@localhost:5432/gov
REDIS_URL=redis://localhost:6379
API_PORT=3001
LOG_LEVEL=info
DATA_DIR=./data/raw
Isto cria todos os schemas, tabelas, indexes, partições e materialized views:
npm run db:migrate -w @gov/apiResultado esperado:
OK 001_create_schemas.sql
OK 002_create_meta_tables.sql
OK 003_create_staging_tables.sql
OK 004_create_core_tables.sql
OK 005_create_indexes.sql
OK 006_create_materialized_views.sql
Migrations complete: 6 applied, 0 skipped.
Agora o passo importante — carregar contratos públicos reais do portal dados.gov.pt.
# Carregar contratos de 2026 (~27k registos, ~45MB JSON)
npm run ingest:year -w @gov/api 2026Tempo estimado: 2-5 minutos (depende da internet e do hardware).
# Carregar todos os anos disponíveis (~200k+ registos)
npm run ingest -w @gov/apiTempo estimado: 15-45 minutos (download de ficheiros XLSX grandes + parsing).
O progresso é mostrado no terminal:
INFO: Phase 1: Downloading...
INFO: Phase 2: Parsing file...
INFO: Phase 3: Transforming and loading...
INFO: Batch progress progress="5000/27484" inserted=4823
INFO: Batch progress progress="10000/27484" inserted=9647
...
INFO: Ingestion completed rowsInserted=26891 rowsDuplicates=0
INFO: Refreshing materialized views...
INFO: All materialized views refreshed
npm run dev:apiA API fica disponível em http://localhost:3001. Testa:
# Health check
curl http://localhost:3001/health
# Ver sumário
curl http://localhost:3001/api/analytics/summary
# Listar contratos
curl "http://localhost:3001/api/contracts?page=1&limit=5"
# Top fornecedores
curl http://localhost:3001/api/analytics/top-suppliers
# Pesquisar fornecedores
curl "http://localhost:3001/api/suppliers?search=Portugal+Telecom"Num terminal separado:
npm run dev:webAbre http://localhost:3000 no browser.
| Comando | Descrição |
|---|---|
npm run dev:api |
Iniciar API em modo development (hot reload) |
npm run dev:web |
Iniciar frontend Next.js (hot reload) |
npm run dev |
Iniciar tudo (API + Web) via Turborepo |
npm run db:migrate -w @gov/api |
Correr migrations da base de dados |
npm run ingest -w @gov/api |
Ingerir todos os anos de contratos |
npm run ingest:year -w @gov/api 2026 |
Ingerir um ano específico |
npm run scheduler -w @gov/api |
Iniciar scheduler de ingestão automática |
npm run build |
Build de produção (API + Web) |
gov/
├── apps/
│ ├── api/ # Backend (Fastify + Kysely + BullMQ)
│ │ └── src/
│ │ ├── config/ # Database, Redis, queues, data sources
│ │ ├── modules/ # Contracts, Suppliers, Analytics (routers + repos)
│ │ ├── ingestion/ # ETL pipeline (downloader, parser, file reader)
│ │ ├── scheduler/ # Cron jobs
│ │ ├── scripts/ # Manual ingestion triggers
│ │ ├── api/ # Fastify server + middleware
│ │ ├── shared/ # Logger, errors, types
│ │ └── db/migrations/ # SQL migrations
│ │
│ └── web/ # Frontend (Next.js 15 + Tailwind v4)
│ └── src/
│ ├── app/ # Pages (Dashboard, Contracts, Suppliers, Analytics)
│ └── lib/ # API client, formatters
│
├── packages/
│ └── shared-types/ # TypeScript types partilhados API ↔ Web
│
└── data/raw/ # Ficheiros descarregados (gitignored)
| Fonte | URL | Formato | Frequência |
|---|---|---|---|
| Contratos Públicos (IMPIC) | dados.gov.pt | JSON/XLSX por ano | Quinzenal |
GET /api/contracts— Lista paginada (filtros:year,entityId,procedureType,contractType,search)GET /api/contracts/:id— Detalhe de contrato (inclui fornecedores)
GET /api/suppliers— Lista paginada (filtros:search,country)GET /api/suppliers/:id— Detalhe de fornecedorGET /api/suppliers/:id/contracts— Contratos de um fornecedor
GET /api/analytics/summary— Totais gerais (contratos, valor, fornecedores, entidades)GET /api/analytics/top-suppliers— Top fornecedores por valor totalGET /api/analytics/yearly-breakdown— Breakdown anual por tipo de procedimentoGET /api/analytics/monthly-timeseries— Série temporal mensalGET /api/analytics/entity-concentration— Índice HHI de concentração por entidade
A base de dados usa 4 schemas:
meta— Controlo do pipeline (ingestion_runs, source_files)staging— Buffer temporário durante ingestãopublic— Dados normalizados finais (entities, suppliers, contracts, budget_executions)analytics— Materialized views pré-computadas para dashboard
As tabelas contracts e budget_executions são particionadas por ano para performance analítica.
As migrations não foram aplicadas. Corre:
npm run db:migrate -w @gov/apiVerifica que o PostgreSQL está a correr:
brew services list | grep postgresql
pg_isreadyVerifica que o Redis está a correr:
brew services list | grep redis
redis-cli ping # deve responder PONGAs views só têm dados após a primeira ingestão. Corre:
npm run ingest:year -w @gov/api 2026