Pular para o conteúdo

Projetos

Lucas Augusto Muniz Menezes

iPsiq

Cofundador e engenheiro full stack – atual

Visão geral

Plataforma em desenvolvimento para agendamento e pagamento de consultas em saúde mental, com página própria para cada médico e backoffice da equipe.

  • Arquitetei e desenvolvi o back-end (NestJS, TypeORM, PostgreSQL, Zod) e o front-end (React, Vite, TanStack Query, Tailwind).
  • Agendamento com reserva temporária e proteção contra reserva dupla, preço em cascata por especialidade e por médico, e auditoria gravada na mesma transação.
  • Pagamento com split no ato do agendamento via Asaas (subcontas, notas fiscais e assinaturas), em andamento.
  • Ambiente local completo em Docker com LocalStack (S3, SES), deploy automático na AWS a cada push e fluxo de anonimização de dados para a LGPD.

Qualidade e entrega

  • Mais de 500 testes automatizados, integração contínua a cada push e deploy automático na AWS.
  • Cerca de 48 mil linhas de TypeScript, 36 migrações de banco e 240 commits em três semanas.
  • O isolamento entre profissionais (nenhum acessa dados de outro, nem pela API) é provado por testes de integração no CI.

Segurança e controle de acesso

  • Acesso fechado por padrão: toda rota declara quem pode chamá-la, e os papéis são conjuntos de permissões, com permissões extras por pessoa, sempre com motivo e histórico.
  • Perfil somente leitura que mostra o paciente só pelo código e oculta nome, e-mail, telefone e CPF, com um teste que percorre todas as rotas.
  • Sessão com token de renovação em cookie HttpOnly, bloqueio de tentativas de login, limites de reservas por IP e por paciente e chaves de terceiros criptografadas.
  • Auditoria de tudo o que muda no sistema, gravada na mesma transação, com filtros por período, pessoa, médico e código do paciente.

Privacidade e LGPD

  • Minimização de dados: nada de paciente em logs, URLs ou eventos de anúncio, e o CPF completo não é guardado, apenas mascarado e em hash com chave para busca.
  • Dados para receita coletados só depois do pagamento, criptografados e com cada acesso do médico auditado; a plataforma não emite receitas.
  • Direitos do titular: anonimização de paciente sob demanda, origem de anúncio apagada depois de 180 dias e backup do banco criptografado, com retenção limitada.
  • Cookies: nenhum pixel antes do aceite, escolha por categoria e consentimento registrado; e-mail e telefone em hash não vão para as plataformas de anúncio.

Regras do CFM e dos conselhos (CRM)

  • Cada profissional aparece com o registro do seu conselho (CRM, CRP ou CRN); psicólogos e nutricionistas não aparecem como médicos nem com especialidade médica.
  • A especialidade médica só é exibida com RQE, conforme as regras do CFM; sem RQE o profissional aparece apenas como "Médico".
  • O rótulo público de cada perfil segue a Resolução CFM 2.336/2023, sobre publicidade médica.
  • Um profissional só vai para o site depois de completar o checklist de ativação e de a equipe ativá-lo.

Infraestrutura na AWS

  • Segredos no SSM Parameter Store, e-mails transacionais no SES e fotos no S3 com URLs assinadas.
  • Backup diário do banco para o S3, criptografado e com retenção de 30 dias, e deploy automático a cada push na branch principal.

Site público

O que o paciente vê: a página inicial e a página de cada profissional, com o agendamento em um só fluxo.

  • O horário escolhido fica reservado por um tempo e há proteção contra reserva dupla.
  • O pagamento é feito no Pix na hora do agendamento, e a consulta é confirmada na hora.
  • A remarcação é sem custo, com 24 horas de antecedência.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Página inicial pública do iPsiq com a proposta de valor e o próximo horário livre
Página inicial pública: proposta de valor e próximo horário livre.
Seletor de dia e horário da página de um médico, com o botão de agendar
Agendamento: escolha do dia e do horário, e confirmação no mesmo fluxo.

Área do médico

Cada profissional entra com o próprio login e vê só os próprios dados: agenda, consultas, notas e horários.

  • Nenhum profissional acessa dados de outro, nem pela API: isso é provado por testes de integração no CI.
  • A consulta é iniciada pelo botão Iniciar, liberado 10 minutos antes; consultas não iniciadas até 15 minutos depois do horário são estornadas automaticamente.
  • Com a Google Agenda conectada, cada consulta ganha uma sala exclusiva do Meet, criada na agenda do próprio profissional; o evento leva só "Consulta iPsiq" e o primeiro nome do paciente.
  • Horários por dia da semana e folgas; mudanças não afetam consultas já marcadas.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Agenda do dia na área do médico, com a conexão com a Google Agenda e as consultas do dia
