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¶
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)¶
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¶
- Navigate to
https://${DOMAIN}/login - Login as admin
- Create test field user
- 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¶
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} |