- 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>
5.9 KiB
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 --buildin the main app, the next script run moves every agency onto the new image (data untouched).
One-time setup (server, as dev; ~30 minutes)
- DNS: add a wildcard record
*.quorlasocial.com A 72.62.169.43(Hestia → DNS → quorlasocial.com → Add record). - 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). - 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.mapfirst, thensudo nginx -t && sudo systemctl reload nginx. app.quorlasocial.com keeps its own Hestia domain; the exact name wins over the wildcard. - 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 - Script settings:
cp deploy/provision.env.example deploy/provision.envand fill it in. Createdeploy/tenant-shared.envwith 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 - Main app .env:
QUORLA_CONTROL=true,TENANT_BASE_DOMAIN=quorlasocial.com, the samePROVISION_TOKEN, and the Stripe values below. Rebuild:docker compose up -d --build. - Cron:
crontab -e→*/5 * * * * /home/dev/APPS/quorlasocial/deploy/provision-tenants.sh >> /home/dev/APPS/quorla-tenants/provision.log 2>&1
Stripe
- Stripe Dashboard (start in Test mode) → Product catalog → Add product "Quorla hosted", recurring monthly price.
Copy the price id (
price_…) →STRIPE_PRICE_ID. - Developers → API keys → secret key (
sk_test_…) →STRIPE_SECRET_KEY. - 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. - Settings → Billing → Customer portal: turn on cancel, update payment method, invoices (used by the Billing button).
- 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 intenant-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.logshowsrunning on 44xx.- The owner gets the welcome email and creates their account from the link; the link stops working afterwards.