Bernardo Knoblauch

VamosMarcar

SaaS multi-tenant de agendamento para barbearias e salões. API em Node.js, Express e Prisma; web em Next.js.

Produto próprio · SaaS · Node.js

Período
2026–
Stack
Node.js, Express, TypeScript, Prisma, PostgreSQL, Zod, Next.js, React, Luxon, Cloudflare

Imagens

Contexto

O VamosMarcar é o meu SaaS de agendamento online para barbearias e salões. Cada negócio tem uma página de links, pensada para a bio do Instagram, e um fluxo de agendamento em /{slug}/agendar. O dono gere tudo num painel; eu opero a plataforma num painel de administração separado.

Desenvolvi sozinho, de abril a agosto de 2026: 111 commits num monorepo pnpm com a API, a web e pacotes de contratos compartilhados.

Problema

Nesse público, o agendamento acontece por mensagem no WhatsApp, sem visão de agenda, sem histórico de clientes e sem controle de faltas.

Um sistema para esse mercado precisa custar quase nada por cliente, funcionar bem no celular e não pedir que o cliente final crie conta.

O que fiz e por quê

Arquitetura

  • API em Node.js 20, Express e TypeScript, organizada por domínio: 11 módulos (auth, agendamentos, horários, serviços, equipe, métricas…), cada um com controller, service e repository, e 55 rotas em /api/v1.
  • PostgreSQL com Prisma: 8 modelos e 12 migrações. Multi-tenant por businessId em toda linha de tenant, com invariantes checadas na aplicação e entrada validada com Zod.
  • Web em Next.js 15 e React 19. O browser fala só com o Next (BFF em Route Handlers), que chama a API: sem CORS, e a página pública de agendamento é renderizada no servidor com cache.
  • Contratos compartilhados entre API e web nos pacotes @sweeney/types e @sweeney/api-client.

Agendamento

  • Modelo pedir-depois-confirmar: o pedido entra como pending, o dono confirma ou cancela e depois marca concluído ou no-show. Pending e confirmed bloqueiam o horário.
  • Horários livres calculados com Luxon no fuso do negócio: janelas semanais, agendamentos existentes, bloqueios de dia inteiro ou de faixa horária e corte global de novos horários. Um agendamento aceita vários serviços, somando as durações.
  • Checagem de sobreposição e gravação do agendamento na mesma transação.
  • Cancelamento pelo cliente com o link recebido e o mesmo telefone da marcação, respeitando uma janela configurável antes do horário.

Segurança e operação

  • JWT em cookie com o instante da última troca de senha (pwdAt): trocar a senha invalida as sessões antigas. Cookies e rotas separados para o admin da plataforma.
  • Rate limit próprio nas rotas de autenticação, sem confiar em X-Forwarded-For quando o proxy não é confiável.
  • CI no GitHub Actions e no Bitbucket Pipelines com typecheck, lint e build. Deploy preparado para Railway, com Cloudflare na borda (DNS, SSL, WAF) e Cloudflare Images para as fotos dos negócios.

Por que o WhatsApp é manual

As mensagens saem por links wa.me, que o dono ou o cliente enviam, sem Twilio nem WhatsApp Business API. Comparei os canais por custo fixo, custo por mensagem e esforço de operação: para o piloto, o wa.me usa o canal que o público já tem, com custo zero por mensagem. O SMS via Twilio existe no código e fica desligado enquanto não há credenciais.

Resultado

  • No ar em vamosmarcar.com.
  • Painel com agenda em dia, semana e mês, clientes agrupados por telefone, métricas por período com exportação em CSV e equipe com papéis de dono e funcionário.
  • Custo por mensagem enviada: zero.

Negócios ativos e agendamentos: [PREENCHER]

Próximos passos

  • Testes automatizados. Hoje a garantia no CI é typecheck, lint e build.
  • Aviso automático ao dono quando entra um pedido novo.

Contato