Skip to content

Repository files navigation

9RTKSync · 9Router Universal Token & Connection Synchronizer

CI Release and Docker Package License: MIT Python Version Docker Package

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.

Documentation

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.


Core Features

  • Numeric Expiration Self-Healing
    • 9Router natively writes the expiresAt field as an ISO date string (for example "2026-09-12T11:54:08.336Z"). This breaks internal numeric validations and causes false HTTP 503 errors.
    • 9RTKSync continuously inspects the SQLite database and automatically converts string timestamps into valid millisecond epoch integers.
  • 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.
  • Rate-Limit Lock Clearing
    • Automatically purges expired rateLimitedUntil locks and resets backoff counters as soon as cooldown periods finish.
  • Built-in Web Dashboard
    • Lightweight web server on port 9090 (published on 9091) featuring a modern interface, live account countdowns, real-time gateway health diagnostics, and manual sync triggers.
  • 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.
  • Strict Virtual Environment Execution
    • All Python execution strictly isolated in dedicated virtual environments both in Docker containers (/opt/venv) and in local setups (.venv).


🔑 Signing in to the dashboard

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.

Did not set a password, and now cannot get in?

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_recovery

Sign 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 em DASHBOARD_USER) e a senha é a que você definir em DASHBOARD_PASSWORD no .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.

Running with Docker

Configuration: .env from the example

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 one

At 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.

Ports, and why each one differs

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:latest

Docker Compose Example

Add 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.

Local Development in Virtual Environment

Following standard environment isolation, local runs strictly use a Python virtual environment with Python 3.14.7:

1. Clone the Repository

git clone https://github.com/pathbit/9RTKSync.git
cd 9RTKSync

2. Create and Activate the Virtual Environment

python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .

3. Configure Environment Variables (.env)

Copy the official template to create your local .env file (the .env file is strictly ignored by git):

cp .env.example .env

4. Available CLI Commands

# 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

Environment Variables

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

Web Dashboard

When running with ENABLE_WEB_DASHBOARD=1, access the dashboard in your browser:

👉 http://localhost:9091

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.


Unit and Integration Testing

You can run the test suite with zero installations on your host machine (Docker only), or locally via your virtual environment.

Option 1. Container Testing (Zero Host Installation)

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 -v

Option 2. Local Virtual Environment (Optional Prerequisites)

If 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"

Option 3. Automated Docker Image Validation with Testcontainers

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

Contributing and Branch Protection

  • The master branch 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.

📄 License

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

Releases

Packages

Contributors

Languages