CTO no Integrar Plus • Publicado em 08 de October de 2026
A maioria das discussões sobre LGPD em sistemas de saúde trava em uma contradição prática que ninguém quer admitir no código. De um lado, a equipe jurídica exige minimização rigorosa e direito ao esquecimento, defendendo que qualquer dado pessoal dispensável saia do banco imediatamente. Do outro lado, conselhos profissionais de classe e normas sanitárias exigem que o histórico clínico e os acessos fiquem intactos por décadas, sob pena de processo ético ou judicial contra a clínica.
Quando essa conversa desce para a camada de persistência, surgem dois extremos problemáticos.
O primeiro grupo trata a trilha de auditoria como um mero dump de eventos em uma tabela genérica do PostgreSQL ou em um bucket S3. Eles serializam o payload inteiro do ActiveRecord em JSON a cada leitura ou alteração. Acreditam que estão seguros porque tudo está registrado
. O problema dessa abordagem é que a tabela de auditoria se torna um vazamento permanente de dados sensíveis. Se um usuário pede a exclusão de um telefone, endereço ou observação administrativa, o registro original na tabela principal é limpo, mas uma cópia em texto puro continua vivendo para sempre em trinta linhas de log espalhadas pelo banco de dados.
O segundo grupo comete o erro oposto. Com medo de manter dados sensíveis em duplicidade, confia apenas em colunas de controle comuns como updated_at e updated_by_id, ou aplica um soft delete ingênuo com sobrescrita de campos. Quando ocorre um incidente de segurança ou uma suspeita de vazamento interno por um operador autorizado, o sistema não consegue responder quem visualizou a tela de evolução clínica em uma terça-feira às três da manhã. Não existe trilha de leitura, apenas o estado atual da linha.
Ambas as posições estão erradas. A auditoria de prontuário eletrônico não pode ser um espelho de dados, mas sim um registro de intenção e autoria de leitura.
Auditoria em saúde precisa cobrir tanto operações de escrita quanto eventos de leitura (o infame SELECT). Em sistemas convencionais, raramente registramos quem leu um registro para evitar gargalos de I/O. Em prontuários, contudo, a invasão de privacidade quase sempre ocorre por curiosidade ou bisbilhotice interna sem nenhuma alteração no cadastro.
Para não explodir o armazenamento e não duplicar dados sensíveis dentro da trilha, a regra de ouro que adoto na modelagem é separar metadados de acesso do conteúdo clínico em si:
CREATE TABLE medical_record_access_logs (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id bigint NOT NULL,
medical_record_id bigint NOT NULL,
user_id bigint NOT NULL,
action varchar(20) NOT NULL, -- read, update, export, sign
ip_address inet NOT NULL,
user_agent text,
accessed_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE INDEX idx_record_access_logs_patient_lookup
ON medical_record_access_logs (patient_id, accessed_at DESC);
Perceba que essa tabela não armazena o diagnóstico, a prescrição médica ou o nome do paciente. Ela armazena chaves estrangeiras, a operação executada e o contexto temporal e de rede. Se o paciente solicitar a exclusão de dados puramente cadastrais amparados pela lei, os identificadores numéricos continuam preservando a integridade legal do histórico de acesso sem violar a retenção mínima.
Um erro comum em projetos Ruby on Rails é pendurar essa auditoria em callbacks de modelo, como after_find ou gems que interceptam ciclos de vida do ActiveRecord sem critério.
Colocar lógica de log em after_find destrói a performance da aplicação. Qualquer rotina em segundo plano, consulta de listagem ou exportação em lote dispara centenas de inserções desnecessárias. Além disso, o modelo muitas vezes não tem a menor ideia de quem é o usuário atual logado, a menos que você recorra a variáveis globais de thread (CurrentAttributes), o que frequentemente gera sujeira quando tarefas do Sidekiq reutilizam a mesma thread.
A auditoria de acesso pertence à camada de serviço ou ao controller, exatamente onde o contexto da requisição HTTP e a intenção do operador são explícitos:
class MedicalRecords::ShowService
def initialize(patient:, current_user:, request_metadata:)
@patient = patient
@current_user = current_user
@request_metadata = request_metadata
end
def call
record = @patient.medical_record
MedicalRecordAccessLog.create!(
patient_id: @patient.id,
medical_record_id: record.id,
user_id: @current_user.id,
action: "read",
ip_address: @request_metadata[:ip],
user_agent: @request_metadata[:user_agent],
accessed_at: Time.current
)
record
end
end
Se o volume de acessos for muito alto, persistir de forma síncrona dentro da transação principal pode degradar a latência percebida pelo médico no consultório. Uma alternativa viável é enfileirar o log ou usar inserções diretas em lote via insert_all após o ciclo da requisição terminar. Mas eu hesito em jogar logs de auditoria jurídica em filas que podem falhar silenciosamente ou perder mensagens se a infraestrutura cair. Gravar direto no PostgreSQL local, mantendo a escrita curta e enxuta, continua sendo a escolha mais previsível para servidores próprios.
A LGPD e as normas sanitárias convergem em um ponto específico: a trilha de auditoria precisa ser confiável e resistente à adulteração. Se um administrador do banco puder alterar um log com um simples comando UPDATE para acobertar um acesso indevido, a trilha perde a validade pericial.
Em vez de contratar ferramentas externas caras para garantir integridade de log, o próprio PostgreSQL resolve isso com permissões restritas e triggers de bloqueio:
CREATE OR REPLACE FUNCTION prevent_audit_tampering()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Registros de auditoria medica sao imutaveis e nao podem ser alterados ou deletados.';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_protect_medical_access_logs
BEFORE UPDATE OR DELETE ON medical_record_access_logs
FOR EACH ROW
EXECUTE FUNCTION prevent_audit_tampering();
Essa barreira impede que erros de aplicação ou comandos acidentais apaguem linhas. Naturalmente, um usuário com privilégios de superuser ainda pode desativar o gatilho se quiser fraudar o sistema, mas para isso o usuário da aplicação que o Rails utiliza jamais deve rodar como dono da tabela ou com privilégios elevados. O isolamento de roles no banco é o mecanismo mais negligenciado em arquiteturas menores.
Existe ainda a questão do prazo. Prontuários médicos no Brasil possuem prazos legais de guarda de até vinte anos segundo o Conselho Federal de Medicina. Logs de acesso geral de conexão têm outros prazos no Marco Civil da Internet. Tratar tudo no mesmo saco cria tabelas gigantescas que sufocam os discos da VPS e inviabilizam rotinas de backup.
O caminho pragmático é particionar a tabela de logs por data (mensal ou anual) nativamente no PostgreSQL. Quando o período legal de um bloco expira, descartar uma partição inteira com DROP TABLE não gera fragmentação de disco, não trava a tabela inteira e não sobrecarrega o processo de autovacuum.
Minimizar dados não significa apagar o que é necessário para a segurança jurídica do negócio. Significa registrar apenas o estritamente necessário para comprovar autoria e contexto, sem carregar o conteúdo sensível a reboque em tabelas secundárias. Quem tenta auditar salvando tudo cria um passivo invisível; quem audita de menos descobre o valor do log no primeiro processo judicial.
CTO e arquiteto full stack especializado em Ruby on Rails, Vue.js, sistemas multi-tenant e infraestrutura distribuída.