A self-hosted web app that lets your users transfer their TeamSpeak 3 server groups to a TeamSpeak 6 server — verified by IP address.
Built with Node.js, Express, and Docker. Connects to TS3 via ts3-nodejs-library (ServerQuery, port 10011) and to TS6 via its HTTP ServerQuery API (port 10080).
- User opens the web page — the app detects their public IP address
- Queries the TS3 server for clients connected from that IP
- Queries the TS6 server for clients connected from that IP
- If exactly one account is found on each server — transfers the server groups (by group ID) from TS3 to TS6
- If more than one account is detected on either server from the same IP — the transfer is blocked and an error is shown
This prevents abuse and ensures a 1-to-1 identity mapping between TS3 and TS6 accounts.
- IP-based verification — no login required, fully automatic identity matching
- Duplicate detection — blocks transfer if multiple accounts share the same IP
- Group names — displays human-readable server group names (not just IDs)
- Multi-language support — ships with Polish and English; add your own in
/locales - Custom branding — set your logo via
.env - Rate limiting — prevents spam (30s cooldown between transfers)
- Docker-ready — single
docker compose upto deploy - Reverse proxy support — proper
X-Forwarded-Forhandling behind nginx
git clone https://github.com/mrfroncu/ts-rank-transfer.git
cd ts-rank-transfercp .env.example .env
nano .envFill in your TeamSpeak server details:
# TeamSpeak 3
TS3_HOST=192.168.1.100
TS3_QUERY_PORT=10011
TS3_USERNAME=serveradmin
TS3_PASSWORD=your_password
TS3_SERVER_ID=1
# TeamSpeak 6
TS6_HOST=192.168.1.100
TS6_QUERY_PORT=10080
TS6_USERNAME=serveradmin
TS6_PASSWORD=your_password
TS6_API_KEY=your_api_key
TS6_SERVER_ID=1
# Connect buttons (public addresses for users)
TS3_CONNECT_ADDRESS=ts3.example.com
TS6_CONNECT_ADDRESS=ts6.example.com
# Web
WEB_PORT=5540
LOGO_URL=https://example.com/logo.png
LANGUAGE=EN # PL or EN
TRUST_PROXY=false # true if behind nginxdocker compose up -d --buildThe app is now running at http://your-server:5540
| Variable | Default | Description |
|---|---|---|
TS3_HOST |
127.0.0.1 |
TS3 server IP address |
TS3_QUERY_PORT |
10011 |
TS3 ServerQuery port |
TS3_USERNAME |
serveradmin |
ServerQuery username |
TS3_PASSWORD |
— | ServerQuery password |
TS3_SERVER_ID |
1 |
Virtual server ID |
TS6_HOST |
127.0.0.1 |
TS6 server IP address |
TS6_QUERY_PORT |
10080 |
TS6 HTTP query port |
TS6_SSH_PORT |
10022 |
TS6 SSH query port (reserved) |
TS6_USERNAME |
serveradmin |
TS6 query username |
TS6_PASSWORD |
— | TS6 query password |
TS6_API_KEY |
— | TS6 API key (preferred over password) |
TS6_SERVER_ID |
1 |
TS6 virtual server ID |
TS3_CONNECT_ADDRESS |
— | Public address for TS3 "Join server" button |
TS6_CONNECT_ADDRESS |
— | Public address for TS6 "Join server" button |
WEB_PORT |
5540 |
Web server port |
LOGO_URL |
— | URL to your logo image |
LANGUAGE |
PL |
Interface language (PL or EN) |
TRUST_PROXY |
false |
Set true if behind a reverse proxy |
- Copy
locales/en.jsontolocales/xx.json - Translate all string values
- Set
LANGUAGE=XXin your.env - Restart the container
If you're serving the app behind nginx with HTTPS:
server {
listen 443 ssl;
server_name roles.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/.../fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/.../privkey.pem;
location / {
proxy_pass http://127.0.0.1:5540;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
}Set TRUST_PROXY=true in .env so the app reads the real client IP from headers.
- Runtime: Node.js 20 (Alpine)
- Framework: Express
- TS3 connection: ts3-nodejs-library (telnet ServerQuery)
- TS6 connection: HTTP ServerQuery API (JSON over HTTP)
- Deployment: Docker + Docker Compose
ts-rank-transfer/
├── server.js # Express backend, TS3 & TS6 services, API routes
├── public/
│ └── index.html # Frontend (single-page, translation-driven)
├── locales/
│ ├── pl.json # Polish translations
│ └── en.json # English translations
├── Dockerfile
├── docker-compose.yml
├── package.json
├── .env.example # Template configuration
└── .gitignore
| Problem | Solution |
|---|---|
| "Cannot connect to TS3" | Verify ServerQuery is running on port 10011 and credentials are correct |
| "Cannot connect to TS6" | Verify HTTP query is running on port 10080, check API key |
IP always shows 127.0.0.1 |
Set TRUST_PROXY=true if behind a reverse proxy |
| Docker can't reach TS on localhost | Use host.docker.internal as host (uncomment extra_hosts in docker-compose.yml) |
| Groups don't transfer | Ensure the query account has i_group_member_add_power permission |
| TS6 "invalid serverID" | Check TS6_SERVER_ID matches your virtual server |
TeamSpeak 6 exposes an HTTP-based ServerQuery on port 10080. The URL format is:
http://host:port/{serverID}/{command}?param=value&-flag
Authentication is done via:
- API Key (preferred) —
x-api-keyheader - Basic Auth —
Authorization: Basic base64(user:pass)
If your TS6 instance uses a different API, you'll need to adjust the TS6Service class in server.js.
MIT
© Alleria.pl 2026 | made by Froncalke