Guides

Guide - Multi-Tenancy & Tenant Slug Resolution

Understanding multi-tenant architecture, data isolation, and tenant slug resolution strategies.

The Booki platform uses subdomain-based multi-tenancy to serve multiple independent organizations from a single API instance. Each organization (tenant) has a unique slug that determines its data scope.


What is Multi-Tenancy?

Multi-tenancy is an architecture where a single application serves multiple independent organizations, each believing they have a private instance but sharing underlying infrastructure. This provides:

Cost efficiency — Reduced infrastructure overhead
Scalability — Single deployment scales to many customers
Data isolation — Complete logical separation between tenants
Simplified management — One API for all tenants


Subdomain vs. Slug

Every organization has a slug — a URL-friendly identifier used in multiple places:

Slug: "booki-salon"

Subdomain URL: https://booki-salon.booki.app
API endpoint: https://api.booki.app
Tenant ID header: X-Tenant-Slug: booki-salon

Organization lookup: GET /api/tenant/booki-salon

Slug rules:

  • Lowercase letters, numbers, hyphens only
  • 3-50 characters long
  • Unique across platform
  • URL-safe

How Tenant Resolution Works

When a customer visits booki-salon.booki.app, browsers send requests to:

https://booki-salon.booki.app/profile

The API extracts the subdomain (booki-salon) from the request origin and automatically scopes all queries to that tenant.

Process:

  1. Browser makes request from subdomain
  2. API receives request headers including Host: booki-salon.booki.app
  3. Middleware extracts subdomain: booki-salon
  4. Database queries filtered to organizationId matching that slug
  5. Response contains only that tenant's data

Request example:

curl -X GET https://booki-salon.booki.app/api/bookings \
  --cookie "access_token=eyJhbGc..." \
  -H "Origin: https://booki-salon.booki.app"

Method 2: Header-Based (API/Backend Use)

For API clients (mobile apps, integration services), pass tenant slug in header:

curl -X GET https://api.booki.app/api/bookings \
  --cookie "access_token=eyJhbGc..." \
  -H "X-Tenant-Slug: booki-salon"

This tells the API: "Scope this request to the booki-salon organization."

Header format:

X-Tenant-Slug: booki-salon

Method 3: Query Parameter (Fallback)

For public endpoints (like tenant lookup), use query parameter:

curl -X GET "https://api.booki.app/api/tenant?slug=booki-salon"

Method 4: Route Parameter (Direct Org ID)

Some endpoints accept organizationId directly in the URL path. No slug lookup is needed — the middleware uses the ID as-is:

curl -X POST "https://api.booki.app/api/v1/bookings/organizations/507f191e810c19729de860ea/type/haircut" \
  -H "X-Branch-Slug: main-branch" \
  -H "Content-Type: application/json" \
  -d '{...}'

This is useful when a subdomain or tenant slug is unavailable (e.g., QR codes, direct links, or non-browser integrations).


Data Flow Diagram

┌─────────────────────────────────────────────┐
│         Client Request                      │
│  GET /api/bookings                          │
│  Header: X-Tenant-Slug: booki-salon        │
│  Cookie: access_token=...                  │
└──────────────┬──────────────────────────────┘
               │
               ↓
┌──────────────────────────────────────────────┐
│    API Tenant Middleware                     │
│  1. Extract tenant slug from:                │
│     - Subdomain (if provided)               │
│     - X-Tenant-Slug header                  │
│     - Query param (if allowed)              │
│  2. Resolve slug → organizationId           │
│  3. Attach to request context               │
└──────────────┬──────────────────────────────┘
               │
               ↓
┌──────────────────────────────────────────────┐
│    Authentication Middleware                │
│  1. Validate JWT token                      │
│  2. Verify user belongs to tenant           │
│  3. Populate req.user with profile          │
└──────────────┬──────────────────────────────┘
               │
               ↓
┌──────────────────────────────────────────────┐
│    Database Query (Scoped)                   │
│  Booking.find({                             │
│    organizationId: tenantContext.orgId,     │
│    customerId: req.user._id                 │
│  })                                         │
└──────────────┬──────────────────────────────┘
               │
               ↓
┌──────────────────────────────────────────────┐
│    Response (Tenant-Isolated)               │
│  Only bookings from booki-salon org         │
│  No other tenants' data leaked              │
└──────────────────────────────────────────────┘

Tenant Slug Resolution Endpoint

To resolve an organization from slug (public endpoint):

GET /api/tenant/:slug

curl -X GET "https://api.booki.app/api/tenant/booki-salon"

Response:

{
  "_id": "507f191e810c19729de860ea",
  "name": "Booki Salon",
  "slug": "booki-salon",
  "description": "Premium salon services",
  "email": "owner@bookisalon.com",
  "phone": "09161234567",
  "website": "https://bookisalon.com",
  "address": {
    "city": "Manila",
    "country": "Philippines"
  },
  "plan": "PREMIUM",
  "status": "ACTIVE"
}

