Outless is a VLESS node management system with tokenized access control. It manages groups of exit nodes and provides subscription URLs for end users.
Frontend Repository: github.com/quonaro/OutlessUI
Key architecture decision: Outless does not embed Xray. Instead, it controls an external Xray instance via gRPC API. This separates concerns: Outless manages business logic (groups, tokens, subscriptions), Xray handles the network layer.
- Node Groups: Organize VLESS nodes into groups (e.g., "Premium", "Free")
- Tokenized Access: Issue access tokens that grant users specific group subscriptions
- Dynamic Routing: Configure Xray in real-time via gRPC API to route users through selected exit nodes
- Subscription API: Generate VLESS subscription URLs for clients (V2Ray, Nekoray, etc.)
- GeoIP Resolution: Automatic country detection for nodes
User → Outless API → Xray gRPC API → External Xray Instance
↓
VLESS Outbound (exit node)
Components:
- Go 1.26.2+ backend (Clean Architecture / Hexagonal)
- Nuxt 4 + shadcn-vue frontend (OutlessUI)
- PostgreSQL for persistence
- External Xray (via gRPC API, not embedded)
- Docker Compose for deployment (includes Frontend + Backend)
Default Ports:
- Backend API:
41220 - Frontend dev:
41221 - Xray gRPC API:
10085 - Xray VLESS:
443
Deploy Outless using pre-built images:
mkdir outless && cd outless
# Download compose file
curl -O https://raw.githubusercontent.com/quonaro/outless/main/backend/docker-compose.yamlCreate xray.config.json for the external Xray instance:
{
"api": {
"tag": "api",
"services": ["HandlerService", "RoutingService"]
},
"inbounds": [{
"tag": "api",
"listen": "0.0.0.0",
"port": 10085,
"protocol": "dokodemo-door",
"settings": { "address": "127.0.0.1" }
}],
"routing": { "domainStrategy": "AsIs", "rules": [] }
}Create outless.yaml for Docker Compose deployment. See outless.yaml.example for full configuration reference.
app:
shutdown_gracetime: "10s"
http_port: 41220
logs:
level: info
type: pretty
access: stdout
error: stderr
auth:
admin:
login: admin
password: CHANGE_ME
jwt:
secret: CHANGE_ME_RANDOM_STRING_MIN_32_CHARS
expiry: 24h
database: "postgres://outless:outless@database:5432/outless?sslmode=disable"
geoip:
db_path: "/tmp/GeoLite2-Country.mmdb"
auto: false
expiry: 24h
router:
url_host: "your-domain.com"
inbound:
port: 443
address: ":443"
sni: "www.google.com"
public_key: "YOUR_PUBLIC_KEY"
private_key: "YOUR_PRIVATE_KEY"
short_id: "YOUR_SHORT_ID"
fingerprint: "chrome"
api: "xray:10085"
sync_interval: "30s"
name_template: "{{vless.country_flag}} {{vless.country}} | {{vless.group}}"docker run --rm ghcr.io/xtls/xray-core x25519Update outless.yaml with the generated keys.
docker compose up -dServices:
| Service | Port | Image |
|---|---|---|
| PostgreSQL | 5432 | postgres:16-alpine |
| Xray | 443, 10085 | ghcr.io/xtls/xray-core |
| Outless Backend | 41220 | quonaro/outless:backend |
| Outless Frontend | 41221 | quonaro/outless:frontend |
# Check Xray API
curl http://localhost:10085
# Check Outless health
curl http://localhost:41220/health
# Login and get token
curl -X POST http://localhost:41220/api/auth/login \
-H "Content-Type: application/json" \
-d '{"login":"admin","password":"CHANGE_ME"}'Access the UI: Open http://localhost:41221 in your browser.
| Field | Required | Description |
|---|---|---|
api.services |
Yes | Must include HandlerService and RoutingService for Outless to work |
inbounds[0].port |
Yes | gRPC API port (default: 10085) |
inbounds[0].listen |
Yes | Bind address (use 0.0.0.0 for Docker) |
| Field | Required | Default | Description |
|---|---|---|---|
shutdown_gracetime |
No | 10s |
Graceful shutdown timeout |
http_port |
No | 41220 |
HTTP API server port |
logs.level |
No | info |
Log level (debug, info, warn, error) |
logs.type |
No | pretty |
Log format (pretty, json) |
| Field | Required | Default | Description |
|---|---|---|---|
admin.login |
Yes | admin |
Admin username |
admin.password |
Yes | - | Admin password (change in production!) |
jwt.secret |
Yes | - | JWT signing secret (random string) |
jwt.expiry |
Yes | 24h |
Token lifetime |
| Field | Required | Default | Description |
|---|---|---|---|
database |
Yes | - | PostgreSQL connection DSN string |
| Field | Required | Default | Description |
|---|---|---|---|
db_path |
No | /tmp/GeoLite2-Country.mmdb |
GeoIP database file path |
auto |
No | false |
Auto-download GeoIP database |
expiry |
No | 24h |
GeoIP cache expiry |
| Field | Required | Default | Description |
|---|---|---|---|
url_host |
Yes | localhost |
Domain for subscription URL generation |
inbound.port |
Yes | 443 |
Listening port for VLESS connections |
inbound.sni |
Yes | - | SNI for REALITY handshake |
inbound.public_key |
Yes | - | REALITY public key |
inbound.private_key |
Yes | - | REALITY private key (base64) |
inbound.short_id |
Yes | - | REALITY short ID |
inbound.fingerprint |
No | chrome |
TLS fingerprint |
api |
Yes | - | Xray gRPC API address |
sync_interval |
No | 30s |
How often to sync DB state to Xray |
name_template |
No | - | Template for VLESS URL remarks |
Templates control how VLESS connection names appear in subscription URLs.
| Variable | Description | Example |
|---|---|---|
vless.name |
Original node name | Poland Premium |
vless.host |
Node host IP/domain | 82.22.41.75 |
vless.port |
Port | 443 |
vless.sni |
SNI | www.google.com |
vless.security |
Security type | reality |
vless.flow |
Flow type | xtls-rprx-vision |
vless.country |
Full country name | Poland |
vless.country_short |
2-letter code | PL |
vless.country_flag |
Flag emoji | 🇵🇱 |
vless.group |
Group name | Premium |
vless.user |
User email | user@example.com |
# Simple substitution
name_template: "{{vless.country}} | {{vless.group}}"
# Result: "Poland | Premium"
# Fallback to literal
name_template: "{{vless.group|\"Default\"}}"
# Result: "Default" (if group is empty)
# Fallback to another variable
name_template: "{{vless.name|vless.host}}"
# Result: host IP if name is emptyPOST /api/auth/login- Get JWT token
GET /api/nodes- List nodesPOST /api/nodes- Add node (VLESS URL)DELETE /api/nodes/:id- Remove node
GET /api/groups- List groupsPOST /api/groups- Create groupPOST /api/groups/:id/sync- Sync nodes from source URL
GET /api/tokens- List tokensPOST /api/tokens- Create tokenDELETE /api/tokens/:id- Revoke token
GET /sub/:token- Get VLESS subscription (base64)
- Contributing: see
CONTRIBUTING.md - License: see
LICENSE
Outless depends on xray-core. Keep upstream license obligations in mind when distributing binaries.