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.
3x-ui is an open-source web control panel for managing Xray-core servers.
github.com/mhsanaei/3x-ui/v3, Gin and GORM.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.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./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/, built
into internal/web/dist/ (gitignored) and embedded with embed.FS.| 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 |
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.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.//go:build, //go:generate, //nolint:,
// Code generated ... DO NOT EDIT.. HTML <!-- --> is fine.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;make gen, or CI's codegen
job fails on a dirty frontend/src/generated or
frontend/public/openapi.json;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;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.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.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.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.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.
internal/sub/.[secrets],
[secret-ad-tags], [secret-limits]) and management API
(PUT /secrets, POST /secrets/{name}/reset-quota) internal/mtproto/
writes and calls..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.
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)x-ui menu on the server shows or resets them./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"./etc — never quote the Linux path to a Windows user.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.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).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".xtls-rprx-vision), not a security setting — never tell anyone to pick XTLS
in the security dropdown.gh release list -L 10, gh search commits, and git log -S.