4.5 KiB
Operator runbook — Cloudron tokens
How the co-op's Cloudron credentials are organized, and how to rotate them without breaking the stack.
The two-token split
There are two separate Cloudron API tokens, deliberately decoupled so rotating one doesn't break the other:
| Token | Purpose | Where it lives |
|---|---|---|
| Agent token | Inferencebot's own Cloudron access | The agent's private .env (gitignored) |
| Portal token | The member portal's Cloudron access (create users, set groups, invite links) | Cloudron's env store, under the portal app's CLOUDRON_TOKEN |
The portal needs only a handful of Cloudron capabilities (user/group management, invite links). It is the most exposed app (public webhook + broker endpoints), so it must not hold the agent's token — a portal compromise shouldn't reach the agent's wider Cloudron access.
Rotating a token
Portal token
- In
my.inference.coop→ Settings → API Tokens, create a "Read and Write" token. - Set it as the portal's
CLOUDRON_TOKENenv var (custom apps have no env-var UI — use the Cloudron CLI or the API; see the next section). - Verify: the portal can reach
GET /api/v1/userswith the new token (HTTP 200).
Agent token
- Create a fresh token in
Settings → API Tokens. - Put it in the agent's
.envasCLOUDRON_TOKEN. - Verify against
/api/v1/cloudron/status.
The trap this split exists to prevent: the portal keeps its own copy of a
Cloudron token. If the agent's token is rotated without also updating the portal
copy, the portal's Cloudron calls start returning 401 — which breaks
provisioning (new members can't be created) and the member dashboard (its
usage/keys data is resolved through the portal's Cloudron calls).
Custom apps have no env-var UI
The member portal is a custom app (packaged by us), not an App Store app. Custom apps do not expose environment variables in the Cloudron dashboard. Env vars for custom apps are set via:
- Cloudron CLI (on your workstation, not the server):
cloudron env set --app portal.inference.coop CLOUDRON_TOKEN=... - Cloudron API:
POST /apps/:appId/configure/env
configure/env is a full replace
POST /apps/:appId/configure/env with body {"env": {KEY: value, ...}}
replaces the entire environment — it does not merge. Sending a partial map
(e.g. a single test variable) wipes every other variable.
- To change one variable, read the current env first (
GET /api/v1/apps/:id→.env), merge in memory, and POST the full map back. - Never probe the endpoint with a throwaway body against a live app.
- If the env is wiped, it is recoverable from a Cloudron app backup:
GET /api/v1/apps/:appId/backups→ newest entry →GET .../backups/:backupId/download(a.tar.gzcontainingconfig.json, whose.envfield holds the original map).
Reference
- Cloudron CLI:
npm install -g cloudron(install on your workstation, not the server), thencloudron login my.inference.coop. - Cloudron API docs:
docs.cloudron.io/api/.
Open Collective email visibility can regress (Sept 2026)
The portal identifies members by email, read via the bot's OC personal token
(OC_PERSONAL_TOKEN) as collective.members.nodes[].account.email with the
... on Individual { email } inline fragment. This is normally populated
because the bot is an admin of the collective.
Failure mode: the GraphQL email field can flip to null for every
member — including the bot's own me { email } — while:
- the bot still authenticates and reports
isAdmin: true, - the OC admin CSV export still shows emails.
When this happens, fetch_members() filters out empty emails and returns [],
so the webhook can't provision anyone by email and the nightly sweep goes into
its empty-list fail-safe (skips, doesn't deactivate). Symptoms: a new member's
webhook fires cleanly but no "Sent email to …" log line follows, and no
Cloudron user is created.
Diagnosis: run me { email } with the bot token. null here (while
isAdmin: true) confirms the regression is OC-side in the GraphQL layer, not
a token rotation and not a lost admin grant. The CSV is the independent
ground-truth check.
Interim workaround: for any member who can't be provisioned by email, get
their address another way and onboard via the manual path — add to
MANUAL_MEMBERS, then POST /admin/provision (X-Admin-Token header). The
guest-with-no-email case is not the cause; verify with me { email } before
concluding a member is unreachable.