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

100 lines
5.9 KiB
Markdown

# 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`:
```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://<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.