WHITE LABEL TROUBLESHOOTING - nself-org/cli GitHub Wiki
Comprehensive troubleshooting guide for nself white-label functionality, covering common issues, diagnostics, and solutions.
Last Updated: January 2026 Version: 1.0.0
- Branding Not Applying
- Logo & Asset Problems
- Custom Domain Issues
- Email Template Problems
- Theme Issues
- Multi-Tenant Branding
- Performance Issues
- Security Issues
- Browser Compatibility
- Common Error Messages
- Diagnostic Tools
- Quick Reference
Symptoms:
- Updated branding settings but old branding still shows
- Logo changes not appearing
- Color changes not reflected
Common Causes:
- Browser cache
- CDN cache
- Redis cache not invalidated
- Database transaction not committed
- Service not restarted
Diagnosis:
# 1. Check if branding exists in database
nself db query "SELECT * FROM public.branding WHERE tenant_id = 'YOUR_TENANT_ID';"
# 2. Check Redis cache
nself redis get "branding:YOUR_TENANT_ID"
# 3. Check CDN cache headers
curl -I https://yourdomain.com/assets/logo.png
# 4. Check service logs
nself logs branding --tail 100
# 5. Verify branding service is running
docker ps | grep brandingSolution:
# Step 1: Clear all caches
nself cache clear --all
# Step 2: Force Redis cache invalidation
nself redis del "branding:*"
# Step 3: Clear browser cache
# Chrome: Ctrl+Shift+Del (Cmd+Shift+Del on Mac)
# Firefox: Ctrl+Shift+Del
# Or use incognito/private mode
# Step 4: Purge CDN cache (if using CDN)
# Cloudflare example:
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
# Step 5: Restart branding-related services
nself restart branding
nself restart nginx
# Step 6: Verify update
curl -H "X-Tenant-ID: YOUR_TENANT_ID" https://api.yourdomain.com/brandingVerification:
# Check branding API response
curl -s https://api.yourdomain.com/branding \
-H "X-Tenant-ID: YOUR_TENANT_ID" | jq .
# Expected output:
# {
# "tenant_id": "YOUR_TENANT_ID",
# "brand_name": "Updated Name",
# "logo_url": "https://cdn.yourdomain.com/logos/new-logo.png",
# "updated_at": "2026-01-30T12:34:56Z"
# }Prevention:
- Implement proper cache invalidation in your update workflow
- Use versioned asset URLs (e.g.,
logo.png?v=1234567890) - Set appropriate cache headers (short TTL during updates)
- Add cache-busting parameters to asset URLs
Symptoms:
- Changes appear for some users but not others
- Different branding on different servers/regions
- Intermittent old branding appearances
Diagnosis:
# Test from multiple regions
curl -I https://yourdomain.com/assets/logo.png
# Look for: Cache-Control, Age, X-Cache headers
# Check CDN edge locations
curl -I https://yourdomain.com/assets/logo.png -H "X-Debug: 1"
# Verify asset version
curl -s https://yourdomain.com/assets/logo.png | md5sumSolution:
# 1. Use cache-busting in asset URLs
# Update your asset URL generation to include version/hash
# Example: logo.png?v=abc123def456
# 2. Set short cache TTL during updates
# Add to nginx config:
location /assets/ {
expires 5m; # Short cache during updates
add_header Cache-Control "public, must-revalidate";
}
# 3. Use CDN API to purge specific URLs
# Cloudflare example:
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"files":["https://yourdomain.com/assets/logo.png"]}'
# 4. Implement stale-while-revalidate
# nginx config:
add_header Cache-Control "public, max-age=300, stale-while-revalidate=60";Prevention:
- Always use versioned asset URLs
- Implement proper cache invalidation strategy
- Use short cache TTLs for branding assets
- Monitor cache hit rates and edge locations
Symptoms:
- "Upload failed" error
- Request timeout
- 413 Request Entity Too Large
- 500 Internal Server Error
Diagnosis:
# 1. Check file size limits
nself config get UPLOAD_MAX_SIZE
# 2. Check nginx limits
grep client_max_body_size /etc/nginx/nginx.conf
# 3. Check storage space
df -h /var/lib/nself/storage
# 4. Check MinIO/S3 permissions
nself storage test-upload
# 5. Check logs
nself logs storage --tail 50 | grep -i error
nself logs nginx --tail 50 | grep -i "413\|502\|504"Solution:
# Step 1: Increase nginx upload limits
# Edit nginx.conf
cat >> /etc/nginx/conf.d/upload-limits.conf <<EOF
client_max_body_size 50M;
client_body_timeout 300s;
EOF
# Step 2: Increase PHP/application upload limits (if applicable)
# Edit .env
echo "UPLOAD_MAX_SIZE=50M" >> .env
echo "UPLOAD_TIMEOUT=300" >> .env
# Step 3: Verify MinIO/S3 bucket permissions
nself storage verify-permissions
# Step 4: Check disk space and clean if needed
docker system prune -a --volumes
# Step 5: Restart services
nself restart nginx
nself restart storage
# Step 6: Test upload
curl -X POST https://api.yourdomain.com/branding/logo \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "logo=@/path/to/logo.png"Verification:
# Upload a test file
dd if=/dev/zero of=/tmp/test-10mb.bin bs=1M count=10
curl -X POST https://api.yourdomain.com/branding/logo \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "logo=@/tmp/test-10mb.bin"
# Should return 200 OK or specific errorPrevention:
- Set reasonable file size limits (10-50MB for logos)
- Implement client-side file size validation
- Add file type validation
- Show clear error messages with size limits
- Implement chunked upload for large files
Symptoms:
- "Invalid file format" error
- Uploaded image appears corrupted
- Image not displaying correctly
- MIME type errors
Diagnosis:
# 1. Check file type
file /path/to/uploaded/logo.png
# 2. Verify MIME type
curl -I https://cdn.yourdomain.com/logos/logo.png | grep Content-Type
# 3. Check image validity
identify /path/to/uploaded/logo.png
# 4. Check for EXIF corruption
exiftool /path/to/uploaded/logo.png
# 5. Check logs for format errors
nself logs branding | grep -i "format\|mime\|invalid"Solution:
# Step 1: Validate and convert images on upload
# Add to your upload handler:
#!/bin/bash
validate_and_convert_image() {
local input="$1"
local output="$2"
# Check if it's a valid image
if ! identify "$input" &>/dev/null; then
echo "Invalid image file"
return 1
fi
# Convert to standard format (remove EXIF, optimize)
convert "$input" \
-strip \
-quality 90 \
-resize 2000x2000\> \
"$output"
# Verify output
if ! identify "$output" &>/dev/null; then
echo "Conversion failed"
return 1
fi
return 0
}
# Step 2: Set proper MIME types in nginx
# Add to nginx config:
types {
image/png png;
image/jpeg jpg jpeg;
image/svg+xml svg;
image/webp webp;
}
# Step 3: Implement server-side validation
# Example validation rules:
# - Allowed formats: PNG, JPEG, SVG, WebP
# - Max dimensions: 2000x2000px
# - Max file size: 10MB
# - Strip metadata/EXIF
# Step 4: Generate multiple formats
convert logo.png -quality 90 logo.jpg
convert logo.png -define webp:lossless=true logo.webp
# Step 5: Restart nginx
nself restart nginxPrevention:
- Accept common formats: PNG, JPEG, SVG, WebP
- Validate file headers, not just extensions
- Strip EXIF/metadata on upload
- Generate optimized versions automatically
- Provide clear format requirements to users
Symptoms:
- 404 errors for logo/assets
- Mixed content warnings (HTTP/HTTPS)
- Incorrect CDN URLs
- Assets not loading on custom domains
Diagnosis:
# 1. Check asset URL in database
nself db query "SELECT logo_url, favicon_url FROM public.branding WHERE tenant_id = 'YOUR_TENANT_ID';"
# 2. Test asset accessibility
curl -I https://cdn.yourdomain.com/logos/logo.png
# 3. Check for mixed content issues
curl -v https://yourdomain.com 2>&1 | grep -i "http:"
# 4. Verify CDN configuration
nslookup cdn.yourdomain.com
# 5. Check nginx routing
nself logs nginx | grep "logo.png"Solution:
# Step 1: Fix asset URLs in database
nself db query "
UPDATE public.branding
SET logo_url = REPLACE(logo_url, 'http://', 'https://'),
favicon_url = REPLACE(favicon_url, 'http://', 'https://')
WHERE logo_url LIKE 'http://%' OR favicon_url LIKE 'http://%';
"
# Step 2: Configure proper asset base URL
# Add to .env:
ASSET_BASE_URL=https://cdn.yourdomain.com
CDN_ENABLED=true
# Step 3: Update nginx to handle assets
cat > /etc/nginx/sites-enabled/assets.conf <<'EOF'
server {
listen 443 ssl http2;
server_name cdn.yourdomain.com;
ssl_certificate /etc/ssl/certs/yourdomain.pem;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
location /logos/ {
alias /var/lib/nself/storage/branding/logos/;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
location /assets/ {
alias /var/lib/nself/storage/branding/assets/;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
}
EOF
# Step 4: Test asset URLs
curl -I https://cdn.yourdomain.com/logos/logo.png
# Step 5: Update asset URLs in application
# Use helper function for consistent URL generation:
get_asset_url() {
local path="$1"
local base_url="${ASSET_BASE_URL:-https://yourdomain.com}"
echo "${base_url}${path}"
}
# Step 6: Restart nginx
nself restart nginxPrevention:
- Always use HTTPS for asset URLs
- Use environment variables for base URLs
- Implement URL helpers/functions
- Validate URLs on save
- Test assets in different environments
Symptoms:
- "Domain verification failed" error
- DNS records not found
- Verification timeout
- TXT record not propagating
Diagnosis:
# 1. Check DNS TXT record
dig TXT _nself-verify.yourdomain.com +short
# 2. Check from multiple DNS servers
dig @8.8.8.8 TXT _nself-verify.yourdomain.com +short
dig @1.1.1.1 TXT _nself-verify.yourdomain.com +short
# 3. Check DNS propagation globally
# Use online tool: https://www.whatsmydns.net/
# 4. Verify expected value
nself db query "SELECT verification_token FROM public.custom_domains WHERE domain = 'yourdomain.com';"
# 5. Check verification logs
nself logs domain-verification --tail 50Solution:
# Step 1: Get verification token
TOKEN=$(nself domain get-verification-token yourdomain.com)
echo "Add TXT record: _nself-verify.yourdomain.com = $TOKEN"
# Step 2: Add DNS TXT record
# Example for Cloudflare API:
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"type": "TXT",
"name": "_nself-verify.yourdomain.com",
"content": "'"$TOKEN"'",
"ttl": 120
}'
# Step 3: Wait for DNS propagation (usually 5-60 minutes)
# Check propagation:
while ! dig TXT _nself-verify.yourdomain.com +short | grep -q "$TOKEN"; do
echo "Waiting for DNS propagation..."
sleep 30
done
# Step 4: Trigger verification
nself domain verify yourdomain.com
# Step 5: Verify status
nself domain status yourdomain.comVerification:
# Check verification status
nself db query "SELECT domain, verified, verified_at FROM public.custom_domains WHERE domain = 'yourdomain.com';"
# Should show:
# domain | verified | verified_at
# ----------------+----------+-------------------------
# yourdomain.com | true | 2026-01-30 12:34:56+00Prevention:
- Set low TTL (120s) for verification records
- Provide clear DNS setup instructions
- Implement retry logic with exponential backoff
- Check multiple DNS servers for verification
- Allow manual re-verification
Symptoms:
- SSL certificate errors in browser
- "Certificate not valid" warnings
- HTTPS not working on custom domain
- Certificate expired
Diagnosis:
# 1. Check certificate validity
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com </dev/null 2>/dev/null | \
openssl x509 -noout -dates
# 2. Check certificate details
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com </dev/null 2>/dev/null | \
openssl x509 -noout -text
# 3. Check Let's Encrypt logs (if using certbot)
cat /var/log/letsencrypt/letsencrypt.log | tail -50
# 4. Check certificate in database
nself db query "SELECT domain, ssl_enabled, ssl_certificate_expires_at FROM public.custom_domains WHERE domain = 'yourdomain.com';"
# 5. Test SSL/TLS configuration
nmap --script ssl-enum-ciphers -p 443 yourdomain.comSolution:
# Step 1: Generate new Let's Encrypt certificate
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d yourdomain.com \
-d www.yourdomain.com
# Alternative: HTTP-01 challenge
certbot certonly \
--webroot \
-w /var/www/html \
-d yourdomain.com \
-d www.yourdomain.com
# Step 2: Update nginx configuration
cat > /etc/nginx/sites-enabled/yourdomain.com.conf <<'EOF'
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://upstream_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
EOF
# Step 3: Test nginx configuration
nginx -t
# Step 4: Reload nginx
nginx -s reload
# Step 5: Set up auto-renewal
# Add to crontab:
# 0 0,12 * * * certbot renew --quiet --deploy-hook "nginx -s reload"
# Step 6: Update database
nself db query "
UPDATE public.custom_domains
SET ssl_enabled = true,
ssl_certificate_expires_at = NOW() + INTERVAL '90 days'
WHERE domain = 'yourdomain.com';
"Verification:
# Test SSL certificate
curl -vI https://yourdomain.com 2>&1 | grep -A 5 "SSL certificate"
# Check certificate expiry
echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Test SSL Labs rating
# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.comPrevention:
- Set up automatic certificate renewal
- Monitor certificate expiration (alert 30 days before)
- Use DNS-01 challenge for wildcard certificates
- Keep certbot updated
- Test renewal process regularly
Symptoms:
- Custom domain shows 404 or default page
- Traffic not routing to correct tenant
- Wrong branding displayed on custom domain
Diagnosis:
# 1. Check DNS resolution
nslookup yourdomain.com
# 2. Check nginx configuration
nginx -T | grep -A 20 "yourdomain.com"
# 3. Check domain mapping in database
nself db query "SELECT domain, tenant_id, enabled FROM public.custom_domains WHERE domain = 'yourdomain.com';"
# 4. Test routing
curl -v https://yourdomain.com
# 5. Check nginx logs
nself logs nginx | grep "yourdomain.com"Solution:
# Step 1: Verify DNS points to your server
# DNS should have:
# A record: yourdomain.com -> YOUR_SERVER_IP
# CNAME record: www.yourdomain.com -> yourdomain.com
# Step 2: Update nginx configuration
cat > /etc/nginx/sites-enabled/custom-domain-routing.conf <<'EOF'
# Map custom domains to tenant IDs
map $host $tenant_id {
default "default";
yourdomain.com "tenant-abc123";
www.yourdomain.com "tenant-abc123";
anotherdomain.com "tenant-xyz789";
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Tenant-ID $tenant_id;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
# Step 3: Reload nginx configuration
# Reload domain mappings from database
nself domain refresh-nginx-config
# Step 4: Test nginx config
nginx -t
# Step 5: Reload nginx
nginx -s reload
# Step 6: Verify routing
curl -v https://yourdomain.com 2>&1 | grep "X-Tenant-ID"Verification:
# Test domain routing
curl -s https://yourdomain.com/api/branding | jq '.tenant_id'
# Should return: "tenant-abc123"
# Check logs for correct tenant
nself logs app | grep "tenant-abc123"Prevention:
- Automate nginx config generation from database
- Validate domain before enabling
- Implement health checks for custom domains
- Monitor domain routing in logs
- Use dynamic upstream configuration
Symptoms:
- Emails showing raw HTML/code
- Variables not replaced (showing {{variable}})
- Broken layout in email clients
- Missing images or styles
Diagnosis:
# 1. Check template syntax
nself email validate-template --template welcome
# 2. Test template rendering
nself email render-test \
--template welcome \
--data '{"user_name":"John","verification_link":"https://..."}'
# 3. Check logs
nself logs email | grep -i "template\|render\|error"
# 4. Verify template exists
nself db query "SELECT id, name, subject, body FROM public.email_templates WHERE name = 'welcome';"
# 5. Test variable substitution
echo "Hello {{user_name}}" | nself email render-string --data '{"user_name":"John"}'Solution:
# Step 1: Validate template syntax
# Check for common issues:
# - Unclosed tags: {{variable
# - Invalid variable names: {{user-name}} (use {{user_name}})
# - Missing quotes in HTML attributes
# Step 2: Fix variable syntax
# WRONG:
# <p>Hello {{user-name}}</p>
# RIGHT:
# <p>Hello {{user_name}}</p>
# Step 3: Test with sample data
cat > /tmp/test-template.html <<'EOF'
<!DOCTYPE html>
<html>
<body>
<h1>Welcome {{user_name}}</h1>
<p>Click here to verify: <a href="{{verification_link}}">Verify Email</a></p>
<img src="{{logo_url}}" alt="Logo">
</body>
</html>
EOF
nself email render-test \
--file /tmp/test-template.html \
--data '{
"user_name": "John Doe",
"verification_link": "https://yourdomain.com/verify?token=abc123",
"logo_url": "https://cdn.yourdomain.com/logos/logo.png"
}'
# Step 4: Update template in database
nself db query "
UPDATE public.email_templates
SET body = '$(cat /tmp/test-template.html)'
WHERE name = 'welcome';
"
# Step 5: Test actual email sending
nself email send-test \
--to [email protected] \
--template welcome \
--data '{"user_name":"John Doe",...}'Verification:
# Send test email and check inbox
nself email send-test --to [email protected] --template welcome
# Check email queue
nself db query "SELECT * FROM public.email_queue WHERE status = 'failed';"
# Check email logs
nself logs email --tail 20Prevention:
- Use template validation before saving
- Test templates with real data
- Implement template versioning
- Use a template preview system
- Validate all variables exist before rendering
Symptoms:
- Layout broken in Gmail/Outlook
- CSS not applying
- Images not displaying
- Mobile responsiveness issues
Diagnosis:
# 1. Test in multiple email clients
# Use service like: https://www.emailonacid.com/
# Or: https://litmus.com/
# 2. Validate HTML
tidy -errors -q /path/to/template.html
# 3. Check inline CSS
# CSS should be inlined for email compatibility
nself email inline-css --template welcome
# 4. Test image URLs
curl -I https://cdn.yourdomain.com/email/logo.pngSolution:
# Step 1: Use email-safe HTML structure
cat > /tmp/email-template-base.html <<'EOF'
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{{subject}}</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 20px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse;">
<!-- Header -->
<tr>
<td align="center" bgcolor="{{brand_primary_color}}" style="padding: 40px 0;">
<img src="{{logo_url}}" alt="{{brand_name}}" width="200" style="display: block;" />
</td>
</tr>
<!-- Content -->
<tr>
<td bgcolor="#ffffff" style="padding: 40px 30px;">
{{content}}
</td>
</tr>
<!-- Footer -->
<tr>
<td bgcolor="#f4f4f4" style="padding: 30px 30px; text-align: center; font-size: 12px; color: #666666;">
© {{current_year}} {{brand_name}}. All rights reserved.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
EOF
# Step 2: Inline all CSS
npm install -g juice
juice /tmp/email-template-base.html /tmp/email-template-inlined.html
# Step 3: Test responsive design
# Add media queries in <style> tag (not inline):
cat >> /tmp/email-template-inlined.html <<'EOF'
<style type="text/css">
@media only screen and (max-width: 600px) {
table[class="container"] {
width: 100% !important;
}
img {
max-width: 100% !important;
height: auto !important;
}
}
</style>
EOF
# Step 4: Use absolute URLs for images
# WRONG: src="/images/logo.png"
# RIGHT: src="https://cdn.yourdomain.com/images/logo.png"
# Step 5: Test in real clients
# Send test emails
nself email send-test --to [email protected] --template welcome
nself email send-test --to [email protected] --template welcome
nself email send-test --to [email protected] --template welcomeEmail CSS Best Practices:
/* Use inline styles for maximum compatibility */
style="font-family: Arial, sans-serif; font-size: 14px; color: #333333;"
/* Avoid these in email CSS: */
/* - position: absolute/fixed */
/* - float */
/* - CSS animations */
/* - flexbox/grid */
/* - background images (use table backgrounds instead) */
/* - web fonts (use safe fallbacks) */
/* Email-safe font stack: */
style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;"Prevention:
- Use table-based layouts for emails
- Inline all CSS styles
- Test in major email clients
- Use email testing services
- Provide plain text alternative
- Use absolute URLs for all resources
Symptoms:
- Theme colors not showing
- Custom CSS ignored
- Styles inconsistent across pages
- Theme switching not working
Diagnosis:
# 1. Check theme configuration
nself db query "SELECT * FROM public.themes WHERE tenant_id = 'YOUR_TENANT_ID';"
# 2. Verify CSS file exists
ls -lh /var/lib/nself/storage/themes/YOUR_TENANT_ID/custom.css
# 3. Check CSS is being loaded
curl -s https://yourdomain.com | grep -i "stylesheet\|<style"
# 4. Check browser console for errors
# Open DevTools (F12) -> Console tab
# 5. Verify CSS variables
curl -s https://yourdomain.com/themes/YOUR_TENANT_ID/variables.cssSolution:
# Step 1: Generate theme CSS from database
nself theme generate --tenant YOUR_TENANT_ID
# Step 2: Verify CSS output
cat /var/lib/nself/storage/themes/YOUR_TENANT_ID/theme.css
# Expected content:
# :root {
# --brand-primary: #007bff;
# --brand-secondary: #6c757d;
# --brand-accent: #28a745;
# }
# Step 3: Ensure CSS is included in HTML
cat > /tmp/theme-include.html <<'EOF'
<head>
<!-- Base theme CSS -->
<link rel="stylesheet" href="/themes/{{tenant_id}}/theme.css">
<!-- Custom CSS (if any) -->
{{#if has_custom_css}}
<link rel="stylesheet" href="/themes/{{tenant_id}}/custom.css">
{{/if}}
<!-- Inline critical CSS -->
<style>
:root {
--brand-primary: {{brand_primary_color}};
--brand-secondary: {{brand_secondary_color}};
}
</style>
</head>
EOF
# Step 4: Clear CSS cache
rm -rf /var/cache/nginx/themes/*
nself cache clear --pattern "theme:*"
# Step 5: Restart services
nself restart nginx
nself restart app
# Step 6: Test theme loading
curl -I https://yourdomain.com/themes/YOUR_TENANT_ID/theme.cssVerification:
# Check CSS variables are defined
curl -s https://yourdomain.com | grep -o "var(--brand-[^)]*)"
# Test theme colors are applied
# Open browser DevTools -> Elements tab
# Inspect element -> Computed styles
# Should show custom colorsPrevention:
- Version theme CSS files (theme.css?v=123456)
- Validate CSS before saving
- Use CSS variables for consistency
- Implement theme preview
- Test theme changes in staging
Symptoms:
- Theme toggle doesn't switch modes
- Wrong theme applied on page load
- Theme preference not saved
- Flash of wrong theme on load
Diagnosis:
# 1. Check theme preference storage
# Browser localStorage
# Open DevTools -> Application -> Local Storage
# Look for: theme_mode or darkMode
# 2. Check database preference
nself db query "SELECT user_id, theme_preference FROM public.user_preferences WHERE user_id = 'USER_ID';"
# 3. Check CSS media query
curl -s https://yourdomain.com/theme.css | grep "@media.*prefers-color-scheme"
# 4. Test JavaScript theme switcher
curl -s https://yourdomain.com/js/theme-switcher.js
# 5. Check for conflicting styles
# DevTools -> Elements -> Styles panelSolution:
# Step 1: Implement proper theme detection and application
cat > /var/www/html/js/theme-manager.js <<'EOF'
// Theme Manager - Load before page renders to prevent flash
(function() {
// Get saved preference or system preference
const getThemePreference = () => {
const saved = localStorage.getItem('theme_mode');
if (saved) return saved;
// Check system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
};
// Apply theme immediately
const theme = getThemePreference();
document.documentElement.setAttribute('data-theme', theme);
// Theme toggle function
window.toggleTheme = function() {
const current = document.documentElement.getAttribute('data-theme');
const newTheme = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme_mode', newTheme);
// Sync to server (optional)
fetch('/api/user/preferences', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme_preference: newTheme })
});
};
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('theme_mode')) {
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
})();
EOF
# Step 2: Add theme CSS with proper specificity
cat > /var/www/html/css/theme.css <<'EOF'
/* Default (light) theme */
:root,
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
}
/* Dark theme */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #f8f9fa;
--text-secondary: #adb5bd;
--border-color: #495057;
}
/* Apply variables */
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s, color 0.3s;
}
/* Support system preference if no explicit choice */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #f8f9fa;
--text-secondary: #adb5bd;
}
}
EOF
# Step 3: Load theme script in <head> (before body)
cat > /tmp/theme-html-structure.html <<'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="light dark">
<!-- Load theme immediately to prevent flash -->
<script src="/js/theme-manager.js"></script>
<!-- Theme CSS -->
<link rel="stylesheet" href="/css/theme.css">
</head>
<body>
<!-- Theme toggle button -->
<button onclick="toggleTheme()" class="theme-toggle">
<span class="theme-icon">๐</span>
</button>
</body>
</html>
EOF
# Step 4: Test theme persistence
curl -c cookies.txt -b cookies.txt https://yourdomain.com
# Step 5: Restart services
nself restart nginxVerification:
# Test theme switching
# 1. Open browser DevTools -> Console
# 2. Run: toggleTheme()
# 3. Check: document.documentElement.getAttribute('data-theme')
# 4. Refresh page - theme should persist
# Check localStorage
# DevTools -> Application -> Local Storage -> theme_modePrevention:
- Load theme script before page renders
- Use CSS variables for all colors
- Respect system preferences
- Persist preference in localStorage and server
- Test on page refresh and navigation
- Implement smooth transitions
Symptoms:
- User sees wrong tenant's branding
- Data leakage between tenants
- Assets accessible across tenants
- Session mixing
Diagnosis:
# 1. Check tenant identification
curl -v https://yourdomain.com/api/branding 2>&1 | grep "X-Tenant-ID"
# 2. Verify database queries use tenant filter
nself db query "EXPLAIN SELECT * FROM public.branding WHERE tenant_id = 'TENANT_ID';"
# 3. Check for missing tenant filters
nself db query "
SELECT query
FROM pg_stat_statements
WHERE query LIKE '%FROM public.branding%'
AND query NOT LIKE '%tenant_id%';
"
# 4. Check session isolation
nself redis keys "session:*" | head -20
# 5. Audit asset access
nself logs nginx | grep "GET.*assets" | grep -v "X-Tenant-ID"Solution:
# Step 1: Enforce tenant context in all queries
# Add database view that automatically filters by tenant
nself db query "
-- Create secure view that enforces tenant filtering
CREATE OR REPLACE VIEW tenant_branding AS
SELECT * FROM public.branding
WHERE tenant_id = current_setting('app.current_tenant_id', true);
-- Set row-level security
ALTER TABLE public.branding ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON public.branding
USING (tenant_id = current_setting('app.current_tenant_id', true));
"
# Step 2: Set tenant context in application middleware
cat > /tmp/tenant-middleware.js <<'EOF'
// Express.js middleware example
const tenantMiddleware = async (req, res, next) => {
// Get tenant from various sources
const tenantId =
req.headers['x-tenant-id'] ||
req.subdomains[0] ||
await getTenantFromDomain(req.hostname) ||
req.session?.tenantId;
if (!tenantId) {
return res.status(400).json({ error: 'Tenant ID required' });
}
// Verify tenant exists and is active
const tenant = await db.query(
'SELECT id, status FROM public.tenants WHERE id = $1',
[tenantId]
);
if (!tenant || tenant.status !== 'active') {
return res.status(403).json({ error: 'Invalid tenant' });
}
// Set tenant context for database queries
req.tenantId = tenantId;
await db.query("SET app.current_tenant_id = $1", [tenantId]);
// Set header for downstream services
res.setHeader('X-Tenant-ID', tenantId);
next();
};
module.exports = tenantMiddleware;
EOF
# Step 3: Enforce tenant in asset URLs
cat > /tmp/asset-middleware.js <<'EOF'
// Asset access control
const assetMiddleware = async (req, res, next) => {
const assetPath = req.path; // e.g., /logos/logo.png
const tenantId = req.headers['x-tenant-id'];
if (!tenantId) {
return res.status(403).json({ error: 'Tenant ID required' });
}
// Verify asset belongs to tenant
const asset = await db.query(
'SELECT tenant_id FROM public.assets WHERE path = $1',
[assetPath]
);
if (!asset || asset.tenant_id !== tenantId) {
return res.status(404).json({ error: 'Asset not found' });
}
next();
};
EOF
# Step 4: Use tenant-scoped Redis keys
cat > /tmp/redis-helper.js <<'EOF'
// Redis key helper
const getTenantKey = (tenantId, key) => {
return `tenant:${tenantId}:${key}`;
};
// Usage:
const brandingKey = getTenantKey(tenantId, 'branding');
await redis.get(brandingKey);
EOF
# Step 5: Audit tenant isolation
cat > /tmp/audit-tenant-isolation.sh <<'EOF'
#!/bin/bash
# Check for queries without tenant filtering
echo "Checking for unsafe queries..."
# Check application code
grep -r "SELECT.*FROM.*branding" src/ | grep -v "WHERE.*tenant_id"
# Check database queries
nself db query "
SELECT query, calls
FROM pg_stat_statements
WHERE query LIKE '%FROM%branding%'
AND query NOT LIKE '%tenant_id%'
ORDER BY calls DESC;
"
EOF
chmod +x /tmp/audit-tenant-isolation.sh
bash /tmp/audit-tenant-isolation.shVerification:
# Test tenant isolation
# 1. Request branding as Tenant A
curl -H "X-Tenant-ID: tenant-a" https://yourdomain.com/api/branding
# 2. Request branding as Tenant B
curl -H "X-Tenant-ID: tenant-b" https://yourdomain.com/api/branding
# 3. Try to access Tenant A's assets as Tenant B
curl -H "X-Tenant-ID: tenant-b" https://yourdomain.com/assets/tenant-a/logo.png
# Should return 404
# 4. Check database policies
nself db query "SELECT * FROM pg_policies WHERE tablename = 'branding';"Prevention:
- Enable row-level security on all tenant tables
- Use database policies for automatic filtering
- Audit all queries for tenant filtering
- Use middleware to set tenant context
- Implement tenant-scoped caching
- Regular security audits
Symptoms:
- Sub-tenant not inheriting parent branding
- Override settings not applying
- Fallback branding not working
Diagnosis:
# 1. Check tenant hierarchy
nself db query "
SELECT t1.id, t1.name, t2.name as parent_name
FROM public.tenants t1
LEFT JOIN public.tenants t2 ON t1.parent_tenant_id = t2.id
WHERE t1.id = 'SUB_TENANT_ID';
"
# 2. Check branding inheritance
nself db query "
WITH RECURSIVE tenant_hierarchy AS (
SELECT id, parent_tenant_id, 1 as level
FROM public.tenants
WHERE id = 'SUB_TENANT_ID'
UNION ALL
SELECT t.id, t.parent_tenant_id, th.level + 1
FROM public.tenants t
JOIN tenant_hierarchy th ON t.id = th.parent_tenant_id
)
SELECT th.level, b.*
FROM tenant_hierarchy th
JOIN public.branding b ON b.tenant_id = th.id
ORDER BY th.level;
"
# 3. Check override settings
nself db query "SELECT * FROM public.branding_overrides WHERE tenant_id = 'SUB_TENANT_ID';"
# 4. Test branding API
curl -H "X-Tenant-ID: SUB_TENANT_ID" https://api.yourdomain.com/brandingSolution:
# Step 1: Implement branding inheritance function
nself db query "
CREATE OR REPLACE FUNCTION get_effective_branding(p_tenant_id UUID)
RETURNS TABLE (
tenant_id UUID,
brand_name TEXT,
logo_url TEXT,
primary_color TEXT,
-- ... other fields
) AS \$\$
BEGIN
RETURN QUERY
WITH RECURSIVE tenant_hierarchy AS (
-- Start with requested tenant
SELECT t.id, t.parent_tenant_id, 1 as level
FROM public.tenants t
WHERE t.id = p_tenant_id
UNION ALL
-- Recursively get parent tenants
SELECT t.id, t.parent_tenant_id, th.level + 1
FROM public.tenants t
JOIN tenant_hierarchy th ON t.id = th.parent_tenant_id
WHERE th.level < 10 -- Prevent infinite loops
),
branding_chain AS (
SELECT
th.level,
b.*
FROM tenant_hierarchy th
LEFT JOIN public.branding b ON b.tenant_id = th.id
ORDER BY th.level ASC -- Child first, then parents
)
SELECT DISTINCT ON (1)
p_tenant_id as tenant_id,
COALESCE(
(SELECT brand_name FROM branding_chain WHERE brand_name IS NOT NULL ORDER BY level LIMIT 1),
'Default Brand'
) as brand_name,
COALESCE(
(SELECT logo_url FROM branding_chain WHERE logo_url IS NOT NULL ORDER BY level LIMIT 1),
'/assets/default-logo.png'
) as logo_url,
COALESCE(
(SELECT primary_color FROM branding_chain WHERE primary_color IS NOT NULL ORDER BY level LIMIT 1),
'#007bff'
) as primary_color;
-- ... repeat for all fields
END;
\$\$ LANGUAGE plpgsql;
"
# Step 2: Use inheritance function in API
cat > /tmp/branding-api-with-inheritance.js <<'EOF'
// GET /api/branding
app.get('/api/branding', async (req, res) => {
const tenantId = req.tenantId;
// Get effective branding (with inheritance)
const branding = await db.query(
'SELECT * FROM get_effective_branding($1)',
[tenantId]
);
// Apply overrides
const overrides = await db.query(
'SELECT * FROM public.branding_overrides WHERE tenant_id = $1',
[tenantId]
);
// Merge branding with overrides
const effective = { ...branding, ...overrides };
res.json(effective);
});
EOF
# Step 3: Create override management UI
cat > /tmp/override-example.sql <<'EOF'
-- Branding overrides table
CREATE TABLE IF NOT EXISTS public.branding_overrides (
tenant_id UUID PRIMARY KEY REFERENCES public.tenants(id),
override_brand_name BOOLEAN DEFAULT false,
override_logo BOOLEAN DEFAULT false,
override_colors BOOLEAN DEFAULT false,
brand_name TEXT,
logo_url TEXT,
primary_color TEXT,
-- ... other override fields
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Example: Sub-tenant overrides only logo
INSERT INTO public.branding_overrides (tenant_id, override_logo, logo_url)
VALUES ('sub-tenant-id', true, 'https://cdn.example.com/sub-tenant-logo.png');
EOF
# Step 4: Test inheritance
nself db query "SELECT * FROM get_effective_branding('SUB_TENANT_ID');"Verification:
# Test inheritance hierarchy
curl -H "X-Tenant-ID: PARENT_TENANT_ID" https://api.yourdomain.com/branding | jq .
curl -H "X-Tenant-ID: SUB_TENANT_ID" https://api.yourdomain.com/branding | jq .
# Sub-tenant should inherit parent's branding except where overriddenPrevention:
- Document tenant hierarchy clearly
- Implement inheritance testing
- Provide override UI
- Cache effective branding per tenant
- Audit inheritance chain
Symptoms:
- Logo/images loading slowly
- High Time to First Byte (TTFB)
- Slow page load times
- Timeout errors
Diagnosis:
# 1. Measure asset load times
curl -w "@-" -o /dev/null -s https://cdn.yourdomain.com/logos/logo.png <<'EOF'
time_namelookup: %{time_namelookup}s
time_connect: %{time_connect}s
time_appconnect: %{time_appconnect}s
time_pretransfer: %{time_pretransfer}s
time_starttransfer: %{time_starttransfer}s
time_total: %{time_total}s
size_download: %{size_download} bytes
speed_download: %{speed_download} bytes/sec
EOF
# 2. Check asset file sizes
ls -lh /var/lib/nself/storage/branding/logos/
# 3. Check CDN performance
curl -I https://cdn.yourdomain.com/logos/logo.png | grep -i "cache\|age\|hit"
# 4. Check nginx performance
nself logs nginx | grep "upstream_response_time"
# 5. Test with WebPageTest
# Visit: https://www.webpagetest.org/Solution:
# Step 1: Optimize images
cd /var/lib/nself/storage/branding/logos/
# Install optimization tools
apt-get install -y optipng jpegoptim webp
# Optimize PNGs
find . -name "*.png" -exec optipng -o7 {} \;
# Optimize JPEGs
find . -name "*.jpg" -exec jpegoptim --strip-all --max=85 {} \;
# Generate WebP versions
find . -name "*.png" -exec sh -c 'cwebp -q 85 "$1" -o "${1%.png}.webp"' _ {} \;
find . -name "*.jpg" -exec sh -c 'cwebp -q 85 "$1" -o "${1%.jpg}.webp"' _ {} \;
# Step 2: Enable nginx caching
cat >> /etc/nginx/conf.d/asset-caching.conf <<'EOF'
# Asset caching
proxy_cache_path /var/cache/nginx/assets
levels=1:2
keys_zone=assets:10m
max_size=1g
inactive=30d
use_temp_path=off;
server {
location /assets/ {
proxy_cache assets;
proxy_cache_valid 200 30d;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
add_header Cache-Control "public, max-age=2592000, immutable";
expires 30d;
}
}
EOF
# Step 3: Enable compression
cat >> /etc/nginx/conf.d/compression.conf <<'EOF'
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
image/svg+xml;
# Brotli compression (if module available)
brotli on;
brotli_comp_level 6;
brotli_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss;
EOF
# Step 4: Implement lazy loading
cat > /tmp/lazy-load-images.html <<'EOF'
<!-- Use native lazy loading -->
<img
src="https://cdn.yourdomain.com/logos/logo.png"
loading="lazy"
alt="Logo"
>
<!-- Or use Intersection Observer -->
<script>
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
imageObserver.unobserve(img);
}
});
});
lazyImages.forEach(img => imageObserver.observe(img));
</script>
EOF
# Step 5: Use CDN for assets
# Configure CDN (Cloudflare example)
# 1. Add CNAME: cdn.yourdomain.com -> yourdomain.com
# 2. Enable "Cache Everything" page rule
# 3. Set Browser Cache TTL: 1 month
# 4. Enable Auto Minify: JS, CSS, HTML
# Step 6: Implement HTTP/2 and HTTP/3
cat >> /etc/nginx/sites-enabled/default.conf <<'EOF'
server {
listen 443 ssl http2;
listen 443 quic reuseport; # HTTP/3
ssl_certificate /etc/ssl/certs/yourdomain.pem;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
# HTTP/3 advertisement
add_header Alt-Svc 'h3=":443"; ma=86400';
}
EOF
# Step 7: Restart nginx
nginx -t && nginx -s reloadVerification:
# Test asset load time (should be < 500ms)
curl -w "Total time: %{time_total}s\n" -o /dev/null -s https://cdn.yourdomain.com/logos/logo.png
# Check cache status
curl -I https://cdn.yourdomain.com/logos/logo.png | grep "X-Cache-Status"
# Should show: HIT
# Test compression
curl -H "Accept-Encoding: gzip" -I https://cdn.yourdomain.com/assets/styles.css | grep "Content-Encoding"
# Should show: gzip
# Measure full page load
curl -w "@-" -o /dev/null -s https://yourdomain.com <<'EOF'
Total time: %{time_total}s
Size: %{size_download} bytes
Speed: %{speed_download} bytes/sec
EOFPrevention:
- Optimize images before upload
- Use modern formats (WebP, AVIF)
- Implement CDN from day one
- Monitor asset sizes
- Set up performance budgets
- Use lazy loading for images
Symptoms:
- Slow branding API responses
- High database CPU usage
- Query timeouts
- Application lag
Diagnosis:
# 1. Check slow queries
nself db query "
SELECT
query,
calls,
total_time,
mean_time,
max_time
FROM pg_stat_statements
WHERE query LIKE '%branding%'
ORDER BY mean_time DESC
LIMIT 10;
"
# 2. Check missing indexes
nself db query "
SELECT
schemaname,
tablename,
attname,
n_distinct,
correlation
FROM pg_stats
WHERE tablename = 'branding'
AND schemaname = 'public';
"
# 3. Analyze table
nself db query "ANALYZE public.branding;"
# 4. Check table statistics
nself db query "
SELECT
relname,
n_tup_ins,
n_tup_upd,
n_tup_del,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'branding';
"
# 5. Explain query performance
nself db query "EXPLAIN ANALYZE SELECT * FROM public.branding WHERE tenant_id = 'TENANT_ID';"Solution:
# Step 1: Add missing indexes
nself db query "
-- Index on tenant_id (most common filter)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_branding_tenant_id
ON public.branding(tenant_id);
-- Index on domain for custom domain lookups
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_custom_domains_domain
ON public.custom_domains(domain);
-- Composite index for common queries
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_branding_tenant_enabled
ON public.branding(tenant_id, enabled)
WHERE enabled = true;
-- Index on updated_at for cache invalidation
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_branding_updated_at
ON public.branding(updated_at DESC);
"
# Step 2: Implement query result caching
cat > /tmp/branding-cache.js <<'EOF'
const redis = require('redis');
const client = redis.createClient();
async function getBrandingCached(tenantId) {
const cacheKey = `branding:${tenantId}`;
// Try cache first
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Query database
const branding = await db.query(
'SELECT * FROM public.branding WHERE tenant_id = $1',
[tenantId]
);
// Cache for 5 minutes
await client.setex(cacheKey, 300, JSON.stringify(branding));
return branding;
}
EOF
# Step 3: Optimize database connection pooling
cat >> .env <<'EOF'
# Database connection pool
DB_POOL_MIN=2
DB_POOL_MAX=10
DB_POOL_IDLE_TIMEOUT=10000
DB_CONNECTION_TIMEOUT=2000
EOF
# Step 4: Partition large tables (if needed)
nself db query "
-- Partition branding table by tenant_id hash
CREATE TABLE public.branding_partitioned (
LIKE public.branding INCLUDING ALL
) PARTITION BY HASH (tenant_id);
-- Create 8 partitions
CREATE TABLE public.branding_part_0 PARTITION OF public.branding_partitioned
FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE public.branding_part_1 PARTITION OF public.branding_partitioned
FOR VALUES WITH (MODULUS 8, REMAINDER 1);
-- ... repeat for 2-7
-- Migrate data (during maintenance window)
-- INSERT INTO public.branding_partitioned SELECT * FROM public.branding;
"
# Step 5: Vacuum and analyze
nself db query "VACUUM ANALYZE public.branding;"
# Step 6: Enable query plan caching (PostgreSQL 12+)
nself db query "ALTER SYSTEM SET plan_cache_mode = 'auto';"
nself db query "SELECT pg_reload_conf();"Verification:
# Test query performance
time nself db query "SELECT * FROM public.branding WHERE tenant_id = 'TENANT_ID';"
# Should be < 10ms
# Check index usage
nself db query "
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE tablename = 'branding';
"
# Verify cache hit rate
nself db query "
SELECT
sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_ratio
FROM pg_statio_user_tables
WHERE relname = 'branding';
"
# Should be > 0.99 (99%)Prevention:
- Create indexes for all filtered columns
- Implement application-level caching
- Monitor query performance regularly
- Set up slow query logging
- Regular VACUUM and ANALYZE
- Use connection pooling
Symptoms:
- JavaScript execution in style tags
- Malicious content injection
- Cross-site scripting attacks
- Data exfiltration attempts
Diagnosis:
# 1. Scan custom CSS for dangerous patterns
nself db query "SELECT tenant_id, custom_css FROM public.branding WHERE custom_css LIKE '%<script%' OR custom_css LIKE '%javascript:%';"
# 2. Check for expression() attacks (IE)
nself db query "SELECT tenant_id, custom_css FROM public.branding WHERE custom_css LIKE '%expression(%';"
# 3. Check for import attacks
nself db query "SELECT tenant_id, custom_css FROM public.branding WHERE custom_css LIKE '%@import%';"
# 4. Audit CSS sanitization
grep -r "sanitizeCSS\|purify" src/
# 5. Test with known XSS payloads
curl -X POST https://api.yourdomain.com/branding/css \
-H "Content-Type: application/json" \
-d '{"custom_css":"body{background:url(javascript:alert(1))}"}'Solution:
# Step 1: Implement CSS sanitization
npm install css-tree
cat > /tmp/css-sanitizer.js <<'EOF'
const csstree = require('css-tree');
function sanitizeCSS(css) {
try {
// Parse CSS
const ast = csstree.parse(css);
// Dangerous patterns to remove
const dangerousPatterns = [
/javascript:/gi,
/expression\(/gi,
/behavior:/gi,
/@import/gi,
/vbscript:/gi,
/data:text\/html/gi,
];
// Walk AST and remove dangerous nodes
csstree.walk(ast, function(node) {
if (node.type === 'Url') {
const value = node.value.value;
for (const pattern of dangerousPatterns) {
if (pattern.test(value)) {
this.remove(node);
return;
}
}
}
if (node.type === 'Atrule' && node.name === 'import') {
this.remove(node);
}
});
// Generate sanitized CSS
return csstree.generate(ast);
} catch (err) {
console.error('CSS parsing error:', err);
return ''; // Return empty string on parse error
}
}
// Whitelist only safe properties
const SAFE_CSS_PROPERTIES = [
'color', 'background-color', 'background',
'font-family', 'font-size', 'font-weight',
'border', 'border-radius', 'padding', 'margin',
'width', 'height', 'display', 'text-align',
// ... add more as needed
];
function sanitizeCSSStrict(css) {
const ast = csstree.parse(css);
csstree.walk(ast, function(node) {
if (node.type === 'Declaration') {
if (!SAFE_CSS_PROPERTIES.includes(node.property)) {
this.remove(node);
}
}
});
return csstree.generate(ast);
}
module.exports = { sanitizeCSS, sanitizeCSSStrict };
EOF
# Step 2: Add Content Security Policy
cat >> /etc/nginx/conf.d/csp.conf <<'EOF'
# Content Security Policy
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.yourdomain.com;
style-src 'self' 'unsafe-inline' https://cdn.yourdomain.com;
img-src 'self' data: https:;
font-src 'self' data: https:;
connect-src 'self' https://api.yourdomain.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
" always;
# Additional security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
EOF
# Step 3: Validate CSS on input
cat > /tmp/css-validation.js <<'EOF'
const { sanitizeCSS } = require('./css-sanitizer');
app.post('/api/branding/css', async (req, res) => {
const { custom_css } = req.body;
// Validate input
if (!custom_css || typeof custom_css !== 'string') {
return res.status(400).json({ error: 'Invalid CSS' });
}
// Size limit (e.g., 50KB)
if (custom_css.length > 50 * 1024) {
return res.status(400).json({ error: 'CSS too large' });
}
// Sanitize CSS
const sanitized = sanitizeCSS(custom_css);
// Save sanitized version
await db.query(
'UPDATE public.branding SET custom_css = $1 WHERE tenant_id = $2',
[sanitized, req.tenantId]
);
res.json({ message: 'CSS updated', css: sanitized });
});
EOF
# Step 4: Audit existing CSS
nself db query "
-- Find potentially dangerous CSS
SELECT
tenant_id,
LENGTH(custom_css) as css_size,
custom_css
FROM public.branding
WHERE custom_css IS NOT NULL
AND (
custom_css LIKE '%javascript:%'
OR custom_css LIKE '%expression(%'
OR custom_css LIKE '%@import%'
OR custom_css LIKE '%behavior:%'
);
"
# Step 5: Sanitize existing CSS in database
cat > /tmp/sanitize-existing-css.js <<'EOF'
const { sanitizeCSS } = require('./css-sanitizer');
async function sanitizeAllCSS() {
const results = await db.query('SELECT tenant_id, custom_css FROM public.branding WHERE custom_css IS NOT NULL');
for (const row of results) {
const sanitized = sanitizeCSS(row.custom_css);
await db.query(
'UPDATE public.branding SET custom_css = $1 WHERE tenant_id = $2',
[sanitized, row.tenant_id]
);
console.log(`Sanitized CSS for tenant ${row.tenant_id}`);
}
}
sanitizeAllCSS().then(() => console.log('Done'));
EOF
node /tmp/sanitize-existing-css.js
# Step 6: Restart nginx
nginx -s reloadVerification:
# Test XSS prevention
curl -X POST https://api.yourdomain.com/branding/css \
-H "Content-Type: application/json" \
-H "Authorization: Bearer TOKEN" \
-d '{"custom_css":"body{background:url(javascript:alert(1))}"}'
# Should reject or sanitize the input
# Check CSP headers
curl -I https://yourdomain.com | grep "Content-Security-Policy"
# Audit for unsafe CSS
nself db query "SELECT COUNT(*) FROM public.branding WHERE custom_css LIKE '%javascript:%';"
# Should return: 0Prevention:
- Always sanitize CSS input
- Use CSS parsers, not regex
- Implement strict CSP
- Whitelist safe CSS properties
- Regular security audits
- Limit CSS size
- Educate users about safe CSS
Symptoms:
- Unauthorized domains verified
- Domain takeover attempts
- Verification token leakage
Diagnosis:
# 1. Check for domains without proper verification
nself db query "
SELECT
domain,
verified,
verification_token,
verified_at,
created_at
FROM public.custom_domains
WHERE verified = true
AND (verified_at IS NULL OR verified_at < created_at);
"
# 2. Check for reused verification tokens
nself db query "
SELECT
verification_token,
COUNT(*) as count
FROM public.custom_domains
GROUP BY verification_token
HAVING COUNT(*) > 1;
"
# 3. Check for expired but verified domains
nself db query "
SELECT *
FROM public.custom_domains
WHERE verified = true
AND verified_at < NOW() - INTERVAL '90 days';
"
# 4. Audit verification logs
nself logs domain-verification | grep -i "bypass\|failure\|error"Solution:
# Step 1: Generate secure verification tokens
cat > /tmp/generate-verification-token.js <<'EOF'
const crypto = require('crypto');
function generateVerificationToken() {
// Generate cryptographically secure random token
const token = crypto.randomBytes(32).toString('hex');
// Format: nself-verify-{timestamp}-{random}
const timestamp = Date.now();
return `nself-verify-${timestamp}-${token}`;
}
// Usage:
const token = generateVerificationToken();
// Example: nself-verify-1706630400000-a1b2c3d4e5f6...
EOF
# Step 2: Implement proper verification flow
cat > /tmp/domain-verification.js <<'EOF'
const dns = require('dns').promises;
async function verifyDomain(domain, expectedToken) {
try {
// 1. Check TXT record
const records = await dns.resolveTxt(`_nself-verify.${domain}`);
const flatRecords = records.flat();
// 2. Find matching token
const found = flatRecords.some(record => record === expectedToken);
if (!found) {
return {
verified: false,
error: 'Verification token not found in DNS'
};
}
// 3. Check from multiple DNS servers
const dnsServers = ['8.8.8.8', '1.1.1.1', '9.9.9.9'];
const verifications = await Promise.all(
dnsServers.map(server =>
verifyFromDNSServer(domain, expectedToken, server)
)
);
// 4. Require majority consensus (2 out of 3)
const successCount = verifications.filter(v => v.verified).length;
if (successCount < 2) {
return {
verified: false,
error: 'DNS verification failed on multiple servers'
};
}
// 5. Update database
await db.query(`
UPDATE public.custom_domains
SET verified = true,
verified_at = NOW(),
verification_attempts = verification_attempts + 1
WHERE domain = $1
AND verification_token = $2
AND verified = false
`, [domain, expectedToken]);
// 6. Generate SSL certificate
await generateSSLCertificate(domain);
return { verified: true };
} catch (err) {
console.error('Verification error:', err);
// Log failed attempt
await db.query(`
UPDATE public.custom_domains
SET verification_attempts = verification_attempts + 1,
last_verification_error = $1
WHERE domain = $2
`, [err.message, domain]);
return {
verified: false,
error: err.message
};
}
}
async function verifyFromDNSServer(domain, token, dnsServer) {
const resolver = new dns.Resolver();
resolver.setServers([dnsServer]);
try {
const records = await resolver.resolveTxt(`_nself-verify.${domain}`);
const found = records.flat().some(r => r === token);
return { verified: found, server: dnsServer };
} catch {
return { verified: false, server: dnsServer };
}
}
EOF
# Step 3: Add rate limiting
cat > /tmp/verification-rate-limit.js <<'EOF'
const rateLimit = require('express-rate-limit');
const verificationLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many verification attempts, try again later',
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => {
// Rate limit per domain
return req.body.domain || req.ip;
}
});
app.post('/api/domains/verify', verificationLimiter, async (req, res) => {
// Verification logic
});
EOF
# Step 4: Implement re-verification
nself db query "
-- Add re-verification schedule
ALTER TABLE public.custom_domains
ADD COLUMN IF NOT EXISTS next_verification_at TIMESTAMPTZ DEFAULT NOW() + INTERVAL '90 days';
-- Create function to schedule re-verification
CREATE OR REPLACE FUNCTION schedule_domain_reverification()
RETURNS void AS \$\$
BEGIN
UPDATE public.custom_domains
SET verified = false,
next_verification_at = NOW() + INTERVAL '7 days'
WHERE verified = true
AND verified_at < NOW() - INTERVAL '90 days';
END;
\$\$ LANGUAGE plpgsql;
"
# Step 5: Set up automated re-verification cron job
cat > /tmp/domain-reverification.sh <<'EOF'
#!/bin/bash
# Run daily via cron
nself db query "SELECT schedule_domain_reverification();"
# Re-verify domains
nself db query "
SELECT domain, verification_token
FROM public.custom_domains
WHERE next_verification_at < NOW()
AND enabled = true;
" | while read domain token; do
echo "Re-verifying $domain..."
nself domain verify "$domain"
done
EOF
chmod +x /tmp/domain-reverification.sh
# Add to crontab
(crontab -l 2>/dev/null; echo "0 2 * * * /tmp/domain-reverification.sh") | crontab -Verification:
# Test verification process
nself domain verify yourdomain.com
# Check verification status
nself db query "SELECT * FROM public.custom_domains WHERE domain = 'yourdomain.com';"
# Test rate limiting
for i in {1..10}; do
curl -X POST https://api.yourdomain.com/domains/verify \
-H "Content-Type: application/json" \
-d '{"domain":"test.com"}'
done
# Should get rate limited after 5 attemptsPrevention:
- Use cryptographically secure tokens
- Verify from multiple DNS servers
- Implement rate limiting
- Regular re-verification (every 90 days)
- Log all verification attempts
- Monitor for suspicious patterns
- Validate token format
Symptoms:
- Layout broken in Safari/Firefox
- Colors rendering differently
- Fonts not loading
- CSS features not working
Diagnosis:
# 1. Check CSS for browser-specific issues
grep -E "(:-moz-|-webkit-|-ms-)" /var/www/html/css/theme.css
# 2. Validate CSS
npm install -g csslint
csslint /var/www/html/css/theme.css
# 3. Check for modern CSS features
grep -E "(grid|flex|var\()" /var/www/html/css/theme.css
# 4. Test in BrowserStack
# Visit: https://www.browserstack.com/
# 5. Check console errors in different browsers
# Safari: Develop โ Show Error Console
# Firefox: Tools โ Web Developer โ Browser ConsoleSolution:
# Step 1: Add vendor prefixes
npm install -g autoprefixer postcss-cli
# Process CSS with autoprefixer
postcss /var/www/html/css/theme.css \
--use autoprefixer \
-o /var/www/html/css/theme-prefixed.css \
--no-map
# Step 2: Create browser compatibility CSS
cat > /var/www/html/css/theme-compat.css <<'EOF'
/* CSS Custom Properties fallbacks */
:root {
--brand-primary: #007bff;
--brand-secondary: #6c757d;
}
.button {
/* Modern */
background-color: var(--brand-primary);
/* Fallback for IE11 */
background-color: #007bff;
}
/* Flexbox with fallback */
.flex-container {
display: flex;
display: -webkit-flex; /* Safari */
display: -ms-flexbox; /* IE10 */
}
/* Grid with fallback */
.grid-container {
display: grid;
/* Fallback for older browsers */
display: flex;
flex-wrap: wrap;
}
@supports (display: grid) {
.grid-container {
display: grid;
}
}
/* Font rendering */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
EOF
# Step 3: Add feature detection
cat > /var/www/html/js/feature-detection.js <<'EOF'
// Feature detection
const supportsGrid = CSS.supports('display', 'grid');
const supportsCustomProperties = CSS.supports('--fake-var', '0');
const supportsFlex = CSS.supports('display', 'flex');
// Add classes to html element
if (supportsGrid) document.documentElement.classList.add('supports-grid');
if (supportsCustomProperties) document.documentElement.classList.add('supports-custom-props');
if (supportsFlex) document.documentElement.classList.add('supports-flex');
// Load polyfills if needed
if (!supportsCustomProperties) {
// Load CSS Variables polyfill
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/css-vars-ponyfill@2';
document.head.appendChild(script);
}
EOF
# Step 4: Add browserslist configuration
cat > /var/www/html/.browserslistrc <<'EOF'
# Browsers to support
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 11
EOF
# Step 5: Test with different user agents
# Test Safari
curl -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15" \
https://yourdomain.com > /tmp/safari-test.html
# Test Firefox
curl -A "Mozilla/5.0 (X11; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0" \
https://yourdomain.com > /tmp/firefox-test.html
# Test Chrome
curl -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" \
https://yourdomain.com > /tmp/chrome-test.htmlBrowser-Specific CSS:
/* Safari-specific fixes */
@supports (-webkit-appearance: none) {
.safari-fix {
/* Safari-specific styles */
}
}
/* Firefox-specific fixes */
@-moz-document url-prefix() {
.firefox-fix {
/* Firefox-specific styles */
}
}
/* IE11 fallbacks (if needed) */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ie11-fix {
/* IE11-specific styles */
}
}Prevention:
- Use autoprefixer in build process
- Test in multiple browsers regularly
- Use feature detection, not browser detection
- Provide progressive enhancement
- Use modern CSS with fallbacks
- Follow web standards
Message: Error: Branding configuration not found for tenant
Causes:
- Tenant ID missing or incorrect
- Branding not initialized for tenant
- Database connection issue
- Cache inconsistency
Fix:
# Check if branding exists
nself db query "SELECT * FROM public.branding WHERE tenant_id = 'TENANT_ID';"
# If empty, initialize branding
nself db query "
INSERT INTO public.branding (tenant_id, brand_name, enabled)
VALUES ('TENANT_ID', 'Default Brand', true)
ON CONFLICT (tenant_id) DO NOTHING;
"
# Clear cache
nself cache clear --pattern "branding:TENANT_ID"
# Verify
curl -H "X-Tenant-ID: TENANT_ID" https://api.yourdomain.com/brandingMessage: Error: Domain already registered to another tenant
Causes:
- Domain already in use
- Previous tenant not cleaned up
- Database constraint violation
Fix:
# Check domain ownership
nself db query "SELECT * FROM public.custom_domains WHERE domain = 'yourdomain.com';"
# If domain should be transferred:
# 1. Verify current tenant is inactive or approves transfer
# 2. Update domain ownership
nself db query "
UPDATE public.custom_domains
SET tenant_id = 'NEW_TENANT_ID',
verified = false,
verification_token = 'NEW_TOKEN'
WHERE domain = 'yourdomain.com';
"
# Clear cache
nself cache clear --pattern "domain:yourdomain.com"Message: Error: Failed to generate SSL certificate
Causes:
- Domain not verified
- Rate limit exceeded (Let's Encrypt)
- DNS not propagated
- Certbot configuration issue
Fix:
# Check domain verification
dig TXT _nself-verify.yourdomain.com +short
# Check Let's Encrypt rate limits
# Visit: https://letsencrypt.org/docs/rate-limits/
# Wait for rate limit reset or use staging
certbot certonly --staging -d yourdomain.com
# Check certbot logs
tail -50 /var/log/letsencrypt/letsencrypt.log
# Manual certificate generation
certbot certonly \
--manual \
--preferred-challenges dns \
-d yourdomain.com \
--agree-tos \
--email [email protected]Message: Error: Asset upload failed
Causes:
- File too large
- Invalid file type
- Storage quota exceeded
- Permission issues
Fix:
# Check storage space
df -h /var/lib/nself/storage
# Check file permissions
ls -la /var/lib/nself/storage/branding/
# Fix permissions
chown -R nself:nself /var/lib/nself/storage/
chmod -R 755 /var/lib/nself/storage/
# Clean up old files
find /var/lib/nself/storage/branding/ -mtime +90 -type f -delete
# Test upload
curl -X POST https://api.yourdomain.com/branding/logo \
-H "Authorization: Bearer TOKEN" \
-F "[email protected]"Message: Error: Failed to render email template
Causes:
- Missing template variables
- Invalid template syntax
- Database connection issue
- Template not found
Fix:
# Check template exists
nself db query "SELECT * FROM public.email_templates WHERE name = 'welcome';"
# Validate template syntax
nself email validate-template --template welcome
# Test with sample data
nself email render-test \
--template welcome \
--data '{"user_name":"Test","verification_link":"https://example.com"}'
# Check required variables
nself db query "SELECT required_variables FROM public.email_templates WHERE name = 'welcome';"
# Fix template
nself db query "
UPDATE public.email_templates
SET body = 'corrected template HTML'
WHERE name = 'welcome';
"#!/bin/bash
# nself-branding-health-check.sh
echo "=== nself White-Label Health Check ==="
echo
# 1. Database connectivity
echo "1. Checking database..."
nself db query "SELECT 1;" >/dev/null 2>&1 && echo "โ Database connected" || echo "โ Database connection failed"
# 2. Redis connectivity
echo "2. Checking Redis..."
nself redis ping >/dev/null 2>&1 && echo "โ Redis connected" || echo "โ Redis connection failed"
# 3. Storage accessibility
echo "3. Checking storage..."
[ -d /var/lib/nself/storage ] && echo "โ Storage accessible" || echo "โ Storage not accessible"
# 4. Branding service
echo "4. Checking branding service..."
docker ps | grep -q branding && echo "โ Branding service running" || echo "โ Branding service not running"
# 5. Nginx
echo "5. Checking nginx..."
docker ps | grep -q nginx && echo "โ Nginx running" || echo "โ Nginx not running"
# 6. SSL certificates
echo "6. Checking SSL certificates..."
cert_count=$(find /etc/letsencrypt/live/ -name "cert.pem" 2>/dev/null | wc -l)
echo " Found $cert_count certificates"
# 7. Branding count
echo "7. Checking branding configurations..."
brand_count=$(nself db query "SELECT COUNT(*) FROM public.branding;" 2>/dev/null | tail -1)
echo " Total: $brand_count configurations"
# 8. Custom domains
echo "8. Checking custom domains..."
domain_count=$(nself db query "SELECT COUNT(*) FROM public.custom_domains WHERE verified = true;" 2>/dev/null | tail -1)
echo " Verified domains: $domain_count"
# 9. Recent errors
echo "9. Checking recent errors..."
error_count=$(nself logs branding --since 1h 2>/dev/null | grep -i error | wc -l)
echo " Errors in last hour: $error_count"
# 10. Cache status
echo "10. Checking cache..."
cache_keys=$(nself redis keys "branding:*" 2>/dev/null | wc -l)
echo " Cached branding: $cache_keys keys"
echo
echo "=== Health Check Complete ==="Usage:
chmod +x nself-branding-health-check.sh
./nself-branding-health-check.sh#!/bin/bash
# nself-branding-diagnostics.sh TENANT_ID
TENANT_ID="$1"
if [ -z "$TENANT_ID" ]; then
echo "Usage: $0 TENANT_ID"
exit 1
fi
echo "=== Branding Diagnostics for $TENANT_ID ==="
echo
# 1. Database record
echo "1. Database Record:"
nself db query "SELECT * FROM public.branding WHERE tenant_id = '$TENANT_ID';"
echo
# 2. Cache status
echo "2. Cache Status:"
nself redis get "branding:$TENANT_ID"
echo
# 3. Custom domains
echo "3. Custom Domains:"
nself db query "SELECT domain, verified, ssl_enabled FROM public.custom_domains WHERE tenant_id = '$TENANT_ID';"
echo
# 4. Assets
echo "4. Assets:"
ls -lh "/var/lib/nself/storage/branding/$TENANT_ID/" 2>/dev/null || echo "No assets found"
echo
# 5. Recent logs
echo "5. Recent Logs (last 20 lines):"
nself logs branding | grep "$TENANT_ID" | tail -20
echo
# 6. API test
echo "6. API Test:"
curl -s -H "X-Tenant-ID: $TENANT_ID" "https://api.yourdomain.com/branding" | jq .
echo
echo "=== Diagnostics Complete ==="Usage:
chmod +x nself-branding-diagnostics.sh
./nself-branding-diagnostics.sh YOUR_TENANT_ID# Health check
nself status
# Clear all caches
nself cache clear --all
# Restart branding services
nself restart branding nginx
# Check logs
nself logs branding --tail 50
nself logs nginx --tail 50
# Database queries
nself db query "SELECT * FROM public.branding WHERE tenant_id = 'ID';"
# Verify domain
nself domain verify yourdomain.com
# Generate SSL certificate
nself ssl generate yourdomain.com
# Test email template
nself email send-test --to [email protected] --template welcome
# Check storage space
df -h /var/lib/nself/storage/var/log/nself/branding.log # Branding service logs
/var/log/nself/domain-verification.log # Domain verification
/var/log/nginx/access.log # Nginx access
/var/log/nginx/error.log # Nginx errors
/var/log/letsencrypt/letsencrypt.log # SSL certificates
/var/log/postgresql/postgresql.log # Database logs
| Metric | Target | Acceptable | Poor |
|---|---|---|---|
| Asset load time | < 200ms | < 500ms | > 1s |
| API response time | < 100ms | < 300ms | > 1s |
| Database query time | < 10ms | < 50ms | > 200ms |
| Cache hit rate | > 95% | > 80% | < 60% |
| SSL handshake time | < 100ms | < 200ms | > 500ms |
| Email send time | < 2s | < 5s | > 10s |
- All custom CSS sanitized
- Content Security Policy enabled
- Domain verification active
- SSL certificates valid
- Row-level security enabled
- Asset access control working
- Rate limiting configured
- Regular security audits scheduled
- Self-service: Check this troubleshooting guide
- Diagnostics: Run health check and diagnostic scripts
- Documentation: Check main white-label documentation
- Support: Contact nself support with diagnostic output
- Emergency: For production issues, escalate immediately
- White-Label Architecture Documentation
- Multi-Tenant Guide
- Security Best Practices
- Performance Optimization
- API Documentation
Document Version: 1.0.0 Last Updated: January 30, 2026 Maintained By: nself Team