Skip to content

Multi-Tenancy

The RAG Chatbot supports multi-tenant architecture, allowing you to run multiple isolated instances from a single codebase. Each tenant has its own database, configuration, and API keys.

Overview

Multi-tenancy enables:

  • Complete Data Isolation - Each tenant has a separate database
  • Independent Configuration - Per-tenant LLM providers, API keys, and settings
  • Shared Infrastructure - Single codebase serves all tenants
  • Flexible Deployment - Add or remove tenants without code changes

Architecture

┌──────────────────────────────────────────────────────────────┐
│                      PHP Application                          │
│                                                               │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐           │
│  │  Tenant A   │  │  Tenant B   │  │  Tenant C   │   ...     │
│  │  Request    │  │  Request    │  │  Request    │           │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘           │
│         │                │                │                   │
│         ▼                ▼                ▼                   │
│  ┌──────────────────────────────────────────────────┐        │
│  │               Tenant Resolver                     │        │
│  │    (Extracts tenant_id from request)              │        │
│  └──────────────────────────────────────────────────┘        │
│         │                │                │                   │
│         ▼                ▼                ▼                   │
│  ┌───────────┐    ┌───────────┐    ┌───────────┐             │
│  │  .env     │    │  .env     │    │  .env     │             │
│  │ Tenant A  │    │ Tenant B  │    │ Tenant C  │             │
│  └──┬────┬───┘    └──┬────┬───┘    └──┬────┬───┘             │
│     │    │           │    │           │    │                  │
│     ▼    │           ▼    │           ▼    │                  │
│  ┌────────┐    ┌────────┐    ┌────────┐   │                  │
│  │ ragdb  │    │ ragdb  │    │ ragdb  │   │                  │
│  │Tenant A│    │Tenant B│    │Tenant C│   │                  │
│  └────────┘    └────────┘    └────────┘   │                  │
│  (per-tenant, may be on remote servers)   │                  │
│                                           │                  │
│           ┌───────────────────────────────┘                  │
│           ▼                                                  │
│  ┌─────────────────────────────────┐                         │
│  │   chatdb (shared)               │                         │
│  │   Widget settings, prompts,     │                         │
│  │   inquiries, tenant prompts     │                         │
│  │   (isolated by tenant_id)       │                         │
│  └─────────────────────────────────┘                         │
└──────────────────────────────────────────────────────────────┘

Tenant CLI

Manage tenants using the command-line interface.

Create a Tenant

php cli/tenant.php create <tenant-id> [options]

Options:

Option Description
--name=<name> Human-readable tenant name
--db-host=<host> Database host (default: localhost)
--db-port=<port> Database port (default: 5432)
--db-name=<name> Database name (default: ragdb_)
--db-user=<user> Database username (default: raguser)
--db-password=<pass> Database password (auto-generated if not provided)

Example:

php cli/tenant.php create acme-corp --name="ACME Corporation" --db-name=acme_ragdb

Output:

Creating tenant: acme-corp
  Name: ACME Corporation
  Directory: /var/www/chatbot/tenants/acme-corp

Generated API Keys:
  ADMIN_API_KEY: 3b637c0957511bc8920f6c9695cfb948...
  CHAT_API_KEY: b0e5040f975ec8f60e9f150012b356ec...

Tenant created successfully!

Next steps:
  1. Edit /var/www/chatbot/tenants/acme-corp/.env
  2. Set your OPENAI_API_KEY
  3. Create the database and run migrations
  4. Test with: php test_db.php acme-corp

List Tenants

php cli/tenant.php list

Output:

Available tenants:
  - acme-corp (ACME Corporation)
  - demo-tenant (Demo Tenant)
  - client-xyz (Client XYZ)

Total: 3 tenants

Show Tenant Details

php cli/tenant.php show <tenant-id>

Output:

Tenant: acme-corp
  Name: ACME Corporation
  Directory: /var/www/chatbot/tenants/acme-corp
  Config File: /var/www/chatbot/tenants/acme-corp/.env