Área do médico: agenda do dia, com a conexão da Google Agenda e as ações de cada consulta (iniciar, falta, remarcar, cancelar).
Lista das consultas do profissional filtrada por situação
Área do médico: as próprias consultas, com a situação e o estado dos dados para receita.
Horários de atendimento por dia da semana e bloqueios na área do médico
Área do médico: horários de atendimento por dia e folgas, que o paciente vê no agendamento.

Backoffice

A equipe acompanha agenda, consultas, médicos e horários em um só lugar.

  • Busca de consultas por nome, e-mail, telefone, CPF ou código do paciente, com filtros por situação e médico.
  • Ações rápidas na agenda: concluir, registrar falta, remarcar e cancelar; remarcar exige um motivo, que vai para a auditoria.
  • Paleta de comandos (Ctrl K) para ir direto a qualquer tela ou configuração.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Agenda do dia no backoffice, com a situação de cada consulta e ações rápidas
Backoffice: agenda do dia com a situação de cada consulta e ações rápidas (concluir, falta, remarcar, cancelar).
Lista de consultas do backoffice filtrada por situação
Backoffice: consultas com busca, filtros por situação e médico, e paginação.
Horários de atendimento por dia da semana e formulário de bloqueios
Horários de atendimento por dia da semana, com cópia entre dias, e bloqueios de férias e compromissos.
Paleta de comandos aberta sobre a lista de roles
Paleta de comandos (Ctrl K) para ir direto a qualquer tela ou configuração.

Acessos e permissões

RBAC por permissões: roles são conjuntos de permissões de recurso e ação, o acesso é fechado por padrão e cada rota declara quem pode chamá-la.

  • Uma permissão é um par recurso:ação, uma role é um conjunto de permissões e cada pessoa tem uma role; a cada requisição o guard junta a role com as permissões extras da pessoa que não foram retiradas.
  • O catálogo fica num arquivo tipado: o decorator só aceita permissões que existem. Na subida, o app sincroniza o catálogo com o banco (cria e remove permissões) com auditoria.
  • Audiência: cada role e cada permissão é da equipe ou do médico, e as permissões de médico (own-*) só entram em role de médico, o que o banco também garante. O isolamento por médico fica na rota: a permissão diz o que a pessoa pode fazer, não de quem.
  • Roles de sistema: o Administrador é fixo (sempre todas as permissões, sem edição, para ninguém se trancar para fora); Médico e Product Manager são ajustáveis pela tela, e o arquivo é só o ponto de partida: o sync nunca desfaz o que foi mudado ali. Nenhuma pode ser excluída ou renomeada.
  • Roles customizadas são montadas numa matriz de recurso por ação, e cada role tem seu histórico (quem mudou, quando, o que entrou e o que saiu); uma role com usuários não pode ser excluída.
  • Regras de concessão: ninguém concede uma permissão que não tem, ninguém dá permissão a si mesmo, e a troca de role nunca é a própria, nunca para uma audiência diferente e nunca do último administrador.
  • Permissões extras por pessoa exigem motivo ao conceder e ao retirar, guardam quem deu e quando, aparecem na mesma linha do tempo da auditoria e valem na próxima ação, sem novo login.
  • Product Manager, somente leitura: só permissões de visualização, com a lista exata de rotas travada em teste (uma rota nova fica fechada para ele até alguém incluir a permissão de propósito) e paciente só pelo código; um teste percorre todas as rotas dele com dados reais e falha se algum dado pessoal aparecer.
  • O front-end decide as áreas e os botões pelas permissões (audiência e permissões vêm do endpoint da sessão), com guards por audiência e por permissão, e o servidor continua sendo quem decide.
// permissions.config.ts
export const PERMISSIONS = {
  'recurso:ver': { audience: 'staff' },
  'recurso:editar': { audience: 'staff' },
  'own-agenda:view': { audience: 'doctor' },
} as const;

export type Permission = keyof typeof PERMISSIONS;

// @Permissions('recurso:ver')    -> compila
// @Permissions('recurso:vre')    -> erro de compilação
Exemplo ilustrativo (nomes genéricos) do catálogo tipado: o decorator só aceita as chaves que existem aqui.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Lista de roles do sistema com tipo, público, permissões e usuários
Acessos: roles do sistema (administrador, médico e product manager) como conjuntos de permissões.
Matriz de permissões por recurso e ação de uma role
Matriz de permissões por recurso e ação; roles customizadas são montadas aqui.

