-
Notifications
You must be signed in to change notification settings - Fork 0
Multi Tenant Deployment
Version: 0.5.0 Date: January 30, 2026 Status: Production Ready
This guide explains how to deploy nself-chat as a multi-tenant SaaS platform with subdomain routing, custom domains, and Stripe billing.
- Architecture Overview
- Prerequisites
- Database Setup
- Environment Configuration
- DNS & Domain Setup
- Stripe Configuration
- Deployment
- Tenant Management
- Monitoring & Maintenance
- Troubleshooting
nself-chat uses schema-level isolation for tenant data:
- Each tenant gets a dedicated PostgreSQL schema (e.g.,
tenant_acme) - Global tenant metadata stored in
public.tenantstable - Row-Level Security (RLS) enforces tenant boundaries
- Middleware resolves tenant from subdomain or custom domain
Benefits:
- β Strong data isolation
- β Independent backups per tenant
- β Efficient resource usage
- β Simplified migrations
- β Cost-effective scaling
Alternatives Considered:
- β Separate database per tenant (too expensive at scale)
- β Shared schema with tenant_id (weaker isolation, risk of data leaks)
1. User visits β acme.nchat.app
2. Middleware extracts subdomain β "acme"
3. Query database β SELECT * FROM tenants WHERE slug = 'acme'
4. Set tenant context β X-Tenant-Id, X-Tenant-Schema headers
5. All queries scoped to β tenant_acme schema
6. Response returned with tenant branding
- Node.js: β₯20.0.0
- PostgreSQL: β₯14.0 (with schema support)
- Redis: β₯6.0 (for caching and rate limiting)
- Docker: β₯20.0 (optional, for local development)
- pnpm: 9.15.4
- Stripe Account: For billing and subscriptions
- DNS Provider: With wildcard subdomain support (e.g., Cloudflare, Route 53)
-
SSL Certificate: Wildcard cert for
*.nchat.app
# Navigate to backend directory
cd .backend
# Run migration
nself db migrate up 030_multi_tenant_system.sqlThis creates:
-
public.tenants- Tenant metadata -
public.tenant_usage- Usage statistics -
public.tenant_settings- Tenant configuration -
public.tenant_invitations- Invite system -
public.tenant_audit_logs- Audit trail -
public.stripe_webhooks- Webhook event log
-- Check tables exist
\dt public.tenants*
-- Check demo tenant
SELECT * FROM public.tenants WHERE slug = 'demo';
-- Check schema creation
\dn tenant_*-- Manual tenant creation (for testing)
INSERT INTO public.tenants (
name, slug, status, owner_email, owner_name,
schema_name, billing_plan
) VALUES (
'Acme Corporation',
'acme',
'active',
'admin@acme.com',
'John Doe',
'tenant_acme',
'pro'
);
-- Create schema
CREATE SCHEMA tenant_acme;
-- Copy table structure from nchat schema
-- (This is automated by TenantService.createTenant())# PostgreSQL
DATABASE_URL=postgresql://user:pass@localhost:5432/nchat_multi
# Redis (for tenant caching)
REDIS_URL=redis://localhost:6379/0
# Hasura
HASURA_GRAPHQL_ADMIN_SECRET=your-admin-secret
HASURA_GRAPHQL_ENABLE_CONSOLE=false
# Auth
JWT_SECRET=your-jwt-secret# Multi-Tenancy
ENABLE_MULTI_TENANCY=true
NEXT_PUBLIC_APP_URL=https://nchat.app
NEXT_PUBLIC_BASE_DOMAIN=nchat.app
# Custom Domains
ENABLE_CUSTOM_DOMAINS=true
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# Backend
NEXT_PUBLIC_GRAPHQL_URL=https://api.nchat.app/v1/graphql
NEXT_PUBLIC_AUTH_URL=https://auth.nchat.app/v1/auth
# Database (for server-side)
DATABASE_URL=postgresql://user:pass@localhost:5432/nchat_multiTo disable multi-tenancy (single organization):
DISABLE_MULTI_TENANCY=true
DEFAULT_TENANT_SLUG=demoConfigure your DNS provider to support wildcard subdomains:
DNS Records:
# A Records
nchat.app β 1.2.3.4
*.nchat.app β 1.2.3.4
# CNAME (alternative)
*.nchat.app β nchat.app
Cloudflare Example:
Type: A
Name: @
Content: 1.2.3.4
Proxy: β Proxied
Type: A
Name: *
Content: 1.2.3.4
Proxy: β Proxied
Option 1: Cloudflare (Recommended)
- Free wildcard SSL
- Automatic renewal
- DDoS protection
Option 2: Let's Encrypt
# Install certbot
sudo apt install certbot
# Generate wildcard cert (requires DNS challenge)
sudo certbot certonly --manual --preferred-challenges dns \
-d nchat.app -d *.nchat.app
# Certificate location
/etc/letsencrypt/live/nchat.app/fullchain.pem
/etc/letsencrypt/live/nchat.app/privkey.pemTo support custom domains (e.g., chat.acme.com):
-
Tenant provides DNS records:
CNAME: chat.acme.com β nchat.app -
Add domain to tenant:
curl -X PUT https://api.nchat.app/tenants/{id} \ -H "Authorization: Bearer {token}" \ -d '{"customDomain": "chat.acme.com"}' -
Configure SSL (if not using Cloudflare):
# Add domain to SSL cert sudo certbot certonly --manual --preferred-challenges dns \ -d chat.acme.com -
Update whitelist:
# .env.local ALLOWED_CUSTOM_DOMAINS=chat.acme.com,team.example.com
- Sign up at https://stripe.com
- Get API keys from Dashboard β Developers β API keys
- Copy Secret Key and Publishable Key
Free Plan (no charge):
- No Stripe product needed
- Handled in application logic
Pro Plan:
# Create product
stripe products create \
--name "nChat Pro" \
--description "For growing teams"
# Create monthly price
stripe prices create \
--product {product_id} \
--unit-amount 1500 \
--currency usd \
--recurring[interval]=month
# Create yearly price (discounted)
stripe prices create \
--product {product_id} \
--unit-amount 15000 \
--currency usd \
--recurring[interval]=yearEnterprise Plan:
# Create product
stripe products create \
--name "nChat Enterprise" \
--description "For large organizations"
# Create monthly price
stripe prices create \
--product {product_id} \
--unit-amount 9900 \
--currency usd \
--recurring[interval]=month
# Create yearly price
stripe prices create \
--product {product_id} \
--unit-amount 99000 \
--currency usd \
--recurring[interval]=yearEdit src/lib/tenants/types.ts:
export const DEFAULT_PLANS: Record<BillingPlan, SubscriptionPlan> = {
pro: {
// ...
stripePriceIdMonthly: 'price_xxx', // From Stripe
stripePriceIdYearly: 'price_yyy', // From Stripe
},
enterprise: {
// ...
stripePriceIdMonthly: 'price_zzz',
stripePriceIdYearly: 'price_www',
},
}-
Go to Stripe Dashboard β Developers β Webhooks
-
Click "Add endpoint"
-
URL:
https://nchat.app/api/billing/webhook -
Events to send:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failedcustomer.subscription.trial_will_end
-
Copy webhook signing secret:
# .env.local STRIPE_WEBHOOK_SECRET=whsec_xxx
# Build image
docker build -t nchat-multi:latest .
# Run container
docker run -d \
--name nchat-multi \
-p 3000:3000 \
--env-file .env.production \
nchat-multi:latest
# Or use docker-compose
docker-compose -f docker-compose.multi-tenant.yml up -d# Install Vercel CLI
pnpm add -g vercel
# Deploy
vercel --prod
# Set environment variables
vercel env add ENABLE_MULTI_TENANCY production
vercel env add STRIPE_SECRET_KEY production
# ... (add all required env vars)Vercel Configuration:
{
"buildCommand": "pnpm build",
"devCommand": "pnpm dev",
"installCommand": "pnpm install",
"framework": "nextjs",
"env": {
"ENABLE_MULTI_TENANCY": "true",
"NEXT_PUBLIC_BASE_DOMAIN": "nchat.app"
}
}See deploy/k8s/multi-tenant/ for Kubernetes manifests.
# Apply manifests
kubectl apply -f deploy/k8s/multi-tenant/
# Check deployment
kubectl get pods -n nchat
kubectl get ingress -n nchatcurl -X POST https://nchat.app/api/tenants/create \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corporation",
"slug": "acme",
"ownerEmail": "admin@acme.com",
"ownerName": "John Doe",
"ownerPassword": "securepass123",
"plan": "pro",
"trial": true
}'curl -X PUT https://acme.nchat.app/api/tenants/{id} \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"customDomain": "chat.acme.com",
"branding": {
"appName": "Acme Chat",
"primaryColor": "#FF5733"
}
}'# Soft delete (cancel subscription)
curl -X DELETE https://acme.nchat.app/api/tenants/{id} \
-H "Authorization: Bearer {token}"
# Hard delete (remove all data)
# This is done via database function for safety
psql -d nchat_multi -c "SELECT hard_delete_tenant('{tenant_id}')"-- List all tenants
SELECT id, name, slug, status, billing_plan, created_at
FROM public.tenants
ORDER BY created_at DESC;
-- Get tenant usage
SELECT * FROM public.tenant_usage
WHERE tenant_id = '{tenant_id}';
-- Check limits
SELECT public.check_tenant_limits('{tenant_id}');
-- Suspend tenant
UPDATE public.tenants
SET status = 'suspended', suspended_at = NOW()
WHERE id = '{tenant_id}';
-- Reactivate tenant
UPDATE public.tenants
SET status = 'active', suspended_at = NULL
WHERE id = '{tenant_id}';# Application health
curl https://nchat.app/api/health
# Tenant health
curl https://acme.nchat.app/api/health
# Database health
psql -d nchat_multi -c "SELECT COUNT(*) FROM public.tenants WHERE status = 'active'"-- Current month usage by tenant
SELECT
t.name,
t.slug,
t.billing_plan,
u.users_total,
u.messages_total,
u.storage_bytes / 1024 / 1024 / 1024 as storage_gb,
u.api_calls_total
FROM public.tenants t
LEFT JOIN public.tenant_usage u ON t.id = u.tenant_id
WHERE u.period = TO_CHAR(NOW(), 'YYYY-MM')
ORDER BY u.storage_bytes DESC;-- Delete expired invitations (run daily)
DELETE FROM public.tenant_invitations
WHERE expires_at < NOW() AND accepted_at IS NULL;
-- Archive cancelled tenants (run monthly)
UPDATE public.tenants
SET status = 'archived'
WHERE status = 'cancelled'
AND cancelled_at < NOW() - INTERVAL '90 days';
-- Clean up old webhook logs (run weekly)
DELETE FROM public.stripe_webhooks
WHERE created_at < NOW() - INTERVAL '30 days'
AND processed = true;# Backup all tenant schemas
pg_dump -h localhost -U postgres -d nchat_multi \
--schema-only \
--schema=public \
--schema=tenant_* \
> backup-$(date +%Y%m%d).sql
# Backup specific tenant
pg_dump -h localhost -U postgres -d nchat_multi \
--schema=tenant_acme \
> tenant-acme-$(date +%Y%m%d).sql
# Restore tenant
psql -h localhost -U postgres -d nchat_multi \
< tenant-acme-20260130.sqlSymptoms: 404 error when accessing subdomain
Solutions:
- Check DNS propagation:
nslookup acme.nchat.app - Verify tenant exists:
SELECT * FROM public.tenants WHERE slug = 'acme' - Check middleware logs for tenant resolution
- Verify SSL certificate includes wildcard
Symptoms: Subscription not updating after payment
Solutions:
- Check webhook signature verification
- Verify
STRIPE_WEBHOOK_SECRETis correct - Check Stripe Dashboard β Webhooks β Events
- Review logs:
SELECT * FROM public.stripe_webhooks WHERE processed = false
Symptoms: Tenant exceeds usage limits without restriction
Solutions:
- Check limit enforcement middleware
- Verify usage tracking:
SELECT * FROM public.tenant_usage - Run limits check:
SELECT public.check_tenant_limits('{tenant_id}') - Review plan configuration in
DEFAULT_PLANS
Symptoms: Tenant seeing data from another tenant
Critical Security Issue - Immediate Action Required:
- Suspend all tenants immediately
- Review RLS policies:
SELECT * FROM pg_policies WHERE schemaname = 'public' - Check search_path configuration
- Audit recent queries for cross-tenant access
- Review middleware tenant context setting
-- Add indexes for common queries
CREATE INDEX CONCURRENTLY idx_tenants_status_plan
ON public.tenants(status, billing_plan);
CREATE INDEX CONCURRENTLY idx_tenant_usage_period_tenant
ON public.tenant_usage(period, tenant_id);// Redis caching for tenant metadata
const cacheTenant = async (slug: string) => {
const cached = await redis.get(`tenant:${slug}`)
if (cached) return JSON.parse(cached)
const tenant = await getTenantBySlug(slug)
await redis.setex(`tenant:${slug}`, 3600, JSON.stringify(tenant))
return tenant
}// Per-tenant rate limiting
const rateLimiter = new RateLimiter({
keyGenerator: (req) => getTenantId(req),
max: (req) => {
const tenant = getTenantFromRequest(req)
return tenant.limits.rateLimitPerMinute
},
windowMs: 60 * 1000,
})- Wildcard SSL certificate installed
- RLS policies enabled on all tenant tables
- Stripe webhook signature verification active
- Rate limiting configured per tenant
- Audit logging enabled
- Database backups automated
- Cross-tenant query prevention tested
- Admin routes protected with super_admin role
- Environment variables secured (not committed to git)
- Custom domain whitelist configured
For issues or questions:
- GitHub Issues: https://github.com/yourusername/nself-chat/issues
- Documentation: https://docs.nchat.app
- Email: support@nchat.app
Last Updated: January 30, 2026 Version: 0.5.0
nself-chat v0.3.0 | GitHub | Issues | Discussions | Demo
Edit this page | MIT License | Β© 2026
(See π Security section below for 2FA, PIN Lock, and security audits.)
(Search lives in π Reference below.)
- π¬ Advanced Messaging
- π E2EE Setup
- π Search Setup
- π Call Management
- πΊ Live Streaming
- π₯οΈ Screen Sharing
- πΉ Video Calling
- ποΈ Voice Calling
- π± Mobile Optimization
- π§ͺ Testing
- π i18n
- π API Overview
- π Complete Reference
- π» API Examples
- π€ Bot API
- π Auth API
- π GraphQL Schema
- π Deployment Overview
- π³ Docker
- βΈοΈ Kubernetes
- β Helm Charts
- β Production Checklist
- π Production Validation
- π’ Multi-Tenant
- ποΈ Architecture
- π Diagrams
- ποΈ Database Schema
- π Project Structure
- π TypeScript Types
- π SPORT Reference
- π 2FA
- π¬ Messaging
- π Call Management
- π Call State Machine
- π E2EE
- πΊ Live Streaming
- π± Mobile Calls
- π PIN Lock
- π Polls
- π₯οΈ Screen Sharing
- π Search
- π Social Media
- ποΈ Voice Calling
- π Security Overview
- π‘οΈ Security Audit
- β‘ Performance
- π Best Practices
- π 2FA
- π PIN Lock
- π E2EE
- π‘οΈ E2EE Audit
v1.0.0 β’ 2026