Arquitetura de Storage
Este documento mapeia tudo que o sistema armazena hoje, os problemas arquiteturais identificados, e a visão de evolução para um modelo de storage em camadas (hot/warm/cold). É um documento vivo — atualizar conforme decisões forem tomadas.1. Estado atual
1.1 PostgreSQL (pacs_postgres — volume pacs_pg_data)
Armazena metadados estruturados mais blobs base64 que deveriam estar fora do DB.
Colunas com conteúdo grande
| Coluna | Formato | Tamanho típico | Crescimento |
|---|---|---|---|
users.signature | base64 data URL (JPEG/PNG) — TEXT | 20-80 KB | 1 por user (~10/mês) |
tenants.logo | base64 data URL (JPEG/PNG) — TEXT | 50-500 KB | 1 por tenant |
report_templates.assets | JSONB com background.data_url base64 | 100 KB – 2 MB | 1-5 templates por unit |
report_templates.layout / page | JSONB (config) | 1-5 KB | — |
report_templates.content | HTML | 5-20 KB | — |
reports.content | HTML do Tiptap | 2-5 KB (típico), até 50 KB | 1 por estudo reportado |
report_revisions.previous_content + new_content | HTML (duplicado) | Mesmo do reports.content | 1-5 por laudo corrigido |
report_presets.default_content | HTML | 5-15 KB | 10-100 por tenant |
Tabelas de metadata (pequenas)
| Tabela | Volume esperado | Crescimento |
|---|---|---|
tenants | 1-10 | Muito lento |
units | 3-20 por tenant | Lento |
users | 10-100 | Lento |
modalities | 10-30 | Mínimo |
studies | 10k-500k | ~500-5k/mês |
patients | 5k-200k | ~200-1k/mês |
series | 30k-2M | 3-10× estudos |
instances | 100k-10M | 10-50× séries |
reports | 5k-100k | 50-500/mês |
1.2 Filesystem (volume pacs_storage)
Montado em /app/pacs_mount (backend) e /app/storage (worker).
Estrutura atual:
- Gerador:
worker/main.pydurante indexação - Formato: JPEG 300×300 via pydicom + Pillow
- Tamanho por arquivo: 10-50 KB
- Retenção: nenhuma — thumbnails nunca são deletados
- Estimativa: 500 estudos × 70 KB ≈ 35 MB/mês
Confirmado: só thumbnails hoje. Nada mais escrito em filesystem pelo backend.
1.3 Orthanc (pacs_orthanc — volume pacs_orthanc_data)
Armazena DICOM e índice SQLite embutido.
Configuração em docker/orthanc/orthanc.json:
| Config | Valor | Implicação |
|---|---|---|
StorageDirectory | /var/lib/orthanc/db | Blobs DICOM + índice no mesmo volume |
IndexDirectory | /var/lib/orthanc/db | Índice SQLite embutido |
StorageCompression | true | gzip nos DICOM |
MaximumStorageSize | 0 | Sem limite de crescimento |
MaximumPatientCount | 0 | Sem limite de pacientes |
| Modalidade | Tamanho/estudo | Exemplo @ 500 exames/mês |
|---|---|---|
| CT (100-300 slices) | 50-200 MB | ~75 GB/mês |
| MR (50-200 slices) | 30-100 MB | ~30 GB/mês |
| CR/DX (raio-x simples) | 1-5 MB | ~4 GB/mês |
| Total estimado (mix típico) | — | ~130 GB/mês |
1.4 Redis (pacs_redis — volume pacs_redis_data)
Dados voláteis com TTL.
| Chave/conjunto | Propósito | TTL | Tamanho |
|---|---|---|---|
viewer_token:{uuid} | Token temporário pro OHIF | 10 min | ~150 B/token |
pacs_python_queue | Fila de jobs do worker | FIFO (sem TTL) | ~500 B/job |
| JWT blacklist | Tokens invalidados | até expirar | variável |
2. Problemas identificados
P1 — report_templates.assets é bomba-relógio 🔴
O campo JSONB contém background.data_url (papel timbrado em base64), de 100 KB a 2 MB por template. O endpoint GET /tenant/templates pagina 50 itens por request → um único request pode trazer 100 MB.
Cada leitura pra preview ou PDF puxa o payload completo. Preview em browser de celular pode engasgar.
Impacto: alto. Usuário percebe na UI quando o tenant tem vários templates.
P2 — Blobs base64 inflando o DB 🟠
users.signature, tenants.logo e report_templates.assets (todos TEXT/JSONB base64) aumentam:
- ~33% overhead (base64 vs binário)
- Backup do PG inflado (backup de DB deve ser rápido; blobs pesados atrapalham)
- WAL (write-ahead log) cresce mais rápido
- Replicação (quando houver) fica lenta
- Query plans —
SELECT *traz blobs sem querer; desenvolvedor precisa lembrar de excluir colunas
P3 — Orthanc com SQLite 🟠
Orthanc usa SQLite embutido por padrão (único arquivo.db). Limitações em SaaS multi-tenant:
- Write serialization: file lock limita escrita concorrente. Várias modalidades de clínicas diferentes fazendo C-STORE simultaneamente = contenção.
- Backup complexo: SQLite em uso precisa de WAL +
.backupcommand; snapshot de volume arrisca corrupção. - Sem replicação nativa: single point of failure. Perda do volume = perda do índice (DICOM continua no storage, mas Orthanc não sabe mais onde).
- Não escala horizontalmente: só um Orthanc pode abrir o SQLite por vez.
orthanc-plugin-postgresql). Nesse modo:
- Metadata DICOM em Postgres (o mesmo
pacs_postgresou dedicado) - Blobs DICOM continuam em filesystem (ou S3 via plugin separado)
- Múltiplas instâncias Orthanc podem compartilhar o mesmo backend (scale out)
P4 — Orthanc sem política de retenção 🟡
MaximumStorageSize: 0. Com ~130 GB/mês, em 2 anos são 3 TB só de DICOM quente.
Exigência legal típica para imagens médicas no Brasil: 20 anos (CFM 1.821/2007, com variações por estado e exame). Sem estratégia de arquivamento, o custo de disco cresce linear indefinidamente.
Impacto: financeiro crescente, não-bloqueante no curto prazo.
P5 — Thumbnails sem retenção nem redundância 🟡
- Cresce linear com estudos (~35 MB/mês)
- Sem limpeza de thumbnails de estudos antigos
- Se o volume
pacs_storagefor perdido, o worker teria que reprocessar todos (funçãopacs:requeuejá existe) - Filesystem local não funciona com múltiplos backends (quando escalar)
P6 — report_revisions duplica conteúdo 🟢
Audit trail imutável grava previous_content + new_content completos a cada correção. Para laudos de 5 KB é irrelevante; pra laudos com imagens inline (50 KB+) cresce rápido.
Mitigação existente: compressão TOAST do Postgres já reduz overhead de TEXT longo. OK deixar como está.
Impacto: mínimo.
3. Visão: Storage em camadas
Inspirado em como clínicas reais e PACS enterprise organizam dados. Não precisa implementar tudo agora — é o norte pra onde evoluir.3.1 Conceito
| Camada | Latência | Custo | O que vai aqui |
|---|---|---|---|
| 🔥 Hot | ms | alto | Dado acessado múltiplas vezes/dia — metadata, estudos recentes, laudos ativos |
| ♨️ Warm | segundos | médio | Acessado ocasionalmente — estudos de meses atrás, thumbnails, logos |
| ❄️ Cold | minutos a horas | muito baixo | Arquivamento legal — estudos >2 anos, retenção regulatória |
3.2 Aplicação ao nosso domínio
PostgreSQL (Hot — metadata)
Fica no Postgres: tudo que é metadado operacional + HTML pequeno.studies,series,instances(índice)patients,users,tenants,units,modalitiesreports.content,report_revisions.*_content(HTML, caso típico 2-5 KB)report_presets.default_content
users.signature→{bucket}/signatures/{user_id}.{ext}tenants.logo→{bucket}/tenants/{tenant_id}/logo.{ext}report_templates.assets.background→{bucket}/templates/{template_id}/background.{ext}
Object Storage (Warm — binários)
Opções:- MinIO (self-hosted, S3-compatible) — mesma infra on-prem, sem depender de cloud
- AWS S3 — gerenciado, paga por GB + transfer
- Backblaze B2 / Wasabi — S3-compatible mais barato, bom pra warm/cold
- Cloudflare R2 — sem egress, bom pra servir assets públicos
- Logos, signatures, template backgrounds (hoje em base64 no DB)
- Thumbnails (quando escalar)
- DICOM “mornos” — estudos com 3-24 meses de idade (via plugin Orthanc object-storage)
- PDFs de laudos pré-gerados (futuro, se gerar no backend)
Cold Storage (arquivamento legal)
- AWS S3 Glacier / Deep Archive — ~$1/TB/mês, retrieval em 12h
- Cloudflare R2 com lifecycle
- Backblaze B2 com retenção
- Tape (off-site, LTO) — para quem já tem infra
- DICOMs com >2 anos (configurável pelo tenant, respeitando a retenção legal)
- Backups antigos do Postgres (além da janela de PITR)
Migração entre camadas
Não é automática no Orthanc. Precisaria de um job cron:- Query Postgres (via índice do Orthanc em Postgres): “estudos com
study_date < today - 2 yearse ainda marcados como quentes” - Move o blob do bucket warm para o bucket cold
- Atualiza referência no Orthanc/Postgres
3.3 Arquitetura alvo resumida
4. Próximos passos sugeridos (ordem de prioridade)
Não é um compromisso — é uma sugestão de ordem lógica.Fase 1 — Tirar blobs do Postgres (~2 semanas de trabalho)
- Subir MinIO (um novo serviço no
docker-compose.yml) com bucketpacs-assets - Migrar
report_templates.assets.background(maior ofensor)- Script one-off: lê base64, faz upload ao MinIO, atualiza coluna pra URL
- Ajustar backend (
ReportTemplateController) pra ler/escrever via S3 SDK - Ajustar preview + PDF pra baixar via URL com signed access
- Migrar
tenants.logoeusers.signature(mesmo padrão) - Remover os campos base64 do DB (após migração validada)
Fase 2 — Orthanc com PostgreSQL index
- Instalar plugin
orthanc-plugin-postgresql - Configurar para apontar ao
pacs_postgres(schema separado) ou DB dedicado - Migração do SQLite → Postgres (Orthanc documenta o procedimento)
- Validar em homolog antes de produção
Fase 3 — DICOM em object storage
- Instalar plugin
orthanc-plugin-object-storage(AWS/Azure/GCS S3-compatible) - Configurar para gravar novos DICOMs no MinIO/S3
- Manter estudos existentes em filesystem (ou migrar em lote depois)
Fase 4 — Retenção e lifecycle
- Definir política por tenant (configurável): quanto tempo em hot/warm/cold
- Job cron de migração
- UI pra tenant admin configurar retenção
5. Questões em aberto
Itens em discussão — sem decisão ainda:- MinIO self-hosted vs AWS S3 — trade-off custo/complexidade (MinIO = mais infra, S3 = dependência de cloud e billing)
- Orthanc: PostgreSQL plugin AGORA ou depois? — ver checklist de revisão
- Política de retenção legal — qual o padrão CFM e variações estaduais aplicáveis? Definir com advogado antes de implementar cold tier
- Pré-geração de PDFs — hoje o PDF é gerado no frontend sob demanda. Vale gravar uma versão no bucket após finalização do laudo (pra evitar regenerar toda vez)?
- Thumbnails em object storage — migrar quando? Só faz sentido se houver >1 instância de backend, ou se o volume
pacs_storagecrescer a ponto de incomodar o backup
6. Metodologia de execução de fases
Antes de encostar em qualquer campo/tabela/módulo, mapear o raio de impacto: tudo que lê, escreve ou expõe aquele dado. Sem essa análise, mudanças grandes quebram silenciosamente componentes esquecidos.6.1 Checklist de raio de impacto (por fase)
Para cada área afetada (um campo, uma tabela, ou um módulo como “Orthanc”), levantar:- Migrations — schema atual e o que muda
- Models — casts,
$hidden, mutators, accessors, relacionamentos - Controllers — quem escreve, quem lê, quem expõe no JSON
- Services e helpers — wrappers como
OrthancService, parsers tipoparseTemplateAssets,makeVisible('signature') - Frontend — editores (uploaders), previews, geradores de PDF, listagens que expõem o dado
- API contracts — páginas do Mintlify (
docs/api/*.md) que precisam atualizar - Testes — unit/integration que validam o comportamento atual
- Consumidores externos — webhook do Orthanc, nginx auth_request, OHIF, worker Python
6.2 Exemplos por fase
Fase 1 — blobs base64 → object storage. Raio de impacto por campo:users.signature— UserController@store/@update (escrita), ReportController@showByStudy/@upsertByStudy/@correctFinal (leitura viamakeVisible),downloadStudyReportPdfno frontendtenants.logo— TenantSettingsController@show/@update, ReportController (se eager load), PDF generator, componente de logo no headerreport_templates.assets.background— ReportTemplateController@index/@show/@store/@update,ReportTemplateA4Preview,downloadStudyReportPdf,parseTemplateAssetshelper
- Webhook Lua do Orthanc que dispara
/api/orthanc/webhook OrthancServiceno Laravel (wrapper REST)- Worker Python (consome via Orthanc REST)
- Nginx CORS proxy (OHIF → Orthanc via DicomWeb + viewer token)
- Queries que dependem de
orthanc_study_id/orthanc_series_id— esses IDs continuam válidos porque são do próprio Orthanc; só o backend de índice muda
- Config do plugin
orthanc-plugin-object-storage - Comportamento do OHIF (WADO-RS continua via Orthanc, ele é quem resolve o blob no S3)
- Latência de primeiro acesso a estudo “warm”
6.3 Fluxo de execução
- Disparar subagentes
Exploreem paralelo, um por área afetada, com prompt pedindo o raio de impacto estruturado - Consolidar em
docs/plans/{fase-x}.md— lista exaustiva de arquivos a tocar, ordem de execução, plano de rollback (como desfazer se der errado) - Aprovação do plano antes de qualquer
Edit— nunca partir pra implementação sem plano validado - PRs pequenos reviewáveis — não um PR de 50 arquivos. Um por sub-passo (ex.: “migrar só backgrounds”, “remover colunas deprecadas”)
- Validação em homolog antes de fundir em
main - Rollback ensaiado — se o plano diz “script X reverte”, testar o revert num ambiente descartável antes
6.4 Aplicabilidade
Esta metodologia vale para qualquer refactor grande — não só storage. Exemplos futuros possíveis: trocar o editor Tiptap, migrar de Octane pra FrankenPHP, consolidar auth cookie + JWT. A mesma disciplina se aplica.7. Referências
- Orthanc PostgreSQL Plugin
- Orthanc Object Storage Plugins
- MinIO Documentation
- Resolução CFM 1.821/2007 — retenção de imagens médicas
