VamosMarcar
Multi-tenant booking SaaS for barbershops and salons. API in Node.js, Express and Prisma; web app in Next.js.
Own product · SaaS · Node.js
Images
Context
VamosMarcar is my online booking SaaS for barbershops and salons. Each business gets a link page, meant for the Instagram bio, and a booking flow at /{slug}/agendar. Owners run everything from a dashboard; I operate the platform from a separate admin panel.
I built it alone, from April to August 2026: 111 commits in a pnpm monorepo with the API, the web app and shared contract packages.
Problem
For this audience, booking happens over WhatsApp messages, with no calendar view, no customer history and no tracking of no-shows.
A system for this market has to cost almost nothing per customer, work well on a phone and not ask the end customer to create an account.
What I did and why
Architecture
- Node.js 20, Express and TypeScript API organized by domain: 11 modules (auth, appointments, availability, services, team, metrics…), each with a controller, service and repository, and 55 routes under /api/v1.
- PostgreSQL with Prisma: 8 models and 12 migrations. Multi-tenant by businessId on every tenant row, with invariants enforced in the application and input validated with Zod.
- Web app in Next.js 15 and React 19. The browser only talks to Next (a BFF in Route Handlers), which calls the API: no CORS, and the public booking page is server-rendered with caching.
- Contracts shared between API and web in the @sweeney/types and @sweeney/api-client packages.
Booking
- Request-then-confirm model: a request comes in as pending, the owner confirms or cancels it, then marks it completed or no-show. Pending and confirmed both block the slot.
- Free slots computed with Luxon in the business time zone: weekly windows, existing appointments, full-day or time-range blocks and a global cutoff for new slots. One booking can include several services, adding up their durations.
- Overlap check and appointment insert run in the same transaction.
- Customers cancel with the link they received and the same phone number used to book, within a configurable window before the appointment.
Security and operations
- JWT in a cookie carrying the time of the last password change (pwdAt): changing the password invalidates old sessions. Separate cookies and routes for the platform admin.
- Custom rate limiting on auth routes, without trusting X-Forwarded-For when the proxy is not trusted.
- CI on GitHub Actions and Bitbucket Pipelines running typecheck, lint and build. Deployment set up for Railway, with Cloudflare at the edge (DNS, SSL, WAF) and Cloudflare Images for business photos.
Why WhatsApp is manual
Messages go out through wa.me links that the owner or the customer sends, with no Twilio and no WhatsApp Business API. I compared channels by fixed cost, cost per message and operational effort: for the pilot, wa.me uses the channel this audience already has, at zero cost per message. SMS through Twilio exists in the code and stays off until credentials are set.
Outcome
- Live at vamosmarcar.com.
- Dashboard with day, week and month calendar views, customers grouped by phone number, metrics by period with CSV export, and a team with owner and staff roles.
- Cost per message sent: zero.
Active businesses and bookings: [PREENCHER]
Next steps
- Automated tests. Today the CI safety net is typecheck, lint and build.
- Automatic alert to the owner when a new request comes in.