Código e decorators

O back-end é em NestJS e TypeScript, com a segurança e a validação declaradas na própria rota, por decorators, pipes e filtros globais, e verificadas por testes.

  • Quatro decorators cobrem o acesso de toda rota: @Public() (aberta), @Authenticated() (qualquer pessoa logada) e @Permissions(...) (a role precisa conceder todas as permissões), mais @CurrentUser() e @CurrentDoctorId() para ler a pessoa da sessão.
  • O guard global falha fechado: uma rota que não declara acesso é recusada, e um teste (access-control.spec) percorre todas as rotas para garantir isso.
  • Os nomes das permissões em @Permissions são checados em tempo de compilação contra um catálogo tipado, então um erro de digitação não compila; o catálogo é sincronizado com o banco na subida, com auditoria.
  • O token é só a chave: papel, permissões e médico vêm da sessão viva a cada requisição, então logout, desativação, troca de senha e mudança de role valem na próxima chamada.
  • @CurrentDoctorId() entrega o médico da sessão: as rotas de médico nunca recebem o id pela URL nem pelo corpo, o que fecha a porta para ler dados de outro médico (IDOR).
  • Validação na borda: o ZodValidationPipe valida e transforma o corpo com um schema Zod e devolve uma mensagem por campo inválido, no formato caminho: mensagem.
  • Um filtro global de exceções padroniza o erro da API, converte erros de integridade do Postgres em 409 ou 400 no lugar de 500, esconde a mensagem real de erros inesperados em produção e registra todo 5xx, sem deixar tokens de links nos logs.
  • Dados pessoais: criptografia de campo com AES-256-GCM, com a chave fora do banco, e hash com chave (HMAC-SHA256) para buscar um valor sem guardá-lo, como o CPF.
  • Todo repositório tem escopo de tenant: o tenantId faz parte de toda consulta e de toda alteração, o que impede o acesso entre contextos.
  • Senhas com Argon2 e política de senha própria, bloqueio de tentativas de login por conta e limitador de requisições nas rotas de login.
  • Padrões que o time segue e os testes cobram: escritas só por repositório (um teste recusa SQL cru), migrações com reversão, enums tipados, componentes genéricos no front e comentários de código em inglês com textos de tela em português.
  • make check roda tipos, lint e testes dentro do Docker, e o CI repete isso a cada push.

O caminho de uma requisição

  1. Requisição

    traz só o token, que é a chave da sessão

  2. Guard global

    carrega a sessão viva (papel, permissões, médico) e falha fechado: rota sem declaração é recusada

  3. Acesso declarado na rota

    @Public()

    rota aberta

    @Authenticated()

    qualquer pessoa logada

    @Permissions(...)

    a role precisa conceder todas; os nomes são checados na compilação

  4. ZodValidationPipe

    valida e transforma o corpo, com uma mensagem por campo inválido

  5. Handler

    @CurrentUser() e @CurrentDoctorId() vêm da sessão, nunca da URL nem do corpo

Da requisição ao handler: o acesso e a validação são declarados na rota e o guard global recusa o que não declara.
@Controller('exemplo')
export class ExemploController {
  // Qualquer pessoa logada, seja qual for a role.
  @Get('eu')
  @Authenticated()
  quemSouEu(@CurrentUser() user: AuthenticatedUser) { /* ... */ }

  // A role precisa conceder TODAS as permissões citadas;
  // o nome é checado em tempo de compilação contra o catálogo tipado.
  @Post()
  @Permissions('recurso:acao')
  criar(@Body(new ZodValidationPipe(CriarSchema)) dto: CriarDto) { /* ... */ }

  // O médico vem da sessão, nunca da URL nem do corpo.
  @Get('minha-agenda')
  @Permissions('own-agenda:view')
  agenda(@CurrentDoctorId() doctorId: string) { /* ... */ }

  // Rota aberta (ex.: página pública), declarada de propósito.
  @Public()
  @Get('publico')
  publico() { /* ... */ }
}
Exemplo ilustrativo (nomes genéricos) de como o acesso e a validação são declarados na rota.

Auditoria e histórico

