iPsiq
Cofundador e arquiteto de software – 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, 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
- Testes e entrega
- Mais de 500 testes automatizados, integração contínua a cada push e deploy automático na AWS.
- Migrações versionadas
- 36 migrações de banco, versionadas e com reversão.
- Isolamento provado por testes
- 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
- 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 e limites
- 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
- 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 de receita
- 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)
- Registro no conselho
- 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.
- Especialidade e RQE
- A especialidade médica só é exibida com RQE, conforme as regras do CFM; sem RQE o profissional aparece apenas como "Médico".
- Rótulo público
- O rótulo público de cada perfil segue a Resolução CFM 2.336/2023, sobre publicidade médica.
- Ativação do profissional
- 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, e-mail e fotos
- Segredos no SSM Parameter Store, e-mails transacionais no SES e fotos no S3 com URLs assinadas.
- Backup e deploy
- 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.
- Reserva temporária
- O horário escolhido fica reservado por um tempo e há proteção contra reserva dupla.
- Pagamento no agendamento
- O pagamento é feito no Pix na hora do agendamento, e a consulta é confirmada na hora.
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.
- Isolamento entre profissionais
- Nenhum profissional acessa dados de outro, nem pela API: isso é provado por testes de integração no CI.
- Início da consulta
- A consulta é iniciada pelo botão Iniciar, que só é liberado perto do horário marcado.
- Sala do Meet
- 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 e folgas
- 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 e filtros
- 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
- 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.
- Permissão, role e pessoa
- 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.
- Catálogo tipado
- 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
- 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
- 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.
- Perfil somente leitura
- 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.
- Front-end por permissão
- 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
- 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.
- Guard global que falha fechado
- 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.
- Permissões checadas na compilação
- 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.
- Médico da sessão
- @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.
- Filtro global de exceções
- 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.
- Escopo de tenant
- 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 e tentativas de login
- 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 cobrados por testes
- 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
- 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.
- BaseRepository
- 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.
- Falhas registradas
- Escritas que falham também são registradas, com resultado de falha, fora da transação que falhou.
- Só os campos alterados
- 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.
- Versão por registro
- Cada registro tem versão (1, 2, 3…) por entidade, com "ver o histórico deste registro" e "ver tudo desta pessoa".
- Tabela só de inclusão
- 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).
- Dados sensíveis mascarados
- 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.
- Nenhuma tabela fica de fora
- 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.
- Eventos na linguagem da equipe
- 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.
- Uma linha do tempo
- 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.
- Eventos ligados ao que tocam
- 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.
- Filtro Ocultar
- 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: um único ponto de gravação
- 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) (etapa planejada)
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.
- Redação central
- 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).
- Por que na gravação
- 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.
- Linhas antigas redigidas
- 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 vê a identidade
- 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.
- Teste do perfil somente leitura
- 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 iPsiq integra a API do Asaas para receber no Pix com repasse automático ao profissional (split): uma subconta por profissional, checkout Pix, webhooks, notas fiscais e assinaturas.
- Subcontas
- Cada profissional tem uma, aberta pela equipe. O cadastro e os dados bancários são preenchidos direto no Asaas, por um link com validade, e o iPsiq não os guarda; o profissional só fica ativo depois que o Asaas aprova a conta, e o status chega por webhook.
- Checkout Pix
- A cobrança de cada consulta é um checkout criado na subconta do profissional, com o split para a carteira do iPsiq e a consulta como referência externa; o prazo do checkout acompanha o da reserva temporária do horário.
- Webhooks por duas portas
- A da conta principal (assinaturas e notas do iPsiq) e uma para cada subconta (consultas, notas e aprovação daquele profissional). Cada uma tem o seu token, comparado em tempo constante; o de cada subconta é derivado (HMAC) do segredo mestre.
- O webhook é só um aviso
- O corpo do webhook é só um aviso: o pagamento, a nota ou o status da conta é lido de novo no Asaas, com a chave da conta certa, antes de qualquer mudança. Um corpo forjado não confirma pagamento.
- Idempotência
- Cada evento é reivindicado de forma atômica pelo id do Asaas antes de ser tratado, então entregas repetidas ou simultâneas não repetem efeitos (e-mails, sala de vídeo, conversões). Se o tratamento falha, a reivindicação é devolvida e o Asaas reenvia.
- Conciliação de status
- O estado do pagamento é aplicado à consulta de forma idempotente. Um pagamento só atualiza consultas do médico da própria subconta, e um pagamento pago nunca volta a falho por causa de um evento atrasado. O "Já paguei" do paciente consulta o Asaas direto (por checkout e por referência), com limite de frequência por consulta para não gastar a cota da API.
- Registro dos webhooks
- O da conta principal é criado ou atualizado a cada deploy, por um comando idempotente, e o de cada subconta é registrado quando ela é criada. Os eventos assinados cobrem pagamento, checkout, notas fiscais e status da conta.
- Notas fiscais
- As NFS-e aplicáveis são emitidas pelo Asaas de forma automática depois do pagamento confirmado, com o status sincronizado por webhook. Cada nota só é atualizada pela conta que a emitiu, e os arquivos baixados têm limite de tamanho.
- Assinaturas
- As assinaturas e as cobranças recorrentes dos profissionais correm pela conta principal e são atualizadas pelos webhooks dela.
- Cliente da API
- Fino, cada chamada informa em qual conta age (a principal ou a subconta), tem timeout de 15 s para que um Asaas lento não prenda requisições e nunca registra as chaves em log.
- Segredos
- 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.
- Erros
- O que o paciente pode corrigir aparece no formulário; um 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.
- Testes sem rede
- Um servidor HTTP que imita o Asaas registra cada chamada e responde conforme o teste, então a integração é verificada sem o provedor.
Do pagamento ao webhook
Paciente reserva e paga no Pix
iPsiq cria o checkout
na subconta do profissional, com a consulta como referência e o split para a carteira do iPsiq
Asaas
processa o pagamento, faz o repasse automático e avisa por webhook
- aviso
Webhook do Asaas, três verificações
1. Token
comparação em tempo constante
2. Id do evento
reivindicado de forma atômica, tratado uma única vez
3. Leitura no Asaas
o pagamento é lido de novo na API, com a chave da conta certa, antes de mudar a consulta
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.
- Telas por tema
- As telas são agrupadas por tema (consultas, financeiro e integrações), e cada alteração fica na trilha de auditoria.
- O que não é mostrado
- 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.
