Skip to main content

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

The Tattoos by Sandor website

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

  1. 1Customer selects date & time slot
  2. 2Uploads reference images (Cloudinary)
  3. 3Pays the booking deposit via Stripe Checkout
  4. 4Webhook confirms → booking auto-confirmed
  5. 5Email confirmation sent to customer & admin
🚶

Walk-In (Flexible Timing)

  1. 1Admin opens walk-in form on dashboard
  2. 2Sets exact time in 5-minute increments
  3. 3Enters custom price for the session
  4. 4Selects Cash or Contactless payment
  5. 5Booking instantly confirmed, no deposit needed
📱

Admin Booking + Tap to Pay

  1. 1Admin creates manual booking for customer
  2. 2System redirects to Tap to Pay page
  3. 3Customer taps card / Apple Pay / Google Pay
  4. 4Card terminal captures payment in real time
  5. 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 Xero

Deposit invoice created in Xero on payment confirmation.

Walk-in (card payment)

Syncs to Xero

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