Toda alteração de dados deixa um registro de quem fez, o quê, quando e o que mudou, e o histórico não pode ser editado nem apagado.

  • Todos os repositórios herdam de um BaseRepository: toda escrita (criar, alterar, excluir e restaurar) grava o evento de auditoria na mesma transação do negócio, então ou os dois são gravados ou nenhum.
  • Escritas que falham também são registradas, com resultado de falha, fora da transação que falhou.
  • Quem chama informa exatamente os campos alterados, então o que é auditado é o que mudou; a exclusão guarda o registro apagado para o histórico dizer o que era.
  • Cada registro tem versão (1, 2, 3…) por entidade, com "ver o histórico deste registro" e "ver tudo desta pessoa".
  • A tabela de auditoria só aceita inclusão: o próprio banco recusa UPDATE, DELETE e TRUNCATE (LGPD, art. 37: o registro das operações precisa ser confiável).
  • Senhas, tokens, CPF, e-mails, IP e dados do paciente são mascarados no diff, porque a trilha não pode ser apagada; pacientes aparecem só pelo código.
  • Todas as tabelas de negócio têm histórico: um teste falha se aparecer INSERT, UPDATE ou DELETE cru fora das exceções. Só registros técnicos (contadores de login, marcas de envio de e-mail e webhook, códigos do OAuth e limpeza de sessões) ficam de fora, com audit: false explícito e justificado.
  • Os eventos viram frases na linguagem da equipe (por exemplo, "Consulta reservada pelo paciente no site" ou "Login (equipe)"), cada uma com título, detalhe e um tom (acesso, problema…); a lista mostra só os nomes dos campos alterados, e o diff completo fica a um clique.
  • A trilha reúne fontes diferentes numa única linha do tempo: alterações de dados, e-mails enviados, avisos do provedor de pagamento (webhooks) e conversões de anúncios, com falhas de login e de renovação de sessão também registradas como eventos.
  • Cada evento é ligado ao que ele toca (médico, consulta e paciente, pelo código), o que permite filtrar por período, origem, entidade, operação, resultado, médico, código do paciente e quem fez (equipe, médico, paciente ou sistema), e agrupar a história por paciente.
  • Um filtro "Ocultar" esconde o que é muito comum (logins, falhas de login, e-mails, webhooks e conversões) para destacar o resto, e entidades novas aparecem na auditoria sozinhas, pelo nome técnico, mesmo antes de ganharem um rótulo no catálogo.
  • Futuro: toda gravação passa por um único ponto (writeAuditEvent), hoje síncrono e na mesma transação porque o iPsiq não tem fila. Como a gravação é idempotente por id, esse ponto pode ser trocado por uma emissão de evento (padrão outbox) para levar a auditoria a um microsserviço, sem mexer no código dos repositórios.

Como uma escrita vira histórico

  1. Rota ou serviço

    pede para criar, alterar, excluir ou restaurar um registro

  2. BaseRepository

    todo repositório herda dele; ao criar audita a linha salva, ao alterar os campos que quem chama informa e ao excluir o registro apagado

  3. Mesma transação do banco

    Grava a mudança

    na tabela do negócio

    Grava o evento (writeAuditEvent)

    quem, o quê, quando e o que mudou, com os dados sensíveis mascarados; idempotente por id

  4. os dois ou nenhum

    Tabela de auditoria

    só aceita inclusão: o próprio banco recusa UPDATE, DELETE e TRUNCATE. Uma escrita que falha também deixa um evento, de resultado falha, fora da transação que falhou.

  5. no futuro

    Emissão de evento (outbox)

    troca só o writeAuditEvent e leva a auditoria a um microsserviço, sem mexer no código dos repositórios

Fluxo de uma escrita: o evento de auditoria nasce na mesma transação da mudança. O bloco tracejado é o caminho futuro, que troca só o ponto de gravação.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Tela de auditoria com filtros por período, origem, entidade e operação
Auditoria: tudo o que acontece no sistema, com filtros de quando, o quê e quem; pacientes só pelo código.
Auditoria dos médicos, com o histórico agrupado por código de paciente
Auditoria dos médicos: o histórico agrupado por paciente, identificado só pelo código.
Linha do tempo dos eventos de um paciente na auditoria
Linha do tempo de um paciente: cada evento diz o que aconteceu, quem fez e quando.
Detalhe de um evento de auditoria com a versão do registro e o JSON do que mudou, com IP e hash mascarados
Detalhe de um evento: versão do registro e o que mudou em JSON, com o IP e o hash do token mascarados ([redacted]).

Mascaramento de dados

