Dev → Produção — Checklist e Estratégia

Análise completa de tudo que precisa mudar antes do primeiro deploy em produção, com estratégia para manter o ambiente local funcionando durante o processo.

Estratégia: dois compose, um codebase

A abordagem adotada é manter dois arquivos Docker Compose separados:
ArquivoUsado quando
docker-compose.ymlDesenvolvimento local (como está hoje)
docker-compose.prod.ymlProdução (ou teste de prod localmente)
O docker-compose.yml não é alterado — o ambiente de dev continua funcionando igual. Todas as mudanças de produção vão para o docker-compose.prod.yml e para Dockerfiles separados com multi-stage build. Para rodar localmente simulando produção:
# Subir com config de prod
docker compose -f docker-compose.prod.yml up -d --build

# Rodar dev normalmente (sem mudança)
docker compose up -d --build
Isso significa que qualquer item desta lista pode ser implementado sem quebrar o fluxo de desenvolvimento.

Arquivos .env usados em produção

O projeto tem três arquivos .env separados — todos precisam existir e estar preenchidos no servidor antes do build:
ArquivoPara quê serveQuem lê
.env (raiz)Infra Docker: credenciais do Postgres, Redis, Orthanc, ORTHANC_WEBHOOK_SECRET, ACME_EMAIL, ALLOWED_ORIGIN e os NEXT_PUBLIC_* que serão passados ao build do frontenddocker-compose.yml, docker-compose.prod.yml, Caddy
backend/.envLaravel: APP_KEY, JWT_SECRET, DB_*, REDIS_*, ORTHANC_URL, ORTHANC_WEBHOOK_SECRET, FRONTEND_URL, SESSION_SECURE_COOKIE=true, APP_DEBUG=false, APP_ENV=production, LOG_LEVEL=errorLaravel/Octane em runtime
frontend/.env (ou .env.local)Somente dev. Em produção as NEXT_PUBLIC_* são embutidas no bundle via ARG do Dockerfile (vindas do .env raiz), então nenhum frontend/.env é lido em runtime de prod. Criar frontend/.env.local só é necessário em devNext.js em dev
Importante: NEXT_PUBLIC_API_BASE_URL e NEXT_PUBLIC_OHIF_VIEWER_URL são obrigatórias no .env raiz antes do docker compose build, porque ficam compiladas no bundle JavaScript. Mudar depois exige rebuild. Valores de exemplo estão nos três .env.example versionados. Copiar cada .env.example.env (ou .env.local no frontend) e preencher com secrets gerados no servidor (openssl rand -hex 32, php artisan jwt:secret).

Checklist por prioridade

🔴 Crítico — segurança (obrigatório antes do deploy)


Problema resolvido: O token JWT era escrito pelo frontend via document.cookie sem httpOnly, expondo-o a XSS. Qualquer script malicioso na página podia ler e exfiltrar o token. Desafio técnico — cross-origin em dev: Frontend (:3000) e backend (:8000) estão em origens diferentes. Um cookie httpOnly setado pelo backend fica scoped para :8000 — o JS em :3000 não lê e o middleware Next.js em :3000 também não. A solução foi usar dois cookies com funções separadas:
  • pacs_access_token → httpOnly, scoped ao backend, enviado automaticamente via withCredentials: true
  • pacs_user_role → não-httpOnly, scoped ao frontend, lido pelo middleware Next.js para controle de rotas
O que foi implementado: Backend — AuthController.php:
// respondWithToken() agora seta cookie httpOnly em vez de retornar o token no body
return response()->json(['token_type' => 'bearer', 'expires_in' => ..., 'user' => ...])
    ->cookie('pacs_access_token', $token, $ttlMinutes, '/', null,
        env('SESSION_SECURE_COOKIE', false),  // false em dev (HTTP), true em prod (HTTPS)
        true,   // httpOnly — JS nunca acessa
        false, 'Lax'
    );

