repo-context.md 9.9 KB

Repository context for the Claude bot

Shared briefing for the jobs in .github/workflows/claude-bot.yml. It exists so these facts live in ONE place next to the code instead of being restated in each prompt, where they went stale silently. (Pull-request review is separate: its code-review skill is briefed with CLAUDE.md and REVIEW.md, not this.)

CLAUDE.md, frontend/CLAUDE.md and docs/architecture.md outrank this file. Where they disagree with it, they win and this file is the thing to fix. docs/architecture.md carries a "Symptom -> File" index and the cron-job table, which answer "which file owns X" in one hop; grepping blind wastes turns on a question it already answers.

Stack

3x-ui is an open-source web control panel for managing Xray-core servers.

  • Backend: Go 1.26, module github.com/mhsanaei/3x-ui/v3, Gin and GORM.
  • It runs Xray-core as a managed child process (internal/xray/process.go) and imports github.com/xtls/xray-core for config types and the gRPC stats/handler/router API. The release the panel BUNDLES is pinned in DockerInit.sh; the version it COMPILES against is pinned in go.mod, and the two are not always the same.
  • MTProto inbounds run a SECOND managed child, the mtg-multi binary (a multi-secret mtg fork, panel-side code in internal/mtproto/), one process per inbound. Client, ad-tag and quota/expiry edits are hot-applied through the fork's management API (PUT /secrets) so connections survive, with a process restart as the fallback on older binaries.
  • Storage: SQLite by default (/etc/x-ui/x-ui.db on Linux, the executable directory on Windows) or PostgreSQL (XUI_DB_TYPE / XUI_DB_DSN). The SQLite driver is CGo, so CGO_ENABLED=0 builds fail.
  • Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in frontend/, built into internal/web/dist/ (gitignored) and embedded with embed.FS.

Where things live

area path
entry point + x-ui CLI main.go
env parsing internal/config/
schema, migrations internal/database/, internal/database/model/
Xray child process + config internal/xray/
MTProto inbounds internal/mtproto/
subscription server internal/sub/
HTTP handlers internal/web/controller/
business logic internal/web/service/
cron jobs (schedules in web.go startTask()) internal/web/job/
master/sub-node over mTLS internal/web/runtime/
i18n internal/web/locale/, internal/web/translation/
UI source frontend/src/
install / upgrade install.sh, x-ui.sh, DockerInit.sh

