Guide - Multi-Tenancy & Tenant Slug Resolution
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
Method 1: Subdomain Auto-Detection (Recommended)
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:
- Browser makes request from subdomain
- API receives request headers including
Host: booki-salon.booki.app - Middleware extracts subdomain:
booki-salon - Database queries filtered to
organizationIdmatching that slug - 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:
- Extracts tenant slug:
booki-salon - Verifies booking request is for that organization
- 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:
- Validates JWT token
- Reads user's organization from token
- Verifies JWT org matches header slug (security check)
- 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
organizationIdunique index - No query bypasses tenant check
- Deleted organizations cannot be accessed
Common Tenant Issues & Troubleshooting
| Error | Cause | Solution |
|---|---|---|
Tenant not found | Invalid or missing slug | Verify slug exists; check spelling; use GET /api/tenant/:slug first |
Unauthorized - tenant mismatch | JWT token from different org | Ensure logged-in user matches requested tenant |
No X-Tenant-Slug header | Missing required header for API call | Add -H "X-Tenant-Slug: booki-salon" to request |
organizationId not found | Submitted ID doesn't match tenant | Verify organization ID belongs to intended tenant |
403 Permission Denied | User not member of tenant | Check user's organizationId in profile |
Debug Checklist
- Do I have the correct slug?
curl -X GET https://api.booki.app/api/tenant/YOUR-SLUG
Should return 200 with org details. - 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. - Are tenant and token aligned?
- Extract
organizationIdfrom JWT (payload) - Compare with slug via
GET /api/tenant/:slugresponse - Both must match
- Extract
- 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:
- Make separate API calls for each tenant
- Merge results in your application
- Each call must have correct
X-Tenant-Slugheader
Summary
Key Points:
- ✅ Every request is automatically scoped to a tenant
- ✅ Tenant determined by subdomain (web) or
X-Tenant-Slugheader (API) - ✅ Data strictly isolated — no cross-tenant leaks
- ✅ User permissions verified — token org must match request tenant
- ✅ All database queries include
organizationIdfilter
Tenant Resolution Priority (Desktop Web → Mobile API):
- Subdomain (highest):
booki-salon.booki.app→ auto-detected - Header:
X-Tenant-Slug: booki-salon - JWT Token: Extract
organizationIdfrom JWT payload - Query Param (lowest, public only):
?slug=booki-salon
Remember: Tenant context is NOT optional. Every request must have one.
