Skip to content
GuideClaude CodeNext.jsPostgres

Build a Zenoti Clone

A complete implementation prompt for a full spa and salon management platform: appointment book, POS, card-present payments, guest self-service booking, staff portal, two-way SMS, an AI assistant, and a migration path off Zenoti.

This spec was reverse-engineered from a spa management platform running in production, then de-identified. It's written to be handed to a coding agent as a single, self-contained prompt — or worked through by hand, part by part.

I wrote this after several years of running a spa on Zenoti and eventually moving off it. The reasoning behind that decision — and the pricing that drove it — is in the post-mortem. This page is the constructive half: what you'd actually have to build to replace it.

The four rules that shape everything

Most of the spec is detail. These four decisions are the ones that determine whether the detail holds together.

Degrade, never crash

Every module has to work with no database, no SMS provider, no payment key, and no AI key. Missing configuration produces a friendly disabled state — never an exception. This is what makes the thing developable by one person.

The server owns money and time

The client is trusted for choices — which service, which slot. Never for values. Prices, durations, and totals are always recomputed server-side from the database. A client-supplied amount is never authoritative.

Idempotency over transactions

The reference runs on a serverless Postgres HTTP driver with no transaction support, so it uses idempotency keys, WHERE-guarded conditional updates, and reconciliation jobs instead of BEGIN/COMMIT. Worth designing this way even when your driver does support transactions — it makes webhook and retry handling correct by construction.

One source of truth for captured money

The payment processor's webhook, and nothing else. Devices may report progress, but a device must never be able to declare a payment successful.

The non-negotiable one: everything lives in Settings

Every configurable value has to be editable by the business owner in-app. Nothing operational may live only in an environment variable or a hardcoded constant — not the business name, not the tax rate, not the tip presets, not the reminder times, not the AI model.

This sounds like a nice-to-have. It isn't. It's the single difference between software the owner operates and software the owner files a ticket against — which is the entire failure mode that made the incumbent expensive in the first place. The acceptance test is blunt: a non-technical owner can change the business name, address, timezone, hours, tax, tip presets, gift-card pricing, reminder times, alert numbers, AI model, and reader branding with no redeploy and no rebuild. Build this early — milestone two, before the features that read it.

Build order

Twelve milestones. The order matters — each one assumes the last.

Foundation

Stack, conventions, the full schema, seed data, and deploy-time migrations. Establish these first — they're load-bearing for everything after.

Settings

The entire settings system, with typed accessors everything downstream reads from. Build it before the features that depend on it, not after.

Catalog & staff

Services, categories, add-ons, rooms, equipment, employees, capabilities, commissions, weekly hours, and per-date overrides.

Appointment book

Geometry, three views, booking panel, popover, move/resize/cut/paste/copy, block-out, notes, waitlist, and live sync. The hardest UI in the system.

Billing & POS

Invoices, numbering, payments across every tender, gift-card sale and redemption, splits, refunds, and PIN-gated financial reporting.

Terminal

Card-present sessions, presence tracking, a watchdog, reconciliation, diagnostics, and tablet apps that fetch their config from the server.

Communications

A dispatcher, two providers, unified inbound with routing, threading, media, templates, and reminders.

Guest surfaces

Booking wizard including couples bookings, OTP identity, card on file, account cabinet, SMS cancellation, gift cards, packages, and card-capture links.

Employee portal

Schedules, fee-aware payroll, and schedule-change requests approved over SMS.

AI assistant

Tools, confirmation gating on every write, multi-provider support, voice, and memories.

Migration

The sync subsystem — only if you're moving off an incumbent platform. This is where the subtle data-loss bugs live.

Hardening

The security model end to end, then the hardening checklist.

The migration is the dangerous part

If you're moving off an incumbent rather than starting clean, the sync subsystem is where you can actually lose money — not metaphorically. The spec covers it in full, but three rules are worth stating here because they're the ones that bite. For the export side specifically — the auth problem, every endpoint, and the bugs that produce duplicate guests and $0 invoices — see how to export your data out of Zenoti.

Never let a sync downgrade a paid invoice

Rows you created are yours and reconcile must never touch them. Before every invoice upsert, guard the open-downgrade: if the incoming status is open but a succeeded local payment exists, keep it paid. Then run a heal pass at the end of every sync, so a clobber from any path never survives a single run.

Lowercase the external ID

The source returns the same GUID in different cases across different endpoints, and Postgres unique keys are case-sensitive. Normalizing case on the way in is a one-line fix that on its own prevents duplicate guests.

An empty response is a failure, not an empty catalog

Reconcile deactivates rows the source no longer returns — so only ever reconcile when the fetched set is non-empty. Treating a failed API call as "the catalog is empty" will cheerfully deactivate your entire business.

Get the full prompt

Roughly 78,000 characters covering the data model, every settings section, all six surfaces, the server logic, the migration subsystem, the security model, and the acceptance criteria. Hand it to a coding agent whole, or work through it part by part.