# 9router Deployment Reference

**Package:** 9router (npm)  
**Version:** 0.4.50 (as of 2026-05)  
**GitHub:** https://github.com/decolua/9router  
**Stars:** 11K+  
**Purpose:** AI model router — connects Claude Code, Codex, Cursor, Cline to 40+ providers with auto-fallback and token savings.

## Installation

```bash
npm install -g 9router
```

Install location: `$(npm root -g)/9router/` (e.g., `~/.hermes/node/lib/node_modules/9router/`)

## Deployment (Non-Interactive / Headless)

### Problem: CLI Command Hangs

The `9router` CLI command often hangs in non-interactive environments. It:
- Spawns a child process with detached TTY
- Checks for auto-updates
- May try to open a browser
- May enter system tray mode

**Note:** `9router --help` and `9router --version` work fine. Only the main server start hangs.

### Solution: Direct Node Execution

```bash
# Find the app directory
ls $(npm root -g)/9router/app/
# Should contain: server.js, package.json, node_modules/, src/, public/

# Run directly
cd $(npm root -g)/9router/app
PORT=20128 HOSTNAME=0.0.0.0 node server.js
```

Output should show:
```
▲ Next.js 16.2.1
- Local:         http://localhost:20128
- Network:       http://0.0.0.0:20128
✓ Ready in 0ms
```

### Background Execution

```bash
# Using nohup
cd $(npm root -g)/9router/app && PORT=20128 HOSTNAME=0.0.0.0 nohup node server.js &

# Or with proxy
cd $(npm root -g)/9router/app && \
  PORT=20128 HOSTNAME=0.0.0.0 \
  HTTP_PROXY=http://127.0.0.1:7890 \
  HTTPS_PROXY=http://127.0.0.1:7890 \
  node server.js &
```

## Configuration

### Data Directory

`~/.9router/`
```
~/.9router/
├── db/
│   └── data.sqlite      # Main config database
├── jwt-secret           # JWT signing secret
├── logs/
└── runtime/             # SQLite native deps
```

### Default Port: 20128

Dashboard: `http://localhost:20128`
API Base: `http://localhost:20128/api/`

### First-Time Setup

1. Access `http://localhost:20128`
2. Redirects to `/login`
3. Set admin password on first use

### Password Management

Password stored as bcrypt hash in `settings` table:

```python
import sqlite3, bcrypt, json

conn = sqlite3.connect(os.path.expanduser("~/.9router/db/data.sqlite"))
cursor = conn.cursor()

# Reset password
new_pw = "admin123"
pw_hash = bcrypt.hashpw(new_pw.encode(), bcrypt.gensalt()).decode()
cursor.execute("UPDATE settings SET data = ? WHERE id = 1;",
               (json.dumps({"password": pw_hash}),))
conn.commit()
```

**Requires:** `pip install bcrypt` or `uv pip install bcrypt`

## API Reference

### Authentication

```bash
# Login
curl -s -X POST http://localhost:20128/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"password":"<password>"}' -c cookies.txt

# Check auth status (no auth required)
curl -s http://localhost:20128/api/auth/status
```

### Settings

```bash
# Get settings (requires auth)
curl -s -b cookies.txt http://localhost:20128/api/settings

# Update settings (PATCH for partial update)
curl -s -X PATCH -b cookies.txt http://localhost:20128/api/settings \
  -H "Content-Type: application/json" \
  -d '{"outboundProxyEnabled":true,"outboundProxyUrl":"http://127.0.0.1:7890"}'
```

**Pitfall:** Use PATCH for partial updates, not PUT. PUT may require all fields.

### Key Settings Fields

| Field | Type | Description |
|-------|------|-------------|
| `outboundProxyEnabled` | bool | Enable outbound proxy |
| `outboundProxyUrl` | string | Proxy URL (e.g., `http://127.0.0.1:7890`) |
| `outboundNoProxy` | string | No-proxy list |
| `rtkEnabled` | bool | RTK token saver (default: true) |
| `requireLogin` | bool | Require authentication |
| `cloudEnabled` | bool | Cloud tunnel enabled |
| `tunnelEnabled` | bool | Tunnel enabled |

### API Keys

```bash
# Create API key (for CLI tools to connect)
curl -s -X POST -b cookies.txt http://localhost:20128/api/keys \
  -H "Content-Type: application/json" \
  -d '{"name":"my-key"}'
```

## Connecting AI Tools

### Claude Code / Codex / Cursor / Cline

```
Endpoint: http://localhost:20128/v1
API Key: <from dashboard or API>
Model: <provider/model-name>
```

### Free Providers (No Signup)

- **Kiro AI** — Free Claude unlimited
- **OpenCode Free** — No auth required
- **Vertex AI** — $300 free credits

## Database Schema

Key tables in `~/.9router/db/data.sqlite`:

| Table | Purpose |
|-------|---------|
| `settings` | App config (password, proxy, etc.) |
| `apiKeys` | API keys for CLI tools |
| `providerConnections` | AI provider connections |
| `providerNodes` | Provider node configs |
| `proxyPools` | Proxy pool configs |
| `combos` | Model combos/fallback chains |
| `usageHistory` | Request history |
| `kv` | Key-value store |

## mihomo/Clash Proxy Integration

### Switch Proxy Node

```bash
# List available proxy groups
curl -s http://127.0.0.1:9090/proxies | python3 -c "import json,sys; print(json.dumps(list(json.load(sys.stdin)['proxies'].keys()), indent=2))"

# Switch node in AUTO group
curl -s -X PUT http://127.0.0.1:9090/proxies/AUTO \
  -H "Content-Type: application/json" \
  -d '{"name":"cf加速|香港动态家宽🇭🇰"}'
```

### Check Node Status

```bash
# URL-encode the node name (emojis/special chars)
curl -s "http://127.0.0.1:9090/proxies/cf%E5%8A%A0%E9%80%9F%7C%E5%8F%B0%E6%B9%BE%F0%9F%87%B9%F0%9F%87%BC" | python3 -c "import json,sys; d=json.load(sys.stdin); print('alive:', d.get('alive'))"
```

**Pitfall:** Node names with emojis must be URL-encoded. Use `urllib.parse.quote()`.

### Common Node Patterns

- `cf加速|台湾🇹🇼` / `cf加速|台湾动态家宽🇹🇼` — Taiwan nodes
- `cf加速|香港动态家宽🇭🇰` — Hong Kong nodes
- `【5x】中转|台湾hinet动态家宽🇹🇼` — 5x multiplier Taiwan Hinet

## Troubleshooting

### Service won't start
- Check if port 20128 is already in use: `ss -tlnp | grep 20128`
- Check Node.js version: `node --version` (requires 18+)

### "Unauthorized" on API calls
- Must authenticate first via `/api/auth/login`
- Cookie-based sessions (use `-c cookies.txt` and `-b cookies.txt`)

### Proxy not working
- Verify proxy is running: `curl -x http://127.0.0.1:7890 https://httpbin.org/ip`
- Check mihomo node status: nodes may be "alive": false
- 9router uses environment variables OR API config (API takes precedence)

### Can't access from other machines
- Ensure `HOSTNAME=0.0.0.0` (not localhost)
- Check firewall rules for port 20128