Use cases:

  • Guest booking form needs to load org details
  • Mobile app needs public organization info
  • Tenant lookup for slug resolution

Multi-Tenancy in Practice

Public Endpoints (No Auth)

Guest users booking without login still need tenant context:

# Guest creates booking for booki-salon
curl -X POST https://api.booki.app/api/bookings/type/haircut \
  -H "X-Tenant-Slug: booki-salon" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "507f191e810c19729de860ea",
    "packageId": "507f1f77bcf86cd799439031",
    "bookingDate": "2026-04-10",
    "bookingTime": "14:30",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "phone": "09161234567"
  }'

The API:

  1. Extracts tenant slug: booki-salon
  2. Verifies booking request is for that organization
  3. Creates booking scoped to booki-salon

Authenticated Endpoints

Logged-in users automatically get their tenant context from token:

# Owner viewing their bookings (tenant auto-detected from JWT)
curl -X GET https://api.booki.app/api/admin/bookings/calendar?date=2026-04-01 \
  --cookie "access_token=OWNER_JWT_TOKEN" \
  -H "X-Tenant-Slug: booki-salon"

The API:

  1. Validates JWT token
  2. Reads user's organization from token
  3. Verifies JWT org matches header slug (security check)
  4. Returns only that organization's bookings

Data Isolation Guarantees

Every database query includes tenant filter:

// CORRECT: Tenant-scoped query
const bookings = await Booking.find({
  organizationId: tenantContext.orgId, // Auto-populated
  customerId: userId,
});

// WRONG: Missing tenant filter (would leak data)
const bookings = await Booking.find({
  customerId: userId,
  // ❌ No organizationId check = could see other tenants' data
});

Security enforcement:

  • Middleware validates tenant on every request
  • All models include organizationId unique index
  • No query bypasses tenant check
  • Deleted organizations cannot be accessed

Common Tenant Issues & Troubleshooting

ErrorCauseSolution
Tenant not foundInvalid or missing slugVerify slug exists; check spelling; use GET /api/tenant/:slug first
Unauthorized - tenant mismatchJWT token from different orgEnsure logged-in user matches requested tenant
No X-Tenant-Slug headerMissing required header for API callAdd -H "X-Tenant-Slug: booki-salon" to request
organizationId not foundSubmitted ID doesn't match tenantVerify organization ID belongs to intended tenant
403 Permission DeniedUser not member of tenantCheck user's organizationId in profile

Debug Checklist

  1. Do I have the correct slug?
    curl -X GET https://api.booki.app/api/tenant/YOUR-SLUG
    

    Should return 200 with org details.
  2. Is my token valid?
    curl -X GET https://api.booki.app/api/auth/user \
      --cookie "access_token=YOUR_TOKEN" \
      -H "X-Tenant-Slug: YOUR-SLUG"
    

    Should return your user profile.
  3. Are tenant and token aligned?
    • Extract organizationId from JWT (payload)
    • Compare with slug via GET /api/tenant/:slug response
    • Both must match
  4. Is the resource scoped correctly?
    • Try fetching with wrong tenant header
    • Should get 404 or 403, not 200

Tenant Contexts: Frontend vs Backend

Frontend (Web App)

// www.booki-salon.booki.app
// Subdomain auto-detected by browser

fetch("https://booki-salon.booki.app/api/bookings", {
  credentials: "include", // Include HttpOnly cookies
});
// Tenant: auto from subdomain "booki-salon"

Backend (API Service, Mobile App)

// Must explicitly pass tenant
const response = await fetch("https://api.booki.app/api/bookings", {
  headers: {
    "X-Tenant-Slug": "booki-salon", // ← Required
    Authorization: `Bearer ${token}`,
  },
});

Advanced: Cross-Tenant Operations

Important: The Booki API does NOT support cross-tenant queries.

# ❌ INVALID: Cannot query bookings from multiple tenants in one request
curl -X POST https://api.booki.app/api/bookings/multi-search \
  -H "Content-Type: application/json" \
  -d '{
    "tenants": ["booki-salon", "fitness-center"],
    "date": "2026-04-10"
  }'
# Would return 403: Cross-tenant access denied

If you need data from multiple tenants:

  1. Make separate API calls for each tenant
  2. Merge results in your application
  3. Each call must have correct X-Tenant-Slug header

Summary

Key Points:

  • ✅ Every request is automatically scoped to a tenant
  • ✅ Tenant determined by subdomain (web) or X-Tenant-Slug header (API)
  • ✅ Data strictly isolated — no cross-tenant leaks
  • ✅ User permissions verified — token org must match request tenant
  • ✅ All database queries include organizationId filter

Tenant Resolution Priority (Desktop Web → Mobile API):

  1. Subdomain (highest): booki-salon.booki.app → auto-detected
  2. Header: X-Tenant-Slug: booki-salon
  3. JWT Token: Extract organizationId from JWT payload
  4. Query Param (lowest, public only): ?slug=booki-salon

Remember: Tenant context is NOT optional. Every request must have one.