Docs
Deployment Guide

Deployment Guide

Production deployment checklist for Tenantx.

Deployment Guide

Production deployment checklist for Tenantx.


Prerequisites

  • PHP 8.2+ (same range as backend/composer.json ^8.2; CI uses PHP 8.2; Docker production image uses PHP 8.3)
  • PostgreSQL 14+ (required — SQLite is dev-only)
  • Node.js 18+ (for building frontend assets)
  • A web server (Nginx / Apache) or Laravel Octane
  • Queue worker (for background jobs)

1. Server Setup

Clone & Install

git clone <repo> /var/www/tenantx
cd /var/www/tenantx/backend
composer install --no-dev --optimize-autoloader
cd ../frontend
npm ci
npm run build

Environment File

cp backend/.env.example backend/.env

Edit backend/.env with your production values. Critical variables:

APP_ENV=production
APP_DEBUG=false
APP_KEY=            # generate with: php artisan key:generate
APP_URL=https://yourdomain.com

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=tenantx
DB_USERNAME=tenantx
DB_PASSWORD=<strong-password>

SANCTUM_STATEFUL_DOMAINS=yourdomain.com,app.yourdomain.com

TRANSLATIONS_PATH=../frontend/src/lib/translations

LOG_CHANNEL=json     # structured logs for Docker aggregators
LOG_LEVEL=error     # never use 'debug' in production

FILESYSTEM_DISK=local   # or 's3' for S3 storage

# Stripe (if billing enabled)
STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_PRO=price_...

Generate App Key

cd backend && php artisan key:generate

Optional Guided Setup Wizard

For first-time environments, you can run:

php artisan tenantx:setup

This helps bootstrap required env and platform defaults interactively.


2. Database

# Run migrations
php artisan migrate --force

# Seed (first deploy only — creates default plans, permissions, demo users)
# WARNING: Change all demo user passwords immediately after seeding
php artisan db:seed --force

PostgreSQL Schema for Activity Logs

The tenantx_logs schema is created automatically by migrations on PostgreSQL. Verify:

\dn  -- should show 'tenantx_logs' schema

3. Storage & Permissions

# Create storage symlink (serves public uploads via /storage/*)
php artisan storage:link

# Set permissions
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache

4. Frontend Assets

The built frontend lives in frontend/dist/. Configure your web server to:

  • Serve frontend/dist/index.html for all non-/api routes (SPA routing)
  • Proxy /api/* to the Laravel backend (e.g. http://127.0.0.1:8000)

Nginx example:

server {
    listen 80;
    server_name yourdomain.com;
    root /var/www/tenantx/frontend/dist;
    index index.html;

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Proxy API to Laravel
    location /api {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Proxy storage to Laravel
    location /storage {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
    }
}

5. Queue Worker

Tenantx uses Laravel queues for background jobs (webhook processing, emails).

# Supervisor config (recommended for production)
# /etc/supervisor/conf.d/tenantx-worker.conf
[program:tenantx-worker]
command=php /var/www/tenantx/backend/artisan queue:work --sleep=3 --tries=3 --max-time=3600
directory=/var/www/tenantx/backend
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/tenantx-worker.log
supervisorctl reread && supervisorctl update && supervisorctl start tenantx-worker:*

Outgoing webhooks (tenant integrations): Delivery uses DeliverOrganizationOutgoingWebhookJob on the default queue. If the queue worker is not running, HTTP callbacks never leave the app. After deploys that change subscription features or permissions, run php artisan db:seed --class=SubscriptionSeeder when you intentionally refresh the plan catalog, and php artisan permissions:sync-default-roles so default roles pick up integration permissions. See docs/OUTGOING_WEBHOOKS.md.


6. Optimize Laravel

cd backend
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

7. Stripe Webhook Endpoint

Register in Stripe Dashboard → Developers → Webhooks:

  • URL: https://yourdomain.com/api/stripe/webhook
  • Events: checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.deleted, customer.subscription.updated
  • Copy signing secret to STRIPE_WEBHOOK_SECRET

See STRIPE.md for full details.


8. Platform Admin Setup

After seeding, promote a user to platform admin:

php artisan platform:assign-admin admin@yourdomain.com --organization=your-org-slug

IMPORTANT: Change all seeded demo passwords before going public.


9. Security Checklist

  • APP_DEBUG=false in production .env
  • APP_ENV=production
  • All demo passwords changed (admin@tenantx.dev, admin@acme.tenantx.dev, etc.)
  • SANCTUM_STATEFUL_DOMAINS set to your actual domain(s)
  • HTTPS enabled (Certbot / load balancer)
  • Database password is strong and not default
  • LOG_LEVEL=error (not debug)
  • Storage directory is not publicly accessible (only storage/app/public via symlink)
  • Queue worker is running via Supervisor

10. Release Preflight

Run these checks from a clean release-candidate commit before tagging or deploying:

cd backend
php artisan route:list --json > /dev/null
find app database/migrations -name '*.php' -print0 | xargs -0 -n1 php -l
php artisan test

cd ../frontend
npm run build
npm run test:run
npx eslint src --ext .ts,.tsx --max-warnings 0

Minimum smoke test for public launch:

  • registration creates org + default workspace
  • login and 2FA challenge/verification work
  • org admin can reach users, workspaces, onboarding, API tokens, system settings, and outgoing webhooks
  • Stripe test checkout + webhook update subscription state
  • platform admin can access /platform/* without leaking tenant-only permission checks

11. Docker (Production)

A production Docker stack is provided in docker-compose.prod.yml. It includes:

  • PHP-FPM container (Laravel backend)
  • Nginx container (serves frontend + proxies API)
  • PostgreSQL container
cp backend/.env.example backend/.env
# edit backend/.env for production

docker-compose -f docker-compose.prod.yml up -d
docker-compose -f docker-compose.prod.yml exec app php artisan migrate --force
docker-compose -f docker-compose.prod.yml exec app php artisan db:seed --force
docker-compose -f docker-compose.prod.yml exec app php artisan storage:link

Certbot renewal (Docker stack)

The certbot service is configured as a long-running renewal loop in docker-compose.prod.yml. Verify it is running and renewing successfully:

docker compose -f docker-compose.prod.yml ps certbot
docker compose -f docker-compose.prod.yml logs --tail=200 certbot

Upgrading

cd /var/www/tenantx
git pull

cd backend
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache

cd ../frontend
npm ci && npm run build

supervisorctl restart tenantx-worker:*