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 |
| 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_idmatches 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:
- The request URL (custom domain or brand slug) is matched to a brand record
- The brand's theme tokens (primary colour, secondary colour, accent, logo URL) are loaded
- CSS custom properties are set on the response:
--brand-primary,--brand-secondary, etc. - 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)
- Lint (
eslint) - Type check (
tsc --noEmit) - Unit tests (
vitest) - E2E tests (
playwright— on preview environment) - Build (
next build) - Deploy to OCI VM via SSH + PM2
Database Migrations
Migrations live in supabase/migrations/. They are applied in sequence:
supabase db pushapplies 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).