Dados que não podem ser divulgados nem propagados são mascarados em camadas: na trilha de auditoria, nos logs, nas telas, nas respostas da API e nos eventos enviados a terceiros.

  • Uma função central de redação é aplicada a todo diff de auditoria antes de gravar: ela percorre o objeto em qualquer profundidade e troca por [redacted] os campos de credenciais (senhas, tokens, hashes, chaves de API, certificados, campos criptografados) e de dados pessoais (CPF, e-mail, IP, nome, e-mail e telefone do paciente, identificadores de anúncio e de visitante, user agent e link da sala).
  • O motivo é a própria regra da trilha: como ela só aceita inclusão, qualquer dado pessoal gravado ali nunca poderia ser apagado (LGPD). Os ids dos registros bastam para seguir a história.
  • Quando a lista de campos protegidos ampliou, uma migração aplicou a mesma redação às linhas antigas da auditoria: o gatilho que protege a tabela é desligado só durante essa limpeza pontual e religado logo depois, mesmo em caso de erro.
  • Logs e telas: e-mails viram, por exemplo, lu***@dominio e o CPF é guardado mascarado, com um hash com chave para busca; tokens de links (agendamento, nota, configuração) viram [token] nos logs, as chaves nunca são registradas e a chave do provedor de pagamento nunca aparece na tela.
  • Quem pode ver a identidade do paciente é uma permissão (patients:view-identity): sem ela, a pessoa vê só o código do paciente, os dados pessoais são omitidos das respostas e a busca por nome, e-mail, telefone ou CPF é ignorada; o código é derivado de um hash com chave e não tem como voltar ao CPF.
  • Um teste percorre todas as rotas do perfil somente leitura com dados reais e falha se algum dado pessoal aparecer.
  • Terceiros: nada de saúde ou do paciente vai para logs, URLs ou eventos de anúncio; e-mail e telefone em hash não são enviados às plataformas de anúncio sem aval jurídico e consentimento específico; e em produção a mensagem real de erros inesperados fica escondida.
auditoria   { "ip": "[redacted]", "refreshTokenHash": "[redacted]", "userId": "..." }
e-mail      lucas@clinica.com            ->  lu***@clinica.com
log (URL)   /agendamento/<token longo>   ->  /agendamento/[token]
paciente    sem patients:view-identity   ->  apenas o código P-XXXX-XXXX
Exemplos ilustrativos do resultado do mascaramento.

Integração com o Asaas

O pagamento do paciente é no Pix, com repasse automático ao profissional (split) e uma conta própria para cada um.

  • Cada profissional tem uma subconta: a cobrança de cada consulta nasce nela, com o split para a carteira do iPsiq.
  • O cliente da API é fino e cada chamada informa em qual conta age (a principal ou a subconta); as chaves nunca vão para logs.
  • A chave fica como segredo na AWS, é lida em tempo de execução e nunca aparece na tela; o ambiente (testes ou produção) é uma configuração.
  • Os webhooks são autenticados por token com comparação em tempo constante, processados uma única vez por id de evento e registrados automaticamente no deploy e na criação de cada subconta.
  • A cobrança carrega a consulta como referência para conciliar os pagamentos, e o estorno integral desfaz o split.
  • Os dados do titular da subconta vão direto ao Asaas, por um link de uso único, e o iPsiq não os guarda.
  • O que o paciente pode corrigir aparece no formulário; erro de configuração mostra uma mensagem genérica e avisa a equipe com o motivo exato, e a mensagem técnica do provedor nunca vai ao paciente.
  • Os testes não dependem de rede: um Asaas falso local simula o provedor.

Do pagamento ao webhook

  1. Paciente reserva e paga no Pix

  2. iPsiq cria a cobrança

    na subconta do profissional, com a consulta como referência e o split para a carteira do iPsiq

  3. Asaas

    processa o pagamento e faz o repasse automático

  4. aviso

    Webhook do Asaas, três verificações

    1. Token

    comparação em tempo constante

    2. Id do evento

    processado uma única vez

    3. Conciliação

    a consulta de referência liga o pagamento; o estorno integral desfaz o split

  5. Consulta atualizada

    e o aviso do provedor entra na linha do tempo de auditoria

A cobrança nasce na subconta do profissional, com split; o aviso do Asaas passa por três verificações antes de mexer em qualquer dado.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Tela de integração com o provedor de pagamento, com a chave configurada mas não exibida e a URL do webhook
Integração com o provedor de pagamento: a chave está configurada, mas nunca é exibida; o webhook é registrado automaticamente.

Configurações

Valores padrão e integrações da plataforma num só lugar, com busca por tela ou campo; as alterações passam pela mesma trilha de auditoria.

  • Exemplo de tela: Atendimento, com a tolerância para iniciar a consulta, depois da qual a consulta não iniciada é estornada automaticamente.
  • As telas de regras comerciais e fiscais não são mostradas aqui.

Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

Menu inicial das configurações, com as telas agrupadas em Consultas, Financeiro e Integrações e uma busca por tela ou campo
Configurações: o menu inicial, com as telas por grupo e a busca por tela ou campo. As descrições das telas comerciais e fiscais foram ocultadas.