Bernardo Knoblauch

VamosMarcar

SaaS multi-tenant de reservas para barberías y salones. API en Node.js, Express y Prisma; web en Next.js.

Producto propio · SaaS · Node.js

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

Imágenes

Contexto

VamosMarcar es mi SaaS de reservas online para barberías y salones de belleza. Cada negocio tiene una página de enlaces, pensada para la bio de Instagram, y un flujo de reserva en /{slug}/agendar. El dueño gestiona todo desde un panel; yo opero la plataforma desde un panel de administración aparte.

Lo desarrollé solo, de abril a agosto de 2026: 111 commits en un monorepo pnpm con la API, la web y paquetes de contratos compartidos.

Problema

En este público, las reservas se hacen por mensajes de WhatsApp, sin vista de agenda, sin historial de clientes y sin control de ausencias.

Un sistema para este mercado tiene que costar casi nada por cliente, funcionar bien en el móvil y no pedirle al cliente final que cree una cuenta.

Qué hice y por qué

Arquitectura

  • API en Node.js 20, Express y TypeScript, organizada por dominio: 11 módulos (auth, reservas, disponibilidad, servicios, equipo, métricas…), cada uno con controller, service y repository, y 55 rutas bajo /api/v1.
  • PostgreSQL con Prisma: 8 modelos y 12 migraciones. Multi-tenant por businessId en cada fila de tenant, con invariantes verificadas en la aplicación y entrada validada con Zod.
  • Web en Next.js 15 y React 19. El navegador solo habla con Next (un BFF en Route Handlers), que llama a la API: sin CORS, y la página pública de reserva se renderiza en el servidor con caché.
  • Contratos compartidos entre API y web en los paquetes @sweeney/types y @sweeney/api-client.

Reservas

  • Modelo pedir y luego confirmar: la solicitud entra como pending, el dueño la confirma o la cancela y después la marca como completada o no-show. Pending y confirmed bloquean el horario.
  • Horarios libres calculados con Luxon en la zona horaria del negocio: franjas semanales, reservas existentes, bloqueos de día completo o de franja horaria y un corte global para nuevos horarios. Una reserva admite varios servicios, sumando sus duraciones.
  • Comprobación de solapamiento e inserción de la reserva en la misma transacción.
  • Cancelación por el cliente con el enlace recibido y el mismo teléfono de la reserva, dentro de una ventana configurable antes del horario.

Seguridad y operación

  • JWT en cookie con el instante del último cambio de contraseña (pwdAt): cambiar la contraseña invalida las sesiones anteriores. Cookies y rutas separadas para el admin de la plataforma.
  • Rate limit propio en las rutas de autenticación, sin confiar en X-Forwarded-For cuando el proxy no es de confianza.
  • CI en GitHub Actions y Bitbucket Pipelines con typecheck, lint y build. Deploy preparado para Railway, con Cloudflare en el borde (DNS, SSL, WAF) y Cloudflare Images para las fotos de los negocios.

Por qué WhatsApp es manual

Los mensajes salen por enlaces wa.me que envía el dueño o el cliente, sin Twilio ni WhatsApp Business API. Comparé los canales por costo fijo, costo por mensaje y esfuerzo de operación: para el piloto, wa.me usa el canal que este público ya tiene, con costo cero por mensaje. El SMS vía Twilio existe en el código y queda desactivado mientras no haya credenciales.

Resultado

  • En línea en vamosmarcar.com.
  • Panel con agenda por día, semana y mes, clientes agrupados por teléfono, métricas por período con exportación a CSV y equipo con roles de dueño y empleado.
  • Costo por mensaje enviado: cero.

Negocios activos y reservas: [PREENCHER]

Próximos pasos

  • Tests automatizados. Hoy la red de seguridad del CI es typecheck, lint y build.
  • Aviso automático al dueño cuando entra una solicitud nueva.

Contacto