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.


Á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.



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.




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çãoCapturas de tela de um ambiente de desenvolvimento, com dados fictícios.


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
Requisição
traz só o token, que é a chave da sessão
Guard global
carrega a sessão viva (papel, permissões, médico) e falha fechado: rota sem declaração é recusada
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
ZodValidationPipe
valida e transforma o corpo, com uma mensagem por campo inválido
Handler
@CurrentUser() e @CurrentDoctorId() vêm da sessão, nunca da URL nem do corpo
@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() { /* ... */ }
}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
Rota ou serviço
pede para criar, alterar, excluir ou restaurar um registro
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
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
- 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.
- 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
Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.




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-XXXXIntegraçã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
Paciente reserva e paga no Pix
iPsiq cria a cobrança
na subconta do profissional, com a consulta como referência e o split para a carteira do iPsiq
Asaas
processa o pagamento e faz o repasse automático
- 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
Consulta atualizada
e o aviso do provedor entra na linha do tempo de auditoria
Capturas de tela de um ambiente de desenvolvimento, com dados fictícios.

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.
