Criei meu serviço médico e deixei o Claude Opus analisar os dados da família inteira. Ele encontrou 11 erros médicos
Olá, Habr. Em artigos anteriores, falei sobre um bot do Telegram no Gemini e a implementação de IA em todos os campos de entrada do Windows. Hoje, a história é diferente: pessoal, experimental. Escrevi um serviço médico para minha família, conectei-o ao Claude Opus e honestamente não sei como tudo vai acabar. Mas até agora o resultado é mais interessante do que eu esperava.
Uma breve história de por que isso tudo. Meu filho tem um atraso no desenvolvimento da fala e, em dois anos, passamos por vários especialistas: neurologistas, psiquiatras, fonoaudiólogos, especialistas em defeitos. Cada um tem seu próprio prontuário, suas próprias anotações, sua própria opinião. E em algum momento percebi duas coisas. Primeiro: minha esposa, que lida com isso 24 horas por dia, 7 dias por semana, não consegue manter tudo na cabeça fisicamente. Segundo: os médicos não conversam entre si e nenhum deles vê o quadro completo.
Houve também um caso com um neurologista, para quem fomos encaminhados por uma clínica. Ela recomendou fortemente um osteopata. Pesquisei no Google e descobri que a osteopatia não é uma medicina baseada em evidências e, nos comentários sobre esse neurologista, fica claro que ela recomenda o mesmo osteopata para quase todos. E eu me perguntei: como posso entender se tenho um profissional na minha frente ou não, se não sou médico? Resposta: de jeito nenhum. A menos que haja uma ferramenta que verifique para você.
A medicina é uma coisa precisa: análises são números, dosagens são números, valores de referência são números. Uma rede neural funciona bem com números e, o mais importante, ela vê o quadro completo, todas as interconexões entre análises, prescrições e diagnósticos, que nenhum médico individual pode cobrir em 15 minutos de consulta. Então eu sentei e escrevi o serviço.
O que foi alcançado até agora
Em resumo: é um banco de dados médico familiar. Você carrega tudo relacionado à saúde (documentos, resultados de análises, transcrições de consultas médicas, prescrições, vacinas, altura e peso) e a rede neural analisa tudo isso como um todo. Não um fragmento que você copiou para um bate-papo, mas toda a história com todas as conexões.
A principal diferença de "apenas perguntar ao ChatGPT" que percebi com minha esposa. Ela usa a versão gratuita, disse "é o suficiente para mim". E é verdade, é o suficiente para perguntas simples. Mas quando você acumulou uma pasta de documentos por cinco anos e está perguntando sobre uma nova análise, a rede neural no bate-papo já esqueceu que três consultas atrás outro médico prescreveu outro medicamento. O contexto não é elástico. À medida que o diálogo avança, as nuances são eliminadas.
No meu caso, Claude recebe o contexto completo a cada vez: todos os diagnósticos, todos os medicamentos, todas as análises, toda a cronologia, todas as conexões entre eles. Em uma única solicitação. Nada se perde entre as sessões. Não é um bate-papo, é uma base de conhecimento persistente com uma rede neural como analista. Explicarei a diferença com mais detalhes um pouco mais adiante, há um ponto interessante com a arquitetura.
E outra coisa importante: a IA neste serviço não faz diagnósticos. Ela explica na linguagem dos fatos o que foi feito corretamente, o que não foi feito e por que isso é importante. Sem "seu filho provavelmente tem", apenas "o médico N não prescreveu a análise X, embora com o diagnóstico Y este seja o controle padrão de acordo com as diretrizes internacionais". A diferença é fundamental.
Tela principal
Ao abrir o aplicativo, você é recebido por uma tela de PIN. Se a biometria estiver configurada neste dispositivo, você pode entrar via Face ID ou Windows Hello em um segundo, caso contrário, 6 dígitos. Haverá uma grande seção separada sobre segurança sobre como tudo isso funciona nos bastidores.
Depois de fazer login, você chega a um resumo. Quatro cartões clicáveis na parte superior: diagnósticos ativos, medicamentos atuais, total de visitas, erros críticos. Clicar leva à página correspondente com detalhes. Abaixo estão os principais diagnósticos ativos, medicamentos atuais, próximos lembretes e resumo de IA do paciente.
O resumo de IA é um resumo estruturado que a rede neural escreveu após analisar todo o banco de dados: prioridades atuais, plano de ação, o que prestar atenção. É atualizado quando um novo documento é carregado.
As outras páginas: plano de exames, lista de erros e inconsistências médicas, uma página separada com todos os diagnósticos, documentos (consultas médicas + documentos independentes como resultados de laboratório e ressonância magnética em um único feed com filtros), um mapa de saúde sobre o qual haverá uma seção separada, um bate-papo de IA e uma seção "mais" onde tudo o mais está - especialistas, vacinas, altura e peso, pesquisa global no banco de dados.
Além disso, tenho uma versão desktop separada, não apenas um "celular adaptado". No desktop, há uma barra lateral à esquerda com navegação, os modais se expandem para tela cheia em vez de aparecerem na parte inferior, tabelas e gráficos usam todo o espaço disponível. Eu mesmo uso metade do desktop, metade do telefone, e era importante que em ambas as plataformas tudo parecesse um aplicativo nativo.
Mapa de saúde e por que as conexões são mais importantes do que o armazenamento
Aqui quero me afastar da interface e contar uma coisa que me parece muito mais importante do que o número de páginas.
Inicialmente, os dados no banco de dados estavam próximos, mas não juntos. Aqui está o medicamento. Aqui está o diagnóstico. Aqui está a consulta com o neurologista. Tudo está registrado, tudo está armazenado. Mas a conexão entre eles não está registrada em lugar nenhum: quem prescreveu este medicamento? De qual diagnóstico? Em qual consulta? A rede neural tinha que procurar essas conexões novamente a cada vez, adivinhar pelo texto do contexto das transcrições e datas. Funcionou enquanto havia poucos dados, depois começou a desmoronar.
Eu estava olhando para bancos de dados de grafos: SurrealDB, Kuzu, Neo4j. Todos bonitos, todos com seus próprios recursos. E em algum momento percebi: tenho 100 entidades e 300 conexões. Esta não é a escala onde um banco de dados de grafos é necessário. O problema não é o mecanismo, o problema é o esquema de dados.
A solução acabou sendo simples: tabelas de junção no mesmo SQLite.
sql-- Uma linha descreve toda a cadeia: médico → prescreveu → medicamento → de diagnóstico → na consulta CREATE TABLE prescriptions ( medication_id REFERENCES medications(id), diagnosis_id REFERENCES diagnoses(id), specialist_id REFERENCES specialists(id), timeline_id REFERENCES timeline(id), prescribed_date TEXT, dosage TEXT, rationale TEXT );
Agora cada conexão tem um ID explícito. E o seguinte SQL tornou-se possível:
sql-- Quais medicamentos foram prescritos por um neurologista específico e quais deles foram posteriormente cancelados SELECT m.name, m.status, m.stop_reason FROM prescriptions p JOIN medications m ON m.id = p.medication_id JOIN specialists s ON s.id = p.specialist_id WHERE s.id = 6 AND m.status = 'cancelled';
Sem tabelas de junção, a rede neural era forçada a analisar as transcrições e adivinhar pelas palavras a cada vez. Agora é apenas um JOIN, e a rede neural pode analisar os dados com uma precisão muito maior.
E em cima do banco de dados relacional, conectei a visualização no Cytoscape.js. Nós: diagnósticos, médicos, medicamentos, consultas, erros. Arestas: quem prescreveu o quê, de qual diagnóstico, em qual consulta. Clicar em um nó destaca apenas suas conexões, o resto desaparece. Parece algo assim:
Coordenador de IA: como funciona sem chamadas de API
O serviço não tem integração direta com a API da rede neural. Absolutamente nenhuma. Nem uma linha de fetch('https://api.anthropic.com/...'). Nenhum @anthropic-ai/sdk nas dependências. Nenhuma chave de API em .env.
"Então, como a IA funciona?"
O servidor tem um arquivo service_instructions.md: um grande documento com um protocolo completo para a rede neural trabalhar com este banco de dados. Como formatar uma avaliação de visita, como construir um plano de exames, em qual formato registrar o resultado da análise, como procurar contradições. É, na verdade, um prompt para Claude, apenas em 50+ KB e com regras SQL específicas.
E então existem dois mecanismos.
Primeiro: uma fila de solicitações.
O banco de dados tem uma tabela ai_requests. Quando clico em "Solicitar análise de IA" em qualquer entidade na interface (diagnóstico, visita, documento, erro), uma entrada com o status pending é adicionada a esta tabela. Apenas uma entrada. Sem enviar para lugar nenhum.
Segundo: um único ponto de contexto.
O endpoint GET /api/patient-context. Ele retorna TUDO o que o banco de dados sabe sobre o paciente: diagnósticos junto com os médicos que os fizeram, medicamentos com prescrições e razões para cancelamento, consultas com todos os documentos relacionados, análises com valores de referência e marcas de desvios, erros com resoluções, plano com status, todo o histórico de alterações nos últimos 30 dias. Um único JSON de 200-500 kilobytes.
Em uma linha, Claude recebe o quadro completo que nenhum médico é capaz de cobrir em 15 minutos de consulta.
O ciclo então é o seguinte:
- Eu carrego um novo documento ou análise
- Abro o Claude Code e digo: "olhe para as instruções, há um novo documento"
- Claude lê
service_instructions.md, faz um GET/api/patient-context, vê um novo documento e a filaai_requests - Analisa, compara com os dados existentes, procura contradições e exames perdidos
- Escreve os resultados diretamente através da API:
ai_assessmentpara entidades, novos erros, atualizações de plano, conclusões - Marca as solicitações processadas como
completed
E isso funciona. Funciona muito melhor do que se eu estivesse inserindo chamadas de API lá. Porque Claude no Claude Code já tem ferramentas (Read, Bash para executar SQL, a capacidade de fazer solicitações HTTP) e ele as usa de forma flexível: viu um indicador incompreensível em um PDF, ele mesmo o renderizou novamente em alta resolução, ele mesmo o leu, ele mesmo o comparou com o original.
Separadamente: na interface sob qualquer entidade (diagnóstico, visita, documento, erro) há um botão "Solicitar análise de IA". Você clica, uma entrada aparece na fila ai_requests e a rede neural na próxima atualização vê o que exatamente precisa ser analisado, não há necessidade de explicar com palavras. A mesma história com os comentários: você pode fazer uma pergunta diretamente sob um documento específico e obter uma resposta levando em consideração este contexto específico.
A desvantagem óbvia: este não é um modo automático. Eu preciso abrir o Claude Code e dizer "olhe para as instruções". Aproximadamente uma vez por semana, se novos documentos aparecerem. Poderia ser automatizado: levantar o Claude CLI no servidor e anexar um hook ao upload do documento para que a análise seja iniciada em tempo real imediatamente após o upload. Mas por enquanto me sinto mais confortável controlando o processo manualmente. Novos dados não aparecem todos os dias, abrir o Claude Code uma vez por semana não é difícil.
Quanto custa
Já que estamos falando de Claude, vamos falar sobre dinheiro. Eu medi o volume real de dados no exemplo do cartão do meu filho (é o mais completo agora):
service_instructions.md(instrução): 51 KB → ~17.000 tokenspatient-context(todo o banco de dados em um JSON): 188 KB → ~63.000 tokens
Total de entrada: ~80.000 tokens
Na saída, Claude geralmente gera 5-10 mil tokens: ai_assessments, novas entradas no plano e erros, edições em análises.
Se calcularmos pelos preços da API através do OpenRouter (Claude Opus 4.6: $5 por milhão de tokens de entrada, $25 por milhão de saída):
- Entrada: 80K tokens × 5 / M = 0.40$
- Saída: ~8K tokens × 25 / M = 0.20$
Total: ~$0.60 por uma análise completa de todo o prontuário médico
Novos documentos aparecem 2-4 vezes por mês. Ou seja, através da API custaria $1.5-2.5 por mês.
Eu uso a assinatura Claude Max por 100$/mês, mas não por causa deste serviço, mas porque Claude é minha principal ferramenta de trabalho para tudo: código, textos, análise. O serviço médico simplesmente segue no mesmo fluxo.
A rede neural comete erros
Uma vez, ao processar um relatório de laboratório, Claude leu incorretamente um valor em um PDF: as linhas estavam próximas, um número da linha adjacente "saltou" e uma discrepância crítica que realmente não existia entrou no banco de dados. A rede neural honestamente construiu análises sobre esses dados, registrou um erro, adicionou um item ao plano. Eu notei ao verificar, pedi para verificar novamente, Claude ele mesmo encontrou a discrepância e corrigiu todas as conclusões errôneas.
Depois disso, adicionei uma verificação obrigatória de duas etapas às instruções para todos os documentos com números: a primeira passagem na resolução padrão, a segunda com a renderização do PDF em 400-500 DPI e verificação linha por linha de cada número com o original. Somente após a correspondência completa os dados vão para o banco de dados.
A conclusão é simples: sempre verifique os dados atrás da rede neural, especialmente quando a saúde depende deles. Mas o progresso está avançando rapidamente e, a cada atualização dos modelos, haverá menos desses erros.
História automática: 40 triggers e zero código para logging
O logging de alterações no serviço é organizado sem uma única linha de código nas rotas. Nenhum await log('medication_added', ...). O histórico de alterações funciona inteiramente através dos triggers do próprio SQLite.
sqlCREATE TRIGGER audit_medications_update AFTER UPDATE ON medications FOR EACH ROW BEGIN INSERT INTO audit_log (patient_id, entity_type, entity_id, action, old_value, new_value) VALUES (NEW.patient_id, 'medications', NEW.id, 'update', json_object('name', OLD.name, 'status', OLD.status, 'dosage', OLD.dosage), json_object('name', NEW.name, 'status', NEW.status, 'dosage', NEW.dosage)); END;
Existem 40 desses triggers no banco de dados, em 13 tabelas médicas, três em cada (insert/update/delete). Qualquer edição, seja do frontend React, seja do painel de administração, seja diretamente através de sqlite3.db no console: é automaticamente gravada em audit_log como um JSON antigo e um JSON novo. Não pode ser contornado, porque o trigger está no nível do banco de dados, não no nível do aplicativo.
E então um módulo separado changelog.js no servidor pega o audit_log bruto e o transforma em entradas compreensíveis para humanos:
- Se o
statusde um erro mudou deopenpararesolved, escreve "Erro resolvido: ESR 24, não levado em consideração" - Se as edições de uma entidade vêm uma após a outra em 60 segundos, agrupa em uma única entrada
- Agrupa por dias: Hoje / Ontem / 3 dias atrás / 2026-04-05
No frontend, isso se transforma em uma página "Histórico". Foi criado para ver rapidamente o que mudou no banco de dados após o processamento do próximo documento: quais lembretes foram adicionados, quais conclusões foram reescritas, quais erros foram fechados. Quando Claude edita o banco de dados, isso também é visível como entradas separadas marcadas como "autor: ai" e pode ser revertido se algo der errado.
Stack e deploy
O serviço vive em um VPS: nginx + TLS por fora, Node.js + better-sqlite3 por dentro, systemd para autostart. Esquema padrão.
Sobre o stack: frontend em React 19 + TypeScript strict + Vite, roteamento em React Router 7, dados através de TanStack Query, formulários em react-hook-form + zod, grafo em Cytoscape, animações em Motion, ícones de @tabler/icons-react, PWA através de vite-plugin-pwa + Workbox. Backend em Express + better-sqlite3, biometria através de @simplewebauthn/server. Sem overengineering, sem exótico.
Todos os roteadores no backend são escritos no estilo PostgreSQL:
javascriptpool.query('SELECT * FROM patients WHERE id = $1', [id])
E no arquivo db.js vive um wrapper fino que traduz em tempo real os placeholders $1, $2 em ?, emula RETURNING através de um SELECT separado, substitui NOW() por datetime('now'). Formalmente, o banco de dados é SQLite, mas o código é escrito de forma que a mudança para PostgreSQL levaria meia hora.
Segurança. Isso vale a pena discutir separadamente
Os dados médicos de uma criança são a coisa mais sensível que se pode imaginar. Eu não quero que ninguém os veja e não quero que o banco de dados se perca se algo acontecer com o servidor. Portanto, a segurança foi uma prioridade desde o primeiro dia.
A proteção é construída em camadas: para comprometer o serviço, é preciso romper várias barreiras independentes. Se uma cair, as outras continuam funcionando.
Rede e TLS
O VPS tem UFW com default-deny: apenas SSH (22), ACME challenge (80) e HTTPS (443) estão abertos. Fail2ban bane automaticamente IPs que tentam invadir via SSH ou escaneiam o site em busca de buracos. Três tentativas malsucedidas em 10 minutos → uma hora de banimento, com tentativas repetidas o prazo aumenta. O próprio SSH aceita apenas chaves, sem senhas (não há nenhuma), então o brute force é basicamente impossível.
TLS 1.2 e 1.3, protocolos antigos estão desativados, certificado de Let’s Encrypt com auto-renovação. Além disso, um conjunto de cabeçalhos de segurança: HSTS por 2 anos, CSP com proibição de scripts de terceiros, X-Frame-Options: DENY, nosniff, Referrer-Policy, Permissions-Policy com desativação de APIs desnecessárias (câmera, microfone, GPS, etc.).
Três maneiras de entrar
Na ordem de preferência:
- Biometria (Face ID / Touch ID / Windows Hello).
Funciona através de WebAuthn, o mesmo padrão que os bancos usam. A chave biométrica é armazenada no Secure Enclave do dispositivo, que é um chip especial ao qual nem mesmo o kernel do sistema operacional tem acesso direto. O servidor vê apenas a chave pública. Assinar uma solicitação só é possível desbloqueando fisicamente um dispositivo específico de uma pessoa específica.
- PIN mais palavra de controle.
O PIN tem 4-10 dígitos. O banco de dados não armazena o PIN em si, mas um hash scrypt. Recuperar o PIN do hash é impossível em um tempo razoável, mesmo teoricamente. Comparação em tempo constante, para que não seja possível adivinhar o PIN pelo tempo de resposta do servidor.
A palavra de controle é solicitada apenas quando você entra de um novo dispositivo. O dispositivo é determinado por um UUID estável no localStorage. Em dispositivos familiares, a palavra de controle não é solicitada. Após a resposta, o dispositivo entra na lista de confiáveis e, no mesmo segundo, uma notificação chega no Telegram com IP, navegador e hora. Se não fui eu quem entrou, eu descubro imediatamente.
- Apenas PIN.
Se o dispositivo já estiver na lista de confiáveis e a biometria não estiver configurada.
Proteção contra brute force
Mesmo que alguém saiba que o PIN tem seis dígitos (um milhão de combinações) e comece a tentar, nada acontecerá.
O servidor implementa um tempo limite exponencial após três tentativas malsucedidas: 3º erro → um minuto de espera, 4º → dois, 5º → quatro, depois dobra até um teto de 24 horas. A partir da 14ª tentativa, cada bloqueio subsequente é por um dia. Tentar um milhão de combinações de PIN neste esquema é impossível em um tempo razoável. E o contador vive no banco de dados, nenhuma limpeza do cache do navegador pode contornar isso.
Separadamente, há um limite de 20 tentativas de login em 15 minutos por IP e 1000 solicitações para todo o resto. Além disso, fail2ban no nível do firewall. Além disso, as próprias sessões duram 14 dias e são invalidadas instantaneamente ao alterar o PIN.
Backend do servidor e dados
O serviço não funciona a partir do root. Um usuário de sistema separado com direitos mínimos é criado. Mesmo que uma vulnerabilidade seja encontrada no código Node que permita executar comandos arbitrários, o invasor receberá apenas os direitos deste usuário. Ele poderá ler o prontuário médico (triste, mas fato), mas não poderá acessar outros serviços no mesmo host, alterar as configurações do sistema, abrir novas portas.
Adicionalmente, a unidade systemd é configurada com isolamento: NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp e outros hardening. Os arquivos de configuração e o banco de dados estão com permissões 600, apenas o proprietário os vê.
A pesquisa no banco de dados é feita através de FTS5 (índice de texto completo embutido no SQLite). Isso significa que nenhuma entrada do usuário é concatenada em uma consulta SQL manualmente: injeções SQL arquitetonicamente não existem.
O upload de documentos é verificado duas vezes: tanto o tipo MIME enviado pelo navegador quanto a extensão do nome do arquivo. PDF, DOC/DOCX, XLS/XLSX, JPG, PNG, WebP, HEIC são permitidos.
SVG é proibido completamente, porque SVG é XML e pode conter JavaScript embutido, e abrir tal arquivo no navegador é o mesmo que executar código de outra pessoa no contexto do site. Cada arquivo carregado recebe um nome UUID, é impossível adivinhar o caminho para o arquivo de outra pessoa. Ao baixar, é verificado se o caminho está dentro da pasta permitida, proteção contra path traversal.
Backups pela regra 3-2-1
Esta é a segunda linha de segurança: não "contra os maus", mas "se algo acontecer". Três cópias, dois ambientes, um offsite.
-
Cópia 1. Snapshots quentes a cada 6 horas através da API nativa
.backupdo SQLite. Os últimos 14 são armazenados. Esta é a proteção contra erros lógicos: excluímos algo acidentalmente, Claude cometeu um erro ao processar um documento. Você pode reverter para qualquer um dos 14. -
Cópia 2. Arquivo diário completo às 02:00. Banco de dados mais todos os arquivos médicos (PDF, fotos, scans), tudo é empacotado em tar e criptografado com AES-256-CBC com PBKDF2 (100.000 iterações, padrão OWASP). Sem a chave, o arquivo se transforma em um conjunto sem sentido de bytes. Os últimos 14 arquivos são armazenados.
-
Cópia 3. O mais interessante. Imediatamente após a criação do arquivo diário, ele é enviado para o Telegram através de um bot. Sim, Telegram. A cópia está fisicamente na nuvem do Telegram, separada do VPS, separada do GitHub, separada do meu notebook. O histórico de mensagens no Telegram é armazenado para sempre, você pode baixar o arquivo de qualquer dispositivo em segundos. O arquivo é criptografado, sem a chave (e está no gerenciador de senhas) é um binário inútil.
No final, o código vive no GitHub (repo privado), os dados no VPS com dois níveis de backups locais e uma cópia criptografada dos dados voa para o Telegram. Mesmo que o VPS queime, o código é clonado do GitHub, os dados são baixados do Telegram, descriptografados com a chave do gerenciador de senhas e tudo é levantado em um novo servidor em uma hora.
Auditoria e notificações
Cada alteração no banco de dados através de triggers vai para audit_log, sobre isso eu escrevi acima. Um auth_log separado escreve todos os eventos de login, logout, alteração de PIN, registro de biometria, revogação de dispositivo: com IP, user-agent e hora.
E notificações instantâneas vão para o Telegram para eventos:
- Um novo dispositivo entrou na conta
- A biometria está configurada
- O PIN foi alterado
- O serviço foi reiniciado após o deploy
- O backup diário foi criado ou (se de repente) caiu
Assim, eu descubro sobre atividades suspeitas no mesmo segundo. Se alguém entrar de um dispositivo desconhecido, receberei uma notificação push.
Honestamente sobre os pontos fracos
Segurança ideal não existe:
- O VPS não é um host dedicado.
É uma máquina virtual em hardware compartilhado, teoricamente se houver uma vulnerabilidade séria no contêiner vizinho, isso pode repercutir. Eu minimizo com um usuário não privilegiado e isolamento systemd, mas o isolamento físico completo é outra classe de orçamento. Para dados médicos de uma família, considero este risco aceitável; para um serviço para mil pacientes, não.
- O banco de dados no disco não é criptografado.
O arquivo está em forma aberta. Se alguém obtiver acesso root ao servidor, ele o lerá. A proteção aqui é apenas através de perms 600 e um usuário não privilegiado. Por que não criptografar: SQLCipher quebra parte das otimizações de pesquisa FTS5, que são críticas para o funcionamento do coordenador de IA. Mitigação: o backup diário no Telegram já está criptografado e o único lugar não criptografado é o arquivo ativo no próprio servidor.
- Tokens de sessão no localStorage.
Teoricamente vulnerável a XSS. Eu reduzi muito o risco através de CSP, proibição de SVG, validação rigorosa de texto do usuário. Mitigação: vida útil curta dos tokens, notificação instantânea ao entrar


