9RTKSync (9Router Universal Token & Connection Synchronizer) is a high-availability self-healing guardian for 9Router gateways. It eliminates sudden disconnects, premature OAuth token expirations, date format corruptions, and lingering rate-limit locks, keeping all connected accounts healthy and persistent.
The full documentation lives in the project wiki: installation, the complete environment-variable contract, the dashboard, authentication and break-glass recovery, persistent logging, architecture, troubleshooting, and the upstream gateway fixes.
Wiki pages are generated from docs/wiki/ — edit them there and open a pull
request; a push to master republishes the wiki automatically.
- Numeric Expiration Self-Healing
- 9Router natively writes the
expiresAtfield as an ISO date string (for example"2026-09-12T11:54:08.336Z"). This breaks internal numeric validations and causes false HTTP 503 errors. 9RTKSynccontinuously inspects the SQLite database and automatically converts string timestamps into valid millisecond epoch integers.
- 9Router natively writes the
- Universal Proactive OAuth Renewal
- Google Antigravity and Gemini CLI connections: renews before expiration (15-minute margin) and synchronizes host tokens generated locally (
~/.gemini/). - Claude OAuth, GitHub Copilot, OpenAI Codex, AWS Kiro, Codeium Windsurf: monitors token validity and triggers renewal before gateway downtime occurs.
- Google Antigravity and Gemini CLI connections: renews before expiration (15-minute margin) and synchronizes host tokens generated locally (
- Rate-Limit Lock Clearing
- Automatically purges expired
rateLimitedUntillocks and resets backoff counters as soon as cooldown periods finish.
- Automatically purges expired
- Built-in Web Dashboard
- Lightweight web server on port
9090(published on9091) featuring a modern interface, live account countdowns, real-time gateway health diagnostics, and manual sync triggers.
- Lightweight web server on port
- Resilience Combos Enforcement
- Keeps fallback combos registered and synchronized in SQLite (
arsenal-supremo,arsenal-rapido,arsenal-offline,claudegravity-fallback,claudegravity-thinking) without primary key conflicts.
- Keeps fallback combos registered and synchronized in SQLite (
- Strict Virtual Environment Execution
- All Python execution strictly isolated in dedicated virtual environments both in Docker containers (
/opt/venv) and in local setups (.venv).
- All Python execution strictly isolated in dedicated virtual environments both in Docker containers (
| Address | http://localhost:9091 |
| User | admin — or whatever you set in DASHBOARD_USER |
| Password | the value of DASHBOARD_PASSWORD in your .env |
There is no factory password, and that is deliberate: a fixed password shipped in an image is public the moment the image is. You choose it once, in one place:
cp .env.example .env
# edit .env:
DASHBOARD_USER=admin
DASHBOARD_PASSWORD=<a senha que voce escolher>Then bring the stack up. That user and that password are what the panel accepts.
On first boot with DASHBOARD_PASSWORD empty, the container generates a
recovery credential and writes it inside the data directory. Read it:
docker exec 9rtk-sync cat /app/data/db/.dashboard_recoverySign in as admin with that value, then set a real password on the screen. The
recovery credential keeps working afterwards — it is break-glass, and one that
stopped working the moment you set a password would be useless exactly when you
need it.
Português: o painel pede usuário e senha. O usuário é
admin(ou o que estiver emDASHBOARD_USER) e a senha é a que você definir emDASHBOARD_PASSWORDno.env— não existe senha de fábrica, porque um valor fixo publicado na imagem é uma credencial pública. Se subiu sem definir senha, use o comando acima para ler a credencial de recuperação e entre com ela.
The whole configuration comes from environment variables, read from a .env
next to docker-compose.yml — Compose finds it on its own, with no flag.
make setup # creates .env from .env.example, never overwriting an existing oneAt the end, the target lists exactly which variables were left blank and need filling in. Fill them and bring the stack up.
The .env is never committed, and .env.example carries no secret value —
a value published in an example file is, by definition, a public credential. A
test guarantees that every variable a compose file requires exists in the
example, so that cp .env.example .env never produces an incomplete .env.
The three synchronizers listen on the same port inside the container
(9090) and publish on different host ports, so that all three can run side by
side. The same goes for the gateways: each one has its own.
| Service | Internal port | Published on the host |
|---|---|---|
| 9Router | 20128 |
8081 |
| OmniRoute | 20128 |
8082 |
| LiteLLM | 4000 |
8083 |
| 9RTKSync (dashboard) | 9090 |
9091 |
| OminiRTkSync (dashboard) | 9090 |
9092 |
| LiteLlmRTKSync (dashboard) | 9090 |
9093 |
The article stack (claudegravity) keeps 20128, the default 9Router port.
The repository stacks deliberately move out of that range: that way you can run
the article and all three synchronizers at the same time, with no conflict.
Everything is bound to 127.0.0.1: the gateway carries real credentials and
must not be reachable on the local network. To change any of them, edit the left
side of the mapping in the compose file — the right side is the internal port,
the one the process listens on.
Official multi-architecture Docker images (linux/amd64 and linux/arm64) are published automatically to the GitHub Container Registry (GHCR) on pushes to master with changes in src/. An automated retention policy maintains strictly the last 3 versions:
docker pull ghcr.io/pathbit/9rtksync:latestAdd 9rtksync to your docker-compose.yml alongside 9Router:
name: 9rtksync-stack
services:
9rtk-router:
image: decolua/9router:latest
container_name: 9rtk-router
hostname: 9rtk-router
networks:
- 9rtksync-net
restart: unless-stopped
ports:
# 20128 dentro do container; 8081 no host, para nao disputar a porta
# padrao do 9Router com a stack do artigo.
- "127.0.0.1:8081:20128"
environment:
- DATA_DIR=/app/data
- PORT=20128
- HOSTNAME=0.0.0.0
# Sem esta linha o fluxo de login e redirecionado para a porta interna,
# que nao existe no host.
- NEXT_PUBLIC_BASE_URL=http://localhost:8081
- NODE_ENV=production
# Sem valor de fallback: um default publicado em arquivo de exemplo vira
# a senha real de toda implantacao que so copiou e colou.
- INITIAL_PASSWORD=${INITIAL_PASSWORD:?defina INITIAL_PASSWORD no .env}
- JWT_SECRET=${JWT_SECRET:?defina JWT_SECRET no .env (openssl rand -hex 32)}
volumes:
- 9router_data:/app/data
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:20128/dashboard"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s
9rtk-sync:
# Mesmo uid do gateway: os dois compartilham o volume de dados.
user: "1000:1000"
image: ghcr.io/pathbit/9rtksync:latest
container_name: 9rtk-sync
hostname: 9rtk-sync
networks:
- 9rtksync-net
restart: unless-stopped
ports:
- "127.0.0.1:9091:9090"
volumes:
- 9router_data:/app/data
- ${HOME}:/root/host:ro
- 9rtksync_logs:/app/logs
environment:
- HOST_HOME=/root/host
- DB_PATH=/app/data/db/data.sqlite
- ROUTER_URL=${ROUTER_URL:-http://9rtk-router:20128}
- SYNC_INTERVAL=${SYNC_INTERVAL:-300}
- REFRESH_MARGIN=${REFRESH_MARGIN:-900}
- ENABLE_WEB_DASHBOARD=${ENABLE_WEB_DASHBOARD:-1}
- WEB_PORT=${WEB_PORT:-9090}
- DASHBOARD_USER=${DASHBOARD_USER:-admin}
- DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-}
- LOG_DIR=${LOG_DIR:-/app/logs}
- LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-30}
depends_on:
9rtk-router:
condition: service_healthy
healthcheck:
test: ["CMD", "/opt/venv/bin/python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9090/healthz', timeout=3)"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
volumes:
9router_data:
9rtksync_logs:
networks:
9rtksync-net:
name: 9rtksync-net
# Rede propria da stack. Na rede default, duas stacks no mesmo daemon
# resolvem o mesmo nome curto e nao da para saber a qual gateway o
# sincronizador se conectou.Following standard environment isolation, local runs strictly use a Python virtual environment with Python 3.14.7:
git clone https://github.com/pathbit/9RTKSync.git
cd 9RTKSyncpython3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .Copy the official template to create your local .env file (the .env file is strictly ignored by git):
cp .env.example .env# View current status of 9Router connections and combos
9RTKSync --status --db-path /path/to/data.sqlite
# Run an immediate one-shot synchronization pass
9RTKSync --once --db-path /path/to/data.sqlite
# Run continuous background daemon with web dashboard on port 9090 (published on 9091)
9RTKSync --daemon --db-path /path/to/data.sqlite| Variable | Default | Description |
|---|---|---|
DB_PATH |
/app/data/db/data.sqlite |
Absolute path to the 9Router SQLite database |
ROUTER_URL |
http://127.0.0.1:20128 |
Base URL of the 9Router gateway for diagnostics and integration |
SYNC_INTERVAL |
300 |
Sync and background cron loop interval in seconds |
REFRESH_MARGIN |
900 |
Proactive token renewal margin in seconds before expiration |
ENABLE_WEB_DASHBOARD |
1 |
Enable the embedded web dashboard (1 to enable, 0 to disable) |
WEB_PORT |
9090 |
HTTP port for the web dashboard |
WEB_HOST |
0.0.0.0 |
Network binding interface for the dashboard web server |
DASHBOARD_USER |
admin |
HTTP Basic Auth username for web dashboard access |
DASHBOARD_PASSWORD |
(vazio) | Panel password. Left empty, the first sign-in uses the recovery credential generated on first boot. |
ANTIGRAVITY_TOKEN_PATH |
auto | Custom path for Antigravity OAuth token file |
When running with ENABLE_WEB_DASHBOARD=1, access the dashboard in your browser:
Dashboard capabilities:
- Live operational metrics (Total Connections, OAuth Accounts, API Keys, Resilience Combos).
- Six domain cards, in the same order as the sibling panels: gateway connection, scheduler, monitored connections, virtual keys, registered models, resilience combos.
- Real-time countdown meters with visual health badges for every connection.
- Gateway diagnostic card with millisecond latency testing.
- Password change modal for credential rotation.
- Manual sync trigger, from the panel or from a script.
The buttons on the panel post to /acoes/… and answer with a redirect
(POST-Redirect-GET), so a reload never repeats the action. The /api/… routes
(POST /api/test-gateway, /api/change-password, /api/sync, /api/cron-run)
do the same work for curl and for monitoring, and they answer JSON. Both exist
on purpose; naming only the API here read as if the buttons used it, which they
do not.
You can run the test suite with zero installations on your host machine (Docker only), or locally via your virtual environment.
The only requirement is Docker. Nothing else needs to be installed on your machine:
# Direct shell test runner
./run_tests.sh
# Or via Makefile target
make test-container
# docker-compose.test.yml nao tem servico de teste: e uma bancada viva
# (gateway real + este sincronizador) para conferir a stack de ponta a ponta.
docker compose -f docker-compose.test.yml up -d
docker compose -f docker-compose.test.yml down -vIf you prefer testing directly on your host with Python 3.14+:
source .venv/bin/activate
make test
# Or directly
PYTHONPATH=src python3 -m unittest discover -s tests -p "test_*.py"Validates that the generated Docker image boots cleanly, exposes the web dashboard on port 9090, responds on /login (200 OK) and /healthz, and executes CLI commands:
# Install package with test dependencies
pip install ".[test]"
# Run Testcontainers validation suite
python3 -m unittest tests/test_container.py -v- The
masterbranch is protected. All contributions must be submitted through Pull Requests and pass all CI checks. - For bug reports or new provider requests, please open an issue in GitHub Issues.
- Official upstream gateway: 9Router on GitHub.
Distributed under the MIT License. The full text is available in LICENSE.
In practice: use, copy, modify, and distribute freely, including commercially, provided that copyright and license notices accompany copies. The software is provided as is, without warranty.
Developed with ❤️ by Pathbit