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