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-ttsThe 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-tts.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 → Zotero-TTS → OpenAI section:
| Field | Value |
|---|---|
| Enable OpenAI voices | on |
| Server | Chatterbox-TTS-Server (key, model and voices gray out — Chatterbox ignores them); for another server, Other OpenAI-compatible server |
| Base URL | https://tts-windows.example.com |
| Extra headers | CF-Access-Client-Id: <CLIENT_ID>; CF-Access-Client-Secret: <CLIENT_SECRET> |
Test connection should answer Connected. 28 voices available. Synthesis works. The voices then appear in Read Aloud's Local tier as
OpenAI-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.