Platform

Architecture

Technical architecture — stack, multi-tenancy, data model, and infrastructure.

Overview

SalonOS is a single-instance, multi-tenant SaaS application built on Next.js (App Router), Supabase (Postgres), and deployed on Oracle Cloud Infrastructure (OCI). All tenants share one database; isolation is enforced by Row-Level Security (RLS) policies at the database layer — not by separate databases or schemas per tenant.

Technology Stack

Layer Technology Why
Web framework Next.js 16 (App Router) Server components, streaming, routing
UI library React 19 + TypeScript Component model, type safety
Styling Tailwind CSS 4 + shadcn/ui Design system, composable components
Database Supabase Postgres SQL, RLS, Auth, Storage, Realtime
Auth Supabase Auth JWT, RLS integration, email/OTP
Realtime Supabase Realtime Calendar and queue live sync
AI Anthropic Claude (Haiku, Sonnet, Opus) Multiple capabilities via AI gateway
Voice Twilio + Dograh Inbound call handling
Payments Razorpay (adapter) UPI, cards; India-first
Messaging WhatsApp Business API (adapter) Primary client channel in India
Accounting Zoho Books (adapter) Accounting sync
Email SendGrid Transactional and campaign emails
Mobile Expo (React Native) iOS and Android native apps
Testing Vitest (unit), Playwright (E2E) Test pyramid
CI/CD GitHub Actions Lint, test, deploy pipeline
Hosting OCI VM / nginx + PM2 (web), Supabase Cloud (data) Self-managed VM, scalable

Three-Plane Structure

Control Plane (/super-admin, /(control))

Platform operators manage tenants, subscriptions, and support here. This plane is not accessible to tenant users.

Tenant Plane (/(tenant))

The operator dashboard. All features from scheduling through analytics live here. The tenant plane is the bulk of the codebase (117+ routes).

Client Plane (/(client), /(embed))

Client-facing surfaces: booking widget, portal, check-in, intake, consent, feedback. These are lightly authenticated (client session or magic link) and white-labelled per brand.

Multi-Tenancy

Hierarchy

org → brand → location → staff

Every data entity carries a combination of org_id, brand_id, and/or location_id. The appropriate foreign keys are set at the boundary of what that entity belongs to:

  • Clients are brand-scoped (brand_id)
  • Appointments are location-scoped (location_id)
  • Services are brand-scoped (brand_id), with optional location overrides

Row-Level Security (RLS)

Every table has RLS enabled. Policies enforce:

  • A user can only read rows where the org_id matches their JWT claim
  • Brand-scoped data: also checks brand_id
  • Location-scoped data: also checks location_id

The auth.uid() and auth.jwt() functions are used in policies to extract the user's identity and claims. There is no application-layer tenant filter that a bug could bypass — the database enforces isolation unconditionally.

Server Actions

All mutations in the tenant plane run as Server Actions under the user's authenticated identity. This means the user's RLS claims apply to every write — there is no service-role bypass for normal operations.

Service Role Usage

The Supabase service_role key (which bypasses RLS) is used only for:

  • Background cron jobs (reminders, journeys, birthday sends)
  • Data imports
  • Platform-level admin operations in the control plane

Service-role code is isolated, audited, and never exposed to the tenant plane.

White-Label Theming

Brand resolution happens at request time:

  1. The request URL (custom domain or brand slug) is matched to a brand record
  2. The brand's theme tokens (primary colour, secondary colour, accent, logo URL) are loaded
  3. CSS custom properties are set on the response: --brand-primary, --brand-secondary, etc.
  4. All components use these variables — no component hard-codes a colour

This means the same codebase serves every brand with its own visual identity with zero per-brand code.

Realtime Architecture

Supabase Realtime is used selectively for surfaces where live updates genuinely matter:

  • Calendar: appointment changes from any connected device appear within 1 second
  • Queue board: walk-in additions and assignment updates are live

Realtime subscriptions are scoped to location_id — a user never receives another location's events.

Background Jobs

Jobs that run asynchronously (reminders, journeys, review requests, birthdays):

  • Scheduled via cron (GitHub Actions schedule or Supabase Edge Functions cron)
  • Run as service-role operations with explicit scope
  • Idempotent: running the same job twice produces the same outcome
  • Logged: every job run, batch size, and outcome is recorded

AI Architecture

The AI gateway sits between the product features and the Anthropic API:

Product feature (booking agent, style advisor, etc.)
         ↓
    AI Gateway
    - Model selection (Haiku / Sonnet / Opus)
    - Prompt with tenant context
    - Tool definitions
    - Guardrails (validate output)
    - Logging (tokens, cost, latency)
         ↓
  Anthropic Claude API

No feature calls the Anthropic API directly. All calls go through the gateway, which ensures correct scoping, logging, and cost tracking per tenant.

Data Model — Key Entities

Domain Core entities
Tenancy Org, Brand, Location, User, RoleAssignment
Catalog ServiceCategory, Service, ServiceVariant, AddOn, Resource
People StaffProfile, Skill, Shift, TimeOff, Client, ClientNote
Scheduling Appointment, AppointmentService, QueueEntry, CalendarBlock
Commerce Invoice, InvoiceLineItem, Payment, Deposit, Refund
Cash CashDrawer, CashMovement, ShiftClose
Retention Membership, Package, LoyaltyAccount, GiftCard, WalletEntry
Inventory Product, StockItem, StockMovement, PurchaseOrder
Staff Ops CommissionRule, CommissionLedger, AttendanceRecord
Finance Expense, VendorBill, BankSettlement, ReconciliationMatch
Marketing Segment, Campaign, Journey, JourneyRun, Offer
Platform Plan, Subscription, AuditLog, ImportJob

Security Posture

  • RLS: every table, every read and write
  • HTTPS only: enforced by nginx (Let's Encrypt) and Supabase
  • Secrets: environment variables only; never in code or logs
  • Input validation: Zod schemas on all Server Action inputs
  • Webhook verification: HMAC signature checks on all inbound webhooks (payment, WhatsApp)
  • Audit log: every state-changing action records who, what, and when
  • PII handling: personal data in analytics views is anonymised; AI prompts redact sensitive fields

Development and Deployment

Environments

Environment Purpose
Local Developer laptop; local Supabase stack
Preview Per-PR branch; tested locally or against staging
Staging Pre-production; mirrors production data (anonymised)
Production Live; OCI VM (nginx + PM2) + Supabase Cloud

CI Pipeline (GitHub Actions)

  1. Lint (eslint)
  2. Type check (tsc --noEmit)
  3. Unit tests (vitest)
  4. E2E tests (playwright — on preview environment)
  5. Build (next build)
  6. Deploy to OCI VM via SSH + PM2

Database Migrations

Migrations live in supabase/migrations/. They are applied in sequence:

  • supabase db push applies new migrations locally
  • CI applies migrations to preview; production migrations are applied manually during deployments with a review step

70+ migrations cover the full feature set from Phase 0 (scaffolding) through Phase 21 (AI style advisor).