Configuration:
  DB_HOST: localhost
  DB_NAME: acme_ragdb
  LLM_PROVIDER: openai
  OPENAI_MODEL: gpt-5.2

Regenerate API Keys

php cli/tenant.php regenerate-keys <tenant-id>

Generates new ADMIN_API_KEY and CHAT_API_KEY for the tenant.

Delete a Tenant

php cli/tenant.php delete <tenant-id>

Warning

This deletes the tenant configuration directory. The database is NOT deleted automatically—you must drop it manually.

Tenant Configuration

Each tenant has its own .env file in /var/www/chatbot/tenants/<tenant-id>/.env.

Directory Structure

/var/www/chatbot/
├── tenants/
│   ├── acme-corp/
│   │   └── .env
│   ├── demo-tenant/
│   │   └── .env
│   └── client-xyz/
│       └── .env
├── public_html/
├── src/
└── ...

Tenant Environment Variables

Each tenant .env file contains:

# ===========================================
# Tenant Identification
# ===========================================
TENANT_ID=acme-corp
TENANT_NAME=ACME Corporation

# ===========================================
# Database (tenant-specific RAG data)
# ===========================================
DB_HOST=localhost
DB_PORT=5432
DB_NAME=acme_ragdb
DB_USER=raguser
DB_PASSWORD=secure_password_here

# ===========================================
# Admin Database (shared chatdb)
# ===========================================
ADMIN_DB_HOST=localhost
ADMIN_DB_PORT=5432
ADMIN_DB_NAME=chatdb
ADMIN_DB_USER=chatuser
ADMIN_DB_PASSWORD=secure_password_here

# ===========================================
# API Keys (unique per tenant)
# ===========================================
ADMIN_API_KEY=your_admin_api_key
CHAT_API_KEY=your_chat_api_key
UPLOAD_API_KEY=your_upload_api_key

# ===========================================
# LLM Provider
# ===========================================
LLM_PROVIDER=openai
OPENAI_API_KEY=sk-proj-your-openai-key
OPENAI_MODEL=gpt-5.2

# Optional: Claude
ANTHROPIC_API_KEY=
CLAUDE_MODEL=claude-sonnet-4-20250514

# ===========================================
# Docling Service
# ===========================================
DOCLING_SERVICE_URL=http://localhost:8001

# ===========================================
# Upload Settings
# ===========================================
MAX_UPLOAD_SIZE_MB=100

# ===========================================
# CORS Allowed Origins
# ===========================================
ALLOWED_ORIGINS=https://your-site.com

# ===========================================
# Admin Dashboard
# ===========================================
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your_admin_password

# ===========================================
# Debug (optional)
# ===========================================
API_DEBUG_LOGGING_ENABLED=false
API_DEBUG_LOG_PATH=/var/www/chatbot/logs/api_debug
API_DEBUG_USERNAME=admin
API_DEBUG_PASSWORD=debug_password

# ===========================================
# Endpoint Toggles (default: true if not set)
# ===========================================
# ENDPOINT_CHAT_ENABLED=true
# ENDPOINT_CUSTOMER_CHAT_ENABLED=true
# ENDPOINT_WIDGET_CONFIG_ENABLED=true
# ENDPOINT_UPLOAD_ENABLED=true
# ENDPOINT_MEDIA_INFO_ENABLED=true
# ENDPOINT_ADMIN_ENABLED=true
# ENDPOINT_API_DEBUG_ENABLED=true

Required Keys

UPLOAD_API_KEY must be set for each tenant. If empty, upload and media-info endpoints return 401 Unauthorized. Keys are auto-generated when creating tenants via the CLI.

Tenant Identification

The system identifies tenants from incoming requests using three methods (checked in order):

1. POST Body

For JSON API requests, include tenant_id in the request body:

curl -X POST https://your-domain.com/chat \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "acme-corp",
    "session_id": "user_123",
    "message": "What is in the report?"
  }'

