Studio Management Case Study
Tattoos by Sandor
A production-grade booking and studio management system, online deposits, Tap to Pay terminals, automated Xero integration, and a full admin dashboard. More engineering than most people expect from a tattoo studio website.
Overview
Not just a website. A full studio operating system.
On the surface, Tattoos by Sandor looks like a clean studio website. Under the hood it's a complete booking management platform, handling online reservations with deposits, walk-in sessions with flexible timing, contactless tap-to-pay collection, and automatic invoice generation in Xero.
The admin dashboard gives the artist full control over every booking from first inquiry to final payment, with built-in Xero sync and enterprise-grade security including TOTP two-factor authentication.
3
Booking types
32+
API endpoints
2
Stripe integrations
6
Security layers

Booking Workflows
Three ways to book. One system.
Each booking type handles a different real-world scenario, with automatic conflict prevention at the database layer.
Online Booking + Deposit
- 1Customer selects date & time slot
- 2Uploads reference images (Cloudinary)
- 3Pays the booking deposit via Stripe Checkout
- 4Webhook confirms → booking auto-confirmed
- 5Email confirmation sent to customer & admin
Walk-In (Flexible Timing)
- 1Admin opens walk-in form on dashboard
- 2Sets exact time in 5-minute increments
- 3Enters custom price for the session
- 4Selects Cash or Contactless payment
- 5Booking instantly confirmed, no deposit needed
Admin Booking + Tap to Pay
- 1Admin creates manual booking for customer
- 2System redirects to Tap to Pay page
- 3Customer taps card / Apple Pay / Google Pay
- 4Card terminal captures payment in real time
- 5Booking auto-confirmed on payment success
Admin Dashboard
Full control from one screen
Booking Management
Filter by status (Pending / Confirmed / Cancelled). Expand any booking to view customer details, reference images, deposit status, and final payment info. Edit, confirm, cancel, or upgrade bookings to full-day sessions.
Final Payment Recording
After each session, the artist enters the final price. The system calculates the balance owed (total minus deposit), records the payment method (Cash or Contactless), and timestamps the completion.
Finance Dashboard
Finance view built on the Xero integration, synced invoices and reporting, plus an in-app summary of bookings and payments by period.
Blocked Dates
Block entire days or specific 3-hour slots for holidays, conventions, or personal time. Blocked dates are hidden from the customer booking calendar in real time.
Xero Sync
Sync individual bookings or bulk-sync all confirmed card payment sessions to Xero with one click. Each invoice is automatically categorised by type.
Security & 2FA
JWT-protected admin with optional TOTP two-factor authentication (Google Authenticator). Token revocation table prevents reuse of logged-out JWTs. 6-layer CSV import security for bulk data.
Payments
Two payment modes, not one
Most booking systems offer one payment mode and call it done. This project uses two distinct integrations to cover every scenario the studio encounters.
Online Checkout, Deposits
Customers pay the booking deposit online. Stripe sends a webhook on success which auto-confirms the booking and triggers the confirmation email.
Tap to Pay Terminal
For in-studio payments, the admin redirects to a Tap to Pay page pre-filled with the booking and amount. The customer taps their card, Apple Pay, or Google Pay directly on the artist's phone. No card reader hardware required.
Accounting
Xero integration
Bookings paid by card sync to Xero as invoices via OAuth 2.0, with the appropriate tax treatment applied automatically.
Regular deposit
Syncs to XeroDeposit invoice created in Xero on payment confirmation.
Walk-in (card payment)
Syncs to XeroFull session invoice created in Xero with the appropriate tax treatment.
Selling Beyond The Chair
Vouchers, discounts and a loyalty card
Three ways money reaches the studio that are not simply a session being paid for, and each one is a rule about money, so none of them is a one-liner buried in a handler.
Gift vouchers
Codes come from a Crockford Base32 alphabet with I, L, O and U removed, the first three because they are indistinguishable from 1 and 0 in most typefaces, and U because these get read out down the phone. Exactly 32 characters also makes the draw unbiased without rejection sampling, which is worth doing by construction in a code that stands for money.
Discount codes
Percentage or fixed amount, limited to a half day, a full day or any session, with an optional window that applies to the date of the SESSION rather than the date of the booking, so a January code still applies to a January slot booked in October. Total redemptions and per-customer limits are set per code from the dashboard.
The loyalty card
The card itself is physical, deliberately, nothing here tracks stamps, and adding that was a decision left unmade rather than assumed. What the system does is the ten seconds at the counter: work out the reduction, and record WHY a lower price was taken. A price with no explanation is the same problem in the books as in the code; six months later nobody can tell a loyalty discount from a mistake.
Tech Stack
Production-grade from day one
Frontend
- Next.js 14
- TypeScript
- Tailwind CSS
- React 18
- react-icons
Backend
- Next.js API Routes
- Raw SQL (mysql2)
- JWT auth (jsonwebtoken)
- bcryptjs
- TOTP 2FA (otplib)
Database
- MySQL 8.0
- Pessimistic locking
- Unique slot constraints
- Atomic transactions
Payments
- Stripe Checkout (deposits)
- Stripe Terminal (Tap to Pay)
- Stripe Webhooks
- Live keys in production
Integrations
- Xero OAuth 2.0
- Cloudinary CDN (images)
- Nodemailer SMTP (email)
- Redis (rate limiting)
- Sentry (error tracking)
Engineering Highlights
The details that matter
Pessimistic locking prevents double-booking
The database has a UNIQUE constraint on (booking_date, slot_start). On top of that, slot availability is checked inside a SELECT ... FOR UPDATE transaction, so two simultaneous bookings for the same slot can never both succeed, even under race conditions.
Full-day bookings as two atomic rows
A 6-hour session is stored as two linked 3-hour slot records, created in a single transaction. If either insert fails, both roll back. The client identifies them with a FULL: prefix; the DB links them by matching customer details.
Walk-in slots bypass the 3-hour constraint
Walk-ins use flexible 5-minute timing (e.g., 14:35) which would conflict with the standard slot unique key. Walk-in bookings have the booking_type column in the unique constraint, so they can coexist with regular bookings on the same day without conflict.
JWT revocation without a session store
Serverless functions are stateless, but logging out still needs to invalidate the token. Each JWT carries a unique identifier. On logout, that identifier is stored server-side and every protected route validates against it before allowing access.
OAuth token refresh in a stateless environment
Xero's OAuth tokens expire frequently. Since Vercel functions have no persistent memory, tokens are securely persisted server-side. Each API call loads, uses, and if needed refreshes the token, then saves the updated credentials back.
Money rules are pure functions
Whether a voucher may pay for a booking, and what a discount code takes off, are decided in functions with no database, payment SDK or email transport in scope. That is not tidiness. The booking endpoint imports all three and none of them load under the test runner, so a money rule written inline there cannot be tested at all, which is how an earlier deposit-calculation defect survived to production and was caught by a real payment rather than by a test.
Content Security Policy with SHA-256 script hashes
A strict CSP blocks all inline scripts except those with precomputed SHA-256 hashes. This prevents XSS even if an attacker injects content into the page. Google Analytics and Stripe are whitelisted by domain; all other sources are blocked.
Need a booking system that actually works?
I build booking platforms with real payment processing, accounting integration and admin tools, not an off-the-shelf plugin bolted onto a template. If that is the shape of your problem, tell me about it.
