3x-ui Agent Skills & AI Rules

GitHub

Xray panel supporting multi-protocol multi-user expire day & traffic & IP limit (Vmess, Vless, Trojan, ShadowSocks, Wireguard, Hysteria, Tunnel, Mixed,...

44,515 stars Go Go 1 file Β· ~13,707 tokens #ech#fail2ban#http#hysteria2#mixed#mtproto#nodes#post-quantum
RAW Rules
3 Discovered Rules β€’ ~15,155 Estimated Tokens
Quick:
Loading skill content...

docs - System Architecture & Module Boundaries

docs/architecture.md project-spec ~10,394 tokens
Raw
# 3x-ui β€” Architecture & Code Map

> Navigation map for contributors and AI coding agents (referenced from `CLAUDE.md`).
> Goal: jump to the right file in one hop instead of grepping the whole tree.
> Tracks the `main` branch β€” paths reflect the latest changes, so verify against the live
> tree rather than a pinned release (Go module `github.com/mhsanaei/3x-ui/v3`).
>
> **How to use this file:** read "Mental model" + "Request lifecycle" first, then
> use the **Symptom β†’ File index** to locate work. Respect the **Layering rules**
> when adding code. Verify with the commands in **Build / Test / Lint**.

---

## 1. Mental model (the 30-second version)

3x-ui is a **web control panel for [Xray-core](https://github.com/XTLS/Xray-core)**. The Go
backend is the source of truth: it stores inbounds/clients/settings in a DB, renders an
Xray JSON config from that state, supervises the Xray child process, and exposes a REST +
WebSocket API. A React SPA (built by Vite, embedded into the Go binary) is the UI. A second,
separate HTTP server serves **subscription links** to end users.

The panel supervises **managed child processes**: Xray-core itself and β€” when MTProto
inbounds exist β€” a dedicated child proxy binary:

- **`mtg-multi` for MTProto inbounds** (`github.com/mhsanaei/mtg-multi`, a multi-secret fork
  built from source; `internal/mtproto/`): One process per inbound serves every attached
  client's FakeTLS secret through the fork's `[secrets]` section, plus optional per-client
  sponsored-channel ad-tags via `[secret-ad-tags]`. A client or ad-tag edit is hot-applied via
  the fork's management API (`PUT /secrets`, guarded by a per-process bearer token), with a
  process restart as the fallback on older binaries.

In contrast, **AmneziaWG** (`internal/amneziawgnet/`) and **TUIC v5** (`internal/tuic/`) run as
**in-process native Go servers** without external child processes, bridging client traffic into
Xray-core via loopback SOCKS5 relays.

Servers and processes, all launched from `main.go`:

| Server / process | Package                           | Purpose                                                            | Default port      |
| ---------------- | --------------------------------- | ------------------------------------------------------------------ | ----------------- |
| **Panel**        | `internal/web`                    | Admin REST/WS API + serves the embedded SPA                        | 2053              |
| **Subscription** | `internal/sub`                    | Public endpoint that hands out client configs (raw / JSON / Clash) | `subPort` setting |
| **Xray-core**    | supervised via `internal/xray`    | The actual proxy engine; a child process, not Go code              | `inbounds[].port` |
| **mtg-multi**    | supervised via `internal/mtproto` | MTProto proxy child process for MTProto inbounds (multi-secret)    | per inbound       |

Two key ideas that explain most of the complexity:

1. **The DB β†’ Xray config pipeline.** Inbounds/clients live in the DB. On every change the
   backend regenerates the Xray config and applies it β€” preferring a _hot diff_ (live gRPC
   API mutation) over a full process restart. See Β§5.1.
2. **The Runtime abstraction (multi-node).** A panel can manage remote "nodes" (other 3x-ui
   instances). Every state-changing inbound/client operation is dispatched through a
   `runtime.Runtime` interface that is either **`Local`** (this box's Xray gRPC API) or
   **`Remote`** (HTTPS call to a child node, with `verify`/`skip`/`pin`/`mtls` TLS modes).
   This is the single most important abstraction in the project. See Β§5.2.

---

## 2. Tech stack

**Backend (Go 1.27):**

- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.
- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).
- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.
- Xray: **xtls/xray-core** vendored as a library; the panel talks to the running core over
  its **gRPC API** and also shells out to manage the process.
- Bots: Telegram bot (**mymmrac/telego**), Discord bot (Discord REST API v10 + **gorilla/websocket** Gateway v10). i18n: **nicksnyder/go-i18n**.
- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).

**Frontend (`frontend/`):**

- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.
- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.
- Router: **react-router 8**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.
- **Build output goes to `internal/web/dist/`** (see `vite.config.js` β†’ `outDir`) and is
  embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),
  `login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend
  deployment.

**Important:** the legacy Go-template UI and `web/assets/` are **gone**. All HTML/JS comes
from the embedded Vite `dist/`. Don't look for `.html` templates in `internal/web`.

---

## 3. Request lifecycle (follow the data)

### 3.1 Admin API request (e.g. "add a client")

```
Browser (React, fetch)
  β†’ POST {basePath}/panel/api/...
    β†’ Gin engine (internal/web/web.go: initRouter)
      β†’ middleware chain: SecurityHeaders β†’ MaxBodyBytes (10 MiB; importDB exempt)
                          β†’ [DomainValidator, if webDomain set] β†’ gzip β†’ sessions("3x-ui")
                          β†’ base-path/cache-control context β†’ Localizer
                          β†’ API routes add: ConfigEnvelope (zstd + SHA-256) β†’ CSRF
        β†’ Controller (internal/web/controller/*.go)   // HTTP concerns only: bind, validate, respond
          β†’ Service (internal/web/service/*.go)        // business logic + transactions
            β†’ GORM β†’ DB (internal/database)            // persistence
            β†’ runtime.Runtime dispatch                 // apply to Xray (Local) or node (Remote)
              β†’ Local:  internal/xray (gRPC API or config regen + restart)
              β†’ Remote: internal/web/runtime/remote.go β†’ HTTPS β†’ child node's API
```

The controller layer is thin. **Business logic lives in services.** When something is wrong
with _behavior_, the bug is almost always in a service file, not a controller.

### 3.2 Subscription request (end-user fetching their config)

```
End user β†’ GET {subPath}/{subId}   (separate server, internal/sub)
  β†’ internal/sub/controller.go (routes: raw / JSON / Clash variants, feature-flagged)
    β†’ internal/sub/service.go (~2.5k lines β€” the link/config builder)
      β†’ reads inbounds+clients+hosts from DB, renders per-protocol share links /
        Clash YAML / JSON (Host rows can override address/SNI/path per inbound)
```

### 3.3 Background work (cron jobs)

Scheduled in `internal/web/web.go` β†’ `startTask()`. Each job is a struct in
`internal/web/job/`. Examples: poll Xray traffic every 5s, check client IP limits every 10s,
node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly). See Β§5.4.

---

## 4. Directory map (what lives where)

```
3x-ui/
β”œβ”€β”€ main.go                     # Entry point: CLI (run / migrate / migrate-db / setting / cert),
β”‚                               #   bootstrap, signal handling, restart loop
β”œβ”€β”€ go.mod / go.sum             # Go deps (module path ends in /v3)
β”‚
β”œβ”€β”€ internal/                   # ALL backend Go code (private packages)
β”‚   β”œβ”€β”€ config/                 # Env-var config: paths, DB kind/DSN, log level, version
β”‚   β”‚                           #   Every XUI_* env var is read here (config.go)
β”‚   β”œβ”€β”€ database/
β”‚   β”‚   β”œβ”€β”€ db.go               # InitDB: connect, AutoMigrate, seeders (~1.4k lines). DB hotspot.
β”‚   β”‚   β”œβ”€β”€ migrate_data.go     # Data migrations (seeders/normalizers beyond AutoMigrate)
β”‚   β”‚   β”œβ”€β”€ dialect.go          # SQLite vs Postgres SQL differences
β”‚   β”‚   β”œβ”€β”€ dump_sqlite.go      # DB export/backup
β”‚   β”‚   └── model/              # **ALL GORM models** (model.go ~1.1k lines + siblings:
β”‚   β”‚                           #   node_client_traffic.go, node_client_ip.go,
β”‚   β”‚                           #   client_global_traffic.go). ⭐ Start here for data shape.
β”‚   β”œβ”€β”€ pia/                    # PIA WireGuard protocol client (auth, signed server list, /addKey)
β”‚   β”œβ”€β”€ eventbus/               # In-process pub/sub (buffered channel): outbound.down|up,
β”‚   β”‚                           #   xray.crash, node.down|up, cpu.high, memory.high, login.attempt
β”‚   β”œβ”€β”€ tunnelmonitor/          # Optional tunnel health probe (XUI_TUNNEL_HEALTH_* env vars):
β”‚   β”‚                           #   HTTP probe (default Cloudflare trace); repeated failures
β”‚   β”‚                           #   trigger an Xray restart hook. Independent of panel settings.
β”‚   β”œβ”€β”€ xray/                   # Xray-core integration (the proxy engine wrapper)
β”‚   β”‚   β”œβ”€β”€ process.go          # Spawn/supervise the Xray child process (~750 lines)
β”‚   β”‚   β”œβ”€β”€ api.go              # gRPC client to a running Xray (add/remove user, stats) (~800 lines)
β”‚   β”‚   β”œβ”€β”€ hot_diff.go         # ⭐ Compute minimal live changes to avoid full restart (~500 lines)
β”‚   β”‚   β”œβ”€β”€ config.go           # Xray config object model
β”‚   β”‚   β”œβ”€β”€ inbound.go          # Inbound JSON shaping
β”‚   β”‚   β”œβ”€β”€ client_traffic.go   # ClientTraffic model (persisted as client_traffics)
β”‚   β”‚   β”œβ”€β”€ traffic.go          # Traffic type helpers
β”‚   β”‚   β”œβ”€β”€ log_writer.go       # Pipe Xray stdout/stderr into the panel logger
β”‚   β”‚   └── geodata/            # Browse geosite/geoip .dat: streaming protowire reader,
β”‚   β”‚                           #   cached category index, routing-token parsing (token.go)
β”‚   β”‚
β”‚   β”œβ”€β”€ web/                    # The panel server
β”‚   β”‚   β”œβ”€β”€ web.go              # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)
β”‚   β”‚   β”œβ”€β”€ controller/         # HTTP handlers (thin). One file per resource:
β”‚   β”‚   β”‚   β”œβ”€β”€ inbound.go      #   /panel/api/inbounds
β”‚   β”‚   β”‚   β”œβ”€β”€ client.go       #   /panel/api/clients (CRUD + bulk + ips + onlines)
β”‚   β”‚   β”‚   β”œβ”€β”€ group.go        #   client-group endpoints
β”‚   β”‚   β”‚   β”œβ”€β”€ node.go         #   /panel/api/nodes   (multi-node management)
β”‚   β”‚   β”‚   β”œβ”€β”€ host.go         #   /panel/api/hosts   (per-inbound subscription host overrides)
β”‚   β”‚   β”‚   β”œβ”€β”€ server.go       #   /panel/api/server  (status, xray version, certs, logs, DB import/export)
β”‚   β”‚   β”‚   β”œβ”€β”€ setting.go      #   /panel/api/setting (settings + API tokens)
β”‚   β”‚   β”‚   β”œβ”€β”€ xray_setting.go #   /panel/api/xray    (raw Xray config editor, WARP/Nord/PIA, geodata)
β”‚   β”‚   β”‚   β”œβ”€β”€ api.go          #   /panel/api gateway (token auth, envelope + CSRF wiring)
β”‚   β”‚   β”‚   β”œβ”€β”€ index.go        #   login/logout/csrf/2FA
β”‚   β”‚   β”‚   β”œβ”€β”€ spa.go          #   SPA fallback for /panel UI routes
β”‚   β”‚   β”‚   └── websocket.go    #   WS upgrade endpoint
β”‚   β”‚   β”œβ”€β”€ service/            # ⭐⭐ Business logic. This is where most real work happens.
β”‚   β”‚   β”‚   β”œβ”€β”€ inbound.go              # Inbound CRUD core (~1.4k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ inbound_node.go         # ⭐ Node sync for inbounds: reconcile, traffic merge (~1.1k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ inbound_traffic.go      # Per-client traffic accounting (~1.1k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ inbound_clients.go      # Client-within-inbound operations
β”‚   β”‚   β”‚   β”œβ”€β”€ inbound_sublink.go      # Inbound-level subscription link helpers
β”‚   β”‚   β”‚   β”œβ”€β”€ inbound_migration.go    # Inbound schema/format migrations
β”‚   β”‚   β”‚   β”œβ”€β”€ client_crud.go          # Client create/read/update/delete
β”‚   β”‚   β”‚   β”œβ”€β”€ client_bulk.go          # Bulk client ops (~1.6k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ client_inbound_apply.go # ⭐ Apply client changes to runtime (Local/Remote) (~1.2k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ client_groups.go        # Client grouping
β”‚   β”‚   β”‚   β”œβ”€β”€ client_link.go          # Per-client share-link generation
β”‚   β”‚   β”‚   β”œβ”€β”€ client_external_link.go # External links attached to clients
β”‚   β”‚   β”‚   β”œβ”€β”€ client_wireguard.go     # WireGuard client specifics
β”‚   β”‚   β”‚   β”œβ”€β”€ client_paging.go        # Server-side pagination/sort/filter for client lists
β”‚   β”‚   β”‚   β”œβ”€β”€ node.go                 # ⭐ NodeService: CRUD, probe, heartbeat, dirty-tracking (~1.1k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ node_mtls.go            # Node mTLS certificate management (master side)
β”‚   β”‚   β”‚   β”œβ”€β”€ node_tree.go            # Node hierarchy / descendants
β”‚   β”‚   β”‚   β”œβ”€β”€ host.go                 # Host rows (subscription output overrides)
β”‚   β”‚   β”‚   β”œβ”€β”€ server.go               # ServerService: status, certs, xray install, DB ops (~2.2k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ setting.go              # SettingService: all panel settings + defaults (~1.3k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ setting_mtls.go         # mTLS settings (node hardening)
β”‚   β”‚   β”‚   β”œβ”€β”€ traffic_writer.go       # Batched persistence of traffic deltas to the DB
β”‚   β”‚   β”‚   β”œβ”€β”€ xray.go                 # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)
β”‚   β”‚   β”‚   β”œβ”€β”€ xray_setting.go         # Raw Xray config persistence
β”‚   β”‚   β”‚   β”œβ”€β”€ geodata.go              # Geo database browsing + routing-token validation
β”‚   β”‚   β”‚   β”œβ”€β”€ xray_metrics.go         # Xray observability metrics
β”‚   β”‚   β”‚   β”œβ”€β”€ metric_history.go       # Historical system/xray metrics
β”‚   β”‚   β”‚   β”œβ”€β”€ reality_scan.go         # REALITY target scanner
β”‚   β”‚   β”‚   β”œβ”€β”€ url_safety.go           # Outbound URL validation (SSRF guards)
β”‚   β”‚   β”‚   β”œβ”€β”€ outbound_subscription.go# Outbound subscription (e.g. Warp/Nord provider configs)
β”‚   β”‚   β”‚   β”œβ”€β”€ port_conflict.go        # Detect inbound port collisions
β”‚   β”‚   β”‚   β”œβ”€β”€ fallback.go             # Xray fallback (SNI/ALPN routing on shared port)
β”‚   β”‚   β”‚   β”œβ”€β”€ email/                  # Email notification service (SMTP)
β”‚   β”‚   β”‚   β”œβ”€β”€ integration/            # External providers: warp.go, nord.go, pia.go
β”‚   β”‚   β”‚   β”œβ”€β”€ outbound/               # Outbound config service
β”‚   β”‚   β”‚   β”œβ”€β”€ panel/                  # Cross-cutting panel services:
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ panel.go            #   panel-level helpers
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ user.go             #   admin user auth (bcrypt)
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ api_token.go        #   API token CRUD (SHA-256 hashed)
β”‚   β”‚   β”‚   β”‚   └── websocket.go        #   WS hub / push service
β”‚   β”‚   β”‚   β”œβ”€β”€ discord/                # Discord bot client, Gateway v10, and subscriber
β”‚   β”‚   β”‚   └── tgbot/                  # Telegram bot command handlers
β”‚   β”‚   β”œβ”€β”€ runtime/            # ⭐⭐ The Local/Remote node abstraction (see Β§5.2)
β”‚   β”‚   β”‚   β”œβ”€β”€ runtime.go      #   the Runtime interface (the contract)
β”‚   β”‚   β”‚   β”œβ”€β”€ local.go        #   Local impl β†’ this box's Xray gRPC API
β”‚   β”‚   β”‚   β”œβ”€β”€ remote.go       #   Remote impl β†’ HTTPS calls to a child node
β”‚   β”‚   β”‚   β”œβ”€β”€ tls_client.go   #   per-node HTTP client: verify / skip / pin / mtls
β”‚   β”‚   β”‚   └── manager.go      #   RuntimeFor(nodeID) β†’ picks Local or Remote
β”‚   β”‚   β”œβ”€β”€ job/               # Cron job structs (one file per job β€” see Β§5.4)
β”‚   β”‚   β”œβ”€β”€ middleware/        # Gin middleware: security.go (headers/HSTS), bodylimit.go,
β”‚   β”‚   β”‚                      #   domainValidator.go, validate.go (CSRF), config_envelope.go
β”‚   β”‚   β”œβ”€β”€ global/           # Global singletons: web server + sub server handles, restart hook
β”‚   β”‚   β”œβ”€β”€ network/          # Custom net listeners (e.g. proxy-protocol aware)
β”‚   β”‚   β”œβ”€β”€ session/          # Session/cookie helpers
β”‚   β”‚   β”œβ”€β”€ websocket/        # WS hub implementation
β”‚   β”‚   β”œβ”€β”€ locale/ + translation/  # i18n middleware + 13 locale JSON catalogs
β”‚   β”‚   β”œβ”€β”€ entity/           # Shared request/response DTOs
β”‚   β”‚   └── dist/             # ⚠️ Vite build output, embedded via go:embed (generated β€” do not hand-edit)
β”‚   β”‚
β”‚   β”œβ”€β”€ sub/                   # The subscription server (separate from panel)
β”‚   β”‚   β”œβ”€β”€ sub.go             #   server bootstrap
β”‚   β”‚   β”œβ”€β”€ controller.go      #   routes for raw / JSON / Clash subscription formats
β”‚   β”‚   β”œβ”€β”€ service.go         # ⭐ The link/config builder (~2.5k lines β€” share-link logic lives here)
β”‚   β”‚   β”œβ”€β”€ json_service.go    #   JSON subscription format
β”‚   β”‚   β”œβ”€β”€ clash_service.go   #   Clash/Mihomo YAML format
β”‚   β”‚   β”œβ”€β”€ clash_external.go  #   external Clash config integration
β”‚   β”‚   β”œβ”€β”€ external_subscription.go / external_config.go  # external sub import/aggregation
β”‚   β”‚   β”œβ”€β”€ host_sub.go        #   Host-row overrides applied to subscription output
β”‚   β”‚   β”œβ”€β”€ endpoint.go        #   subscription endpoint configuration
β”‚   β”‚   β”œβ”€β”€ vless_route.go     #   VLESS route shaping
β”‚   β”‚   β”œβ”€β”€ remark_vars.go     #   remark variable expansion
β”‚   β”‚   └── links.go           #   link helpers
β”‚   β”‚
β”‚   β”œβ”€β”€ mtproto/              # Embedded MTProto (Telegram) proxy: manager.go + per-OS
β”‚   β”‚                         #   process supervision + orphan cleanup
β”‚   β”œβ”€β”€ logger/              # App logger (op/go-logging + lumberjack rotation)
β”‚   └── util/                # Leaf helpers (no business logic):
β”‚       β”œβ”€β”€ common/          #   errors, misc
β”‚       β”œβ”€β”€ crypto/          #   key/cert generation (x25519, ML-KEM/ML-DSA, ECH)
β”‚       β”œβ”€β”€ link/            #   outbound share-link building primitives
β”‚       β”œβ”€β”€ wirecodec/ + wireguard/  # WireGuard codec + integration helpers
β”‚       └── random/, json_util/, reflect_util/, sys/, netproxy/, netsafe/, ldap/
β”‚
β”œβ”€β”€ frontend/                 # React SPA (built into internal/web/dist)
β”‚   β”œβ”€β”€ vite.config.js        # ⭐ Build config: outDir β†’ ../internal/web/dist, dev on :5173
β”‚   β”‚                         #   (strict) proxying to :2053, entries index/login/subpage.html
β”‚   β”œβ”€β”€ package.json          # scripts: dev / build / preview / lint / typecheck / test / gen
β”‚   └── src/
β”‚       β”œβ”€β”€ main.tsx / routes.tsx / queryClient.ts   # SPA entry, router, query client
β”‚       β”œβ”€β”€ entries/          # Extra HTML entry points: login.tsx, subpage.tsx
β”‚       β”œβ”€β”€ pages/            # ⭐ Route screens. Mirrors the panel's feature areas:
β”‚       β”‚   β”œβ”€β”€ inbounds/     #   inbound list + the big inbound form (protocols/security/transport)
β”‚       β”‚   β”œβ”€β”€ clients/      #   client management screens
β”‚       β”‚   β”œβ”€β”€ nodes/        #   multi-node UI
β”‚       β”‚   β”œβ”€β”€ hosts/        #   subscription host-override UI
β”‚       β”‚   β”œβ”€β”€ xray/         #   raw Xray config UI (routing, dns, outbounds, balancers, overrides)
β”‚       β”‚   β”œβ”€β”€ index/        #   dashboard/home
β”‚       β”‚   └── settings/, groups/, sub/, login/, api-docs/
β”‚       β”œβ”€β”€ api/              # ⭐ Data layer: http-init, QueryProvider, queryKeys, websocket bridge
β”‚       β”‚   └── queries/      #   TanStack Query hooks (useNodesQuery, useStatusQuery, …)
β”‚       β”œβ”€β”€ schemas/          # Zod schemas: protocols, forms, api, primitives
β”‚       β”œβ”€β”€ generated/        # ⚠️ GENERATED from Go (see Β§5.5): schemas.ts, types.ts, zod.ts, examples.ts
β”‚       β”œβ”€β”€ components/       # Reusable UI (clients/ form/ geodata/ ui/ viz/ feedback/ utility/)
β”‚       β”œβ”€β”€ lib/              # Frontend domain logic (xray/ inbounds/ clients/)
β”‚       β”œβ”€β”€ hooks/, models/, layouts/, i18n/, utils/, styles/
β”‚       └── test/             # Vitest + golden fixtures (config-generation snapshot tests)
β”‚
β”œβ”€β”€ tools/openapigen/         # ⭐ Go program that emits frontend/src/generated/* from Go types (Β§5.5)
β”œβ”€β”€ docs/                     # Markdown docs (this file, custom-subscription-templates.md, …)
β”œβ”€β”€ media/                    # README images
β”‚
β”œβ”€β”€ Dockerfile / docker-compose.yml / DockerEntrypoint.sh / DockerInit.sh   # Container build/run
β”œβ”€β”€ install.sh / update.sh / x-ui.sh                        # VPS install + management CLI
β”œβ”€β”€ x-ui.service.*  / x-ui.rc                               # systemd units (debian/rhel/arch) + rc script
β”œβ”€β”€ windows_files/                                          # Windows service support
└── .github/workflows/        # CI: ci.yml, codeql.yml, docker.yml, release.yml,
                              #     mutation.yml, cleanup_caches.yml, claude-pr-review.yml,
                              #     claude-issue-analyst.yml
```

---

## 5. Cross-cutting subsystems (the parts that span many files)

### 5.1 DB β†’ Xray config pipeline (config generation & application)

The panel never edits Xray's running config directly from controllers. The flow is:

1. A service mutates DB state (inbound/client/setting).
2. `XrayService` (`service/xray.go`) builds a fresh `xray.Config` from DB state
   (`GetXrayConfig`).
3. It tries a **hot apply** (`tryHotApply` β†’ `xray/hot_diff.go`): diff old vs new config and
   push only the deltas over the Xray gRPC API (add/remove inbound, add/remove user) β€” **no
   process restart**, so live connections survive.
4. If the diff isn't hot-applicable (structural change), it falls back to a **full restart**
   of the Xray process (`xray/process.go`).

Restart is debounced via an atomic "need restart" flag (`SetToNeedRestart` /
`IsNeedRestartAndSetFalse`), consumed by a `@every 30s` cron task registered in `startTask()`
β€” any number of mutations inside the window causes at most one restart.

**Key files:** `service/xray.go` (orchestration), `xray/hot_diff.go` (the diff algorithm),
`xray/process.go` (process lifecycle), `xray/api.go` (gRPC calls), `xray/config.go` (config model).

### 5.2 Runtime abstraction β€” Local vs Remote (multi-node) ⭐ most important

A "node" (`model.Node`) is another 3x-ui instance this panel controls. Every state-changing
inbound/client operation goes through the `runtime.Runtime` interface so the _same service
code_ works whether the target is the local Xray or a remote node.

- **Interface:** `internal/web/runtime/runtime.go` β€” `Name`, `AddInbound`, `DelInbound`,
  `UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,
  `RestartXray`, `ResetClientTraffic`, `ResetInboundTraffic`, `ResetAllTraffics`.
- **`Local`** (`local.go`): calls this box's Xray gRPC API directly.
- **`Remote`** (`remote.go`): serializes the operation and sends it over HTTPS to the child
  node's API.
- **TLS modes** (`tls_client.go`, per-node `TlsVerifyMode`):
  `verify` (system CAs, default) / `skip` (no validation) / `pin` (leaf cert SHA-256 must
  match `PinnedCertSha256`) / `mtls` (master presents a client certificate; node cert checked
  against system roots; API token optional). Master-side cert management:
  `service/node_mtls.go` + `service/setting_mtls.go`.
- **Dispatch:** `manager.go` β†’ `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID β†’ `Local`,
  otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client.

Bearer tokens use the narrowest scope that matches the caller's duties:

| Scope        | Access                                                                 |
| ------------ | ---------------------------------------------------------------------- |
| `monitor`    | Read-only status, version, and metric endpoints.                       |
| `node-sync`  | Node configuration, inbound/client synchronization, traffic, and Xray. |
| `node-admin` | All `node-sync` access plus `POST /panel/api/server/updatePanel`.      |
| `admin`      | Every panel API endpoint; intended for fully trusted automation.       |

`node-admin` remains a node synchronization identity when inbound rules are applied. This
preserves the node-specific validation path while keeping software-update authority separate
from ordinary `node-sync` credentials. On an mTLS connection, only a valid `node-admin` bearer
elevates the request; other bearer scopes retain the historical mTLS `node-sync` behavior.
Existing tokens retain their stored scope, and legacy tokens without one continue to migrate to
`admin`.

**Node identity & attribution (the hard part).** Inbounds carry a `NodeID` _and_ an
`OriginNodeGuid`. Because inbounds can be pushed across hops, the panel attributes traffic and
online clients back to the originating panel using **stable GUIDs** rather than local IDs.
Relevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,
`synthNodeGuid`, `panelGuid`) and `service/node.go` (`effectiveNodeGuid`, heartbeat, dirty
tracking). Node "dirty" flags drive an **anti-entropy reconciliation** so an offline node's
inbound edits converge once it reconnects.

**Where to look for node bugs:**

- Operation not reaching a node β†’ `runtime/remote.go` + `runtime/manager.go`.
- Wrong traffic/online attribution across hops β†’ `service/inbound_node.go` (GUID merge paths).
- Node shown offline / stale status β†’ `job/node_heartbeat_job.go` + `service/node.go` (`Probe`, `UpdateHeartbeat`).
- Edits to an offline node not applying on reconnect β†’ dirty/reconcile logic in `service/inbound_node.go` + `service/node.go` (`MarkNodeDirty`/`ClearNodeDirty`/`NodeSyncState`).
- TLS/mTLS handshake failures β†’ `runtime/tls_client.go`, `service/node_mtls.go`, `service/node.go` (`FetchCertFingerprint`).

### 5.3 Traffic accounting

Per-client and per-inbound up/down counters originate from Xray's stats API and are persisted
to the DB. The Xray traffic job polls the core; node traffic is pulled from child nodes and
merged with GUID-based baselines to avoid double counting after resets.

**Key files:** `service/inbound_traffic.go`, `service/traffic_writer.go`,
`job/xray_traffic_job.go`, `job/node_traffic_sync_job.go`, `service/inbound_node.go`
(`SetRemoteTraffic` / `upsertNodeBaseline`), models `xray.ClientTraffic`,
`model.NodeClientTraffic`, `model.ClientGlobalTraffic` (cross-master totals).
A client reset is queued per hosting node in `model.NodePendingReset` (`service/node_reset_queue.go`)
and replayed by the node sync until the node accepts it.
Periodic resets: `job/periodic_traffic_reset_job.go` (keyed off `Inbound.TrafficReset`).

### 5.4 Background jobs (cron)

All registered in `web.go` β†’ `startTask()`. Each is a struct with a `Run()` method in `internal/web/job/`:

| Schedule            | Job                                                                                              | Purpose / condition                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `@every 1s`         | `check_xray_running_job`                                                                         | Restart Xray if it died (2 consecutive down checks)                                   |
| `@every 30s`        | (inline func in `startTask`)                                                                     | Debounced Xray restart β€” consumes the "need restart" flag (Β§5.1)                      |
| `@every 5s`         | `xray_traffic_job`                                                                               | Pull traffic stats from Xray (5s start delay)                                         |
| `@every 5s`         | `node_heartbeat_job`                                                                             | Probe child nodes (online/offline)                                                    |
| `@every 5s`         | `node_traffic_sync_job`                                                                          | Pull + merge node traffic; push reconciliation                                        |
| `@every 10s`        | `check_client_ip_job`                                                                            | Enforce per-client IP limits                                                          |
| `@every 10s`        | `mtproto_job`                                                                                    | Reconcile `mtg` sidecars against enabled MTProto inbounds                             |
| `@every 10s`        | `amneziawg_job`                                                                                  | Reconcile embedded AmneziaWG interfaces against enabled local inbounds                |
| `@every 5m`         | `outbound_subscription_job`                                                                      | Refresh outbound provider configs                                                     |
| `@every 10m`        | `clear_logs_job` (`PruneXrayLogsJob`)                                                            | Truncate Xray access/error logs once either exceeds 64 MiB                            |
| `@hourly`           | `warp_ip_job`, `periodic_traffic_reset_job("hourly")`                                            | WARP IP rotation; traffic resets                                                      |
| `@daily`            | `clear_logs_job`, `periodic_traffic_reset_job("daily")`, `periodic_traffic_reset_job("monthly")` | IP-limit and Xray access/error log cleanup; daily resets and due monthly resets       |
| `@weekly`           | `periodic_traffic_reset_job("weekly")`                                                           | Weekly traffic resets                                                                 |
| default `@every 1m` | `ldap_sync_job`                                                                                  | Only if LDAP enabled; schedule configurable                                           |
| default `@daily`    | `stats_notify_job`                                                                               | Only if TG bot enabled; schedule configurable                                         |
| default `@daily`    | `discord_notify_job`                                                                             | Only if Discord bot enabled; schedule configurable                                    |
| `@every 2m`         | `check_hash_storage`                                                                             | Only if TG bot enabled; expires bot callback hashes                                   |
| `@every 1m`         | `check_cpu_usage`                                                                                | Only if a CPU alarm is configured (TG, Discord, or email); publishes `cpu.high`       |
| `@every 1m`         | `check_memory_usage`                                                                             | Only if a memory alarm is configured (TG, Discord, or email); publishes `memory.high` |
| configurable        | `free_os_memory`                                                                                 | Only if `sys.MemoryReleaseIntervalMinutes() > 0`; returns heap to OS                  |

To change _when_ something runs, edit `startTask()`. To change _what_ it does, edit the job file.

### 5.5 Type generation (Go β†’ TypeScript) ⚠️ don't hand-edit generated files

The Go backend is the schema source of truth. `tools/openapigen` (a Go program, with a
`StructAllow` allowlist of exported types) emits
`frontend/src/generated/{schemas,types,zod,examples}.ts`. The frontend build runs this first:

- `npm run gen:zod` β†’ `go run ./tools/openapigen` (regenerate from Go)
- `npm run gen:api` β†’ builds the OpenAPI doc (`scripts/build-openapi.mjs`, driven by the
  hand-maintained endpoint registry `src/pages/api-docs/endpoints.ts`)
- `npm run build` runs `gen:api` then `vite build`.

**Implication:** if you change a Go model/DTO that crosses the API boundary, regenerate the
frontend types (`cd frontend && npm run gen`) instead of editing `src/generated/` by hand.

### 5.6 Share-link / subscription generation

Two distinct code paths produce client configs:

- **Per-client links in the panel** (the "copy link" / QR in the UI): `service/client_link.go`
  - `util/link/outbound.go`.
- **Subscription endpoint** (what a client app polls): `internal/sub/service.go` (raw links),
  `internal/sub/json_service.go` (JSON), `internal/sub/clash_service.go` (Clash YAML).
  **`Host` rows** (`model.Host`, edited under /panel/api/hosts) override address/SNI/path/
  security per inbound in subscription output β€” applied in `sub/host_sub.go`.

Both paths must agree per protocol. A malformed link for a specific protocol/transport combo
(e.g. XHTTP + Reality) is usually a field-lookup mismatch in **`internal/sub/service.go`** (and
its tests `service_test.go` / golden fixtures), or in `util/link/outbound.go`. The frontend
also has protocol schemas under `frontend/src/schemas/protocols/` and `frontend/src/lib/xray/`.

### 5.7 Event bus (in-process pub/sub)

`internal/eventbus/` is a minimal buffered-channel pub/sub. Producers call a non-blocking
`Publish(Event)`; all subscribers receive every event. Event types: `outbound.down|up`,
`xray.crash`, `node.down|up`, `cpu.high`, `memory.high`, `login.attempt`, with structured
payloads (OutboundHealthData, NodeHealthData, LoginEventData, SystemMetricData). Producers
include the CPU/memory jobs, node heartbeat, and login handling; consumers include the
Telegram bot, the Discord bot (`service/discord/`), and the email notifier (`service/email/`). Use it for cross-cutting
notifications instead of importing notification services into producers.

### 5.8 Tunnel health monitor

`internal/tunnelmonitor/` is an optional watchdog configured **only via env vars**
(`XUI_TUNNEL_HEALTH_*`, read in `internal/config/`), deliberately independent of panel
settings so it can be enabled from a systemd `EnvironmentFile` even when the panel is
unreachable. It periodically probes an HTTP URL (default: Cloudflare trace endpoint) through
the tunnel; after N successive failures (default 3) it fires a recovery callback wired to an
Xray restart.

---

## 6. Data model cheat-sheet

GORM models in `internal/database/model/` (main file `model.go` + siblings); all registered
for AutoMigrate in `internal/database/db.go`.

| Model                           | Table role                                | Notable fields                                                                                                                                                     |
| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `User`                          | Admin login                               | bcrypt password, `LoginEpoch` (invalidates sessions)                                                                                                               |
| `Inbound`                       | An Xray inbound                           | `Tag` (unique), `Port`, `Protocol`, `Settings`/`StreamSettings`/`Sniffing` (JSON), `Enable`, `TrafficReset`, `NodeID`, **`OriginNodeGuid`**, `ClientStats` (assoc) |
| `Client`                        | In-memory client view                     | UUID/email/flow/limits (parsed from inbound JSON; not persisted)                                                                                                   |
| `ClientRecord`                  | Persisted client (`clients`)              | `Email` (unique), `SubID`, `UUID`, `TotalGB`, `ExpiryTime`, `LimitIP`, `Group`, `Reset`                                                                            |
| `ClientGroup` / `ClientInbound` | Grouping + client↔inbound join            | many-to-many wiring, `FlowOverride`                                                                                                                                |
| `ClientExternalLink`            | Extra links attached to a client          | `Kind`, `Value`, `Remark`, `SortIndex`                                                                                                                             |
| `Host`                          | Subscription host overrides (per inbound) | `Address`, `Port`, `Sni`, `Path`, `Security`, `Fingerprint`, `SortOrder`, visibility/exclusion flags                                                               |
| `Node`                          | A managed child panel                     | `Guid`, `Address`, `Status`, `TlsVerifyMode`, `PinnedCertSha256`, `ConfigDirty`, version/heartbeat/metric fields                                                   |
| `NodeClientTraffic`             | Per-node client traffic baseline          | cross-node merge (anti-double-count)                                                                                                                               |
| `NodePendingReset`              | Client resets a node has not confirmed    | `NodeId`, `Email`, `QueuedAt`; replayed by the node sync, freezes that client's node verdict until delivered                                                       |
| `NodeClientIp`                  | Per-node client IP attribution            | `NodeGuid`, `Email`, `Ips`                                                                                                                                         |
| `ClientGlobalTraffic`           | Cross-master usage totals                 | `MasterGuid`, `Email`, `Up`, `Down`                                                                                                                                |
| `xray.ClientTraffic`            | Per-client counters (`client_traffics`)   | `Email`, `Up`, `Down`, `Total`, `ExpiryTime`, `LastOnline`                                                                                                         |
| `InboundClientIps`              | IP set per client email                   | drives IP-limit enforcement                                                                                                                                        |
| `OutboundTraffics`              | Outbound counters                         | per outbound tag                                                                                                                                                   |
| `OutboundSubscription`          | External provider subs                    | Warp/Nord style                                                                                                                                                    |
| `Setting`                       | Key/value panel settings                  | everything configurable                                                                                                                                            |
| `ApiToken`                      | REST API tokens                           | SHA-256 hash (plaintext shown once)                                                                                                                                |
| `InboundFallback`               | Fallback routing on a shared port         | SNI/ALPN/path β†’ dest                                                                                                                                               |
| `HistoryOfSeeders`              | Seeder bookkeeping                        | prevents re-running one-off migrations                                                                                                                             |

---

## 7. Symptom β†’ File index (start here when debugging)

| Symptom / task                                                                    | Primary file(s)                                                              | Then check                                                                                          |
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Add/modify an **API endpoint**                                                    | `controller/<resource>.go` (route registration at top of each file)          | corresponding `service/*.go`, `frontend/src/pages/api-docs/endpoints.ts`                            |
| **Inbound** create/update/delete behavior                                         | `service/inbound.go`, `service/inbound_clients.go`                           | `runtime/*`, `service/xray.go`                                                                      |
| **Client** CRUD / limits / expiry                                                 | `service/client_crud.go`, `service/client_inbound_apply.go`                  | model `ClientRecord`, `service/inbound_traffic.go`                                                  |
| **Bulk** client operations slow/wrong                                             | `service/client_bulk.go`                                                     | `service/client_paging.go`                                                                          |
| Xray **won't apply** a config change                                              | `service/xray.go` (`RestartXray`, `tryHotApply`)                             | `xray/hot_diff.go`, `xray/process.go`                                                               |
| Xray **restarts when it shouldn't** (kills connections)                           | `xray/hot_diff.go` (diff not classified as hot)                              | `service/xray.go`                                                                                   |
| **Traffic** counts wrong / reset behavior                                         | `service/inbound_traffic.go`, `job/xray_traffic_job.go`                      | `service/traffic_writer.go`, `job/periodic_traffic_reset_job.go`                                    |
| **Node** operation not propagating                                                | `runtime/remote.go`, `runtime/manager.go`                                    | `service/inbound_node.go`                                                                           |
| **Multi-hop / cross-node attribution** (traffic or online clients on wrong panel) | `service/inbound_node.go` (GUID merge, `synthNodeGuid`, `effectiveNodeGuid`) | `service/node.go`, model `OriginNodeGuid`/`Node.Guid`                                               |
| Node stuck **offline / stale**                                                    | `job/node_heartbeat_job.go`, `service/node.go` (`Probe`, `UpdateHeartbeat`)  | `runtime/tls_client.go` (TLS verify)                                                                |
| Node **TLS / mTLS** auth failures                                                 | `runtime/tls_client.go`, `service/node_mtls.go`, `service/setting_mtls.go`   | `service/node.go` (`FetchCertFingerprint`)                                                          |
| Offline node edits **not reconciling** on reconnect                               | `service/inbound_node.go` (`ReconcileNode`, dirty flags)                     | `service/node.go` (`MarkNodeDirty`/`NodeSyncState`)                                                 |
| **Share link / QR** malformed (per protocol)                                      | `service/client_link.go`, `util/link/outbound.go`                            | `frontend/src/lib/xray/`, `frontend/src/schemas/protocols/`                                         |
| **Subscription** output wrong (raw/JSON/Clash)                                    | `internal/sub/service.go`                                                    | `sub/json_service.go`, `sub/clash_service.go`, sub golden tests                                     |
| Subscription **host overrides** not applied                                       | `service/host.go`, `sub/host_sub.go`                                         | model `Host`, `frontend/src/pages/hosts/`                                                           |
| **External subscription** import/aggregation                                      | `sub/external_subscription.go`, `sub/external_config.go`                     | `sub/clash_external.go`                                                                             |
| **Settings** not saving / defaults                                                | `service/setting.go`, `controller/setting.go`                                | model `Setting`                                                                                     |
| **Login / 2FA / sessions / CSRF**                                                 | `controller/index.go`, `service/panel/user.go`, `middleware/`                | `session/`                                                                                          |
| **API tokens**                                                                    | `service/panel/api_token.go`, `controller/setting.go`                        | model `ApiToken`                                                                                    |
| **Port conflict** on inbound add                                                  | `service/port_conflict.go`                                                   | `controller/inbound.go`                                                                             |
| **Fallbacks** (shared 443, SNI routing)                                           | `service/fallback.go`, `controller/inbound.go`                               | model `InboundFallback`                                                                             |
| **Geo category browser** empty / won't open                                       | `xray/geodata/` (`Store`, `reader.go`), `service/geodata.go`                 | `controller/xray_setting.go` (`/panel/api/xray/geodata/*`), asset dir = `config.GetBinFolderPath()` |
| **`geosite:`/`geoip:` token** reported unknown in a routing rule                  | `xray/geodata/token.go`, `service/geodata.go` (`Validate`)                   | `frontend/src/lib/xray/geoTokens.ts`, `frontend/src/components/geodata/`                            |
| **Telegram bot** commands                                                         | `service/tgbot/`                                                             | `job/stats_notify_job.go`                                                                           |
| **Discord bot** commands & reports                                                | `service/discord/`                                                           | `job/discord_notify_job.go`                                                                         |
| **Email notifications**                                                           | `service/email/`                                                             | `internal/eventbus/` (consumers)                                                                    |
| **CPU / memory alerts** not firing                                                | `job/check_cpu_usage.go`, `job/check_memory_usage.go`                        | `internal/eventbus/`, notifier settings in `service/setting.go`                                     |
| Xray auto-restart on **dead tunnel**                                              | `internal/tunnelmonitor/`                                                    | `XUI_TUNNEL_HEALTH_*` in `internal/config/`                                                         |
| **WARP / Nord / PIA** outbound integration                                        | `service/integration/warp.go` / `nord.go` / `pia.go`                         | `internal/pia/`, `frontend/src/pages/xray/overrides/`                                               |
| **MTProto** proxy issues                                                          | `internal/mtproto/manager.go`, `mtproto/process*.go`                         | `job/mtproto_job.go`                                                                                |
| **DB migration** / new column                                                     | `internal/database/db.go` (AutoMigrate list), `migrate_data.go`              | `model/model.go`                                                                                    |
| **Cron schedule** changes                                                         | `web.go` β†’ `startTask()`                                                     | the specific `job/*.go`                                                                             |
| **CORS / security headers / HTTPS**                                               | `middleware/`, `web.go` (`initRouter`, TLS setup)                            | `config/` (env)                                                                                     |
| **Env vars / paths / DB type**                                                    | `internal/config/config.go`                                                  | `.env.example`                                                                                      |
| **Frontend route / screen**                                                       | `frontend/src/pages/<area>/`, `frontend/src/routes.tsx`                      | `frontend/src/api/queries/`                                                                         |
| **Frontend ↔ backend type mismatch**                                              | regenerate: `cd frontend && npm run gen` (`tools/openapigen`)                | `frontend/src/generated/`                                                                           |
| **System status / CPU / metrics**                                                 | `service/server.go`, `service/xray_metrics.go`, `service/metric_history.go`  | `controller/server.go`, gopsutil                                                                    |

---

## 8. Layering rules (where new code belongs)

1. **Controllers are thin.** Only: bind/validate input, call one service, shape the HTTP
   response. No DB queries, no Xray calls, no business rules in `controller/`.
2. **Services own the logic and transactions.** All business rules, DB access, and decisions
   about applying changes live in `service/`. If you're tempted to query GORM from a
   controller, move it to a service.
3. **Never touch Xray's running state from a controller or job directly.** Go through
   `XrayService` / the `runtime.Runtime` interface so local vs node dispatch stays correct.
4. **Any state-changing inbound/client o