2. Query Parameter

For GET requests or form submissions, use the tenant query parameter:

curl "https://your-domain.com/debug-rag.php?tenant=acme-corp&query=test"

3. HTTP Header

For admin requests or when the body can't be modified, use the X-Tenant-ID header:

curl -X GET https://your-domain.com/admin/documents \
  -H "X-Tenant-ID: acme-corp" \
  -H "X-API-Key: your_admin_api_key"

Priority Order

1. POST body (tenant_id)     ← Checked first
2. GET parameter (tenant)    ← Checked second
3. HTTP header (X-Tenant-ID) ← Checked last

API Usage by Tenant

All API endpoints require tenant identification. Here are examples for each endpoint:

Chat Endpoint

curl -X POST https://your-domain.com/chat \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "acme-corp",
    "session_id": "user_123",
    "message": "What are the quarterly results?"
  }'

Upload Endpoint

# File upload (requires UPLOAD_API_KEY)
curl -X POST "https://your-domain.com/upload?tenant=acme-corp" \
  -H "X-API-Key: your_upload_api_key" \
  -F "file=@document.pdf"

# Media URL import (requires UPLOAD_API_KEY)
curl -X POST "https://your-domain.com/upload?tenant=acme-corp" \
  -H "X-API-Key: your_upload_api_key" \
  -F "url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"

Debug RAG Endpoint

# List documents
curl "https://your-domain.com/debug-rag.php?tenant=acme-corp"

# Test search query
curl "https://your-domain.com/debug-rag.php?tenant=acme-corp&query=quarterly+report"

Admin Endpoints

# List documents
curl -X GET "https://your-domain.com/admin/documents" \
  -H "X-Tenant-ID: acme-corp" \
  -H "X-API-Key: your_admin_api_key"

# View chat sessions
curl -X GET "https://your-domain.com/admin/sessions" \
  -H "X-Tenant-ID: acme-corp" \
  -H "X-API-Key: your_admin_api_key"

API Debug Logs

curl -u "admin:debug_password" \
  "https://your-domain.com/api-debug/logs/?tenant=acme-corp"

Widget Configuration

Configure the chat widget for a specific tenant:

Basic Widget (chat-widget.js)

<script>
    // Set tenant before loading widget
    window.RAG_CHATBOT_TENANT = 'acme-corp';
    window.RAG_CHATBOT_API = '/chat';
</script>
<script src="/chat-widget.js"></script>

Customer Widget (customer-widget.js)

<script>
    window.RAG_CHATBOT_TENANT = 'acme-corp';
    window.RAG_CHATBOT_WIDGET_KEY = 'default';
</script>
<script src="/customer-widget.js"></script>

The widget automatically includes the tenant ID in all API requests.

Admin Dashboard

Access the admin dashboard for a specific tenant:

https://your-domain.com/admin.php?tenant=acme-corp

The dashboard includes a tenant selector dropdown when multiple tenants exist.

Authentication

Each tenant has its own ADMIN_USERNAME and ADMIN_PASSWORD. Login credentials are tenant-specific.

CLI Scripts

All CLI scripts require the --tenant parameter:

Import Documents

php scripts/import.php --tenant=acme-corp /path/to/documents/

Reindex Documents

php scripts/reindex_documents.php --tenant=acme-corp

Cleanup Chat History

php scripts/cleanup_chat_history.php --tenant=acme-corp --days=30

Test Database Connection

php test_db.php acme-corp

Run Tests

php tests/test_improvements.php --tenant=acme-corp --verbose
php tests/test_file_uploads.php --tenant=acme-corp --dry-run
php tests/test_integration.php --tenant=acme-corp --cleanup

Database Setup

The system uses two databases:

  1. ragdb (per-tenant) - RAG data (documents, chunks, embeddings, chat history). Each tenant can use a separate database on a different server for data isolation.
  2. chatdb (shared) - Admin/widget configuration (widget settings, prompts, inquiries, tenant prompts). Shared across all tenants with tenant_id isolation.

