- 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>
100 lines
5.9 KiB
Markdown
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.
|