# Cloudflare Tunnel — the dashboard is the source of truth

Each installation has its own Cloudflare account. The **tunnel** sign-in is OAuth: the agent invokes `cloudflared tunnel login` via Bash; the Cloudflare Authorize URL streams into the admin chat PTY and the native terminal renders it as a clickable link. Click it, authorise in your own browser, and `cloudflared` writes `cert.pem` to the brand's config directory. For **everything else** (DNS, Pages, D1, Access) the agent uses the Cloudflare API, authenticated by a short-lived narrow token it mints from a master token you provision once in the dashboard (an advanced step the agent never automates). Some account-side jobs — adding a domain, switching accounts — are still easiest in your browser, and the agent relays those click-paths; the rest it can do directly via the API.

Where one installation serves several client accounts from one Cloudflare account, that master is held by the house account alone, and a client account cannot mint from it — it is account-wide, so handing it to one client would hand them every other client's data. Publishing a client's website therefore runs house-side on the client's behalf: the client's session asks, the house does the deploy, and no Cloudflare token ever enters the client's account. Which website belongs to which client is recorded once by the house and is never inferred from folder names, so a site nobody has recorded cannot be published by anyone until it is.

## Identity model

| Concept | Source |
|------|--------|
| **Product identity** (Real Agent vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
| **Cloudflare account identity** | `cert.pem` from OAuth. One account per brand per device. |
| **Domain scope** (which zones the operator can route) | The operator picks the zone in the dashboard during OAuth or names it in chat; the agent can also enumerate zones via the API with a minted read-scoped token. |
| **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `alias-domains.json`. |

Tunnel auth on the operator-owned path (Mode A) is the OAuth cert (`cert.pem`); API operations use a narrow token the agent mints from your master token. That master may be account-scoped (`cfat_…`) or user-scoped (`cfut_…`); the agent detects the type and routes API endpoints accordingly (the account vs user token endpoints) — see `plugins/cloudflare/references/api.md`. To switch Cloudflare accounts, the agent runs the reset flow from `plugins/cloudflare/references/reset-guide.md` (deletes the cert and every tunnel on the current account), then the manual-setup flow again — `cloudflared tunnel login` picks a fresh account when you sign in.

## Setup flow

Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and collects the inputs in plain chat:

- **Admin address** — the hostname that will serve the admin chat (e.g. `admin.yourdomain.com`).
- **Public address** — optional hostname for the public agent (e.g. `public.yourdomain.com` or `chat.yourdomain.com`).
- **Proxy apex** — optional bare-domain hostname (e.g. `yourdomain.com`) that should also serve the public agent.
- **Admin password** — the password used to gate remote access to the admin surface.

The agent then sets the admin password via `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password` (same endpoint the local onboarding form uses), and works through `plugins/cloudflare/references/manual-setup.md` Steps 1–7 directly via the Bash tool. `cloudflared`'s stdout streams into the PTY verbatim. The OAuth URL is linkified by the terminal; click it in your own browser to authorise. After the tunnel is up, the agent appends each non-`public.*` public or apex hostname to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public, and starts the brand's cloudflared user service.

If any step's `cloudflared` invocation exits non-zero, the agent names the literal exit code, surfaces the stderr verbatim, and cites `reset-guide.md` for the next action — no retry under a different flag, no Playwright-driven dashboard inspection.

The setup-done claim only fires after the agent runs `curl -I https://<admin-hostname>` from outside the local network and the response shows a `200` line. That HTTP response is the only success terminal.

## Getting a domain on Cloudflare

The tunnel needs a domain on the Cloudflare account the device will sign into. Two paths, both in your browser:

**Option A: Buy a new domain through Cloudflare.** Navigate to cloudflare.com → Domains and buy one. Cloudflare sets everything up.

**Option B: Add an existing domain.** In the dashboard: Websites → Add a site. Cloudflare imports the existing DNS records; review them to confirm your website and email entries are preserved. Cloudflare gives you two nameservers; replace the registrar's nameservers with those. Propagation is usually minutes (up to 24 hours); the zone shows **Active** when ready.

Existing website traffic continues to work during and after the switch. Only DNS resolution changes owners.

## Reset / account switch

Ask the agent to reset Cloudflare. The agent executes the reset flow from `plugins/cloudflare/references/reset-guide.md`:

- Deletes every tunnel on the brand's current Cloudflare account (via the bound cert).
- Wipes the brand's `${CFG_DIR}`.
- Stops the brand's cloudflared user service.

The agent does **not** stop token-mode connector processes or delete stray misrouted CNAMEs in the dashboard. If any of those apply, the agent guides you through the manual cleanup — `pkill -f 'cloudflared.*tunnel run --token'` on the device, or deleting the stray CNAME in the dashboard.

After reset, run setup again. The fresh `cloudflared tunnel login` will pick whichever Cloudflare account you sign into.

## Manual runbook

The step-by-step runbook at `plugins/cloudflare/references/manual-setup.md` is the contract the agent follows. It is also what an operator runs by hand when needed — every numbered step is an isolated `cloudflared` command block with success conditions and troubleshooting.

## Dashboard operations the CLI cannot do

The CLI cannot add a domain, switch accounts, edit an apex CNAME, or delete stray records. `plugins/cloudflare/references/dashboard-guide.md` has one numbered click-path per operation. The agent quotes the relevant steps verbatim when you need to do one of these things.

## Troubleshooting

### Tunnel won't start

Ask the agent to check. The agent reads `systemctl --user status ${BRAND}-cloudflared.service` and the cloudflared log under `~/{configDir}/cloudflared/`. Common states:

- **No cloudflared process running** — the cloudflared service exited or never started. The agent runs the manual-setup flow to re-issue tunnel creation.
- **`tunnel not found`** — the UUID in `config.yml` does not match any tunnel on the currently-bound account. Usually follows an account switch that didn't reset local state. The agent runs the reset flow and then a fresh setup.

### URL returns 530

DNS propagation or account mismatch. Wait 30–60 seconds and retry first. If the 530 persists:

- The domain may be on a Cloudflare account different from the one `cert.pem` is bound to — the agent re-runs the manual setup steps to re-validate.
- The UDP buffer for QUIC may be undersized on this device — check the cloudflared log for `failed to sufficiently increase receive buffer size`.

### URL returns connection refused

The tunnel is live but nothing is listening on the platform port. Start the platform service: `systemctl --user start ${BRAND}.service`.

### Admin hostname serves the public agent

`admin.yourdomain` is being misclassified as public. The platform UI treats a host as public when either (a) the hostname starts with `public.`, or (b) the hostname appears in `${CFG_DIR}/alias-domains.json`. Older install flows wrote every routed hostname into `alias-domains.json`; the pollution survives across reinstalls.

The agent reads `alias-domains.json`, removes the offending `admin.*` entry, and the platform UI hot-reloads — no restart needed. See `plugins/cloudflare/references/reset-guide.md` § "Remove a rogue entry from alias-domains.json" for the exact `jq` command.

### DNS not resolving

The most common cause is wrong nameservers on the domain. The domain must use Cloudflare's nameservers, not the registrar's defaults. In the dashboard: Websites → your domain → status must say **Active**, not **Pending**. If Pending, follow the dashboard's nameserver instructions and wait for propagation.

### Remote login issues

- 5 failed login attempts → 15-minute lockout — wait for expiry.
- The remote password is set during Cloudflare Tunnel onboarding — the agent asks for one in chat and stores it deterministically. The browser form at `/__remote-auth/setup` remains available for resets on the local network.

### Large `/data` upload fails with a connection error

- Cloudflare proxies a request body of at most ~100 MB on the current plan. A single upload larger than that is cut at the edge before it reaches the server, so the browser sees a connection error rather than a clean size rejection.
- The `/data` page handles this itself: files over 95 MB are split into ~48 MB parts and sent one at a time under the cap, and the server reassembles them. No plan change or dashboard action is needed for uploads up to the 2 GB app ceiling.
- Raising the proxied body limit is a paid-plan change made on the Cloudflare dashboard, not something the agent does.

## The client file portal

A portal gives named people their own passcode-protected page on a custom domain. Two things sit on it.

**What they send you.** Anything they upload lands in the account's own storage, one area per person, and nobody sees anyone else's files.

**What you have prepared for them.** The same page shows the account's deliverable folders — the work produced for that client. Which folders those are is not a setting to maintain: it follows the account's own filing structure, so a business whose work is organised into quotes sees quotes, and a business organised some other way sees that instead. Operator working data, the client's own uploads, and anything internal are never shown.

Folder names and sizes are published to the portal on a schedule, so the client can browse even while the device is off. The files themselves stay on the device. Opening one checks the device is reachable and then hands the browser a link that expires after a few minutes; if the device is down the client is told exactly that, rather than getting a broken download.

Each person is bound to one account when they are enrolled. Someone enrolled before this existed has no account bound and sees no folders until they are enrolled again.

## What the agent does and does not do

**Does:** invokes `cloudflared` directly via Bash, following `plugins/cloudflare/references/manual-setup.md` step by step; quotes click-paths from the reference files verbatim; verifies external reachability with `curl -I` and surfaces the response.

**Does not:** drive the Cloudflare dashboard via Playwright, browser-automate master-token creation, synthesise alternative `cloudflared` flag sequences not in the runbook, write or echo any API token, write or edit `cert.pem` / `config.yml` directly outside the runbook's instructions.

When a command fails, the agent reports the failure and cites the relevant recovery step. It does not improvise.
