Skip to content

Tenant Provisioning Checklist

Complete checklist for deploying a new cbapp tenant instance.

Pre-Deployment

1. Gather Tenant Information

  • Tenant name (e.g., ky04, mi20-clevenger)
  • Domain name (e.g., ky04.nominate.ai)
  • Campaign name for display (e.g., "Ed Gallrein for Congress")
  • Short name for compact UI (e.g., "KY04")
  • Admin email (e.g., admin@ky04.nominate.ai)
  • Port allocation (check existing tenants, increment by 10):
  • Frontend port: ______
  • Backend port: ______

2. Port Allocation Reference

Tenant Frontend Backend
testsite 32300 32301
mi20-clevenger 32310 32311
ky04 32320 32321
zach-lahn-for-governor 32350 32351
next tenant 32360 32361

Deployment Steps

3. Create Tenant Directory

TENANT_NAME="newtenant"
TENANT_DIR="/home/bisenbek/projects/nominate/$TENANT_NAME"

# Clone from cbapp repo
git clone git@github.com:Nominate-AI/cbapp.git $TENANT_DIR
cd $TENANT_DIR

# Verify
ls -la

4. Create Virtual Environment

cd $TENANT_DIR

# Create venv (use system Python or pyenv)
python3 -m venv venv

# Or use shared pyenv environment
# Just ensure the right Python is available

# Install dependencies
./venv/bin/pip install -r requirements.txt

5. Configure Environment

# Copy template
cp .env.example .env

# Edit with tenant-specific values
nano .env

Required .env values:

# Identity
APP_NAME="Campaign Name"
APP_NAME_SHORT="ABBR"
PROJECT_NAME="tenant-name"
ADMIN_EMAIL="admin@tenant.nominate.ai"

# Ports (from allocation above)
FRONTEND_PORT=32360
BACKEND_PORT=32361

# URLs
FRONTEND_URL="https://tenant.nominate.ai"
BACKEND_API_URL="http://localhost:32361/api"
WS_BASE_URL="wss://tenant.nominate.ai"

# Database (use DB_PATH, not DATABASE_URL)
DB_PATH="db/pocket.db"

# Secret key (generate fresh)
SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"

# API key for service-to-service auth
API_KEYS="$(python -c 'import secrets; print(secrets.token_hex(16))')"

6. Initialize Database

cd $TENANT_DIR

# Create database directory
mkdir -p db

# Run schema creation
python scripts/create_schema.py

# Verify
ls -la db/
# Should see: pocket.db

7. Create Admin User

cd $TENANT_DIR

# Create initial admin user
python scripts/create_admin_user.py \
    --username admin \
    --email admin@tenant.nominate.ai \
    --password 'secure_password_here'

8. Create Systemd Services

Create API service: /etc/systemd/system/${TENANT_NAME}-api.service

[Unit]
Description=${TENANT_NAME} API Backend
After=network.target

[Service]
Type=simple
User=bisenbek
WorkingDirectory=/home/bisenbek/projects/nominate/${TENANT_NAME}
Environment=PATH=/home/bisenbek/.pyenv/versions/nominates/bin:/usr/bin
ExecStart=/home/bisenbek/.pyenv/versions/nominates/bin/python run_api.py --host 0.0.0.0 --port ${BACKEND_PORT}
Restart=always
RestartSec=5
SyslogIdentifier=${TENANT_NAME}-api

[Install]
WantedBy=multi-user.target

Create Frontend service: /etc/systemd/system/${TENANT_NAME}-frontend.service

[Unit]
Description=${TENANT_NAME} Frontend
After=network.target

[Service]
Type=simple
User=bisenbek
WorkingDirectory=/home/bisenbek/projects/nominate/${TENANT_NAME}
Environment=PATH=/home/bisenbek/.pyenv/versions/nominates/bin:/usr/bin
ExecStart=/home/bisenbek/.pyenv/versions/nominates/bin/python run_frontend.py --host 0.0.0.0 --port ${FRONTEND_PORT}
Restart=always
RestartSec=5
SyslogIdentifier=${TENANT_NAME}-frontend

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable ${TENANT_NAME}-api ${TENANT_NAME}-frontend
sudo systemctl start ${TENANT_NAME}-api ${TENANT_NAME}-frontend

# Verify
sudo systemctl status ${TENANT_NAME}-api
sudo systemctl status ${TENANT_NAME}-frontend

9. Configure Nginx

Create config: /etc/nginx/sites-available/${DOMAIN}.conf

upstream ${TENANT_SHORT}_frontend {
    server 127.0.0.1:${FRONTEND_PORT};
}

upstream ${TENANT_SHORT}_backend {
    server 127.0.0.1:${BACKEND_PORT};
}

# HTTP redirect
server {
    listen 80;
    server_name ${DOMAIN};
    return 301 https://$server_name$request_uri;
}

