English · 简体中文
You run Chatterbox-TTS-Server (or another OpenAI-compatible server) on a machine with a GPU at home, and you want to use it from Zotero on your laptop somewhere else. This tutorial puts the server behind a Cloudflare Tunnel and locks it with a Cloudflare Access service token, so that:
https://tts.example.com like any other server.Everything here is on Cloudflare's free plan. Time: about twenty minutes.
example.com below; the
worked example uses tts-windows.example.com).Zero Trust → Networks → Tunnels → Create a tunnel → Cloudflared, give it a
name (home-tts), and pick how to run the connector:
In Docker (the home machine already has Docker if it runs Chatterbox or Kokoro this way). Copy the token shown in the dashboard and run:
docker run -d --name cloudflared --restart unless-stopped \
cloudflare/cloudflared:latest tunnel --no-autoupdate run --token <TUNNEL_TOKEN>
Inside a container localhost is the container itself, not your PC, so the
service URL in the next step must be http://host.docker.internal:8004
(Docker Desktop on Windows and macOS resolves that name to the host).
As a system service (Windows installer, macOS brew install cloudflared,
Linux package): follow the dashboard's one-line install command. Then the
service URL can simply be http://localhost:8004.
Either way the tunnel shows Healthy in the dashboard once the connector is up.
Still in the tunnel's page, Public Hostname → Add a public hostname:
| Field | Value |
|---|---|
| Subdomain | tts-windows |
| Domain | example.com |
| Path | leave empty |
| Type | HTTP |
| URL | localhost:8004 — or host.docker.internal:8004 for the Docker connector |
Cloudflare creates the DNS record for you. At this point
https://tts-windows.example.com/docs already opens Chatterbox's Swagger page
from anywhere — for everyone. The next two steps close that.
The plugin cannot log in through a browser, so it authenticates with a service token: a Client ID and a Client Secret that Cloudflare checks on every request.
Zero Trust → Access controls → Service credentials → Create service token:
zotero-openreaderThe page shows the Client ID and the Client Secret once. Save both now; the secret cannot be shown again (you would create a new token instead).
Zero Trust → Access controls → Applications → Add an application → Self-hosted (the 2026 dashboard calls it Self-hosted and private):
Chatterbox TTStts-windows, domain example.com,
path empty.service-token-onlyzotero-openreader.curl -s -o /dev/null -w "%{http_code}\n" https://tts-windows.example.com/v1/audio/voices
prints 403 (or 302, a redirect to Cloudflare's login page): the door is
locked. Now with the token:
curl -s \
-H "CF-Access-Client-Id: <CLIENT_ID>" \
-H "CF-Access-Client-Secret: <CLIENT_SECRET>" \
https://tts-windows.example.com/v1/audio/voices
prints Chatterbox's voice list, {"status":"ok","voices":["Abigail.wav",...]}.
Zotero → Settings → OpenReader → OpenAI Compatible section:
| Field | Value |
|---|---|
| Address | https://tts-windows.example.com |
| API key | empty — Chatterbox has none |
| Model | anything, tts-1 say — Chatterbox ignores it |
| Extra headers | CF-Access-Client-Id: <CLIENT_ID>; CF-Access-Client-Secret: <CLIENT_SECRET> |
Test connection should answer Connected. 28 voices available. Synthesis works., then Enable. The voices then appear in the player under
OpenAI Compatible as Emily.wav and so on.
The two header values are as sensitive as an API key: they are stored in Zotero's preferences in plain text, and anyone holding them can use your server. To revoke access, delete the service token in Cloudflare (or create a new one and update the policy) and the old values stop working at once.
| Symptom | Cause |
|---|---|
403 even with the headers |
The policy's action is Allow instead of Service Auth, or the token in the policy is not the one you pasted. |
| Cloudflare's login page (HTML) instead of JSON | Same: the policy wants a browser login. |
502 Bad Gateway / Cannot connect |
The tunnel cannot reach the server: with the Docker connector the service URL must be host.docker.internal:8004, not localhost; or the TTS container is down (docker ps). |
| Tunnel shows Down | The connector is not running: docker ps should list cloudflared; start it with docker start cloudflared. |
| Works in curl, plugin says "The server rejected the API key" | A header is missing in Extra headers — it must contain both CF-Access-Client-Id and CF-Access-Client-Secret, separated by ;. |
| Long sentences fail only remotely | Cloudflare closes requests after 100 s. Chatterbox needs around 8 s for a long sentence on a 3080 Ti; a CPU-only server may not make it. |
kokoro.example.com → localhost:8880, or host.docker.internal:8880
for the Docker connector) and its own Access application — the same
service token may be used in its policy. In the plugin, Kokoro-FastAPI:
Address https://kokoro.example.com, Extra headers the same
CF-Access-Client-Id: …; CF-Access-Client-Secret: … pair. A missing or
wrong token shows up as "The server rejected the API key", not as the
server being down.Authorization: Bearer <key> (Caddy, nginx)
also works with the plugin's API-key field, with the difference that
unauthenticated requests reach your network before being refused.