// logout() limpa o cookie via Set-Cookie
return response()->json([...])->withoutCookie('pacs_access_token');
Backend — app/Http/Middleware/SetBearerFromCookie.php (novo):
// Lê o JWT do cookie httpOnly e promove para Authorization: Bearer
// Permite que o guard JWT (tymon/jwt-auth) valide o token sem nenhuma mudança
if (!$request->bearerToken() && $token = $request->cookie('pacs_access_token')) {
    $request->headers->set('Authorization', 'Bearer ' . $token);
}
Registrado em bootstrap/app.php via $middleware->prependToGroup('api', ...). Backend — config/cors.php (novo):
'supports_credentials' => true,                             // obrigatório para withCredentials
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],  // não pode ser '*'
Configurado via FRONTEND_URL no backend/.env. Frontend — client.ts:
export const apiClient = axios.create({
  baseURL: API_BASE_URL,
  withCredentials: true,  // browser envia pacs_access_token automaticamente
});
// Interceptor de Authorization: Bearer removido — não é mais necessário
Frontend — cookies.ts:
  • Remove toda manipulação do pacs_access_token
  • Mantém apenas leitura/escrita do pacs_user_role
Frontend — auth-store.ts:
  • bootstrapSession: chama GET /me diretamente (sem ler cookie JS) — se o cookie httpOnly for válido, o backend autentica; se não, 401
  • loginWithCredentials: não salva mais o token no estado — apenas o user
  • logout: limpa só o role cookie (backend limpa o JWT via Set-Cookie)
Frontend — proxy.ts:
  • Usa pacs_user_role como indicador de sessão (substituiu pacs_access_token)
  • Lógica de redirecionamento por role mantida
Como funciona em dev: SESSION_SECURE_COOKIE=false, cookie funciona em HTTP. Não precisa de HTTPS. Como funciona em prod: SESSION_SECURE_COOKIE=true, cookie exige HTTPS.

✅ 2. CORS * no nginx proxy