Hard rules a change must respect

  • Dispatch through runtime.Runtime. Every state-changing inbound or client operation goes through the interface in internal/web/runtime/, never straight to internal/xray/api.go. A direct call passes every local test and silently breaks every multi-node deployment; it is invisible in a single-box reading of a diff.
  • Layering. Controllers are thin — bind, validate, respond — with no GORM queries, no Xray calls and no business rules. internal/util/* is leaf-only and must not import service, controller or database. internal/web/dist/ and frontend/src/generated/ are generated; a hand-edit is a violation.
  • Comments in committed Go/TS/TSX: 2 lines MAX per block, spent on the why a name cannot hold — an invariant, an issue number, a non-obvious constraint. Exempt, never flag: //go:build, //go:generate, //nolint:, // Code generated ... DO NOT EDIT.. HTML <!-- --> is fine.
  • The route contract chain, which breaks in four distinct places:
    1. a new g.POST/g.GET in internal/web/controller/ needs a matching entry in frontend/src/pages/api-docs/endpoints.ts — pinned BOTH ways by TestRouteRegistryContract in internal/web/routes_contract_test.go, so a renamed or removed route that leaves a stale entry fails too;
    2. generated artefacts must be regenerated with make gen, or CI's codegen job fails on a dirty frontend/src/generated or frontend/public/openapi.json;
    3. a NEW struct crossing the API boundary must be added to the StructAllow allowlist in tools/openapigen/main.go, or it is SILENTLY dropped from the schemas and frontend/scripts/build-openapi.mjs then fails — a guaranteed CI break, not a style nit;
    4. the step NOTHING checks — frontend/public/openapi.json must be copied to docs/public/openapi.json and the MDX regenerated with cd docs && pnpm gen:api, because docs-ci.yml fires only on docs/**. Step 4 is the one that reaches production wrong.
  • i18n. A new English key goes in EVERY locale JSON in internal/web/translation/ (13 files) AND must be referenced from frontend/src or Go in the SAME change. frontend/src/test/i18n-dead-keys.test.ts fails on a missing locale file and on an orphan key alike.
  • Migrations. Schema changes are GORM AutoMigrate PLUS hand-written migrations in internal/database/db.go. There are no migration files and no down-migrations, and everything has to work on SQLite AND PostgreSQL.
  • Tests. Stdlib testing only (no testify), table-driven with t.Run subtests and t.Helper() on helpers. An assertion must pin the exact value, typed error or emitted string — err != nil and len(x) > 0 are findings, not nits. Prefer real dependencies: a throwaway DB via database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")) with t.Cleanup, and httptest for HTTP. internal/sub's initSubDB(t) is the template. A test must FAIL without its fix; one that passes either way certifies nothing and then gets cited as proof the fix works.

The three link implementations

Link and subscription generation is implemented three times, independently:

language path what it feeds
Go internal/util/link/, internal/sub/ what the panel serves
TS frontend/src/lib/xray/ what the panel UI shows
TS docs/lib/xray/ what the docs site shows

A change to share-link or subscription output that touches one and not the others is how they drift apart.

Downstream programs that must accept what the panel emits

  • XTLS/Xray-core — the Xray config the panel generates, and the VLESS/VMess transport and security fields.
  • MetaCubeX/mihomo — consumes the Clash YAML from internal/sub/.
  • SagerNet/sing-box — parses the share links the panel emits.
  • mhsanaei/mtg-multi — the MTProto sidecar whose TOML ([secrets], [secret-ad-tags], [secret-limits]) and management API (PUT /secrets, POST /secrets/{name}/reset-quota) internal/mtproto/ writes and calls.

What CI runs

.github/workflows/ci.yml, on every pull request touching Go or frontend code. It is paths-filtered, so a docs-only or workflow-only change produces no run.

job what it proves
go-test go test -shuffle=on -count=1 over every package except frontend/node_modules
race the same set under -race -shuffle=on
postgres-durable-first live PostgreSQL 16: the PostgresCommitFailure tests plus TestHostAutoMigrateCreatesColumns_Postgres and TestMigrate_Postgres. Both steps COUNT passes rather than assert on SKIP, so a renamed or deleted test fails the job
govulncheck known vulnerabilities
golangci golangci-lint
fuzz-smoke 30s each on FuzzParseLink and FuzzDecodeCertPin
codegen npm run gen then git diff --exit-code on the generated files
frontend MSW worker drift, lint, format:check, typecheck, npm test (Vitest + headless-Chromium Storybook), build, build-storybook, npm audit

What CI does NOT prove. These test families t.Skip unless an environment variable is set, and CI sets only the PostgreSQL ones above:

gate covers
XUI_TEST_PG_DSN PostgreSQL-specific paths
XUI_DB_TYPE + XUI_DB_DSN dialect-dependent behaviour
XRAY_E2E_BINARY the Xray gRPC end-to-end tests in internal/xray/
XUI_SCALE_TEST scale tests in internal/sub/, internal/web/job/, internal/web/service/

Mutation testing (mutation.yml) runs nightly and never on a pull request, so a test that cannot fail is invisible to CI. make verify is the local gate.

Support facts reporters get wrong

  • Linux install: bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
  • Install generates a RANDOM username, password and web base path — never admin/admin. The x-ui menu on the server shows or resets them.
  • The installer service environment file is DISTRO-DEPENDENT: /etc/default/x-ui (Debian/Ubuntu), /etc/conf.d/x-ui (Arch), /etc/sysconfig/x-ui (RHEL/Fedora). Naming the wrong one means the reporter's edit is silently never read by systemd — a common cause of "I set the variable and nothing happened".
  • Windows is supported. There the database sits next to the executable, not in /etc — never quote the Linux path to a Windows user.
  • SQLite to PostgreSQL: x-ui migrate-db --dsn "postgres://...", then set XUI_DB_TYPE/XUI_DB_DSN in that file and systemctl restart x-ui. The source SQLite file is left in place.
  • Docker image ghcr.io/mhsanaei/3x-ui; PostgreSQL profile docker compose --profile postgres up -d. Fail2ban IP-limit enforcement needs NET_ADMIN + NET_RAW (compose grants them; a bare docker run must add --cap-add=NET_ADMIN --cap-add=NET_RAW).
  • Never state that a XUI_* variable does not exist without grepping internal/config/ and internal/tunnelmonitor/ first. The XUI_TUNNEL_HEALTH_* family is the usual answer to "the panel restarts Xray every few minutes".
  • Security per inbound is none / tls / reality. XTLS is a VLESS flow (xtls-rprx-vision), not a security setting — never tell anyone to pick XTLS in the security dropdown.
  • Never hardcode a version. For "is this already fixed" use gh release list -L 10, gh search commits, and git log -S.