Create Tenant RAG Database

# Connect as postgres admin
sudo -u postgres psql

# Create database for tenant
CREATE DATABASE acme_ragdb;

# Create user (or use existing)
CREATE USER raguser WITH PASSWORD 'secure_password';

# Grant privileges
GRANT ALL PRIVILEGES ON DATABASE acme_ragdb TO raguser;

# Connect to the new database
\c acme_ragdb

# Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;

# Exit psql
\q

Import Schema

# Apply RAG schema (per-tenant database)
psql -U raguser -d acme_ragdb -f database/ragdb.sql

The shared chatdb database only needs to be set up once (see Database Setup)—all tenants share it.

# Apply chatdb schema (shared, one-time setup)
sudo -u postgres psql -d chatdb -f database/chatdb.sql

Security Considerations

API Key Isolation

Each tenant has unique API keys:

  • ADMIN_API_KEY - For admin dashboard and management endpoints
  • CHAT_API_KEY - For chat endpoints (if required)
  • UPLOAD_API_KEY - For upload and media-info endpoint protection (required)

CORS Origin Whitelist

Each tenant can restrict cross-origin access via ALLOWED_ORIGINS in their .env. IPs that repeatedly send unrecognized origins are rate-limited.

IP Blocklist

The system maintains a shared IP blocklist for abuse prevention:

  • After 10 failed tenant resolution attempts, the IP is blocked for 15 minutes
  • Blocklist data is stored in /var/www/chatbot/data/ip_blocklist/

Data Isolation

  • RAG data (documents, embeddings, chat history) is stored in a per-tenant ragdb database, which can be on a separate remote server for privacy
  • Admin/widget config (widget settings, prompts, inquiries) is stored in the shared chatdb database, isolated by tenant_id
  • No cross-tenant data access is possible
  • File uploads are processed in isolated temporary directories

Troubleshooting

"Tenant not found" Error

{"error": "Tenant 'xyz' not found"}

Causes:

  1. Tenant doesn't exist - run php cli/tenant.php list to verify
  2. Missing .env file in tenant directory
  3. Permission issues on tenant directory

Fix:

# Check tenant exists
php cli/tenant.php show xyz

# Check permissions
ls -la /var/www/chatbot/tenants/xyz/

# Fix permissions if needed
sudo chown -R apache:apache /var/www/chatbot/tenants/xyz/
sudo chmod 640 /var/www/chatbot/tenants/xyz/.env

"Missing tenant_id" Error

{"error": "This chat is not configured. Please contact support."}

Causes:

  1. No tenant identifier in request
  2. Wrong parameter name used

Fix:

Ensure you're passing the tenant using one of:

  • POST body: "tenant_id": "your-tenant"
  • Query param: ?tenant=your-tenant
  • Header: X-Tenant-ID: your-tenant

Database Connection Failed

SQLSTATE[08006] Connection refused

Causes:

  1. Wrong database credentials in tenant .env
  2. Database doesn't exist
  3. PostgreSQL not running

Fix:

# Test connection
php test_db.php your-tenant

# Check credentials in .env
cat /var/www/chatbot/tenants/your-tenant/.env | grep DB_

# Verify database exists
sudo -u postgres psql -c "\l" | grep your_db_name

Best Practices

Naming Conventions

  • Use lowercase alphanumeric characters and hyphens for tenant IDs
  • Examples: acme-corp, client-123, demo-tenant
  • Avoid special characters, spaces, or uppercase letters

Backup Strategy

  • Back up each tenant's database separately
  • Include tenant .env files in configuration backups
  • Document tenant-specific customizations

Monitoring

  • Monitor each tenant's database size and performance
  • Track API usage per tenant
  • Set up alerts for tenant-specific errors

Next Steps

  1. Create your first tenant
  2. Configure the tenant environment
  3. Set up the database
  4. Test with the admin dashboard