Problema resolvido:
# antes — docker/orthanc-cors-proxy/nginx.conf
add_header Access-Control-Allow-Origin * always;
Qualquer domínio na internet podia fazer requisições DicomWeb ao proxy. O que foi implementado: O arquivo nginx.conf foi transformado em nginx.conf.template. O ${ALLOWED_ORIGIN} é substituído pelo envsubst antes do nginx iniciar, via command no compose. Os demais $var do nginx ($host, $remote_addr, etc.) ficam intactos porque envsubst recebe a lista explícita de variáveis a substituir. docker/orthanc-cors-proxy/nginx.conf.template:
add_header Access-Control-Allow-Origin "${ALLOWED_ORIGIN}" always;
docker-compose.yml (dev):
orthanc-cors-proxy:
  volumes:
    - ./docker/orthanc-cors-proxy/nginx.conf.template:/etc/nginx/nginx.conf.template:ro
  environment:
    - ALLOWED_ORIGIN=${ALLOWED_ORIGIN:-http://localhost:3001}
  command: >
    sh -c "envsubst '$$ALLOWED_ORIGIN' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'"
docker-compose.prod.yml (prod):
orthanc-cors-proxy:
  environment:
    - ALLOWED_ORIGIN=${ALLOWED_ORIGIN}
.env.example:
ALLOWED_ORIGIN=http://localhost:3001   # produção: https://viewer.seudominio.com.br
Origem correta: ALLOWED_ORIGIN deve ser a origem do OHIF viewer (:3001), não do frontend (:3000). É o browser abrindo o OHIF que faz as requisições CORS para o proxy — o header Origin que o nginx recebe é http://localhost:3001. Como funciona em dev: ALLOWED_ORIGIN usa o fallback http://localhost:3001 — sem precisar definir no .env. Como funciona em prod: definir ALLOWED_ORIGIN=https://viewer.seudominio.com.br no .env do servidor.

✅ 3. PostgreSQL e Redis expostos ao host

Problema atual (docker-compose.yml):
postgres:
  ports:
    - "5432:5432"   # exposto ao host — desnecessário em prod

redis:
  ports:
    - "6379:6379"   # exposto ao host — sem auth
Solução no docker-compose.prod.yml: simplesmente não declarar os ports: — os serviços continuam acessíveis internamente via pacs_network. Em dev: manter os ports expostos é útil para usar ferramentas locais como TablePlus, RedisInsight, etc.

✅ 4. Redis sem senha

Problema: Redis acessível sem autenticação dentro da rede Docker. Implementado com expansão condicional — só ativa --requirepass se REDIS_PASSWORD não estiver vazia. Com isso, o mesmo docker-compose.prod.yml funciona em dev (sem senha) e em prod (com senha):
redis:
  command: >
    sh -c "redis-server $${REDIS_PASSWORD:+--requirepass $$REDIS_PASSWORD}"
  healthcheck:
    test: ["CMD", "sh", "-c", "redis-cli $${REDIS_PASSWORD:+-a $$REDIS_PASSWORD} ping"]
.env raiz (prod): definir REDIS_PASSWORD=senha-forte-gerada backend/.env (prod): definir REDIS_PASSWORD=senha-forte-gerada (mesmo valor) worker/main.py: já lê REDIS_PASSWORD do ambiente e passa na conexão Redis. docker-compose.yml (dev): sem senha — continua igual.

✅ 5. Secrets fracos

Resolvido no servidor: todos os secrets foram gerados com openssl rand -hex 32 e php artisan jwt:secret. Nenhum valor entra no repositório — ficam apenas no .env e backend/.env do servidor.

🟠 Alto — build e performance (obrigatório antes do deploy)


✅ 6 e 7. Dockerfiles de produção (frontend, backend, worker)

Problema: todos os serviços usavam volume mount do código fonte e next dev / sem build otimizado. O que foi criado: docker/php/Dockerfile.prod:
  • Copia o código do backend para dentro da imagem (COPY backend/ .)
  • composer install --no-dev --optimize-autoloader — sem pacotes de dev, autoloader otimizado
  • ./vendor/bin/rr get-binary — baixa o binário do RoadRunner (está no .gitignore, precisa ser baixado no build)
  • php artisan config:cache && route:cache && view:cache — caches de produção do Laravel
  • chown www-data storage bootstrap/cache — permissões corretas
docker/nextjs/Dockerfile.prod:
  • 3 stages: deps (npm ci) → builder (npm run build) → runner (node server.js)
  • NEXT_PUBLIC_* passadas como ARG no build — obrigatório pois são embutidas no bundle em compile time
  • Runtime com usuário não-root nextjs:nodejs
  • Imagem final usa apenas o output .next/standalone — muito menor que a de dev
  • Requer output: "standalone" no next.config.tsadicionado
docker/python/Dockerfile.prod:
  • Código baked na imagem, sem volume mount
docker-compose.prod.yml — overrides de build:
backend:
  build:
    dockerfile: ./docker/php/Dockerfile.prod
  volumes: !reset          # remove bind mount do código
    - pacs_storage:/app/pacs_mount

frontend:
  build:
    dockerfile: ./docker/nextjs/Dockerfile.prod
    args:
      NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
      NEXT_PUBLIC_OHIF_VIEWER_URL: ${NEXT_PUBLIC_OHIF_VIEWER_URL}
  volumes: !reset []       # remove bind mount do código e node_modules

worker:
  build:
    dockerfile: ./docker/python/Dockerfile.prod
  volumes: !reset          # remove bind mount do código
    - pacs_storage:/app/storage
Dev: continua com docker-compose.yml → Dockerfiles originais com hot reload e volume mount.

✅ 8. APP_DEBUG, LOG_LEVEL e DEBUG_WORKER

VariávelDevProd
APP_DEBUGtruefalse
LOG_LEVELdebugerror
DEBUG_WORKER10 ou removido
VERBOSE_ENABLED (Orthanc)truefalse ou removido
APP_ENVlocalproduction
Em produção, erros não devem expor detalhes internos nas respostas HTTP.

✅ 9. Octane com workers fixos

Atual: --workers=4 Prod: --workers=auto (usa todos os CPUs do servidor)
# docker-compose.prod.yml
backend:
  command: php artisan octane:start --server=roadrunner --host=0.0.0.0 --port=8000 --workers=auto

🟡 Médio — infraestrutura (idealmente antes, pode ser pós-deploy inicial)


✅ 10. HTTPS / TLS

Implementado com Caddy como container no docker-compose.prod.yml. Arquitetura final — Caddy roteia 3 subdomínios e fecha as portas de todos os serviços internos:
Internet :443/:80
  → pacs_caddy (Caddy 2 Alpine — TLS automático via Let's Encrypt)
      → frontend:3000          (app.suaempresa.com.br)
      → backend:8000           (api.suaempresa.com.br)
      → ohif:80                (viewer.suaempresa.com.br)
      → orthanc-cors-proxy:80  (viewer.suaempresa.com.br/dicom-proxy/*)
O proxy DicomWeb é servido como path sob o viewer (/dicom-proxy/), eliminando a necessidade de um 4º subdomínio. O app-config.prod.js do OHIF aponta para esse path. Arquivos criados:
  • docker/caddy/Caddyfile — configuração dos 3 virtual hosts
  • docker/ohif/app-config.prod.js — URLs de produção, patch de token atualizado para checar DICOM_PROXY_ROOT ao invés de :8043
docker-compose.prod.yml — Caddy service:
caddy:
  image: caddy:2-alpine
  container_name: pacs_caddy
  ports:
    - "80:80"
    - "443:443"
    - "443:443/udp"
  volumes:
    - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
    - caddy_data:/data
    - caddy_config:/config
  environment:
    - ACME_EMAIL=${ACME_EMAIL}
Variáveis no .env raiz do servidor:
ACME_EMAIL=seuemail@suaempresa.com.br
NEXT_PUBLIC_API_BASE_URL=https://api.suaempresa.com.br/api
NEXT_PUBLIC_OHIF_VIEWER_URL=https://viewer.suaempresa.com.br
ALLOWED_ORIGIN=https://viewer.suaempresa.com.br
Atenção: o Caddy só emite certificado se o DNS já propagou para o IP do servidor antes do primeiro boot. Certificados ficam persistidos no volume caddy_data.

✅ 11. Rate limiting no login

Implementado em backend/routes/api.php:
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:10,1');
// 10 tentativas por minuto por IP — após isso, retorna 429 Too Many Requests
O Laravel usa o cache (Redis) para controlar o contador por IP. Não afeta o dev — 10 tentativas por minuto é permissivo para qualquer uso normal.

✅ 12. allowedDevOrigins no next.config.ts

frontend/next.config.ts:
// Dev (atual)
const nextConfig: NextConfig = {
  allowedDevOrigins: ["localhost", "127.0.0.1", "0.0.0.0", "172.17.0.1"]
};

// Prod — remover a chave inteira, não tem efeito em next start
const nextConfig: NextConfig = {};
Como allowedDevOrigins só é lida pelo servidor de dev (next dev), não tem efeito em produção mesmo que fique no arquivo. Mas é boa prática remover.

13. JWT TTL e renovação

Atual: cookie com max-age de 30 dias, sem refresh. Recomendação para produção:
  • TTL do JWT: 8 horas (configurável via JWT_TTL no config/jwt.php)
  • Implementar refresh automático: quando o interceptor do Axios receber 401, tentar POST /api/refresh antes de redirecionar para o login
Isso pode ser implementado depois do deploy inicial sem urgência.

🔵 Não bloqueante (backlog pós-deploy)


14. Backup dos volumes

VolumeConteúdoEstratégia
pacs_pg_dataBanco de dados completopg_dump em cron diário → S3
pacs_orthanc_dataImagens DICOM originaisSnapshot de volume → S3
pacs_storageThumbnails geradosPodem ser regerados — baixa prioridade

15. Resource limits nos containers

# docker-compose.prod.yml
backend:
  deploy:
    resources:
      limits:
        cpus: '2'
        memory: 1G

worker:
  deploy:
    resources:
      limits:
        cpus: '1'
        memory: 512M

✅ 16. Health checks no backend e frontend

Implementado no docker-compose.prod.yml:
  • Backend: curl -f http://localhost:8000/up a cada 30s
  • Frontend: node http.get a cada 30s

✅ 17. Queue worker Laravel automático

O job ProcessStudyMetadata (disparo pelo webhook do Orthanc) rodava manualmente. Foi adicionado um container dedicado pacs_queue nos dois composes rodando php artisan queue:work --tries=3.

18. Reprocessamento de estudos com job falho (conhecido)

Problema: quando o Orthanc recebe uma imagem DICOM e o job ProcessStudyMetadata falha (ex: configuração ausente, timeout), a imagem fica salva no Orthanc mas o estudo não aparece no sistema. Como o Orthanc não envia webhook para imagens duplicadas, o job não é re-disparado automaticamente. Workaround disponível — o comando pacs:requeue re-enfileira todos os estudos do Orthanc que não estão indexados:
# Re-enfileira todos os estudos do Orthanc não indexados
docker exec pacs_backend php artisan pacs:requeue

# Só os estudos sem modalidade indexada
docker exec pacs_backend php artisan pacs:requeue --only-missing-modalities
Quando usar: sempre que um estudo chegar no Orthanc mas não aparecer no sistema após alguns segundos. Melhoria futura: monitorar a fila de jobs falhos (queue:failed) e alertar via log ou webhook quando houver falhas acumuladas.

✅ 19. OHIF carregando config padrão (CloudFront) em vez do proxy interno

Problema identificado em homolog: o docker-compose.prod.yml tentava montar o app-config.prod.js via volumes: !reset + bind mount, mas a imagem ohif/app:latest não recebia o mount (Mounts: [] no docker inspect). Com isso o OHIF usava seu config padrão apontando para d14fa38qiwhyfd.cloudfront.net — o servidor demo público da OHIF. O 403 retornado era da CloudFront rejeitando o estudo (não CORS, não backend). Como o entrypoint do OHIF funciona (/usr/src/entrypoint.sh):
  1. Se APP_CONFIG env var estiver definida, escreve seu conteúdo em app-config.js
  2. Se app-config.js existir e não estiver vazio: gzipa → cria app-config.js.gz → esvazia app-config.js com touch
  3. nginx com gzip_static on serve o .gz — o .js sempre fica em 0 bytes após o boot
Fix aplicado: criado docker/ohif/Dockerfile.prod que bake o config na imagem antes do entrypoint rodar:
FROM ohif/app:latest
COPY docker/ohif/app-config.prod.js /usr/share/nginx/html/app-config.js
RUN rm -f /usr/share/nginx/html/app-config.js.gz
O entrypoint detecta o arquivo não-vazio, gzipa para app-config.js.gz e esvazia o .js. nginx serve o .gz com o config correto. O docker-compose.prod.yml usa build: dockerfile: ./docker/ohif/Dockerfile.prod em vez de volume mount. Observação: após rebuild do OHIF, limpar cache do browser (Ctrl+Shift+R ou aba anônima) — o browser pode servir o config antigo da CloudFront por horas.

✅ 20. Volumes não montados no backend/worker em produção (!reset vs !override)

Problema: docker inspect pacs_backend/pacs_worker mostrava Mounts: [] mesmo com volumes declarados no docker-compose.prod.yml. Thumbnails escritos pelo worker não apareciam no backend. Causa raiz: no Docker Compose, !reset aplicado a uma sequência zera a lista para vazia e ignora os itens abaixo. O comportamento correto é:
TagComportamento
!reset []Substitui por lista vazia — correto para remover ports
!reset + itens abaixoZera para vazio, itens são ignorados — bug silencioso
!override + itens abaixoSubstitui a lista base pelos novos itens — correto para substituir volumes
Fix: substituir volumes: !reset por volumes: !override nos serviços que precisam de volumes nomeados:
# ERRADO — volumes ficam vazios
backend:
  volumes: !reset
    - pacs_storage:/app/pacs_mount

# CORRETO — substitui os bind mounts do dev pelo volume nomeado
backend:
  volumes: !override
    - pacs_storage:/app/pacs_mount
ports: !reset [] continua correto (lista explicitamente vazia).

Resumo de prioridades

#ItemPrioridadeQuebra dev?
1Cookie JWT httpOnly🔴 CríticoNão — dev usa HTTP sem Secure
2CORS * → domínio real🔴 CríticoNão — variável de ambiente
3PostgreSQL/Redis sem ports expostos🔴 CríticoNão — só no compose.prod
4Redis com senha🔴 CríticoNão — só no compose.prod
5Secrets fortes✅ FeitoNão — valores diferentes por env
6Frontend multi-stage build🟠 AltoNão — Dockerfile.prod separado
7Backend/worker código baked🟠 AltoNão — Dockerfile.prod separado
8DEBUG desativado em prod🟠 AltoNão — variáveis de ambiente
9Octane —workers=auto🟠 AltoNão — só no compose.prod
10HTTPS/TLS✅ FeitoNão — Caddy em Docker
11Rate limiting no login🟡 MédioNão — throttle permissivo em dev
12Remover allowedDevOrigins🟡 MédioNão — afeta só next dev
13JWT TTL menor + refresh✅ FeitoNão — configurável por env
14Backup dos volumes✅ FeitoNão — scripts/backup.sh + cron
15Resource limits✅ FeitoNão — via variáveis de ambiente
16Health checks✅ FeitoNão — backend e frontend
17Queue worker automático✅ FeitoNão — container pacs_queue
18Reprocessamento de estudos falhos🔵 WorkaroundNão — pacs:requeue disponível
19OHIF carregando config padrão (CloudFront)✅ FeitoNão — Dockerfile.prod com bake
20Volumes não montados (!reset vs !override)✅ FeitoNão — só no compose.prod
Nenhum item desta lista quebra o ambiente de desenvolvimento local. A separação docker-compose.yml (dev) / docker-compose.prod.yml (prod) é justamente para garantir isso.

Ordem de implementação sugerida

1. ✅ docker-compose.prod.yml base (items 3, 4, 8, 9)
2. ✅ Dockerfiles de produção para backend, frontend e worker (items 6, 7)
3. ✅ Cookie httpOnly — AuthController, SetBearerFromCookie, client.ts, cors.php (item 1)
4. ✅ CORS do nginx proxy → variável de ambiente (item 2)
5. ✅ Rate limiting no login (item 11)
6. ✅ Caddy em Docker como reverse proxy com TLS automático (item 10)
7. ✅ Gerar e configurar secrets fortes (item 5) — feito no servidor, não em código
8. Backup, resource limits, health checks (items 14-16)

V1 Stable — o que entra no primeiro release

Esta seção marca o escopo do primeiro release estável (v1) — o que está incluso e o que fica para versões futuras. Após o deploy em produção, ver V1_STABLE.md (criado pós-release) para descrição do estado congelado.

O que está pronto (código)

  • ✅ Stack completa Docker Compose (9 containers em dev, +1 Caddy em prod)
  • ✅ Ingestão DICOM (Orthanc → webhook → worker Python → PostgreSQL)
  • ✅ Multi-tenant com 3 roles (super admin, tenant admin, doutor)
  • ✅ Sistema de laudos: rascunho, finalização, correção, audit trail (ReportRevision)
  • ✅ Templates de laudo com editor visual + preview A4 fiel ao PDF
  • ✅ Presets de texto por tenant (sugestão automática por modalidade/descrição)
  • ✅ OHIF Viewer com isolamento multi-tenant via token temporário + nginx auth_request
  • ✅ Refactor UI/UX do sistema de laudos concluído (ver REPORTS_SYSTEM.md)
  • ✅ Hardening de produção (cookie httpOnly, CORS, rate limiting, HTTPS via Caddy)

Pendente antes do PR develop → main

  1. Deploy da branch develop em homolog (servidor de staging)
  2. Rodar migrations no homolog
  3. Validação manual completa via HOMOLOG_CHECKLIST.md (checklist por persona + fluxos transversais)
  4. Abrir PR develop → main após checklist 100% verde

Após o deploy em produção

  • Criar V1_STABLE.md descrevendo o estado congelado da v1 (features entregues, limites conhecidos, funcionalidades que virão em v1.x/v2)
  • Publicar doc Mintlify em mintlify.com (passo 6 do projeto docs — ver project_mintlify.md)

Fora do escopo da v1 (roadmap)

Refactors grandes planejados depois do release v1 estável. Ver MEMBERSHIPS_REFACTOR.md e STORAGE_ARCHITECTURE.md para detalhes:
ItemOnde está planejado
Memberships — usuário em múltiplas clínicas/unidades + role referring_physician (médico solicitante)MEMBERSHIPS_REFACTOR.md
Orthanc SQLite → PostgreSQL — index do Orthanc em Postgres para multi-tenant concorrenteSTORAGE_ARCHITECTURE.md §2
Object storage — MinIO ou S3 para extrair blobs base64 do PostgreSQL (Fase 1 do tiered storage)STORAGE_ARCHITECTURE.md §3
Cold storage — arquivamento de estudos antigos (S3 Glacier) após período de retenção legalSTORAGE_ARCHITECTURE.md §3
VPN/WireGuard — infraestrutura para aparelhos enviarem DICOM via túnel isolado (código pronto, infra pendente)memory/project_vpn_ingestion_plan.md
Painel super admin redesign — onboarding guiado, visão consolidada por tenant (depende do refactor de memberships)memory/project_rbac_refactor.md
Esses itens não bloqueiam o release v1. A v1 sobe com o modelo atual (single-tenant por user) e os refactors viram v1.x ou v2.