Migrating from self-hosted coturn
You're running coturn yourself — a relay process, a credential-minting endpoint, TLS certs, and firewall/ACL upkeep. Moving to Baton replaces all of that with one API call. Your client code barely changes; the only real work is swapping how credentials are issued.
Before you start
A typical self-hosted setup has three parts: the coturn process (with a static-auth-secret), a small backend endpoint that mints time-limited TURN credentials from that secret, and clients that fetch those credentials and build an iceServers array. You'll keep the client, replace the backend endpoint, and retire coturn.
Grab a project API key from the dashboard before step 1.
Step 1 — Replace credential minting
Today your endpoint computes a username/credential from your local secret. Baton issues those for you and returns a complete, multi-transport iceServers array — so the endpoint gets shorter. Drop the HMAC code; call the API instead.
// you mint REST credentials from coturn's static-auth-secret
app.get("/api/ice", (req, res) => {
const expiry = Math.floor(Date.now() / 1000) + 3600;
const username = `${expiry}:${req.user.id}`;
const hmac = crypto.createHmac("sha1", process.env.TURN_STATIC_SECRET);
hmac.update(username);
const credential = hmac.digest("base64");
res.json({ iceServers: [{
urls: ["turn:turn.example.com:3478", "turns:turn.example.com:443?transport=tcp"],
username, credential
}]});
});// Baton returns the whole iceServers array — no secret, no HMAC
app.get("/api/ice", async (req, res) => {
const r = await fetch("https://api.usebaton.io/v1/credentials", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.BATON_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ ttl: 3600, region: "eu-central" })
});
const { iceServers } = await r.json();
res.json({ iceServers }); // all four transports included
});Step 2 — Leave the client alone
Your client already fetches iceServers from that endpoint and hands it to RTCPeerConnection — that doesn't change. If you ever hardcoded coturn URLs in the client, delete them and rely on the array from your endpoint, which now carries all four Baton transports.
const { iceServers } = await fetch("/api/ice").then(r => r.json());
const pc = new RTCPeerConnection({ iceServers });Step 3 — Run in parallel and verify
Roll the new endpoint out gradually
Point a fraction of traffic at the Baton-backed endpoint while coturn keeps serving the rest. Both can run side by side — there's no client release to coordinate.
Confirm relay candidates come from Baton
In
chrome://webrtc-internals, check for a selected candidate pair withtyp relayatrelay.usebaton.io— especially overturns/443. See Troubleshooting if you only seehost/srflx.Watch a real call end to end
Verify media flows on a connection that previously required your coturn relay (a strict-firewall client is the best test).
Step 4 — Decommission coturn
Once Baton is carrying traffic cleanly, retire the old stack. You stop running and maintaining:
- The coturn process and its public IP/ports.
- The
static-auth-secretand your credential-minting HMAC code. - TLS certificates for the TURN listener and their renewal.
denied-peer-ip/ relay ACLs and capacity planning — Baton handles these.