# 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// (own .env, data, clients…), runs container quorla- on 127.0.0.1:44xx, │ updates the nginx map so .quorlasocial.com reaches it, reports back ▼ app emails the owner a one-time setup link: https://.quorlasocial.com/?setup= ``` - 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`: ```nginx 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://.quorlasocial.com/oauth//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.