> For the complete documentation index, see [llms.txt](https://acoservice.gitbook.io/acoservice-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://acoservice.gitbook.io/acoservice-documentation/for-platform-operators/provisioning-a-bot.md).

# Provisioning a tenant bot

Why a saved bot token does not start a bot, and how the provisioner script turns that token into a running process routed to the tenant.

Saving a bot token in the platform console stores an encrypted string on the tenant record. That is all it does. No process starts, no bot appears in Discord, no slash commands register.

Turning that token into a running bot is a second, deliberate step: `scripts/provision-tenant-bot.sh` in the app repo, run on the app host.

{% hint style="info" %}
**Owners can now request this from the console**, rather than asking an operator to run the script. The request is queued and a provisioning agent (`scripts/provision-agent.py`) claims it and runs **this same script**, as the same user, on the same host.

Nothing below changes: the agent does not reimplement provisioning, precisely because every safety property described on this page lives in the script. What changed is who triggers it. Running it by hand is still correct and still the right move when you are debugging a failure.

The agent claims one job at a time with a database row lock — two agents provisioning the same tenant would race on the same run directory, the same `ecosystem.config.js` and the same allowlist line, and the wreckage would look like a corrupted PM2 config rather than a race.
{% endhint %}

```
./scripts/provision-tenant-bot.sh <tenant-slug>
```

{% hint style="info" %}
This is an operator-run shell script rather than a button in the console on purpose. Spawning long-lived OS processes from a web request handler is not a trade worth making for something that happens a handful of times a year.
{% endhint %}

## Why the token alone is not enough

A tenant's Discord application carries three credentials, and they serve two unrelated jobs:

| Credential(s)                          | Job                                | If it is missing                            |
| -------------------------------------- | ---------------------------------- | ------------------------------------------- |
| Client ID + Client Secret (OAuth2 tab) | The site's Discord sign-in         | Members cannot sign in                      |
| Bot Token (Bot tab → Reset Token)      | A bot process logging into Discord | Sign-in works, the server has no bot at all |

Collecting the first two and not the third is the common mistake: the client ID and secret sit together on the OAuth2 tab, and the bot token lives on a different tab entirely.

There is no shared bot behind the scenes picking up tenants that lack their own. A tenant with sign-in configured and no provisioned bot has a fully working website and a completely silent Discord server. That is a real state a live tenant has been found in.

## What the script does

Run with the tenant's slug, the script works through eight stages. The ordering is load-bearing — see the warning below.

1. **Looks the tenant up** by slug through a live bot's tenant API, and reads back its Discord client ID, whether a bot token is stored, and its first linked guild. If no token is stored it stops and tells you to add one in Platform → Tenants → *slug* → Discord Bot Token first.
2. **Picks a port.** If this tenant already has a run directory with an `API_PORT`, it reuses it. Otherwise it scans every existing run directory and takes the next free port from 8044 upward.
3. **Writes the run directory** at `~/run/<slug>-bot` — a small `launch.py` and a `.env` written mode `0600`. The `.env` carries the tenant's `DISCORD_TOKEN`, its `API_PORT`, a `CORS_ORIGINS` entry for its subdomain, an optional `BOT_BRAND_NAME` derived from the tenant's branding, and the three shared values copied from the reference bot's env: `DATABASE_URL`, `ENCRYPTION_KEY` and `BOT_API_KEY`. The token is piped straight from the API response into Python, so it never lands in a shell variable, an argv line or a trace.
4. **Adds a PM2 entry** to `ecosystem.config.js`, named `<slug>-bot`, with autorestart on. If an entry with that name already exists it is left alone.
5. **Extends the allowlist.** It appends `http://localhost:<port>` to `BOT_API_URL_FALLBACK` in the web app's production env file, after taking a timestamped backup. If the port is already listed, nothing changes.
6. **Starts the process and verifies its identity.** It polls the new bot's `bot-info` endpoint for up to a minute. If the bot never answers, the script prints the last 30 lines of its PM2 log and stops. If the bot answers but reports a different Discord application than the tenant's stored client ID, the script stops with an explicit error: the stored token belongs to somebody else's application.
7. **Routes the tenant to it**, by setting `bot_api_url` on the tenant record, then reading it back to confirm it took. It then saves the PM2 process list and restarts the web app so it picks up the new allowlist.
8. **Prints the invite link** you still have to use — the bot must be added to the customer's Discord server by someone with Manage Server there, and the link requests the Administrator permission.

The script is idempotent. Re-running it after a token rotation updates the token and restarts that one bot. It never touches any other tenant's process.

## The allowlist fails closed

`bot_api_url` is a writable column on the tenant record. The web app never fetches whatever that column happens to say — it first checks the value against an allowlist made of `BOT_API_URL` plus every entry in `BOT_API_URL_FALLBACK`, and only fetches if it matches.

{% hint style="danger" %}
**A `bot_api_url` that is not on the allowlist resolves to nothing, and every call that depends on it fails.** It does not quietly fall back to the default bot. Falling back would mean asking a *different customer's* bot for this tenant's data, so the code refuses.
{% endhint %}

This is why the script extends the allowlist and starts the bot **before** it sets `bot_api_url`. Routing first would point the tenant at an address the web app is not allowed to call, which takes the tenant's site down rather than degrading it. The failure is loud and total: sign-in access checks, admin gating, SKU requests and the platform's per-tenant stats all stop working for that tenant.

Comparison is on a normalised string — one trailing slash is stripped from both sides, and nothing else. No case-folding, no port collapsing, no URL parsing. `https://bot.example.com/` and `https://bot.example.com` match; anything else must be spelled exactly as the operator configured it.

## When something goes wrong

| Symptom                                                          | Cause                                                                                                                       |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `no tenant with slug '…'`                                        | The slug is wrong, or the tenant was never created                                                                          |
| `no bot token stored for '…'`                                    | Only the client ID/secret were collected — go add the token                                                                 |
| `<slug>-bot never answered on :<port>`                           | The process failed to start; the script dumps its PM2 log                                                                   |
| `bot logged in as application X but the tenant's OAuth app is Y` | The stored token is from a different Discord application than the tenant's OAuth app                                        |
| `failed to set bot_api_url`                                      | The bot is running but the tenant is still routed elsewhere. Nothing is broken — the tenant continues on its previous route |
| Tenant page shows a 502 mentioning `BOT_API_URL_FALLBACK`        | The tenant's `bot_api_url` is not on the allowlist                                                                          |

After a successful run, confirm the result on the tenant's [setup status checklist](/acoservice-documentation/for-platform-operators/setup-status.md) — the "Bot process running for this tenant" row should name the tenant's own Discord application, and "Bot is in the guild" should turn green once the invite is accepted.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://acoservice.gitbook.io/acoservice-documentation/for-platform-operators/provisioning-a-bot.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
