quorlasocial/docs/multi-tenant.md
metatroncubeswdev 075b79f6b2 Hosted Quorla: sign-up, Stripe billing, one workspace per agency
- Control centre (QUORLA_CONTROL=true on app.quorlasocial.com): public /signup → Stripe Checkout,
  verified Stripe webhook (paid, updated, cancelled, payment failed), Admin → Agencies
  (approve without Stripe, suspend, resume, billing portal, resend welcome, remove)
- deploy/provision-tenants.sh (cron): one container per agency with its own .env and data,
  nginx map for <agency>.quorlasocial.com, upgrades agencies after a rebuild, archives removed data
- New workspaces: first admin only via the emailed one-time setup link (SETUP_TOKEN)
- Meta system user token can be saved in Settings (encrypted) for workspaces that can't edit .env
- docs/multi-tenant.md: DNS, wildcard cert, nginx, sudoers, cron, Stripe setup; tests for the control centre

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:35:23 -04:00

5.9 KiB

Hosted Quorla: one workspace per agency

How the hosted plan works and how to switch it on. Self-hosted customers don't need any of this: they run one Quorla with docker compose up -d --build as in docs/hosting.html.

How it fits together

agency signs up at app.quorlasocial.com/signup ──► Stripe Checkout ──► Stripe webhook ──► app marks the agency "setting up"
                                                                                                   │
cron every 5 min: deploy/provision-tenants.sh ◄── asks app what to do (PROVISION_TOKEN) ◄──────────┘
   │  creates ~/APPS/quorla-tenants/<agency>/ (own .env, data, clients…), runs container quorla-<agency> on 127.0.0.1:44xx,
   │  updates the nginx map so <agency>.quorlasocial.com reaches it, reports back
   ▼
app emails the owner a one-time setup link: https://<agency>.quorlasocial.com/?setup=<token>
  • Each agency is a separate container with its own data folder: no agency can see another's data.
  • The main app (app.quorlasocial.com) is the control centre (QUORLA_CONTROL=true): sign-up page, Stripe webhook, Admin → Agencies page (suspend, resume, billing portal, resend welcome, remove).
  • The web app never runs Docker. Only the provisioning script on the server does.
  • A new workspace's first admin can only be created with the emailed setup link (SETUP_TOKEN).
  • Payment fails: the agency shows "Payment overdue" (still running) until Stripe gives up, then it's stopped. Data is kept.
  • Subscription cancelled: workspace stopped, data kept. Remove archives the data to quorla-tenants/_archive/ first.
  • Upgrades: after git pull && docker compose up -d --build in the main app, the next script run moves every agency onto the new image (data untouched).

One-time setup (server, as dev; ~30 minutes)

  1. DNS: add a wildcard record *.quorlasocial.com A 72.62.169.43 (Hestia → DNS → quorlasocial.com → Add record).
  2. Wildcard certificate for *.quorlasocial.com. With DNS on Hestia: sudo v-add-letsencrypt-domain user quorlasocial.com "*.quorlasocial.com" (or certbot with the DNS challenge).
  3. nginx (Hestia includes /etc/nginx/conf.d/*.conf). Create /etc/nginx/conf.d/quorla-tenants.conf:
    map $host $quorla_tenant {
      default "";
      include /home/dev/APPS/quorla-tenants/nginx-tenants.map;
    }
    server {
      listen 72.62.169.43:443 ssl;
      http2 on;
      server_name *.quorlasocial.com;
      ssl_certificate     /home/user/conf/web/quorlasocial.com/ssl/quorlasocial.com.pem;   # adjust to the wildcard cert path
      ssl_certificate_key /home/user/conf/web/quorlasocial.com/ssl/quorlasocial.com.key;
      client_max_body_size 500m;
      proxy_read_timeout 600s;
      location / {
        if ($quorla_tenant = "") { return 404; }
        proxy_pass http://$quorla_tenant;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
      }
    }
    
    touch /home/dev/APPS/quorla-tenants/nginx-tenants.map first, then sudo nginx -t && sudo systemctl reload nginx. app.quorlasocial.com keeps its own Hestia domain; the exact name wins over the wildcard.
  4. Let the script reload nginx without a password: sudo visudo -f /etc/sudoers.d/quorla → dev ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/bin/systemctl reload nginx
  5. Script settings: cp deploy/provision.env.example deploy/provision.env and fill it in. Create deploy/tenant-shared.env with what every agency gets, at least email:
    SMTP_HOST=mail.quorlasocial.com
    SMTP_PORT=587
    SMTP_USER=support@quorlasocial.com
    SMTP_PASS=…
    SMTP_FROM=support@quorlasocial.com
    
  6. Main app .env: QUORLA_CONTROL=true, TENANT_BASE_DOMAIN=quorlasocial.com, the same PROVISION_TOKEN, and the Stripe values below. Rebuild: docker compose up -d --build.
  7. Cron: crontab -e → */5 * * * * /home/dev/APPS/quorlasocial/deploy/provision-tenants.sh >> /home/dev/APPS/quorla-tenants/provision.log 2>&1

Stripe

  1. Stripe Dashboard (start in Test mode) → Product catalog → Add product "Quorla hosted", recurring monthly price. Copy the price id (price_…) → STRIPE_PRICE_ID.
  2. Developers → API keys → secret key (sk_test_…) → STRIPE_SECRET_KEY.
  3. Developers → Webhooks → Add endpoint https://app.quorlasocial.com/control/stripe/webhook, events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed. Copy the signing secret (whsec_…) → STRIPE_WEBHOOK_SECRET.
  4. Settings → Billing → Customer portal: turn on cancel, update payment method, invoices (used by the Billing button).
  5. Optional STRIPE_TRIAL_DAYS=14. Test with card 4242 4242 4242 4242, then repeat in Live mode with live keys.

Without Stripe keys, sign-ups still work: they land on Admin → Agencies as "Awaiting payment" and you Approve them.

What each agency sets up in their workspace

  • Meta: Settings → Meta: paste their own system user token (their Business Manager, their Pages).
  • Other platforms: Connections → each platform → Set up with their own developer app keys, or you add their callback URL (https://<agency>.quorlasocial.com/oauth/<platform>/callback) to Quorla's TikTok/Google/LinkedIn apps and put those keys in tenant-shared.env (TIKTOK_CLIENT_ID… ). Platform apps need each exact callback URL listed.
  • Workspace: Settings → Workspace: their name, logo and colour (white-label).

Check it works

  • Admin → Agencies shows the agency move: Awaiting payment → Setting up → Active.
  • tail -f ~/APPS/quorla-tenants/provision.log shows running on 44xx.
  • The owner gets the welcome email and creates their account from the link; the link stops working afterwards.