Languages: English | Русский
Subscription server for XTLS/Xray-core. Reads your Xray server configs, auto-discovers users, and gives each one a personal subscription URL — so their VPN client always has the latest connection settings.
- What it does
- Features
- How it works
- Quick Start
- Configuration
- Subscription URLs
- Admin API
- Routing Rules
- Supported Protocols & Transports
- Docker
- Contributing
When you run an Xray proxy server, each user needs a client config file with the correct server address, port, UUID, and transport settings. Keeping those configs up to date manually is tedious.
Raven Subscribe solves this:
- It reads your Xray server configs from
/etc/xray/config.d - Discovers all users (clients) defined in those configs automatically
- Generates a ready-to-use Xray client config for each user
- Serves it via a unique subscription URL
Users just add their subscription URL to any Xray-compatible client (V2RayNG, NekoBox, V2Box, Hiddify, etc.) — and the client fetches fresh settings automatically.
- Personal subscription URL — one link that always returns the latest config
- Multiple formats — full Xray JSON, plain share links, Base64-encoded links
- Protocol-specific links — get only VLESS, VMess, Trojan, or Shadowsocks links
- Mobile-optimized configs — auto-detected from User-Agent (Android, iPhone, NekoBox, V2RayNG) or via
?profile=mobile - Custom routing rules — per-user rules to route specific sites directly, through proxy, or block them
- Database as source of truth — when
api_user_inbound_tagis set, add/remove/enable/disable users via API and they sync to Xray immediately - Zero-touch user sync — users can also be discovered from Xray config
emailfields - File watcher — detects config changes instantly (fsnotify + periodic polling)
- Full REST API — manage users, tokens, routing rules, and balancer settings
- Per-user client control — enable/disable a user's access to specific inbounds
- Token rotation — regenerate any user's subscription token without downtime
- Balancer support — automatic load balancing across multiple outbounds (leastPing, leastLoad, random)
- Global routing rules — apply routing rules to all users at once
- VLESS, VMess, Trojan, Shadowsocks, SOCKS
- TCP, WebSocket, gRPC, HTTP/2, KCP, QUIC, HTTPUpgrade, XHTTP (SplitHTTP)
- TLS and REALITY security layers with automatic key derivation
/etc/xray/config.d/
├── vless-reality.json ← Xray server inbound configs
├── vmess-ws.json
└── trojan-tls.json
│
▼
Raven Subscribe
(watches for changes)
│
├─ Parses inbounds, discovers users
├─ Stores in SQLite
├─ Serves subscription URLs
└─ API-created users → Xray (config files or gRPC API)
│
▼
https://your-server.com/sub/{token}
│
▼
V2RayNG / NekoBox / Hiddify / V2Box
(fetches config automatically)
Each user gets a unique token. When their client fetches the subscription URL, Raven Subscribe builds a complete Xray client config on the fly — with all their enabled inbounds, routing rules, DNS settings, and balancer config.
From binary:
curl -Lo xray-subscription https://github.com/AlchemyLink/Raven-subscribe/releases/latest/download/xray-subscription-linux-amd64
chmod +x xray-subscription
sudo mv xray-subscription /usr/local/bin/From source:
git clone https://github.com/AlchemyLink/Raven-subscribe.git
cd Raven-subscribe
make build
sudo cp build/xray-subscription /usr/local/bin/sudo mkdir -p /etc/xray-subscription
sudo cp config.json.example /etc/xray-subscription/config.json
sudo nano /etc/xray-subscription/config.jsonMinimum required settings:
{
"server_host": "your-server-ip-or-domain",
"admin_token": "your-secret-admin-token",
"base_url": "http://your-server-ip-or-domain:8080"
}xray-subscription -config /etc/xray-subscription/config.jsonAs a systemd service:
sudo cp xray-subscription.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now xray-subscriptionThe service runs as User=xray so Raven and Xray share ownership of config files. When api_user_inbound_tag is set, Raven writes to config_dir; Xray must read those files. Ensure:
# Create xray user/group if missing (Xray package usually does this)
sudo useradd -r -s /usr/sbin/nologin xray 2>/dev/null || true
# Let xray own config_dir and data dir when using file-based sync
sudo chown -R xray:xray /etc/xray/config.d /var/lib/xray-subscription# List all users and their subscription URLs
curl -H "X-Admin-Token: your-secret-admin-token" http://localhost:8080/api/usersResponse:
[
{
"user": {
"id": 1,
"username": "alice",
"token": "a3f8c2...",
"enabled": true
},
"sub_url": "http://your-server:8080/sub/a3f8c2...",
"sub_urls": {
"full": "http://your-server:8080/sub/a3f8c2...",
"links_txt": "http://your-server:8080/sub/a3f8c2.../links.txt",
"links_b64": "http://your-server:8080/sub/a3f8c2.../links.b64",
"compact": "http://your-server:8080/c/a3f8c2...",
"compact_txt": "http://your-server:8080/c/a3f8c2.../links.txt",
"compact_b64": "http://your-server:8080/c/a3f8c2.../links.b64"
}
}
]Give each user their sub_urls.compact URL — they add it to their VPN client and are ready to go.
Configuration is loaded from a JSON file (default: config.json in the current directory). Use -config flag to specify a path:
xray-subscription -config /etc/xray-subscription/config.json{
"listen_addr": ":8080",
"server_host": "your-server.com",
"config_dir": "/etc/xray/config.d",
"db_path": "/var/lib/xray-subscription/db.sqlite",
"sync_interval_seconds": 60,
"base_url": "http://your-server.com:8080",
"admin_token": "your-secret-token",
"balancer_strategy": "leastPing",
"balancer_probe_url": "https://www.gstatic.com/generate_204",
"balancer_probe_interval": "30s",
"socks_inbound_port": 2080,
"http_inbound_port": 1081,
"rate_limit_sub_per_min": 60,
"rate_limit_admin_per_min": 30,
"api_user_inbound_tag": "vless-reality",
"xray_api_addr": ""
}| Field | Default | Description |
|---|---|---|
listen_addr |
:8080 |
Address and port to bind. Use :8080 for all interfaces, or 127.0.0.1:8080 for localhost only (e.g. behind nginx). |
server_host |
— | Required. Your server IP or domain. Used as the outbound address in generated client configs. Must match what clients will connect to. |
base_url |
http://localhost:8080 |
Full base URL for subscription links. Shown to users in API responses. Use https:// if behind TLS reverse proxy. |
| Field | Default | Description |
|---|---|---|
config_dir |
/etc/xray/config.d |
Directory containing Xray server inbound JSON configs. Raven watches this for changes (fsnotify + periodic scan). |
db_path |
/var/lib/xray-subscription/db.sqlite |
SQLite database path. Stores users, tokens, routing rules, and synced client data. |
sync_interval_seconds |
60 |
Interval (seconds) for re-scanning config_dir. Also triggered on file changes. |
| Field | Default | Description |
|---|---|---|
admin_token |
— | Required. Secret token for Admin API. Pass in X-Admin-Token header. Use a long random string; generate with openssl rand -hex 32. |
Used when your Xray config has multiple outbounds (e.g. several proxy nodes). Controls how the client chooses between them.
| Field | Default | Description |
|---|---|---|
balancer_strategy |
leastPing |
Strategy: leastPing (lowest latency), leastLoad (least connections), random, roundRobin. |
balancer_probe_url |
https://www.gstatic.com/generate_204 |
URL for latency probes (used by leastPing). Must be reachable from the server. |
balancer_probe_interval |
30s |
How often to probe outbounds. Go duration: 30s, 1m, etc. |
| Field | Default | Description |
|---|---|---|
socks_inbound_port |
2080 |
Local SOCKS5 proxy port in generated client configs. Clients use this for system/app proxy. |
http_inbound_port |
1081 |
Local HTTP proxy port in generated client configs. |
Limits requests per IP per minute. 0 = disabled. Helps prevent abuse.
| Field | Default | Description |
|---|---|---|
rate_limit_sub_per_min |
0 |
Max requests/min per IP for /sub/* and /c/*. Recommended: 60 for production. |
rate_limit_admin_per_min |
0 |
Max requests/min per IP for /api/*. Recommended: 30. |
api_user_inbound_tag |
"" |
When set, the database is the source of truth: users created via API are added to this Xray inbound; deleted users are removed; enable/disable syncs to Xray. Uses config_dir file write or Xray API (if xray_api_addr is set). |
xray_api_addr |
"" |
When set, users are synced via Xray gRPC API instead of config files. E.g. 127.0.0.1:8080. Requires api_user_inbound_tag. Xray must have API enabled with HandlerService. |
api_user_inbound_protocol |
"" |
Fallback when config_dir has no inbounds: protocol (vless, vmess, trojan, shadowsocks) to create the inbound in DB. Use when Xray configs are elsewhere. |
api_user_inbound_port |
443 |
Port for the inbound when using api_user_inbound_protocol fallback. |
xray_config_file_mode |
(omit) | Octal mode for JSON files Raven writes under config_dir (e.g. "0644" so another local user can read configs for testing). Default 0600. Only permission bits 0–7 (max 0777). |
DB ↔ Xray sync (when api_user_inbound_tag is set): The database is the source of truth. All changes propagate to Xray immediately:
| Action | DB | Xray |
|---|---|---|
Create user (POST /api/users) |
Add | Add to inbound |
Delete user (DELETE /api/users/{id}) |
Remove | Remove from inbound |
Disable user (PUT /api/users/{id}/disable) |
enabled=false |
Remove from inbound |
Enable user (PUT /api/users/{id}/enable) |
enabled=true |
Add to inbound |
Xray API mode (when xray_api_addr is set): Users are synced via gRPC instead of config files. Xray must have API enabled with HandlerService in services.
- Restore on startup: Raven restores all users from the database to Xray via API (survives Xray restarts).
- Periodic DB→config sync: Raven periodically writes users to config files, so they persist across both Raven and Xray restarts.
{
"server_host": "vpn.example.com",
"admin_token": "your-secret-token",
"base_url": "https://vpn.example.com"
}All other parameters use defaults.
{
"listen_addr": "127.0.0.1:8080",
"server_host": "vpn.example.com",
"base_url": "https://vpn.example.com",
"admin_token": "your-secret-token",
"rate_limit_sub_per_min": 60,
"rate_limit_admin_per_min": 30
}Use 127.0.0.1 when running behind nginx/caddy as reverse proxy.
Each user has two subscription endpoints:
| Endpoint | Description |
|---|---|
/c/{token} |
Primary. Lightweight config — geosite:/geoip: selectors stripped. Works great on all devices. |
/sub/{token} |
Full config with all routing rules including geo databases. |
The compact endpoint is the recommended URL to give to users. It returns a complete Xray client config with routing rules optimized for lower memory usage — geosite: and geoip: selectors are stripped, keeping only explicit domain and IP rules.
Works on all clients: V2RayNG, NekoBox, V2Box, Hiddify, and desktop clients.
| What you want | URL |
|---|---|
| Full Xray JSON config | /c/{token} |
| All share links (plain text) | /c/{token}/links.txt |
| All share links (Base64) | /c/{token}/links.b64 |
Returns the complete config including geosite: and geoip: routing rules. Use this if your client supports geo databases and you want full routing control.
| What you want | URL |
|---|---|
| Full Xray JSON config | /sub/{token} |
| All share links (plain text) | /sub/{token}/links.txt |
| All share links (Base64) | /sub/{token}/links.b64 |
| VLESS links only | /sub/{token}/vless |
| VMess links only | /sub/{token}/vmess |
| Trojan links only | /sub/{token}/trojan |
| Shadowsocks links only | /sub/{token}/ss |
| Specific inbound only | /sub/{token}/inbound/{tag} |
| Lightweight config (explicit) | /sub/{token}?profile=mobile |
- Open V2RayNG → tap + → Import config from URL
- Paste:
http://your-server:8080/c/YOUR_TOKEN - Tap OK — done. The app fetches and imports all your connections.
Use the same /c/{token} URL. These clients support Xray JSON format natively.
curl http://your-server:8080/c/YOUR_TOKEN/links.txtOutput:
vless://uuid@your-server:443?type=ws&security=tls&...#vless-ws-tls
vmess://eyJ2IjoiMiIsInBzIjoidm1lc3MtdGNwIiwiYWRkIjoieW91ci1zZXJ2ZXIiLCJwb3J0IjoiODA4MCIsImlkIjoiLi4uIn0=
trojan://password@your-server:443?security=tls&...#trojan-tls
When a mobile client fetches /sub/{token}, Raven Subscribe automatically detects it from the User-Agent header (Android, iPhone, iPad, V2RayNG, NekoBox, V2Box) and applies the lightweight profile automatically. The /c/{token} endpoint always uses the lightweight profile regardless of User-Agent.
All admin endpoints require authentication. Pass your admin_token as a header or query parameter:
# As header (recommended)
curl -H "X-Admin-Token: your-secret-token" http://localhost:8080/api/users
# As query parameter
curl "http://localhost:8080/api/users?admin_token=your-secret-token"GET /api/userscurl -H "X-Admin-Token: secret" http://localhost:8080/api/users[
{
"user": {"id": 1, "username": "alice@example.com", "token": "abc123", "enabled": true},
"sub_url": "http://your-server:8080/sub/abc123",
"sub_urls": {
"full": "http://your-server:8080/sub/abc123",
"links_txt": "http://your-server:8080/sub/abc123/links.txt",
"links_b64": "http://your-server:8080/sub/abc123/links.b64",
"compact": "http://your-server:8080/c/abc123",
"compact_txt": "http://your-server:8080/c/abc123/links.txt",
"compact_b64": "http://your-server:8080/c/abc123/links.b64"
}
}
]POST /api/users
Content-Type: application/json
{"username": "bob"}On create, email is not accepted; internally it matches username for Xray. API JSON does not include email (use username).
curl -X POST -H "X-Admin-Token: secret" -H "Content-Type: application/json" \
-d '{"username":"bob"}' http://localhost:8080/api/users{
"user": {"id": 2, "username": "bob", "token": "xyz789", "enabled": true},
"sub_url": "http://your-server:8080/sub/xyz789",
"sub_urls": {
"full": "http://your-server:8080/sub/xyz789",
"links_txt": "http://your-server:8080/sub/xyz789/links.txt",
"links_b64": "http://your-server:8080/sub/xyz789/links.b64",
"compact": "http://your-server:8080/c/xyz789",
"compact_txt": "http://your-server:8080/c/xyz789/links.txt",
"compact_b64": "http://your-server:8080/c/xyz789/links.b64"
}
}When api_user_inbound_tag is set, the user is also added to Xray (config file or API).
GET /api/users/{id}DELETE /api/users/{id}{id} accepts a numeric id or a username (including email format like alice@example.com). Applies to GET, DELETE, enable, disable, token, routes, and clients routes.
When api_user_inbound_tag is set, the user is also removed from Xray.
HOST="http://localhost:8080"
ADMIN="your-secret-admin-token"
# 1) Create user
CREATE_JSON=$(curl -sS -X POST "$HOST/api/users" \
-H "X-Admin-Token: $ADMIN" \
-H "Content-Type: application/json" \
-d '{"username":"alice@example.com"}')
echo "$CREATE_JSON"
# 2) Delete by username (no jq needed)
curl -sS -X DELETE "$HOST/api/users/alice@example.com" \
-H "X-Admin-Token: $ADMIN"
# {"status":"deleted"}
# — or by numeric id (needs jq)
USER_ID=$(echo "$CREATE_JSON" | jq -r '.user.id')
curl -sS -X DELETE "$HOST/api/users/$USER_ID" \
-H "X-Admin-Token: $ADMIN"
# 3) Confirm gone
curl -sS -H "X-Admin-Token: $ADMIN" "$HOST/api/users/alice@example.com"
# {"error":"user not found"}PUT /api/users/{id}/enable
PUT /api/users/{id}/disableWhen api_user_inbound_tag is set, the user is added to or removed from Xray accordingly.
POST /api/users/{id}/tokenReturns new {token, sub_url}. The old URL stops working immediately.
GET /api/users/{id}/clientsShows which inbounds the user is enrolled in and whether each is enabled.
POST /api/users/{id}/clients
Content-Type: application/json
{
"tag": "vless-xhttp-in",
"protocol": "vless"
}Example:
curl -H "X-Admin-Token: <admin-token>" \
-X POST http://<host>:8080/api/users/16/clients \
-d '{"tag":"vless-xhttp-in"}'tagis required.protocolis optional. If omitted, it is resolved bytagfrom synced inbounds, then falls back toapi_user_inbound_protocol.- If the user is already enrolled in this inbound, the existing client record is returned (idempotent behavior).
PUT /api/users/{userId}/clients/{inboundId}/enable
PUT /api/users/{userId}/clients/{inboundId}/disableUse this to give a user access to only certain servers/protocols.
GET /api/inbounds[
{
"id": 1,
"tag": "vless-reality",
"protocol": "vless",
"port": 443,
"config_file": "/etc/xray/config.d/vless-reality.json"
}
]POST /api/syncForces an immediate re-scan of config_dir. Useful after editing Xray configs.
GET /api/config/balancerPUT /api/config/balancer
Content-Type: application/json
{
"strategy": "leastPing",
"probe_url": "https://www.gstatic.com/generate_204",
"probe_interval": "30s"
}PUT /api/config/balancer
Content-Type: application/json
{"reset": true}GET /health{"status": "ok"}No authentication required. Use this for uptime monitoring.
Raven Subscribe generates Xray client configs with a three-tier routing system:
User rules → Global rules → Built-in defaults
(highest priority) (lowest priority)
Every generated config includes these rules automatically:
- Direct: Russian services (Yandex, VK, Lamoda, etc.), private IPs,
geoip:ru - Proxy:
geosite:ru-blocked,geoip:ru-blocked - Block: Ads and public torrent trackers
POST /api/routes/global
Content-Type: application/json
{
"type": "field",
"outboundTag": "direct",
"domain": ["example.com", "geosite:cn"]
}POST /api/users/{id}/routes
Content-Type: application/json
{
"type": "field",
"outboundTag": "block",
"domain": ["ads.example.com"]
}{
"id": "optional-id",
"type": "field",
"outboundTag": "direct | proxy | block",
"domain": ["example.com", "geosite:ru-blocked"],
"ip": ["1.1.1.1/32", "geoip:ru"],
"network": "tcp | udp",
"port": "443",
"protocol": ["http", "tls"],
"inboundTag": ["socks"]
}outboundTag must be one of: direct, proxy, block.
| Protocol | Share link format | Notes |
|---|---|---|
| VLESS | vless://uuid@host:port?...#tag |
Supports REALITY, TLS, plain |
| VMess | vmess://base64(json) |
|
| Trojan | trojan://password@host:port?...#tag |
|
| Shadowsocks | ss://base64(method:pass)@host:port#tag |
Single and multi-user |
| SOCKS | — | No share link format |
| Transport | Description |
|---|---|
| TCP | Raw TCP, with optional HTTP header obfuscation |
| WebSocket | WS with path and host headers |
| gRPC | gRPC with serviceName |
| HTTP/2 | H2 with host and path |
| mKCP | UDP-based, with header types |
| QUIC | QUIC transport |
| HTTPUpgrade | HTTP upgrade handshake |
| XHTTP / SplitHTTP | Split HTTP for CDN-friendly connections |
| Security | Notes |
|---|---|
| TLS | Strips server certificates, sets fingerprint: chrome by default |
| REALITY | Auto-derives publicKey from server's privateKey, picks first serverName and shortId |
# docker-compose.yml
services:
raven-subscribe:
image: ghcr.io/alchemylink/raven-subscribe:latest
ports:
- "8080:8080"
volumes:
- ./config.json:/etc/xray-subscription/config.json:ro
- /etc/xray/config.d:/etc/xray/config.d:ro
- raven-data:/var/lib/xray-subscription
restart: unless-stopped
volumes:
raven-data:docker compose up -ddocker build -t raven-subscribe .
docker run -p 8080:8080 \
-v ./config.json:/etc/xray-subscription/config.json:ro \
-v /etc/xray/config.d:/etc/xray/config.d:ro \
raven-subscribeSee CONTRIBUTING.md for guidelines.
go test ./... -race
golangci-lint run --timeout=5mmake release VERSION=v1.2.3MIT © AlchemyLink