LoanFlow é um MVP full stack de gestão de empréstimos criado para demonstrar uma jornada de crédito completa: cadastro, autenticação, simulação, aprovação automática ou manual, cronograma de parcelas, pagamento demonstrativo e operação administrativa.
Este projeto usa dados e pagamentos fictícios. Ele é adequado para estudo, demonstração e portfólio; não deve processar crédito real sem as adaptações descritas em Limites do projeto.
Outras telas: cadastro, painel do cliente e fila administrativa.
- cadastro público exclusivo para clientes;
- login com JWT e navegação por perfil;
- papel do usuário revalidado no banco em toda requisição protegida;
- simulação com acréscimo demonstrativo fixo de 5%;
- aprovação automática ou encaminhamento para análise manual;
- solicitação idempotente, segura contra reenvios após timeout;
- dashboards de cliente e administrador;
- listagem, detalhe e cronograma de empréstimos;
- pagamento fictício de parcelas;
- aprovação e rejeição administrativa com confirmação;
- separação entre cliente e administrador, inclusive bloqueio de autorrevisão;
- contas e dados opcionais para demonstração.
Uma proposta é aprovada automaticamente quando atende simultaneamente a estes critérios:
- valor solicitado de até R$ 5.000;
- score simulado de pelo menos 650;
- parcela de até 30% da renda mensal.
Quando algum critério não é atendido, a proposta recebe o status pending_review e entra na fila administrativa.
- Node.js, TypeScript e Fastify;
- OpenAPI 3.0 e Swagger UI;
- PostgreSQL com
postgres.jse migrations versionadas pelo Prisma; - JWT e bcrypt;
- testes com
node:test.
- Vue 3 e Vite;
- Vue Router e Pinia;
- Axios;
- Lucide;
- Cypress.
.
├── .github/workflows/ # integração contínua
├── src/
│ ├── controllers/ # tradução entre HTTP e serviços
│ ├── services/ # regras de negócio
│ ├── repositories/ # acesso ao PostgreSQL
│ ├── routes/ # rotas e schemas HTTP
│ ├── middlewares/ # autenticação e autorização
│ └── scripts/ # reset e seed do banco
├── prisma/ # schema declarativo e histórico de migrations
├── web/ # aplicação Vue
├── cypress/ # testes de interface com API simulada
├── docs/ # decisões e referências visuais
└── compose.yaml # PostgreSQL local isolado
- Node.js 24 LTS recomendado;
- npm 10 ou superior;
- Docker com Docker Compose.
O arquivo .nvmrc fixa a versão usada no CI. Com nvm, execute nvm use na raiz.
Confira as versões instaladas:
node --version
npm --version
docker --version
docker compose versionNa raiz do repositório:
cp .env.example .env
openssl rand -hex 32Copie a saída do openssl para JWT_SECRET no arquivo .env. O valor de exemplo é recusado
intencionalmente. Em seguida:
npm install
npm run db:up
npm run db:deploy
npm run devO PostgreSQL local fica disponível apenas em 127.0.0.1:5433, e a API inicia em:
http://localhost:3333
Valide a API:
curl http://localhost:3333Resposta esperada:
{ "status": "ok", "name": "LoanFlow API" }Em outro terminal, a partir da raiz:
cp web/.env.example web/.env
cd web
npm install
npm run devAbra:
http://localhost:5173
Com a API em execução, a documentação interativa fica disponível em:
http://localhost:3333/docs
O contrato também pode ser consumido diretamente:
http://localhost:3333/docs/json
http://localhost:3333/docs/yaml
Para testar rotas protegidas:
- execute
POST /auth/login; - copie o valor de
tokenda resposta; - clique em Authorize no Swagger UI;
- informe somente o token JWT, sem adicionar o prefixo
Bearer.
O documento descreve bodies, parâmetros, filtros, respostas, erros e autenticação de todas as rotas. Os endpoints de documentação são públicos neste MVP; restrinja-os no proxy ou por autenticação antes de expor uma implantação real.
O estado esperado do banco está descrito em prisma/schema.prisma. O histórico SQL fica em prisma/migrations e inclui as constraints que o Prisma Schema não representa diretamente.
Para criar uma migration durante o desenvolvimento:
npm run db:migrate -- --name descricao_da_alteracaoPara aplicar migrations pendentes sem apagar dados:
npm run db:deployEm CI, staging e produção, use somente db:deploy. O comando db:create foi mantido como alias compatível de db:deploy e deixou de resetar tabelas.
Confira o estado do histórico com:
npm run db:status
npm run db:validateSe o banco já foi criado pelo antigo db:create, ainda não possui histórico em
_prisma_migrations e não recebeu alterações manuais de estrutura, faça backup e execute:
npm run db:adoptO wrapper também detecta o estado intermediário em que somente a baseline está registrada.
db:deploy bloqueia esse caminho porque o hardening histórico publicado não é transacional;
db:adopt compara colunas, tipos, defaults, chaves, índices únicos e enums com um fingerprint
estrutural publicado — incluindo o estado válido/pronto dos índices — antes de registrar qualquer
histórico. Em seguida, registra deliberadamente o hardening histórico e deixa as migrations
compensatórias executarem todas as mudanças dentro de transações.
As migrations de reconciliação bloqueiam escritas e executam preflight antes de alterar dados ou estrutura. Se houver colisões de CPF/e-mail após normalização, valores incompatíveis ou pagamentos duplicados, a migration aborta sem mesclar, excluir ou ajustar registros silenciosamente. Consulte o preflight correspondente para localizar a regra, corrija os dados deliberadamente e confirme que a transação reverteu. Quando o Prisma registrar a tentativa como falha, libere uma nova tentativa antes de executar novamente:
npm exec prisma -- migrate resolve --rolled-back NOME_DA_MIGRATION
npm run db:deployNão use migrate resolve para esconder divergências. O wrapper compara o checksum de toda
migration aplicada com seu arquivo local, recusa migrations desconhecidas, schemas parciais e
históricos duplicados, incompletos ou fora de ordem, e exige db:adopt para qualquer adoção de
histórico.
npm run db:reset apaga o schema configurado, reaplica todas as migrations e perde os dados. O
wrapper exige confirmação explícita para toda execução e informa banco, usuário, host e porta antes
de recusar. Mantenha a confirmação restrita ao comando:
ALLOW_DATABASE_RESET=true npm run db:resetMesmo a configuração Compose só é reconhecida como padrão quando usa loanflow_user,
localhost:5433 e o banco loanflow; outra porta ou usuário é tratada como alvo excepcional. Nunca
habilite essa variável globalmente ou em produção.
| Variável | Obrigatória | Uso |
|---|---|---|
DATABASE_URL |
sim | conexão PostgreSQL |
DATABASE_SSL |
não | use true para TLS com certificado e hostname verificados |
JWT_SECRET |
sim | assinatura dos tokens; troque o valor de exemplo |
PORT |
não | porta da API, padrão 3333 |
CORS_ORIGINS |
não | origens permitidas, separadas por vírgula |
TRUST_PROXY |
não | IPs/CIDRs explícitos de proxies confiáveis |
ALLOW_DATABASE_RESET |
no reset | confirmação explícita para toda execução de db:reset |
ALLOW_INTEGRATION_DATABASE_RESET |
só em teste | confirma o truncamento do banco local terminado em _test |
ALLOW_DEMO_SEED |
só no demo | confirmação explícita para toda execução de db:seed |
DEMO_MODE |
não | habilita o seed demonstrativo |
DEMO_USER_EMAIL |
no demo | e-mail do cliente demo |
DEMO_USER_PASSWORD |
no demo | senha do cliente demo |
DEMO_ADMIN_EMAIL |
no demo | e-mail do administrador demo |
DEMO_ADMIN_PASSWORD |
no demo | senha do administrador demo |
Para gerar um segredo JWT de desenvolvimento:
openssl rand -hex 32Copie o resultado para JWT_SECRET. O placeholder de .env.example nunca é aceito pela API.
Quando DATABASE_SSL=true ou a URL contém sslmode=verify-full, tanto a aplicação quanto o
Prisma verificam certificado e hostname. Configurações conflitantes ou modos que não validam o
hostname são recusados. Em desenvolvimento local, omita sslmode; não use sslmode=require, que
cifra sem validar a identidade do servidor. Parâmetros TLS alternativos como ssl e sslrootcert
também são recusados para impedir divergência entre o runtime e o Prisma.
Sem CORS_ORIGINS, a API aceita os frontends locais em localhost:5173 e 127.0.0.1:5173. Em uma implantação, informe somente origens HTTPS conhecidas. A API limita cada IP a 120 requisições por minuto; login aceita até 10 tentativas por minuto, cadastro até 5 por hora e pagamentos até 20 por minuto.
O Fastify ignora headers de IP encaminhado por padrão. Defina TRUST_PROXY apenas quando houver
um proxy conhecido, usando IPs, CIDRs ou aliases locais separados por vírgula, por exemplo
TRUST_PROXY="loopback,10.0.0.0/8". Valores universais são recusados. Ambientes com múltiplas
instâncias ainda precisam de armazenamento compartilhado para os contadores.
| Variável | Uso |
|---|---|
VITE_API_URL |
URL da API, padrão http://localhost:3333 |
VITE_DEMO_MODE |
exibe os atalhos de demonstração |
VITE_DEMO_USER_EMAIL |
e-mail público preenchido pelo atalho |
VITE_DEMO_USER_PASSWORD |
senha pública preenchida pelo atalho |
VITE_DEMO_ADMIN_EMAIL |
e-mail público preenchido pelo atalho |
VITE_DEMO_ADMIN_PASSWORD |
senha pública preenchida pelo atalho |
Variáveis VITE_* fazem parte do bundle do navegador e nunca devem conter segredos reais.
O seed cria:
Cliente Demo;Administrador Demo;- uma proposta aguardando revisão;
- um empréstimo ativo;
- seis parcelas pendentes.
No .env do backend:
DEMO_MODE=true
DEMO_USER_EMAIL=cliente@loanflow.demo
DEMO_USER_PASSWORD=ClienteDemo123
DEMO_ADMIN_EMAIL=admin@loanflow.demo
DEMO_ADMIN_PASSWORD=AdminDemo123No web/.env:
VITE_DEMO_MODE=true
VITE_DEMO_USER_EMAIL=cliente@loanflow.demo
VITE_DEMO_USER_PASSWORD=ClienteDemo123
VITE_DEMO_ADMIN_EMAIL=admin@loanflow.demo
VITE_DEMO_ADMIN_PASSWORD=AdminDemo123Depois que o schema existir, mantenha a confirmação restrita ao comando:
ALLOW_DEMO_SEED=true npm run db:seedO seed inteiro roda em uma única transação e sempre requer ALLOW_DEMO_SEED=true. Fora do alvo
Compose exato, ele também recusa as senhas públicas deste exemplo; use credenciais únicas e um
banco dedicado. Uma falha reverte contas, propostas e parcelas criadas pela tentativa.
Credenciais:
Cliente
cliente@loanflow.demo
ClienteDemo123
Administrador
admin@loanflow.demo
AdminDemo123
O seed é idempotente: pode ser repetido sem duplicar as contas, empréstimos ou parcelas fixas. Decisões e pagamentos já realizados na demonstração são preservados. Os CPFs fixos ficam vinculados à identidade demo original; se nome ou e-mail não coincidirem, o seed aborta toda a transação em vez de sobrescrever a conta existente.
Não habilite o modo demo em ambientes com dados reais.
| Comando | Descrição |
|---|---|
npm run dev |
inicia a API com reload |
npm run build |
compila o backend |
npm start |
executa o backend compilado |
npm test |
executa os testes automatizados do backend |
npm run db:up |
inicia o PostgreSQL local |
npm run db:down |
encerra o Compose local |
npm run db:migrate -- --name nome |
cria e aplica uma migration de desenvolvimento |
npm run db:deploy |
aplica migrations pendentes sem apagar dados |
npm run db:adopt |
adota deliberadamente um schema legado ou baseline-only |
npm run db:create |
alias compatível de db:deploy |
npm run db:status |
mostra o estado das migrations |
npm run db:validate |
valida o Prisma Schema |
npm run db:reset |
reset destrutivo protegido, somente para desenvolvimento |
npm run db:seed |
provisiona os dados demonstrativos |
npm run lint |
executa o ESLint no backend, frontend e Cypress |
npm run format |
formata os arquivos suportados com Prettier |
npm run format:check |
verifica a formatação sem alterar arquivos |
npm run test:integration |
testa o ciclo completo em um PostgreSQL descartável |
npm run test:migrations |
testa upgrade, rollback e papel restrito em banco _test |
npm run test:e2e |
executa o Cypress headless |
npm run cy:open |
abre o Cypress interativo |
cd web
npm run dev
npm run build
npm run previewBackend:
npm test
npm run build
npm run db:validate
npm run lint
npm run format:checkFrontend:
npm --prefix web run buildInterface completa:
env -u ELECTRON_RUN_AS_NODE npm run test:e2eO script E2E reserva 127.0.0.1:5173 com porta estrita. Se ela estiver ocupada, a execução falha claramente em vez de testar outro servidor.
Os testes Cypress interceptam as respostas da API. Eles validam cadastro, autenticação, guards, responsividade, simulação, solicitação, pagamento e revisão administrativa de maneira determinística, mas não substituem testes de integração entre Fastify e PostgreSQL.
test:integration executa cadastro, login, simulação, solicitação idempotente, vencimento, pagamento idempotente, separação de papéis, listagem administrativa, aprovação, rejeição e bloqueio de autorrevisão contra um banco real.
O conteúdo do banco é truncado no início da execução. Por segurança, isso só é permitido quando o DATABASE_URL usa PostgreSQL, aponta para um host local conhecido, identifica um banco terminado em _test e recebe a confirmação exata ALLOW_INTEGRATION_DATABASE_RESET=true.
Exemplo com um banco descartável já criado:
DATABASE_URL=postgresql://user:password@localhost:5432/loanflow_test npm run db:deploy
DATABASE_URL=postgresql://user:password@localhost:5432/loanflow_test \
JWT_SECRET=loanflow-integration-only-secret-at-least-32-bytes \
ALLOW_INTEGRATION_DATABASE_RESET=true \
npm run test:integrationMantenha essa confirmação restrita ao comando do teste; não a defina globalmente em ambientes compartilhados.
O workflow .github/workflows/ci.yml é executado em pushes para main, pull requests direcionados a main e manualmente por workflow_dispatch.
Os jobs são independentes e executam em paralelo:
| Job | Verificações |
|---|---|
Backend |
instalação limpa, Prisma Schema, ESLint, Prettier, TypeScript e testes |
Frontend |
instalação limpa, build Vue e auditoria |
PostgreSQL integration |
deploy, upgrade legado, rollback, drift e ciclo real da API |
Cypress E2E |
aplicação Vite em porta estrita e cenários de interface |
O workflow usa Node.js 24, permissões somente de leitura, actions fixadas por commit SHA e cancelamento automático de execuções antigas da mesma branch ou pull request.
- SQL parametrizado;
- senhas armazenadas com bcrypt;
- limite global e limites específicos de login, cadastro, empréstimos e pagamentos por IP;
- CORS restrito a uma allowlist configurável e headers HTTP de segurança;
- JWT de uma hora com algoritmo, emissor, audiência e finalidade validados;
- papel e existência da conta revalidados no banco em cada requisição;
- cadastro público sempre persistido como
user; - separação explícita entre rotas de cliente e administrador;
- administradores impedidos de revisar propostas da própria conta;
- verificação de propriedade antes de expor empréstimos e parcelas;
- validação runtime de bodies, parâmetros e filtros;
- contrato OpenAPI gerado a partir dos schemas usados pelo Fastify;
- limites de valor e quantidade de parcelas;
- decisões administrativas atômicas;
- solicitações de empréstimo idempotentes por usuário e chave;
- pagamentos idempotentes por parcela;
- transações para operações financeiras compostas;
- distribuição de centavos para que parcelas somem exatamente o total;
- datas mensais civis ajustadas ao último dia válido de cada mês;
- parcelas vencidas derivadas como
overduenas consultas.
| Método | Rota | Descrição |
|---|---|---|
GET |
/ |
saúde da API |
POST |
/users |
cadastro de cliente |
POST |
/auth/login |
autenticação |
GET |
/me |
perfil autenticado |
POST |
/loans/simulate |
simulação e decisão |
POST |
/loans |
solicitação; exige Idempotency-Key UUID |
GET |
/loans/my |
empréstimos do cliente |
GET |
/loans/:id |
detalhe e parcelas |
POST |
/installments/:id/pay |
pagamento demonstrativo |
GET |
/admin/loans |
fila administrativa |
GET |
/admin/loans/:id |
detalhe administrativo |
PATCH |
/admin/loans/:id/approve |
aprovação manual |
PATCH |
/admin/loans/:id/reject |
rejeição manual |
| Rota | Acesso |
|---|---|
/login |
público |
/register |
público |
/app |
cliente autenticado |
/app/simulate |
cliente autenticado |
/app/loans |
cliente autenticado |
/app/loans/:id |
cliente autenticado |
/admin |
administrador |
/admin/loans |
administrador |
/admin/loans/:id |
administrador |
- score e renda são valores simulados informados no cadastro;
- pagamentos não movimentam dinheiro;
- não há integração com bureaus de crédito, antifraude ou instituição financeira;
- JWT não possui refresh token ou blacklist central, embora conta e papel sejam revalidados;
- o frontend guarda a sessão no armazenamento local;
- o Cypress usa contratos HTTP simulados;
- migrations de produção são aplicadas de forma incremental com
prisma migrate deploy; - o reset do banco continua sendo uma ferramenta exclusiva de desenvolvimento.
Antes de uso real seriam necessários, entre outros pontos, gestão de segredos, cookies seguros ou estratégia equivalente, refresh/revogação central de sessão, armazenamento distribuído para rate limiting, auditoria, observabilidade, ensaio de migrations com snapshots reais, LGPD, integração financeira e testes de carga e integração ponta a ponta.
Já existe um processo usando a porta da API:
lsof -nP -iTCP:3333 -sTCP:LISTENEncerre a instância anterior ou execute:
PORT=3334 npm run devNesse caso, atualize VITE_API_URL no web/.env.
Se o terminal herdou ELECTRON_RUN_AS_NODE=1:
env -u ELECTRON_RUN_AS_NODE npm run test:e2eEvite diretórios sincronizados com nuvem. Use, por exemplo, C:\Dev\LoanFlow.
As decisões de interface, responsividade e acessibilidade estão em docs/frontend-redesign.md.
MVP educacional em desenvolvimento, voltado à demonstração de arquitetura backend, regras de negócio, autenticação e experiência Vue.