# HTTPS
server {
    listen 443 ssl http2;
    server_name ${DOMAIN};

    ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # PIN Gate Auth (if using)
    include snippets/pin-gate-auth.conf;

    client_max_body_size 50M;

    # WebSocket
    location /ws {
        proxy_pass http://${TENANT_SHORT}_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400;
    }

    # API docs
    location /docs { proxy_pass http://${TENANT_SHORT}_backend; }
    location /redoc { proxy_pass http://${TENANT_SHORT}_backend; }
    location /openapi.json { proxy_pass http://${TENANT_SHORT}_backend; }

    # API
    location /api {
        proxy_pass http://${TENANT_SHORT}_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;
    }

    # Frontend
    location / {
        auth_request /internal/auth/verify;
        error_page 401 = @pin_redirect;
        proxy_pass http://${TENANT_SHORT}_frontend;
        proxy_set_header Host $host;
    }
}

Enable and test:

sudo ln -sf /etc/nginx/sites-available/${DOMAIN}.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

10. SSL Certificate

# Generate certificate
sudo certbot certonly --nginx -d ${DOMAIN}

# Or if DNS already configured
sudo certbot --nginx -d ${DOMAIN}

# Verify
sudo certbot certificates | grep ${DOMAIN}

11. Local DNS Resolution (Required)

IMPORTANT: Add the tenant domain to /etc/hosts for local loopback. This is required because the server must be able to connect to itself via the domain name (for health checks, e2e tests, and internal service calls).

Without this entry, requests from the server to its own domain will route through the public IP and fail due to NAT hairpinning issues.

# Add to /etc/hosts
echo "127.0.0.1 ${DOMAIN}" | sudo tee -a /etc/hosts

# Verify
dig +short ${DOMAIN}
# Should return: 127.0.0.1

Current entries:

127.0.0.1    testsite.nominate.ai
127.0.0.1    ky04.nominate.ai
127.0.0.1    mi20.nominate.ai
127.0.0.1    lfg.nominate.ai


Service Integration

13. YASP Survey Platform

# On YASP server or locally if CLI available
cd /home/bisenbek/projects/nominate/cbsurveys
python -m yasp.cli create-key \
    --user-email admin@${DOMAIN} \
    --key-name ${TENANT_NAME}

# Add to tenant .env
echo "YASP_API_KEY=yasp_xxx" >> $TENANT_DIR/.env

14. CBFiles Storage

# Create bucket (via CBFiles API or CLI)
curl -X POST https://files.nominate.ai/api/v1/buckets \
    -H "Authorization: Bearer $ADMIN_KEY" \
    -d '{"name": "${TENANT_NAME}"}'

# Generate tenant API key
# Add to .env:
echo "FILES_BASE_URL=https://files.nominate.ai/api/v1" >> $TENANT_DIR/.env
echo "FILES_API_KEY=cbfiles_xxx" >> $TENANT_DIR/.env
echo "FILES_TENANT_BUCKET=${TENANT_NAME}" >> $TENANT_DIR/.env

15. CBModels Campaign Data

# Add to .env:
echo "CBMODELS_BASE_URL=http://localhost:32411" >> $TENANT_DIR/.env
echo "CBMODELS_TENANT_ID=${TENANT_NAME}" >> $TENANT_DIR/.env

16. Anthropic API (Shared Key)

# Use shared Anthropic key
echo 'ANTHROPIC_API_KEY="sk-ant-xxx"' >> $TENANT_DIR/.env

Validation

17. Run Validation Script

cd /home/bisenbek/projects/nominate/cbapp

# Validate the new tenant
python scripts/validate_tenant_env.py --tenant ${TENANT_NAME} --fix

Expected output:

✅ ${TENANT_NAME}
   Path: /home/bisenbek/projects/nominate/${TENANT_NAME}
   Version: v0.4.x
   Services: API=active, Frontend=active

18. Test Endpoints

# Health check
curl https://${DOMAIN}/api/health

# Login page
curl -I https://${DOMAIN}/login

# API docs
curl https://${DOMAIN}/docs

19. Create Test User & Login

  1. Navigate to https://${DOMAIN}/login
  2. Login as admin
  3. Create test field user
  4. Verify role-based access

Post-Deployment

20. Add to Validation Script

Edit scripts/validate_tenant_env.py to include new tenant in TENANTS dict.

21. Update Documentation

  • Add tenant to port allocation table above
  • Update any internal wikis/docs

22. Monitor

# Check logs
sudo journalctl -u ${TENANT_NAME}-api -f
sudo journalctl -u ${TENANT_NAME}-frontend -f

# Check nginx logs
sudo tail -f /var/log/nginx/${DOMAIN}.access.log
sudo tail -f /var/log/nginx/${DOMAIN}.error.log

Troubleshooting

Service Won't Start

# Check logs
sudo journalctl -u ${TENANT_NAME}-api --no-pager -n 50

# Common issues:
# - Wrong Python path in service file
# - Missing dependencies
# - Port already in use
# - .env file missing or malformed

502 Bad Gateway

# Check if backend is running
curl http://localhost:${BACKEND_PORT}/api/health

# Check nginx upstream
sudo nginx -t

Database Errors

# Check database exists
ls -la $TENANT_DIR/db/

# Recreate if needed
python scripts/create_schema.py

Quick Reference

Step Command
Clone git clone git@github.com:Nominate-AI/cbapp.git $TENANT_DIR
Install deps pip install -r requirements.txt
Init DB python scripts/create_schema.py
Start services sudo systemctl start ${TENANT_NAME}-api ${TENANT_NAME}-frontend
Test nginx sudo nginx -t && sudo systemctl reload nginx
Get SSL sudo certbot --nginx -d ${DOMAIN}
Validate python scripts/validate_tenant_env.py --tenant ${TENANT_NAME}