Переглянути джерело

docs: vendor the documentation site into the monorepo

Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to
docs.sanaei.dev) into docs/ so the panel and its documentation share a single
source of truth, the way sing-box keeps its docs in-tree. The old repo becomes
redundant and can be retired.

- Import the full site under docs/ (app, components, content, lib, public,
  scripts, config). The self-contained pnpm project sits alongside the existing
  engineering notes with no filename collisions.
- Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's
  docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx).
- Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to
  docs/** and run with working-directory: docs, since GitHub only runs
  workflows from the repo-root .github/. deploy-static.yml's GitHub Pages
  publish (CNAME docs.sanaei.dev) carries over unchanged.

Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to
this repository's Pages (or set the Vercel project's root directory to docs),
confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
MHSanaei 10 годин тому
батько
коміт
9b91f0f42e
100 змінених файлів з 8681 додано та 0 видалено
  1. 46 0
      .github/workflows/docs-ci.yml
  2. 67 0
      .github/workflows/docs-deploy.yml
  3. 2 0
      docs/.gitattributes
  4. 31 0
      docs/.gitignore
  5. 10 0
      docs/.prettierignore
  6. 7 0
      docs/.prettierrc.json
  7. 34 0
      docs/CONTRIBUTING.md
  8. 674 0
      docs/LICENSE
  9. 133 0
      docs/README.md
  10. 28 0
      docs/app/[lang]/(home)/layout.tsx
  11. 143 0
      docs/app/[lang]/(home)/page.tsx
  12. 80 0
      docs/app/[lang]/docs/[[...slug]]/page.tsx
  13. 12 0
      docs/app/[lang]/docs/layout.tsx
  14. 30 0
      docs/app/[lang]/layout.tsx
  15. 22 0
      docs/app/api/search/route.ts
  16. 32 0
      docs/app/global.css
  17. 31 0
      docs/app/layout.tsx
  18. 10 0
      docs/app/llms-full.txt/route.ts
  19. 23 0
      docs/app/llms.mdx/docs/[[...slug]]/route.ts
  20. 8 0
      docs/app/llms.txt/route.ts
  21. 29 0
      docs/app/og/docs/[...slug]/route.tsx
  22. 12 0
      docs/app/robots.ts
  23. 33 0
      docs/app/sitemap.ts
  24. 49 0
      docs/components/home/features.tsx
  25. 67 0
      docs/components/home/github-stats.tsx
  26. 56 0
      docs/components/home/install-command.tsx
  27. 45 0
      docs/components/home/language-switcher.tsx
  28. 17 0
      docs/components/icons.tsx
  29. 15 0
      docs/components/logo.tsx
  30. 47 0
      docs/components/mdx.tsx
  31. 44 0
      docs/components/mdx/mermaid.tsx
  32. 6 0
      docs/components/openapi-page.tsx
  33. 56 0
      docs/components/search-dialog.tsx
  34. 76 0
      docs/components/tools/api-request-builder.tsx
  35. 71 0
      docs/components/tools/firewall-rules-generator.tsx
  36. 87 0
      docs/components/tools/install-command-builder.tsx
  37. 277 0
      docs/components/tools/outbound-generator.tsx
  38. 79 0
      docs/components/tools/protocol-wizard.tsx
  39. 152 0
      docs/components/tools/reality-config-generator.tsx
  40. 69 0
      docs/components/tools/reverse-proxy-generator.tsx
  41. 203 0
      docs/components/tools/routing-builder.tsx
  42. 82 0
      docs/components/tools/share-link-inspector.tsx
  43. 46 0
      docs/components/tools/shared/copy-button.tsx
  44. 99 0
      docs/components/tools/shared/fields.tsx
  45. 31 0
      docs/components/tools/shared/output-block.tsx
  46. 209 0
      docs/components/tools/subscription-builder.tsx
  47. 114 0
      docs/components/tools/telegram-setup-helper.tsx
  48. 44 0
      docs/components/tools/tool-frame.tsx
  49. 67 0
      docs/content/docs/en/config/clients.mdx
  50. 97 0
      docs/content/docs/en/config/inbounds.mdx
  51. 14 0
      docs/content/docs/en/config/meta.json
  52. 77 0
      docs/content/docs/en/config/panel.mdx
  53. 135 0
      docs/content/docs/en/config/reality.mdx
  54. 82 0
      docs/content/docs/en/config/share-links.mdx
  55. 181 0
      docs/content/docs/en/config/ssl-certificates.mdx
  56. 86 0
      docs/content/docs/en/config/subscription.mdx
  57. 199 0
      docs/content/docs/en/config/transports.mdx
  58. 124 0
      docs/content/docs/en/guide/first-login.mdx
  59. 71 0
      docs/content/docs/en/guide/index.mdx
  60. 148 0
      docs/content/docs/en/guide/installation.mdx
  61. 5 0
      docs/content/docs/en/guide/meta.json
  62. 99 0
      docs/content/docs/en/guide/update-uninstall.mdx
  63. 64 0
      docs/content/docs/en/help/contributing.mdx
  64. 47 0
      docs/content/docs/en/help/faq.mdx
  65. 5 0
      docs/content/docs/en/help/meta.json
  66. 52 0
      docs/content/docs/en/help/migration.mdx
  67. 42 0
      docs/content/docs/en/help/troubleshooting.mdx
  68. 54 0
      docs/content/docs/en/index.mdx
  69. 3 0
      docs/content/docs/en/meta.json
  70. 57 0
      docs/content/docs/en/operations/backup-restore.mdx
  71. 12 0
      docs/content/docs/en/operations/meta.json
  72. 97 0
      docs/content/docs/en/operations/multi-node.mdx
  73. 157 0
      docs/content/docs/en/operations/outbounds-routing.mdx
  74. 51 0
      docs/content/docs/en/operations/reverse-proxy.mdx
  75. 66 0
      docs/content/docs/en/operations/security.mdx
  76. 112 0
      docs/content/docs/en/operations/telegram-bot.mdx
  77. 79 0
      docs/content/docs/en/reference/api/api-tokens.mdx
  78. 74 0
      docs/content/docs/en/reference/api/authentication.mdx
  79. 37 0
      docs/content/docs/en/reference/api/backup.mdx
  80. 623 0
      docs/content/docs/en/reference/api/clients.mdx
  81. 108 0
      docs/content/docs/en/reference/api/hosts.mdx
  82. 242 0
      docs/content/docs/en/reference/api/inbounds.mdx
  83. 40 0
      docs/content/docs/en/reference/api/index.mdx
  84. 19 0
      docs/content/docs/en/reference/api/meta.json
  85. 183 0
      docs/content/docs/en/reference/api/nodes.mdx
  86. 377 0
      docs/content/docs/en/reference/api/server.mdx
  87. 122 0
      docs/content/docs/en/reference/api/settings.mdx
  88. 72 0
      docs/content/docs/en/reference/api/subscription-server.mdx
  89. 46 0
      docs/content/docs/en/reference/api/websocket.mdx
  90. 257 0
      docs/content/docs/en/reference/api/xray-settings.mdx
  91. 64 0
      docs/content/docs/en/reference/database.mdx
  92. 84 0
      docs/content/docs/en/reference/env-vars.mdx
  93. 5 0
      docs/content/docs/en/reference/meta.json
  94. 36 0
      docs/content/docs/en/reference/ports-firewall.mdx
  95. 67 0
      docs/content/docs/fa/config/clients.mdx
  96. 97 0
      docs/content/docs/fa/config/inbounds.mdx
  97. 14 0
      docs/content/docs/fa/config/meta.json
  98. 77 0
      docs/content/docs/fa/config/panel.mdx
  99. 135 0
      docs/content/docs/fa/config/reality.mdx
  100. 81 0
      docs/content/docs/fa/config/share-links.mdx

+ 46 - 0
.github/workflows/docs-ci.yml

@@ -0,0 +1,46 @@
+name: Docs CI
+
+on:
+  push:
+    branches: [main]
+    paths:
+      - 'docs/**'
+      - '.github/workflows/docs-ci.yml'
+  pull_request:
+    paths:
+      - 'docs/**'
+      - '.github/workflows/docs-ci.yml'
+
+defaults:
+  run:
+    working-directory: docs
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v7
+
+      - uses: pnpm/action-setup@v6
+        with:
+          version: 11
+
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 22
+          cache: pnpm
+          cache-dependency-path: docs/pnpm-lock.yaml
+
+      - run: pnpm install --frozen-lockfile
+
+      - name: Typecheck
+        run: pnpm typecheck
+
+      - name: Lint
+        run: pnpm lint
+
+      - name: Test
+        run: pnpm test
+
+      - name: Build
+        run: pnpm build

+ 67 - 0
.github/workflows/docs-deploy.yml

@@ -0,0 +1,67 @@
+name: Docs Deploy (GitHub Pages)
+
+# Static-export deploy of docs/ to GitHub Pages. Pages must be enabled in repo
+# settings (Source: GitHub Actions) and the docs.sanaei.dev custom domain
+# attached to this repository. The site URL defaults to the production domain in
+# docs/lib/shared.ts, so NEXT_PUBLIC_SITE_URL is optional.
+on:
+  push:
+    branches: [main]
+    paths:
+      - 'docs/**'
+      - '.github/workflows/docs-deploy.yml'
+  workflow_dispatch:
+
+permissions:
+  contents: read
+  pages: write
+  id-token: write
+
+concurrency:
+  group: pages
+  cancel-in-progress: true
+
+defaults:
+  run:
+    working-directory: docs
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v7
+      - uses: pnpm/action-setup@v6
+        with:
+          version: 11
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 22
+          cache: pnpm
+          cache-dependency-path: docs/pnpm-lock.yaml
+      - run: pnpm install --frozen-lockfile
+      - name: Build (static export)
+        env:
+          DEPLOY_TARGET: static
+          NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL }}
+        run: pnpm build
+      - name: Mirror default locale (en) to the site root
+        # hideLocale makes most English links unprefixed (/, /docs/...), but the
+        # language switcher still targets /en/... . The export emits pages only
+        # under /en/, and there is no i18n middleware on a static host — so copy
+        # the English build to the root (data files included) while KEEPING /en/
+        # in place. That way both /docs/... and /en/docs/... resolve. Other
+        # locales stay under /fa, /ru, /zh.
+        run: cp -a out/en/. out/
+      - uses: actions/upload-pages-artifact@v5
+        with:
+          path: docs/out
+
+  deploy:
+    needs: build
+    runs-on: ubuntu-latest
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    steps:
+      - id: deployment
+        uses: actions/deploy-pages@v5

+ 2 - 0
docs/.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 31 - 0
docs/.gitignore

@@ -0,0 +1,31 @@
+# deps
+/node_modules
+
+# generated content
+.source
+
+# test & build
+/coverage
+/.next/
+/out/
+/build
+*.tsbuildinfo
+
+# misc
+.DS_Store
+*.pem
+/.pnp
+.pnp.js
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# others
+.env*.local
+.vercel
+next-env.d.ts
+# npm (this project uses pnpm)
+package-lock.json
+
+# claude code (local settings/plans; keep shared project instructions)
+/.claude/*

+ 10 - 0
docs/.prettierignore

@@ -0,0 +1,10 @@
+node_modules
+.next
+.source
+out
+pnpm-lock.yaml
+public/openapi.json
+# Don't let Prettier reflow MDX prose — it merges headings into paragraphs and
+# collapses lists inside JSX components (Steps/Callout). Author MDX by hand.
+content/**/*.mdx
+content/docs/**/reference/api

+ 7 - 0
docs/.prettierrc.json

@@ -0,0 +1,7 @@
+{
+  "semi": true,
+  "singleQuote": true,
+  "trailingComma": "all",
+  "printWidth": 100,
+  "tabWidth": 2
+}

+ 34 - 0
docs/CONTRIBUTING.md

@@ -0,0 +1,34 @@
+# Contributing to 3x-ui-docs
+
+Thanks for helping improve the 3x-ui documentation and product site!
+
+## Prerequisites
+
+This project uses **[pnpm](https://pnpm.io)** (not npm — `package-lock.json` is
+gitignored). Install dependencies and start the dev server:
+
+```bash
+pnpm install
+pnpm dev        # http://localhost:3000
+```
+
+## Scripts
+
+| Script           | Description                                           |
+| ---------------- | ----------------------------------------------------- |
+| `pnpm dev`       | Start the dev server                                  |
+| `pnpm build`     | Production build                                      |
+| `pnpm start`     | Serve the production build                            |
+| `pnpm typecheck` | Generate MDX/route types and run `tsc --noEmit`       |
+| `pnpm lint`      | ESLint (flat config)                                  |
+| `pnpm format`    | Format with Prettier                                  |
+| `pnpm test`      | Run unit tests (Vitest) for `lib/xray/*` pure logic   |
+| `pnpm gen:api`   | Generate the API reference from `public/openapi.json` |
+
+Before opening a pull request, please run `pnpm typecheck`, `pnpm lint`, and
+`pnpm test` — these are the same checks that CI runs on every PR.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the
+project's [GPL-3.0](./LICENSE) license.

+ 674 - 0
docs/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 133 - 0
docs/README.md

@@ -0,0 +1,133 @@
+<p align="center">
+  <a href="https://docs.sanaei.dev">
+    <picture>
+      <source media="(prefers-color-scheme: dark)" srcset="public/logo-dark.png" />
+      <img src="public/logo-light.png" alt="3x-ui" width="180" />
+    </picture>
+  </a>
+</p>
+
+<h1 align="center">3x-ui Documentation</h1>
+
+<p align="center">
+  The official documentation and product site for
+  <a href="https://github.com/MHSanaei/3x-ui"><b>3x-ui</b></a> —
+  an advanced web panel for managing Xray-core servers.
+</p>
+
+<p align="center">
+  <a href="https://docs.sanaei.dev"><img src="https://img.shields.io/badge/docs-docs.sanaei.dev-22d3ee?style=flat-square" alt="Live site" /></a>
+  <a href="https://github.com/MHSanaei/3x-ui/actions/workflows/docs-ci.yml"><img src="https://github.com/MHSanaei/3x-ui/actions/workflows/docs-ci.yml/badge.svg" alt="CI" /></a>
+  <a href="LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-blue?style=flat-square" alt="License: GPL-3.0" /></a>
+  <img src="https://img.shields.io/badge/Next.js-16-black?style=flat-square&logo=next.js" alt="Next.js 16" />
+  <img src="https://img.shields.io/badge/Fumadocs-16-0ea5e9?style=flat-square" alt="Fumadocs 16" />
+</p>
+
+<p align="center">
+  <a href="https://docs.sanaei.dev"><b>Read the docs →</b></a>
+</p>
+
+---
+
+## Overview
+
+This directory (`docs/` in the [3x-ui](https://github.com/MHSanaei/3x-ui) monorepo) contains
+the source for [docs.sanaei.dev](https://docs.sanaei.dev) — a static-first documentation and
+marketing site built with [Fumadocs](https://fumadocs.dev) on Next.js. It has **no backend,
+no database, and no auth**: every page is prerendered and every tool runs entirely in the
+browser.
+
+## What's inside
+
+The documentation walks you through 3x-ui from first install to day-to-day operation:
+
+- **Getting Started** — installation, first login, and updating or uninstalling the panel.
+- **Configuration** — the panel, inbounds, REALITY, transports, clients, subscriptions, and share links.
+- **Operations** — reverse proxy, multi-node setups, outbounds & routing, backup/restore, the Telegram bot, and security.
+- **Reference** — environment variables, the database, ports & firewall, and the HTTP API.
+- **Help** — troubleshooting, FAQ, migration, and how to contribute.
+
+## Interactive tools
+
+The site ships with in-browser helpers that generate configuration for you — **no data
+ever leaves your browser**:
+
+| Tool                         | What it does                                            |
+| ---------------------------- | ------------------------------------------------------- |
+| **REALITY Config Generator** | Build a valid REALITY inbound configuration.            |
+| **Share Link Inspector**     | Decode and inspect `vless://` / `vmess://` share links. |
+| **Install Command Builder**  | Assemble the right install command for your setup.      |
+| **Reverse Proxy Generator**  | Generate reverse-proxy configs (Nginx / Caddy).         |
+| **Protocol Wizard**          | Pick and configure the right protocol for your needs.   |
+| **Firewall Rules Generator** | Produce firewall rules for your ports.                  |
+
+## Tech stack
+
+| Layer      | Technology                                                  |
+| ---------- | ---------------------------------------------------------- |
+| Framework  | [Next.js 16](https://nextjs.org) (App Router) · React 19   |
+| Docs       | [Fumadocs](https://fumadocs.dev) (`-ui` / `-core` / `-mdx`) |
+| Styling    | [Tailwind CSS v4](https://tailwindcss.com)                 |
+| Search     | [Orama](https://orama.com) static index                    |
+| Language   | TypeScript (strict)                                         |
+| Tests      | [Vitest](https://vitest.dev) for the pure `lib/xray` logic  |
+| Tooling    | pnpm · ESLint 9 · Prettier                                  |
+
+## Quick start
+
+This project uses **[pnpm](https://pnpm.io)** (npm lockfiles are gitignored). Run everything
+from the `docs/` directory:
+
+```bash
+cd docs
+pnpm install
+pnpm dev        # http://localhost:3000
+```
+
+Useful scripts:
+
+| Script           | Description                                  |
+| ---------------- | -------------------------------------------- |
+| `pnpm dev`       | Start the dev server                         |
+| `pnpm build`     | Production build (also typechecks)           |
+| `pnpm typecheck` | Generate MDX/route types and `tsc --noEmit`  |
+| `pnpm lint`      | Run ESLint                                    |
+| `pnpm test`      | Run unit tests (Vitest)                       |
+
+See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full list and project conventions.
+
+## Project structure
+
+```
+app/             # Next.js App Router — layouts, home, docs, OG images, search, llms.txt
+components/      # React components — interactive tools, home sections, MDX bindings
+content/docs/    # MDX documentation, one folder per locale (en · fa · ru · zh)
+lib/             # source config, i18n, GitHub stats, and the unit-tested lib/xray logic
+public/          # static assets — logos, favicon, openapi.json, CNAME
+scripts/         # build-time scripts (API reference generation)
+source.config.ts # Fumadocs MDX schema & collection config
+next.config.mjs  # Next.js config (static-export gating)
+proxy.ts         # i18n middleware
+```
+
+## Internationalization
+
+Documentation is authored in **English**. Persian (`fa`, RTL), Russian (`ru`), and
+Chinese (`zh`) locales are wired up; untranslated pages fall back to English so they
+never 404. English URLs are unprefixed; other locales live under `/fa`, `/ru`, `/zh`.
+
+## Deployment
+
+The site builds for two targets:
+
+- **Vercel / Node** — `pnpm build` (static search index + prerendered OG images).
+- **GitHub Pages (static export)** — `DEPLOY_TARGET=static pnpm build` → `out/`.
+
+## Contributing
+
+Contributions are welcome! Setup, scripts, and project conventions live in
+[`CONTRIBUTING.md`](./CONTRIBUTING.md).
+
+## License
+
+Licensed under [GPL-3.0](./LICENSE).

+ 28 - 0
docs/app/[lang]/(home)/layout.tsx

@@ -0,0 +1,28 @@
+import { HomeLayout } from 'fumadocs-ui/layouts/home';
+import { baseOptions } from '@/lib/layout.shared';
+import { HomeLanguageSwitcher } from '@/components/home/language-switcher';
+
+export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
+  const { lang } = await params;
+  const options = baseOptions(lang);
+  return (
+    <HomeLayout
+      {...options}
+      // Disable fumadocs' built-in popover language switcher here: nested in the
+      // home navbar's Radix NavigationMenu its item clicks don't fire. We inject
+      // an anchor-based one instead (see HomeLanguageSwitcher). Docs keep the
+      // built-in switcher (its sidebar isn't a NavigationMenu, so it works).
+      i18n={false}
+      links={[
+        ...(options.links ?? []),
+        {
+          type: 'custom',
+          secondary: true,
+          children: <HomeLanguageSwitcher current={lang} />,
+        },
+      ]}
+    >
+      {children}
+    </HomeLayout>
+  );
+}

+ 143 - 0
docs/app/[lang]/(home)/page.tsx

@@ -0,0 +1,143 @@
+import Link from 'next/link';
+import { ArrowRight, BookOpen, Heart } from 'lucide-react';
+import { GitHubIcon, TelegramIcon } from '@/components/icons';
+import { Logo } from '@/components/logo';
+import { Features } from '@/components/home/features';
+import { GitHubStatsRow } from '@/components/home/github-stats';
+import { InstallCommand } from '@/components/home/install-command';
+import { getGitHubStats } from '@/lib/github-stats';
+import { i18n } from '@/lib/i18n';
+import { appName, productRepoUrl, deepWikiUrl, telegramChannelUrl, donateUrl } from '@/lib/shared';
+import { getSiteMessages, type SiteMessages } from '@/lib/site-i18n';
+
+export function generateStaticParams() {
+  return i18n.languages.map((lang) => ({ lang }));
+}
+
+const INSTALL_COMMAND =
+  'bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)';
+
+export default async function HomePage({ params }: PageProps<'/[lang]'>) {
+  const { lang } = await params;
+  const prefix = lang === 'en' ? '' : `/${lang}`;
+  const m = getSiteMessages(lang);
+  const stats = await getGitHubStats();
+
+  return (
+    <main className="flex flex-1 flex-col">
+      {/* Hero */}
+      <section className="relative overflow-hidden border-b">
+        <div
+          className="pointer-events-none absolute inset-x-0 -top-40 h-80 bg-gradient-to-b from-brand/15 to-transparent blur-3xl"
+          aria-hidden
+        />
+        <div className="mx-auto flex w-full max-w-5xl flex-col items-center px-4 py-20 text-center sm:py-28">
+          <Logo className="h-20 drop-shadow-sm" />
+          <h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-6xl">
+            <span className="bg-gradient-to-r from-cyan-500 to-sky-600 bg-clip-text text-transparent dark:from-cyan-300 dark:to-sky-400">
+              {appName}
+            </span>
+          </h1>
+          <p className="mt-4 max-w-2xl text-lg text-fd-muted-foreground sm:text-xl">{m.tagline}</p>
+
+          <div className="mt-8 flex flex-col items-center gap-3 sm:flex-row">
+            <Link
+              href={`${prefix}/docs`}
+              className="inline-flex items-center gap-2 rounded-xl bg-fd-primary px-5 py-3 font-medium text-fd-primary-foreground transition-opacity hover:opacity-90"
+            >
+              {m.getStarted}
+              <ArrowRight className="size-4 rtl:rotate-180" aria-hidden />
+            </Link>
+            <a
+              href={productRepoUrl}
+              target="_blank"
+              rel="noreferrer noopener"
+              className="inline-flex items-center gap-2 rounded-xl border px-5 py-3 font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
+            >
+              <GitHubIcon className="size-4" />
+              {m.viewOnGitHub}
+            </a>
+          </div>
+
+          <InstallCommand
+            command={INSTALL_COMMAND}
+            copyLabel={m.copyCommand}
+            copiedLabel={m.copied}
+            className="mt-8 w-full max-w-2xl"
+          />
+
+          {/* Build-time stats as the initial render; refreshed live on the client. */}
+          <GitHubStatsRow
+            initial={stats}
+            labels={{ stars: m.stars, forks: m.forks, latest: m.latest }}
+          />
+        </div>
+      </section>
+
+      <Features heading={m.featuresHeading} subtitle={m.featuresSubtitle} items={m.features} />
+
+      <Footer prefix={prefix} m={m} />
+    </main>
+  );
+}
+
+function Footer({ prefix, m }: { prefix: string; m: SiteMessages }) {
+  return (
+    <footer className="border-t">
+      <div className="mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 px-4 py-8 text-sm text-fd-muted-foreground sm:flex-row">
+        <div className="inline-flex items-center gap-2">
+          <Logo className="h-6" />
+          <span>
+            {appName} — {m.licenseBefore}
+            <a
+              href={`${productRepoUrl}/blob/main/LICENSE`}
+              className="underline hover:text-fd-foreground"
+            >
+              GPL-3.0
+            </a>
+            {m.licenseAfter}
+          </span>
+        </div>
+        <nav className="flex items-center gap-4">
+          <Link href={`${prefix}/docs`} className="hover:text-fd-foreground">
+            {m.docs}
+          </Link>
+          <a
+            href={productRepoUrl}
+            className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
+          >
+            <GitHubIcon className="size-4" />
+            GitHub
+          </a>
+          <a
+            href={deepWikiUrl}
+            target="_blank"
+            rel="noreferrer noopener"
+            className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
+          >
+            <BookOpen className="size-4" />
+            DeepWiki
+          </a>
+          <a
+            href={telegramChannelUrl}
+            target="_blank"
+            rel="noreferrer noopener"
+            className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
+          >
+            <TelegramIcon className="size-4" />
+            Telegram
+          </a>
+          <a
+            href={donateUrl}
+            target="_blank"
+            rel="noreferrer noopener"
+            className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
+          >
+            <Heart className="size-4" />
+            {m.donate}
+          </a>
+        </nav>
+      </div>
+    </footer>
+  );
+}

+ 80 - 0
docs/app/[lang]/docs/[[...slug]]/page.tsx

@@ -0,0 +1,80 @@
+import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
+import {
+  DocsBody,
+  DocsDescription,
+  DocsPage,
+  DocsTitle,
+  MarkdownCopyButton,
+  ViewOptionsPopover,
+} from 'fumadocs-ui/layouts/docs/page';
+import { notFound } from 'next/navigation';
+import { getMDXComponents } from '@/components/mdx';
+import { OpenAPIPage as BaseOpenAPIPage } from '@/components/openapi-page';
+import { openapi } from '@/lib/openapi';
+import type { Metadata } from 'next';
+import type { ComponentProps } from 'react';
+import { createRelativeLink } from 'fumadocs-ui/mdx';
+import { gitConfig } from '@/lib/shared';
+
+export default async function Page(props: PageProps<'/[lang]/docs/[[...slug]]'>) {
+  const { lang, slug } = await props.params;
+  const page = source.getPage(slug, lang);
+  if (!page) notFound();
+
+  const MDX = page.data.body;
+  const markdownUrl = getPageMarkdownUrl(page).url;
+  const editUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/${gitConfig.docsDir}/${page.path}`;
+
+  // Generated API reference pages carry `_openapi` metadata. Preload the spec
+  // on the server (highlighting included) so the client OpenAPIPage doesn't have
+  // to load it at render time.
+  const isOpenAPI = Boolean((page.data as { _openapi?: unknown })._openapi);
+  const extraComponents: Record<string, unknown> = {};
+  if (isOpenAPI) {
+    const preloaded = await openapi.preloadOpenAPIPage(page);
+    function PreloadedOpenAPIPage(p: ComponentProps<typeof BaseOpenAPIPage>) {
+      return <BaseOpenAPIPage {...p} {...preloaded} />;
+    }
+    extraComponents.OpenAPIPage = PreloadedOpenAPIPage;
+  }
+
+  return (
+    <DocsPage toc={page.data.toc} full={page.data.full}>
+      <DocsTitle>{page.data.title}</DocsTitle>
+      <DocsDescription className="mb-0">{page.data.description}</DocsDescription>
+      <div className="flex flex-row items-center gap-2 border-b pb-6">
+        <MarkdownCopyButton markdownUrl={markdownUrl} />
+        <ViewOptionsPopover markdownUrl={markdownUrl} githubUrl={editUrl} />
+      </div>
+      <DocsBody>
+        <MDX
+          components={getMDXComponents({
+            // allows linking to other pages with relative file paths
+            a: createRelativeLink(source, page),
+            ...extraComponents,
+          })}
+        />
+      </DocsBody>
+    </DocsPage>
+  );
+}
+
+export async function generateStaticParams() {
+  return source.generateParams();
+}
+
+export async function generateMetadata(
+  props: PageProps<'/[lang]/docs/[[...slug]]'>,
+): Promise<Metadata> {
+  const { lang, slug } = await props.params;
+  const page = source.getPage(slug, lang);
+  if (!page) notFound();
+
+  return {
+    title: page.data.title,
+    description: page.data.description,
+    openGraph: {
+      images: getPageImage(page).url,
+    },
+  };
+}

+ 12 - 0
docs/app/[lang]/docs/layout.tsx

@@ -0,0 +1,12 @@
+import { source } from '@/lib/source';
+import { DocsLayout } from 'fumadocs-ui/layouts/docs';
+import { baseOptions } from '@/lib/layout.shared';
+
+export default async function Layout({ params, children }: LayoutProps<'/[lang]/docs'>) {
+  const { lang } = await params;
+  return (
+    <DocsLayout tree={source.getPageTree(lang)} {...baseOptions(lang)}>
+      {children}
+    </DocsLayout>
+  );
+}

+ 30 - 0
docs/app/[lang]/layout.tsx

@@ -0,0 +1,30 @@
+import '../global.css';
+import { RootProvider } from 'fumadocs-ui/provider/next';
+import { Inter, Vazirmatn } from 'next/font/google';
+import { i18n, localeDirection } from '@/lib/i18n';
+import { provider } from '@/lib/i18n-ui';
+import SearchDialog from '@/components/search-dialog';
+
+const inter = Inter({ subsets: ['latin'], display: 'swap' });
+// Persian UI font; covers Arabic + Latin glyphs so mixed content renders well.
+const vazirmatn = Vazirmatn({ subsets: ['arabic'], display: 'swap' });
+
+export function generateStaticParams() {
+  return i18n.languages.map((lang) => ({ lang }));
+}
+
+export default async function LangLayout({ params, children }: LayoutProps<'/[lang]'>) {
+  const { lang } = await params;
+  const dir = localeDirection(lang);
+  const fontClassName = lang === 'fa' ? vazirmatn.className : inter.className;
+
+  return (
+    <html lang={lang} dir={dir} className={fontClassName} suppressHydrationWarning>
+      <body className="flex min-h-screen flex-col" suppressHydrationWarning>
+        <RootProvider i18n={provider(lang)} search={{ SearchDialog }}>
+          {children}
+        </RootProvider>
+      </body>
+    </html>
+  );
+}

+ 22 - 0
docs/app/api/search/route.ts

@@ -0,0 +1,22 @@
+import { source } from '@/lib/source';
+import { createFromSource } from 'fumadocs-core/search/server';
+
+// Required for `output: 'export'` — the search index is fully static.
+export const revalidate = false;
+export const dynamic = 'force-static';
+
+// Static search index: works under both SSR/Vercel and static export
+// (`output: 'export'`). The client loads this prebuilt index and searches
+// in-browser (see the `type: 'static'` search option in app/[lang]/layout.tsx).
+// All locales currently hold English (fallback) content, and Orama has no
+// Persian tokenizer, so map every locale to the English tokenizer. When real
+// translations land, switch ru -> 'russian', zh -> 'mandarin' (with
+// @orama/tokenizers), etc. See https://docs.orama.com/open-source/supported-languages
+export const { staticGET: GET } = createFromSource(source, {
+  localeMap: {
+    en: 'english',
+    fa: 'english',
+    ru: 'english',
+    zh: 'english',
+  },
+});

+ 32 - 0
docs/app/global.css

@@ -0,0 +1,32 @@
+@import 'tailwindcss';
+@import 'fumadocs-ui/css/neutral.css';
+@import 'fumadocs-ui/css/preset.css';
+
+/* 3x-ui brand: cyan / turquoise accent (matches the panel logo). */
+:root {
+  --color-fd-primary: hsl(190, 95%, 39%);
+  --color-fd-primary-foreground: hsl(0, 0%, 100%);
+  --color-fd-ring: hsl(190, 95%, 39%);
+}
+
+.dark {
+  --color-fd-primary: hsl(187, 90%, 55%);
+  --color-fd-primary-foreground: hsl(190, 80%, 8%);
+  --color-fd-ring: hsl(187, 90%, 55%);
+}
+
+/* Expose the brand color as Tailwind utilities (bg-brand, text-brand, ...). */
+@theme {
+  --color-brand: hsl(190, 95%, 39%);
+  --color-brand-foreground: hsl(0, 0%, 100%);
+}
+
+html {
+  scrollbar-gutter: stable;
+}
+
+/* In RTL the scroll-lock padding must be applied to the logical inline-end. */
+html > body[data-scroll-locked] {
+  margin-inline-end: 0px !important;
+  --removed-body-scroll-bar-size: 0px !important;
+}

+ 31 - 0
docs/app/layout.tsx

@@ -0,0 +1,31 @@
+import type { Metadata } from 'next';
+import type { ReactNode } from 'react';
+import { appName, appTagline, siteUrl } from '@/lib/shared';
+
+// Global SEO defaults. The real <html>/<body> live in `app/[lang]/layout.tsx`
+// so we can set `lang`/`dir` per locale (RTL for fa); this root layout is a
+// pass-through that only carries site-wide metadata.
+export const metadata: Metadata = {
+  metadataBase: new URL(siteUrl),
+  title: {
+    default: `${appName} — ${appTagline}`,
+    template: `%s — ${appName}`,
+  },
+  description: appTagline,
+  applicationName: appName,
+  openGraph: {
+    siteName: appName,
+    type: 'website',
+  },
+  twitter: {
+    card: 'summary_large_image',
+  },
+  icons: {
+    icon: '/favicon.png',
+    apple: '/icon.png',
+  },
+};
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+  return children;
+}

+ 10 - 0
docs/app/llms-full.txt/route.ts

@@ -0,0 +1,10 @@
+import { getLLMText, source } from '@/lib/source';
+
+export const revalidate = false;
+
+export async function GET() {
+  const scan = source.getPages().map(getLLMText);
+  const scanned = await Promise.all(scan);
+
+  return new Response(scanned.join('\n\n'));
+}

+ 23 - 0
docs/app/llms.mdx/docs/[[...slug]]/route.ts

@@ -0,0 +1,23 @@
+import { getLLMText, getPageMarkdownUrl, source } from '@/lib/source';
+import { notFound } from 'next/navigation';
+
+export const revalidate = false;
+
+export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
+  const { slug } = await params;
+  const page = source.getPage(slug?.slice(0, -1));
+  if (!page) notFound();
+
+  return new Response(await getLLMText(page), {
+    headers: {
+      'Content-Type': 'text/markdown',
+    },
+  });
+}
+
+export function generateStaticParams() {
+  return source.getPages().map((page) => ({
+    lang: page.locale,
+    slug: getPageMarkdownUrl(page).segments,
+  }));
+}

+ 8 - 0
docs/app/llms.txt/route.ts

@@ -0,0 +1,8 @@
+import { source } from '@/lib/source';
+import { llms } from 'fumadocs-core/source';
+
+export const revalidate = false;
+
+export function GET() {
+  return new Response(llms(source).index());
+}

+ 29 - 0
docs/app/og/docs/[...slug]/route.tsx

@@ -0,0 +1,29 @@
+import { getPageImage, source } from '@/lib/source';
+import { notFound } from 'next/navigation';
+import { ImageResponse } from 'next/og';
+import { generate as DefaultImage } from 'fumadocs-ui/og';
+import { appName } from '@/lib/shared';
+
+export const dynamic = 'force-static';
+export const revalidate = false;
+
+export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
+  const { slug } = await params;
+  const page = source.getPage(slug.slice(0, -1));
+  if (!page) notFound();
+
+  return new ImageResponse(
+    <DefaultImage title={page.data.title} description={page.data.description} site={appName} />,
+    {
+      width: 1200,
+      height: 630,
+    },
+  );
+}
+
+export function generateStaticParams() {
+  return source.getPages().map((page) => ({
+    lang: page.locale,
+    slug: getPageImage(page).segments,
+  }));
+}

+ 12 - 0
docs/app/robots.ts

@@ -0,0 +1,12 @@
+import type { MetadataRoute } from 'next';
+import { siteUrl } from '@/lib/shared';
+
+// Required for `output: 'export'`.
+export const dynamic = 'force-static';
+
+export default function robots(): MetadataRoute.Robots {
+  return {
+    rules: { userAgent: '*', allow: '/' },
+    sitemap: `${siteUrl}/sitemap.xml`,
+  };
+}

+ 33 - 0
docs/app/sitemap.ts

@@ -0,0 +1,33 @@
+import type { MetadataRoute } from 'next';
+import { source } from '@/lib/source';
+import { i18n } from '@/lib/i18n';
+import { siteUrl } from '@/lib/shared';
+
+// Required for `output: 'export'`.
+export const dynamic = 'force-static';
+
+// Locale home pages + the canonical (English) docs pages. Other locales
+// currently fall back to English content, so we don't list them separately
+// to avoid duplicate-content entries until real translations exist.
+export default function sitemap(): MetadataRoute.Sitemap {
+  const entries: MetadataRoute.Sitemap = [];
+
+  for (const lang of i18n.languages) {
+    const prefix = lang === 'en' ? '' : `/${lang}`;
+    entries.push({
+      url: `${siteUrl}${prefix}` || siteUrl,
+      changeFrequency: 'weekly',
+      priority: 1,
+    });
+  }
+
+  for (const page of source.getPages('en')) {
+    entries.push({
+      url: `${siteUrl}${page.url}`,
+      changeFrequency: 'weekly',
+      priority: 0.8,
+    });
+  }
+
+  return entries;
+}

+ 49 - 0
docs/components/home/features.tsx

@@ -0,0 +1,49 @@
+import {
+  Boxes,
+  Network,
+  Send,
+  ShieldCheck,
+  TerminalSquare,
+  Users,
+  type LucideIcon,
+} from 'lucide-react';
+
+// Icons map by position to the localized feature items in lib/site-i18n.ts
+// (Every major protocol, REALITY, Clients, Multi-node, Telegram, Self-hosted).
+const ICONS: LucideIcon[] = [Boxes, ShieldCheck, Users, Network, Send, TerminalSquare];
+
+export function Features({
+  heading,
+  subtitle,
+  items,
+}: {
+  heading: string;
+  subtitle: string;
+  items: { title: string; description: string }[];
+}) {
+  return (
+    <section className="mx-auto w-full max-w-6xl px-4 py-16 sm:py-24">
+      <div className="mx-auto max-w-2xl text-center">
+        <h2 className="text-2xl font-bold tracking-tight sm:text-3xl">{heading}</h2>
+        <p className="mt-3 text-fd-muted-foreground">{subtitle}</p>
+      </div>
+      <div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
+        {items.map(({ title, description }, i) => {
+          const Icon = ICONS[i] ?? Boxes;
+          return (
+            <div
+              key={title}
+              className="rounded-2xl border bg-fd-card p-6 transition-colors hover:border-fd-primary/40"
+            >
+              <div className="inline-flex size-11 items-center justify-center rounded-xl bg-brand/10 text-brand">
+                <Icon className="size-6" aria-hidden />
+              </div>
+              <h3 className="mt-4 font-semibold">{title}</h3>
+              <p className="mt-2 text-sm text-fd-muted-foreground">{description}</p>
+            </div>
+          );
+        })}
+      </div>
+    </section>
+  );
+}

+ 67 - 0
docs/components/home/github-stats.tsx

@@ -0,0 +1,67 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { GitFork, Star, Tag } from 'lucide-react';
+import { fetchGitHubStats, formatCount, type GitHubStats } from '@/lib/github-stats';
+
+/**
+ * Stars / forks / latest-release row. Renders the build-time numbers
+ * immediately (no layout shift, works without JS), then swaps in live ones
+ * from the GitHub API after hydration.
+ */
+export function GitHubStatsRow({
+  initial,
+  labels,
+}: {
+  initial: GitHubStats;
+  labels: { stars: string; forks: string; latest: string };
+}) {
+  const [stats, setStats] = useState(initial);
+
+  useEffect(() => {
+    let cancelled = false;
+    // Plain fetch, no custom headers — keeps the request preflight-free.
+    void fetchGitHubStats().then((live) => {
+      if (cancelled || !live) return;
+      setStats((prev) => ({ ...live, latestVersion: live.latestVersion || prev.latestVersion }));
+    });
+    return () => {
+      cancelled = true;
+    };
+  }, []);
+
+  return (
+    <dl className="mt-8 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm">
+      <Stat icon={<Star className="size-4" aria-hidden />} label={labels.stars}>
+        {formatCount(stats.stars)}
+      </Stat>
+      <Stat icon={<GitFork className="size-4" aria-hidden />} label={labels.forks}>
+        {formatCount(stats.forks)}
+      </Stat>
+      <Stat icon={<Tag className="size-4" aria-hidden />} label={labels.latest}>
+        {stats.latestVersion}
+      </Stat>
+    </dl>
+  );
+}
+
+function Stat({
+  icon,
+  label,
+  children,
+}: {
+  icon: React.ReactNode;
+  label: string;
+  children: React.ReactNode;
+}) {
+  return (
+    <div className="inline-flex items-center gap-2">
+      <span className="text-brand">{icon}</span>
+      <dt className="sr-only">{label}</dt>
+      <dd>
+        <span className="font-semibold">{children}</span>{' '}
+        <span className="text-fd-muted-foreground">{label}</span>
+      </dd>
+    </div>
+  );
+}

+ 56 - 0
docs/components/home/install-command.tsx

@@ -0,0 +1,56 @@
+'use client';
+
+import { useState } from 'react';
+import { Check, Copy } from 'lucide-react';
+import { cn } from '@/lib/cn';
+
+export function InstallCommand({
+  command,
+  className,
+  copyLabel = 'Copy install command',
+  copiedLabel = 'Copied',
+}: {
+  command: string;
+  className?: string;
+  copyLabel?: string;
+  copiedLabel?: string;
+}) {
+  const [copied, setCopied] = useState(false);
+
+  async function copy() {
+    try {
+      await navigator.clipboard.writeText(command);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    } catch {
+      // Clipboard unavailable (insecure context) — silently ignore.
+    }
+  }
+
+  return (
+    <div
+      className={cn(
+        'flex items-center gap-3 rounded-xl border bg-fd-card py-2.5 pe-2 ps-4 text-sm shadow-sm',
+        className,
+      )}
+    >
+      <span className="select-none font-mono text-fd-muted-foreground">$</span>
+      {/* Commands are always LTR, even on RTL pages. */}
+      <code dir="ltr" className="flex-1 overflow-x-auto whitespace-nowrap text-start font-mono">
+        {command}
+      </code>
+      <button
+        type="button"
+        onClick={copy}
+        aria-label={copied ? copiedLabel : copyLabel}
+        className="inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground focus-visible:outline-2 focus-visible:outline-fd-ring"
+      >
+        {copied ? (
+          <Check className="size-4 text-brand" aria-hidden />
+        ) : (
+          <Copy className="size-4" aria-hidden />
+        )}
+      </button>
+    </div>
+  );
+}

+ 45 - 0
docs/components/home/language-switcher.tsx

@@ -0,0 +1,45 @@
+import Link from 'next/link';
+import { Languages } from 'lucide-react';
+import { i18n, locales } from '@/lib/i18n';
+import { cn } from '@/lib/cn';
+
+// Home-navbar language switcher.
+//
+// fumadocs' built-in popover switcher (`LanguageSelect`) has its item clicks
+// swallowed when it is nested inside HomeLayout's Radix `NavigationMenu` — the
+// dropdown opens but selecting a locale never fires `onChange`/`router.push`.
+// The docs sidebar isn't wrapped in a NavigationMenu, so the built-in one works
+// there and is kept. Here we use a native `<details>` toggle + real `<Link>`
+// anchors, which navigate reliably inside the navbar (like the other nav links).
+//
+// The home navbar only renders on the landing page, so the targets are simply
+// each locale's home (`/`, `/fa`, `/ru`, `/zh`).
+export function HomeLanguageSwitcher({ current }: { current: string }) {
+  return (
+    <details className="group relative [&>summary::-webkit-details-marker]:hidden">
+      <summary
+        aria-label="Choose a language"
+        className="flex cursor-pointer list-none items-center rounded-lg p-1.5 text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground group-open:bg-fd-accent"
+      >
+        <Languages className="size-5" />
+      </summary>
+      <div className="absolute end-0 z-50 mt-1.5 flex min-w-40 flex-col gap-0.5 rounded-lg border bg-fd-popover p-1 text-fd-popover-foreground shadow-lg">
+        <p className="p-2 text-xs font-medium text-fd-muted-foreground">Choose a language</p>
+        {locales.map(({ locale, name }) => (
+          <Link
+            key={locale}
+            href={locale === i18n.defaultLanguage ? '/' : `/${locale}`}
+            className={cn(
+              'rounded-md px-2 py-1.5 text-start text-sm transition-colors',
+              locale === current
+                ? 'bg-fd-primary/10 text-fd-primary'
+                : 'text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
+            )}
+          >
+            {name}
+          </Link>
+        ))}
+      </div>
+    </details>
+  );
+}

+ 17 - 0
docs/components/icons.tsx

@@ -0,0 +1,17 @@
+// Brand icons that are not part of lucide-react.
+
+export function GitHubIcon({ className }: { className?: string }) {
+  return (
+    <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden>
+      <path d="M12 .5C5.73.5.5 5.74.5 12.02c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56 0-.27-.01-1.16-.02-2.1-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.69-1.28-1.69-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11.03 11.03 0 0 1 5.8 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.18 1.84 1.18 3.1 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14 0 1.55-.01 2.8-.01 3.18 0 .31.21.68.8.56A10.53 10.53 0 0 0 23.5 12.02C23.5 5.74 18.27.5 12 .5Z" />
+    </svg>
+  );
+}
+
+export function TelegramIcon({ className }: { className?: string }) {
+  return (
+    <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden>
+      <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.139-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
+    </svg>
+  );
+}

+ 15 - 0
docs/components/logo.tsx

@@ -0,0 +1,15 @@
+import { cn } from '@/lib/cn';
+
+// Official 3x-ui logo (media/3x-ui-{light,dark}.png from the upstream repo).
+// Theme-aware via Tailwind's `dark:` variant. Pass a height class (e.g. `h-6`);
+// width scales automatically (the artwork is 2:1).
+export function Logo({ className }: { className?: string }) {
+  return (
+    <>
+      {/* eslint-disable-next-line @next/next/no-img-element */}
+      <img src="/logo-light.png" alt="3x-ui" className={cn('w-auto dark:hidden', className)} />
+      {/* eslint-disable-next-line @next/next/no-img-element */}
+      <img src="/logo-dark.png" alt="3x-ui" className={cn('hidden w-auto dark:block', className)} />
+    </>
+  );
+}

+ 47 - 0
docs/components/mdx.tsx

@@ -0,0 +1,47 @@
+import defaultMdxComponents from 'fumadocs-ui/mdx';
+import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
+import { Step, Steps } from 'fumadocs-ui/components/steps';
+import { Mermaid } from '@/components/mdx/mermaid';
+import { RealityConfigGenerator } from '@/components/tools/reality-config-generator';
+import { ShareLinkInspector } from '@/components/tools/share-link-inspector';
+import { InstallCommandBuilder } from '@/components/tools/install-command-builder';
+import { ReverseProxyGenerator } from '@/components/tools/reverse-proxy-generator';
+import { ProtocolWizard } from '@/components/tools/protocol-wizard';
+import { FirewallRulesGenerator } from '@/components/tools/firewall-rules-generator';
+import { OutboundGenerator } from '@/components/tools/outbound-generator';
+import { RoutingBuilder } from '@/components/tools/routing-builder';
+import { SubscriptionBuilder } from '@/components/tools/subscription-builder';
+import { TelegramSetupHelper } from '@/components/tools/telegram-setup-helper';
+import { ApiRequestBuilder } from '@/components/tools/api-request-builder';
+import { OpenAPIPage } from '@/components/openapi-page';
+import type { MDXComponents } from 'mdx/types';
+
+export function getMDXComponents(components?: MDXComponents) {
+  return {
+    ...defaultMdxComponents,
+    Tab,
+    Tabs,
+    Step,
+    Steps,
+    Mermaid,
+    RealityConfigGenerator,
+    ShareLinkInspector,
+    InstallCommandBuilder,
+    ReverseProxyGenerator,
+    ProtocolWizard,
+    FirewallRulesGenerator,
+    OutboundGenerator,
+    RoutingBuilder,
+    SubscriptionBuilder,
+    TelegramSetupHelper,
+    ApiRequestBuilder,
+    OpenAPIPage,
+    ...components,
+  } satisfies MDXComponents;
+}
+
+export const useMDXComponents = getMDXComponents;
+
+declare global {
+  type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
+}

+ 44 - 0
docs/components/mdx/mermaid.tsx

@@ -0,0 +1,44 @@
+'use client';
+
+import { useEffect, useId, useState } from 'react';
+import { useTheme } from 'next-themes';
+
+// Client-side, theme-aware Mermaid renderer. Mermaid is imported dynamically so
+// it stays out of the initial bundle and only loads on pages that use a diagram.
+export function Mermaid({ chart }: { chart: string }) {
+  const rawId = useId();
+  const id = `mmd-${rawId.replace(/[^a-zA-Z0-9]/g, '')}`;
+  const { resolvedTheme } = useTheme();
+  const [svg, setSvg] = useState('');
+
+  useEffect(() => {
+    let active = true;
+    void (async () => {
+      const mermaid = (await import('mermaid')).default;
+      mermaid.initialize({
+        startOnLoad: false,
+        securityLevel: 'strict',
+        theme: resolvedTheme === 'dark' ? 'dark' : 'default',
+        fontFamily: 'inherit',
+      });
+      try {
+        const { svg } = await mermaid.render(id, chart.trim());
+        if (active) setSvg(svg);
+      } catch {
+        if (active) setSvg('');
+      }
+    })();
+    return () => {
+      active = false;
+    };
+  }, [chart, resolvedTheme, id]);
+
+  return (
+    <div
+      className="my-6 flex justify-center overflow-x-auto rounded-xl border bg-fd-card p-4 [&_svg]:max-w-full"
+      role="img"
+      aria-label="Architecture diagram"
+      dangerouslySetInnerHTML={{ __html: svg }}
+    />
+  );
+}

+ 6 - 0
docs/components/openapi-page.tsx

@@ -0,0 +1,6 @@
+'use client';
+
+import { createOpenAPIPage } from 'fumadocs-openapi/ui';
+
+// The component used by the generated API reference MDX pages.
+export const OpenAPIPage = createOpenAPIPage();

+ 56 - 0
docs/components/search-dialog.tsx

@@ -0,0 +1,56 @@
+'use client';
+
+import { create } from '@orama/orama';
+import { useDocsSearch } from 'fumadocs-core/search/client';
+import { oramaStaticClient } from 'fumadocs-core/search/client/orama-static';
+import {
+  SearchDialog,
+  SearchDialogClose,
+  SearchDialogContent,
+  SearchDialogHeader,
+  SearchDialogIcon,
+  SearchDialogInput,
+  SearchDialogList,
+  SearchDialogOverlay,
+} from 'fumadocs-ui/components/dialog/search';
+import { useI18n } from 'fumadocs-ui/contexts/i18n';
+import { useMemo } from 'react';
+
+interface SharedProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+}
+
+// The static search index is keyed by locale code (en/fa/ru/zh). Fumadocs'
+// default static dialog feeds those codes to Orama as a tokenizer language, but
+// Orama only accepts full names ("english") and throws on "en" — which silently
+// breaks search entirely. All docs content is English (other locales fall back
+// to it), so re-create the dialog — the documented escape hatch for custom Orama
+// setups — with an initOrama that always builds an English index.
+export default function SearchDialogClient(props: SharedProps) {
+  const { locale } = useI18n();
+  const client = useMemo(
+    () =>
+      oramaStaticClient({
+        from: '/api/search',
+        locale,
+        initOrama: () => create({ schema: { _: 'string' }, language: 'english' }),
+      }),
+    [locale],
+  );
+  const { search, setSearch, query } = useDocsSearch({ client });
+
+  return (
+    <SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
+      <SearchDialogOverlay />
+      <SearchDialogContent>
+        <SearchDialogHeader>
+          <SearchDialogIcon />
+          <SearchDialogInput />
+          <SearchDialogClose />
+        </SearchDialogHeader>
+        <SearchDialogList items={query.data !== 'empty' ? query.data : null} />
+      </SearchDialogContent>
+    </SearchDialog>
+  );
+}

+ 76 - 0
docs/components/tools/api-request-builder.tsx

@@ -0,0 +1,76 @@
+'use client';
+
+import { useId, useState } from 'react';
+import { buildCurl, buildFetchSnippet, type ApiRequestInput, type HttpMethod } from '@/lib/xray/api-client';
+import { ToolFrame } from './tool-frame';
+import { TextField, SelectField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+const METHODS: readonly HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE'];
+
+export function ApiRequestBuilder() {
+  const [baseUrl, setBaseUrl] = useState('https://panel.example.com:2053');
+  const [token, setToken] = useState('');
+  const [path, setPath] = useState('/panel/api/inbounds/list');
+  const [method, setMethod] = useState<HttpMethod>('GET');
+  const [body, setBody] = useState('');
+  const bodyId = useId();
+
+  const showBody = method === 'POST' || method === 'PUT';
+  const input: ApiRequestInput = { baseUrl, token: token || '<token>', path, method, body };
+
+  function reset() {
+    setBaseUrl('https://panel.example.com:2053');
+    setToken('');
+    setPath('/panel/api/inbounds/list');
+    setMethod('GET');
+    setBody('');
+  }
+
+  return (
+    <ToolFrame
+      title="API request builder"
+      description="Build an authenticated cURL command or fetch() snippet for any 3x-ui panel API endpoint under /panel/api/*."
+      onReset={reset}
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+        <TextField label="Panel base URL" value={baseUrl} onChange={setBaseUrl} />
+        <TextField
+          label="API token (Bearer)"
+          value={token}
+          onChange={setToken}
+          placeholder="Settings → Security → API Token"
+        />
+        <TextField label="Endpoint path" value={path} onChange={setPath} />
+        <SelectField
+          label="Method"
+          value={method}
+          onChange={(v) => setMethod(v as HttpMethod)}
+          options={METHODS}
+        />
+      </div>
+
+      {showBody ? (
+        <div className="mt-4 flex flex-col gap-1.5">
+          <label htmlFor={bodyId} className="text-sm font-medium">
+            Request body (JSON)
+          </label>
+          <textarea
+            id={bodyId}
+            dir="ltr"
+            value={body}
+            onChange={(e) => setBody(e.target.value)}
+            rows={4}
+            placeholder='{"id": 1}'
+            className="rounded-lg border bg-fd-background px-3 py-2 font-mono text-sm outline-none transition-colors focus-visible:border-fd-primary focus-visible:ring-2 focus-visible:ring-fd-ring/30"
+          />
+        </div>
+      ) : null}
+
+      <div className="mt-4 grid grid-cols-1 gap-4">
+        <OutputBlock label="cURL" value={buildCurl(input)} />
+        <OutputBlock label="fetch()" value={buildFetchSnippet(input)} />
+      </div>
+    </ToolFrame>
+  );
+}

+ 71 - 0
docs/components/tools/firewall-rules-generator.tsx

@@ -0,0 +1,71 @@
+'use client';
+
+import { useState } from 'react';
+import {
+  buildUfwCommands,
+  buildNftablesRuleset,
+  type FirewallOptions,
+  type PortProtocol,
+  type PortRule,
+} from '@/lib/xray/firewall';
+import { ToolFrame } from './tool-frame';
+import { CheckboxField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+interface Row {
+  label: string;
+  port: string;
+  protocol: PortProtocol;
+  enabled: boolean;
+}
+
+const DEFAULT_ROWS: Row[] = [
+  { label: 'panel', port: '2053', protocol: 'tcp', enabled: true },
+  { label: 'subscription', port: '2096', protocol: 'tcp', enabled: false },
+  { label: 'inbound (HTTPS)', port: '443', protocol: 'tcp', enabled: true },
+  { label: 'inbound (UDP)', port: '443', protocol: 'udp', enabled: false },
+];
+
+export function FirewallRulesGenerator() {
+  const [allowSsh, setAllowSsh] = useState(true);
+  const [sshPort] = useState('22');
+  const [rows, setRows] = useState<Row[]>(DEFAULT_ROWS);
+
+  function toggle(index: number, enabled: boolean) {
+    setRows((prev) => prev.map((r, i) => (i === index ? { ...r, enabled } : r)));
+  }
+
+  const ports: PortRule[] = rows
+    .filter((r) => r.enabled && Number(r.port) > 0)
+    .map((r) => ({ port: Number(r.port), protocol: r.protocol, label: r.label }));
+
+  const options: FirewallOptions = { ports, allowSsh, sshPort: Number(sshPort) || 22 };
+
+  return (
+    <ToolFrame
+      title="Firewall rules generator"
+      description="Pick the ports to open and copy ready-made ufw and nftables rules."
+    >
+      <div className="flex flex-col gap-2">
+        <CheckboxField
+          label={`Allow SSH (port ${sshPort})`}
+          checked={allowSsh}
+          onChange={setAllowSsh}
+        />
+        {rows.map((row, i) => (
+          <CheckboxField
+            key={`${row.label}-${row.protocol}`}
+            label={`${row.label} — ${row.port}/${row.protocol}`}
+            checked={row.enabled}
+            onChange={(c) => toggle(i, c)}
+          />
+        ))}
+      </div>
+
+      <div className="mt-4 grid grid-cols-1 gap-4">
+        <OutputBlock label="ufw" value={buildUfwCommands(options)} />
+        <OutputBlock label="nftables (/etc/nftables.conf)" value={buildNftablesRuleset(options)} />
+      </div>
+    </ToolFrame>
+  );
+}

+ 87 - 0
docs/components/tools/install-command-builder.tsx

@@ -0,0 +1,87 @@
+'use client';
+
+import { useState } from 'react';
+import {
+  buildScriptCommand,
+  buildDockerRun,
+  buildDockerCompose,
+  type InstallMethod,
+  type InstallOptions,
+} from '@/lib/xray/install';
+import { ToolFrame } from './tool-frame';
+import { TextField, SelectField, CheckboxField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+export function InstallCommandBuilder() {
+  const [method, setMethod] = useState<InstallMethod>('script');
+  const [version, setVersion] = useState('');
+  const [enableFail2ban, setEnableFail2ban] = useState(true);
+  const [panelPort, setPanelPort] = useState('');
+  const [webBasePath, setWebBasePath] = useState('');
+
+  const options: InstallOptions = {
+    method,
+    version,
+    enableFail2ban,
+    panelPort,
+    webBasePath,
+  };
+
+  return (
+    <ToolFrame
+      title="Install command builder"
+      description="Build the exact install command for your setup. It is assembled in your browser."
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+        <SelectField
+          label="Method"
+          value={method}
+          onChange={(v) => setMethod(v as InstallMethod)}
+          options={['script', 'docker']}
+        />
+        <TextField
+          label="Version"
+          value={version}
+          onChange={setVersion}
+          placeholder="latest"
+          hint="blank = latest stable · a tag like v3.4.0 · or dev-latest for the rolling dev build"
+        />
+        {method === 'docker' ? (
+          <>
+            <TextField
+              label="Panel port"
+              value={panelPort}
+              onChange={setPanelPort}
+              placeholder="2053"
+            />
+            <TextField
+              label="Web base path"
+              value={webBasePath}
+              onChange={setWebBasePath}
+              placeholder="/panel"
+            />
+          </>
+        ) : null}
+      </div>
+
+      <div className="mt-3">
+        <CheckboxField
+          label="Enable Fail2ban"
+          checked={enableFail2ban}
+          onChange={setEnableFail2ban}
+        />
+      </div>
+
+      <div className="mt-4 grid grid-cols-1 gap-4">
+        {method === 'script' ? (
+          <OutputBlock label="Run on your server" value={buildScriptCommand(options)} />
+        ) : (
+          <>
+            <OutputBlock label="docker run" value={buildDockerRun(options)} />
+            <OutputBlock label="docker-compose.yml" value={buildDockerCompose(options)} />
+          </>
+        )}
+      </div>
+    </ToolFrame>
+  );
+}

+ 277 - 0
docs/components/tools/outbound-generator.tsx

@@ -0,0 +1,277 @@
+'use client';
+
+import { useState } from 'react';
+import {
+  buildOutboundJson,
+  type OutboundInput,
+  type OutboundKind,
+  type Network,
+  type Security,
+  type ProxyServerInput,
+  type StreamInput,
+  type WireguardInput,
+} from '@/lib/xray/outbounds';
+import { ToolFrame } from './tool-frame';
+import { TextField, SelectField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+const KINDS: readonly OutboundKind[] = [
+  'freedom',
+  'blackhole',
+  'vless',
+  'vmess',
+  'trojan',
+  'shadowsocks',
+  'socks',
+  'http',
+  'wireguard',
+  'warp',
+];
+const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
+const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
+const FINGERPRINTS = ['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'];
+const DOMAIN_STRATEGIES = ['AsIs', 'UseIP', 'UseIPv4', 'UseIPv6', 'ForceIP'];
+
+const PROXY_KINDS = new Set<OutboundKind>([
+  'vless',
+  'vmess',
+  'trojan',
+  'shadowsocks',
+  'socks',
+  'http',
+]);
+const STREAM_KINDS = new Set<OutboundKind>(['vless', 'vmess', 'trojan', 'shadowsocks']);
+const WG_KINDS = new Set<OutboundKind>(['wireguard', 'warp']);
+
+export function OutboundGenerator() {
+  const [kind, setKind] = useState<OutboundKind>('vless');
+  const [tag, setTag] = useState('proxy');
+
+  // proxy server
+  const [address, setAddress] = useState('example.com');
+  const [port, setPort] = useState('443');
+  const [id, setId] = useState('');
+  const [password, setPassword] = useState('');
+  const [method, setMethod] = useState('2022-blake3-aes-128-gcm');
+  const [flow, setFlow] = useState('');
+  const [username, setUsername] = useState('');
+
+  // stream
+  const [network, setNetwork] = useState<Network>('tcp');
+  const [security, setSecurity] = useState<Security>('reality');
+  const [host, setHost] = useState('');
+  const [path, setPath] = useState('/');
+  const [serviceName, setServiceName] = useState('');
+  const [sni, setSni] = useState('www.microsoft.com');
+  const [fingerprint, setFingerprint] = useState('chrome');
+  const [publicKey, setPublicKey] = useState('');
+  const [shortId, setShortId] = useState('');
+
+  // freedom
+  const [domainStrategy, setDomainStrategy] = useState('AsIs');
+
+  // wireguard
+  const [wgSecretKey, setWgSecretKey] = useState('');
+  const [wgAddress, setWgAddress] = useState('172.16.0.2/32');
+  const [wgPublicKey, setWgPublicKey] = useState('');
+  const [wgEndpoint, setWgEndpoint] = useState('');
+
+  const isProxy = PROXY_KINDS.has(kind);
+  const hasStream = STREAM_KINDS.has(kind);
+  const isWg = WG_KINDS.has(kind);
+  const hasPath = network === 'ws' || network === 'httpupgrade' || network === 'xhttp';
+
+  const server: ProxyServerInput = {
+    address,
+    port: Number(port),
+    id,
+    password,
+    method,
+    flow,
+    username,
+  };
+
+  const stream: StreamInput = {
+    network,
+    security,
+    host,
+    path,
+    serviceName,
+    sni,
+    fingerprint,
+    publicKey,
+    shortId,
+  };
+
+  const wireguard: WireguardInput = {
+    secretKey: wgSecretKey,
+    address: wgAddress
+      .split(',')
+      .map((a) => a.trim())
+      .filter(Boolean),
+    publicKey: wgPublicKey,
+    endpoint: wgEndpoint,
+  };
+
+  const input: OutboundInput = {
+    kind,
+    tag,
+    server: isProxy ? server : undefined,
+    wireguard: isWg ? wireguard : undefined,
+    stream: hasStream ? stream : undefined,
+    domainStrategy: kind === 'freedom' ? domainStrategy : undefined,
+  };
+
+  function reset() {
+    setKind('vless');
+    setTag('proxy');
+    setAddress('example.com');
+    setPort('443');
+    setId('');
+    setPassword('');
+    setMethod('2022-blake3-aes-128-gcm');
+    setFlow('');
+    setUsername('');
+    setNetwork('tcp');
+    setSecurity('reality');
+    setHost('');
+    setPath('/');
+    setServiceName('');
+    setSni('www.microsoft.com');
+    setFingerprint('chrome');
+    setPublicKey('');
+    setShortId('');
+    setDomainStrategy('AsIs');
+    setWgSecretKey('');
+    setWgAddress('172.16.0.2/32');
+    setWgPublicKey('');
+    setWgEndpoint('');
+  }
+
+  return (
+    <ToolFrame
+      title="Outbound config generator"
+      description="Build an Xray outbound object — freedom, blackhole, a proxy protocol, WireGuard, or WARP — to paste into your Xray configuration."
+      onReset={reset}
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+        <SelectField
+          label="Kind"
+          value={kind}
+          onChange={(v) => setKind(v as OutboundKind)}
+          options={KINDS}
+        />
+        <TextField label="Tag" value={kind === 'warp' ? 'warp' : tag} onChange={setTag} />
+
+        {kind === 'freedom' ? (
+          <SelectField
+            label="Domain strategy"
+            value={domainStrategy}
+            onChange={setDomainStrategy}
+            options={DOMAIN_STRATEGIES}
+          />
+        ) : null}
+
+        {isProxy ? (
+          <>
+            <TextField label="Address" value={address} onChange={setAddress} />
+            <TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
+            {(kind === 'vless' || kind === 'vmess') && (
+              <TextField label="UUID (id)" value={id} onChange={setId} />
+            )}
+            {kind === 'vless' && (
+              <TextField
+                label="Flow"
+                value={flow}
+                onChange={setFlow}
+                placeholder="xtls-rprx-vision (optional)"
+              />
+            )}
+            {(kind === 'trojan' || kind === 'shadowsocks') && (
+              <TextField label="Password" value={password} onChange={setPassword} />
+            )}
+            {kind === 'shadowsocks' && (
+              <TextField label="Method (cipher)" value={method} onChange={setMethod} />
+            )}
+            {(kind === 'socks' || kind === 'http') && (
+              <>
+                <TextField
+                  label="Username"
+                  value={username}
+                  onChange={setUsername}
+                  placeholder="optional"
+                />
+                <TextField label="Password" value={password} onChange={setPassword} />
+              </>
+            )}
+          </>
+        ) : null}
+
+        {isWg ? (
+          <>
+            <TextField
+              label="Private key (secretKey)"
+              value={wgSecretKey}
+              onChange={setWgSecretKey}
+            />
+            <TextField label="Local address" value={wgAddress} onChange={setWgAddress} />
+            <TextField label="Peer public key" value={wgPublicKey} onChange={setWgPublicKey} />
+            <TextField
+              label="Peer endpoint"
+              value={wgEndpoint}
+              onChange={setWgEndpoint}
+              placeholder={kind === 'warp' ? 'engage.cloudflareclient.com:2408' : 'host:51820'}
+            />
+          </>
+        ) : null}
+      </div>
+
+      {hasStream ? (
+        <div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
+          <SelectField
+            label="Transport"
+            value={network}
+            onChange={(v) => setNetwork(v as Network)}
+            options={NETWORKS}
+          />
+          <SelectField
+            label="Security"
+            value={security}
+            onChange={(v) => setSecurity(v as Security)}
+            options={SECURITIES}
+          />
+          {hasPath ? (
+            <>
+              <TextField label="Path" value={path} onChange={setPath} />
+              <TextField label="Host" value={host} onChange={setHost} placeholder="optional" />
+            </>
+          ) : null}
+          {network === 'grpc' ? (
+            <TextField label="serviceName" value={serviceName} onChange={setServiceName} />
+          ) : null}
+          {security !== 'none' ? (
+            <>
+              <TextField label="SNI (serverName)" value={sni} onChange={setSni} />
+              <SelectField
+                label="Fingerprint"
+                value={fingerprint}
+                onChange={setFingerprint}
+                options={FINGERPRINTS}
+              />
+            </>
+          ) : null}
+          {security === 'reality' ? (
+            <>
+              <TextField label="Public key (pbk)" value={publicKey} onChange={setPublicKey} />
+              <TextField label="Short ID (sid)" value={shortId} onChange={setShortId} />
+            </>
+          ) : null}
+        </div>
+      ) : null}
+
+      <div className="mt-4">
+        <OutputBlock label="Outbound (Xray JSON)" value={buildOutboundJson(input)} />
+      </div>
+    </ToolFrame>
+  );
+}

+ 79 - 0
docs/components/tools/protocol-wizard.tsx

@@ -0,0 +1,79 @@
+'use client';
+
+import { useState } from 'react';
+import Link from 'next/link';
+import { Sparkles } from 'lucide-react';
+import {
+  recommend,
+  type UseCase,
+  type CensorshipLevel,
+  type ClientSupport,
+} from '@/lib/xray/protocols';
+import { ToolFrame } from './tool-frame';
+import { SelectField } from './shared/fields';
+
+const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
+
+export function ProtocolWizard() {
+  const [useCase, setUseCase] = useState<UseCase>('general');
+  const [censorship, setCensorship] = useState<CensorshipLevel>('medium');
+  const [clientSupport, setClientSupport] = useState<ClientSupport>('modern');
+
+  const result = recommend({ useCase, censorship, clientSupport });
+
+  return (
+    <ToolFrame
+      title="Protocol wizard"
+      description="Answer a few questions to get a recommended protocol and transport."
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
+        <SelectField
+          label="Primary goal"
+          value={cap(useCase)}
+          onChange={(v) => setUseCase(v.toLowerCase() as UseCase)}
+          options={['Censorship', 'General', 'Speed']}
+        />
+        <SelectField
+          label="Censorship level"
+          value={cap(censorship)}
+          onChange={(v) => setCensorship(v.toLowerCase() as CensorshipLevel)}
+          options={['High', 'Medium', 'Low']}
+        />
+        <SelectField
+          label="Client support"
+          value={cap(clientSupport)}
+          onChange={(v) => setClientSupport(v.toLowerCase() as ClientSupport)}
+          options={['Modern', 'Broad']}
+        />
+      </div>
+
+      <div className="mt-4 rounded-xl border bg-fd-background p-4">
+        <div className="flex items-center gap-2 text-brand">
+          <Sparkles className="size-4" aria-hidden />
+          <span className="text-sm font-medium">Recommended</span>
+        </div>
+        <div className="mt-2 flex flex-wrap gap-2">
+          <Badge>{result.protocol}</Badge>
+          <Badge>{result.transport}</Badge>
+          <Badge>{result.security}</Badge>
+        </div>
+        <p className="mt-3 text-sm text-fd-muted-foreground">{result.rationale}</p>
+        <div className="mt-3 flex flex-wrap gap-3 text-sm">
+          {result.links.map((link) => (
+            <Link key={link.href} href={link.href} className="text-fd-primary hover:underline">
+              {link.title} →
+            </Link>
+          ))}
+        </div>
+      </div>
+    </ToolFrame>
+  );
+}
+
+function Badge({ children }: { children: React.ReactNode }) {
+  return (
+    <span className="rounded-lg bg-brand/10 px-2.5 py-1 text-sm font-medium text-brand">
+      {children}
+    </span>
+  );
+}

+ 152 - 0
docs/components/tools/reality-config-generator.tsx

@@ -0,0 +1,152 @@
+'use client';
+
+import { useCallback, useEffect, useState } from 'react';
+import { RefreshCw } from 'lucide-react';
+import {
+  generateX25519KeyPair,
+  isX25519Available,
+  randomShortId,
+  randomUuid,
+  realityClientLink,
+  realityServerInbound,
+  type RealityConfig,
+  type X25519KeyPair,
+} from '@/lib/xray/reality';
+import { ToolFrame } from './tool-frame';
+import { TextField, SelectField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+import { CopyButton } from './shared/copy-button';
+
+const FINGERPRINTS = ['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'] as const;
+
+export function RealityConfigGenerator() {
+  const [address, setAddress] = useState('your-server.com');
+  const [port, setPort] = useState('443');
+  const [dest, setDest] = useState('www.microsoft.com:443');
+  const [sni, setSni] = useState('www.microsoft.com');
+  const [fingerprint, setFingerprint] = useState<string>('chrome');
+  const [uuid, setUuid] = useState('');
+  const [shortId, setShortId] = useState('');
+  const [keys, setKeys] = useState<X25519KeyPair | null>(null);
+  const [unavailable, setUnavailable] = useState(false);
+
+  const regenerate = useCallback(async () => {
+    setUuid(randomUuid());
+    setShortId(randomShortId(4));
+    if (!isX25519Available()) {
+      setUnavailable(true);
+      return;
+    }
+    try {
+      setKeys(await generateX25519KeyPair());
+      setUnavailable(false);
+    } catch {
+      setUnavailable(true);
+    }
+  }, []);
+
+  // Generate keys/identifiers on the client after hydration. This is a genuine
+  // client-only side effect (WebCrypto + randomness), not derived render state.
+  useEffect(() => {
+    // eslint-disable-next-line react-hooks/set-state-in-effect
+    void regenerate();
+  }, [regenerate]);
+
+  const config: RealityConfig | null =
+    keys && uuid
+      ? {
+          address,
+          port: Number(port) || 443,
+          uuid,
+          dest,
+          serverNames: [sni],
+          shortIds: [shortId],
+          privateKey: keys.privateKey,
+          publicKey: keys.publicKey,
+          fingerprint,
+          spiderX: '/',
+          flow: 'xtls-rprx-vision',
+        }
+      : null;
+
+  const serverJson = config ? JSON.stringify(realityServerInbound(config), null, 2) : '';
+  const clientLink = config ? realityClientLink(config) : '';
+
+  return (
+    <ToolFrame
+      title="REALITY config generator"
+      description="Generate a VLESS + REALITY inbound and client link. Keys are created in your browser — nothing is sent anywhere."
+      onReset={() => void regenerate()}
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+        <TextField
+          label="Server address"
+          value={address}
+          onChange={setAddress}
+          hint="Your domain or IP"
+        />
+        <TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
+        <TextField
+          label="Dest (camouflage target)"
+          value={dest}
+          onChange={setDest}
+          hint="A real TLS 1.3 site, e.g. www.microsoft.com:443"
+        />
+        <TextField label="SNI / Server name" value={sni} onChange={setSni} />
+        <SelectField
+          label="Fingerprint"
+          value={fingerprint}
+          onChange={setFingerprint}
+          options={FINGERPRINTS}
+        />
+      </div>
+
+      {unavailable ? (
+        <div className="mt-4 rounded-xl border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
+          Your browser can&apos;t generate X25519 keys here. Generate them on the server instead:
+          <div className="mt-2">
+            <OutputBlock label="run on the server" value="xray x25519" />
+          </div>
+        </div>
+      ) : (
+        <>
+          <div className="mt-4 flex flex-wrap items-center gap-2">
+            <span className="text-sm font-medium">Generated keys &amp; identifiers</span>
+            <button
+              type="button"
+              onClick={() => void regenerate()}
+              className="inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
+            >
+              <RefreshCw className="size-3.5" aria-hidden />
+              Regenerate
+            </button>
+          </div>
+
+          <div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
+            <KeyRow label="Public key" value={keys?.publicKey ?? ''} />
+            <KeyRow label="Private key" value={keys?.privateKey ?? ''} />
+            <KeyRow label="UUID" value={uuid} />
+            <KeyRow label="Short ID" value={shortId} />
+          </div>
+
+          <div className="mt-4 grid grid-cols-1 gap-4">
+            <OutputBlock label="Server inbound (Xray JSON)" value={serverJson} />
+            <OutputBlock label="Client share link" value={clientLink} qr />
+          </div>
+        </>
+      )}
+    </ToolFrame>
+  );
+}
+
+function KeyRow({ label, value }: { label: string; value: string }) {
+  return (
+    <div className="flex items-center gap-2 rounded-lg border bg-fd-background px-3 py-2">
+      <span className="shrink-0 text-xs font-medium text-fd-muted-foreground">{label}</span>
+      <code dir="ltr" className="flex-1 truncate text-start text-xs">
+        {value}
+      </code>
+      <CopyButton value={value} label="" className="px-1.5" />
+    </div>
+  );
+}

+ 69 - 0
docs/components/tools/reverse-proxy-generator.tsx

@@ -0,0 +1,69 @@
+'use client';
+
+import { useState } from 'react';
+import {
+  buildProxyConfig,
+  buildCertCommand,
+  type ProxyServer,
+  type CertTool,
+  type ReverseProxyOptions,
+} from '@/lib/xray/reverse-proxy';
+import { ToolFrame } from './tool-frame';
+import { TextField, SelectField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+export function ReverseProxyGenerator() {
+  const [server, setServer] = useState<ProxyServer>('nginx');
+  const [domain, setDomain] = useState('panel.example.com');
+  const [panelPort, setPanelPort] = useState('2053');
+  const [panelPath, setPanelPath] = useState('/panel');
+  const [certTool, setCertTool] = useState<CertTool>('certbot');
+
+  const options: ReverseProxyOptions = { server, domain, panelPort, panelPath, certTool };
+
+  return (
+    <ToolFrame
+      title="Reverse-proxy config generator"
+      description="Generate an Nginx or Caddy reverse-proxy config (with WebSocket support) and a matching certificate command."
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+        <SelectField
+          label="Server"
+          value={server}
+          onChange={(v) => setServer(v as ProxyServer)}
+          options={['nginx', 'caddy']}
+        />
+        <TextField label="Domain" value={domain} onChange={setDomain} />
+        <TextField
+          label="Panel port"
+          value={panelPort}
+          onChange={setPanelPort}
+          inputMode="numeric"
+        />
+        <TextField label="Panel web base path" value={panelPath} onChange={setPanelPath} />
+        {server === 'nginx' ? (
+          <SelectField
+            label="Certificate tool"
+            value={certTool}
+            onChange={(v) => setCertTool(v as CertTool)}
+            options={['certbot', 'acme.sh']}
+          />
+        ) : null}
+      </div>
+
+      <div className="mt-4 grid grid-cols-1 gap-4">
+        <OutputBlock
+          label={server === 'nginx' ? 'nginx server block' : 'Caddyfile'}
+          value={buildProxyConfig(options)}
+        />
+        {server === 'nginx' ? (
+          <OutputBlock label="Obtain a certificate" value={buildCertCommand(options)} />
+        ) : (
+          <p className="text-sm text-fd-muted-foreground">
+            Caddy obtains and renews TLS certificates automatically — no extra command needed.
+          </p>
+        )}
+      </div>
+    </ToolFrame>
+  );
+}

+ 203 - 0
docs/components/tools/routing-builder.tsx

@@ -0,0 +1,203 @@
+'use client';
+
+import { useState } from 'react';
+import {
+  buildRoutingJson,
+  type DomainStrategy,
+  type RoutingInput,
+  type RuleNetwork,
+  type Strategy,
+} from '@/lib/xray/routing';
+import { ToolFrame } from './tool-frame';
+import { TextField, SelectField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+interface BalancerRow {
+  tag: string;
+  selector: string;
+  strategy: Strategy;
+  fallbackTag: string;
+}
+
+interface RuleRow {
+  domain: string;
+  ip: string;
+  port: string;
+  network: string;
+  inboundTag: string;
+  targetKind: 'outbound' | 'balancer';
+  targetTag: string;
+}
+
+const STRATEGIES: readonly Strategy[] = ['random', 'roundRobin', 'leastPing', 'leastLoad'];
+const NETWORKS = ['any', 'tcp', 'udp', 'tcp,udp'];
+const TARGET_KINDS = ['outbound', 'balancer'];
+const DOMAIN_STRATEGIES: readonly DomainStrategy[] = ['AsIs', 'IPIfNonMatch', 'IPOnDemand'];
+
+const DEFAULT_BALANCERS: BalancerRow[] = [
+  { tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' },
+];
+const DEFAULT_RULES: RuleRow[] = [
+  { domain: 'geosite:category-ads-all', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'block' },
+  { domain: '', ip: 'geoip:private', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'direct' },
+];
+
+function list(s: string): string[] {
+  return s
+    .split(',')
+    .map((x) => x.trim())
+    .filter(Boolean);
+}
+
+const addBtn =
+  'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
+
+export function RoutingBuilder() {
+  const [domainStrategy, setDomainStrategy] = useState<DomainStrategy>('IPIfNonMatch');
+  const [balancers, setBalancers] = useState<BalancerRow[]>(DEFAULT_BALANCERS);
+  const [rules, setRules] = useState<RuleRow[]>(DEFAULT_RULES);
+
+  function patchBalancer(i: number, patch: Partial<BalancerRow>) {
+    setBalancers((prev) => prev.map((b, j) => (i === j ? { ...b, ...patch } : b)));
+  }
+  function patchRule(i: number, patch: Partial<RuleRow>) {
+    setRules((prev) => prev.map((r, j) => (i === j ? { ...r, ...patch } : r)));
+  }
+
+  const input: RoutingInput = {
+    domainStrategy,
+    balancers: balancers
+      .filter((b) => b.tag.trim())
+      .map((b) => ({
+        tag: b.tag.trim(),
+        selector: list(b.selector),
+        strategy: b.strategy,
+        fallbackTag: b.fallbackTag.trim() || undefined,
+      })),
+    rules: rules
+      .filter((r) => r.targetTag.trim())
+      .map((r) => ({
+        domain: list(r.domain),
+        ip: list(r.ip),
+        port: r.port.trim() || undefined,
+        network: r.network === 'any' ? undefined : (r.network as RuleNetwork),
+        inboundTag: list(r.inboundTag),
+        target: { kind: r.targetKind, tag: r.targetTag.trim() },
+      })),
+  };
+
+  function reset() {
+    setDomainStrategy('IPIfNonMatch');
+    setBalancers(DEFAULT_BALANCERS);
+    setRules(DEFAULT_RULES);
+  }
+
+  return (
+    <ToolFrame
+      title="Balancer & routing builder"
+      description="Compose Xray balancers and routing rules, then copy the routing block (with a matching observatory for leastPing/leastLoad)."
+      onReset={reset}
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+        <SelectField
+          label="Domain strategy"
+          value={domainStrategy}
+          onChange={(v) => setDomainStrategy(v as DomainStrategy)}
+          options={DOMAIN_STRATEGIES}
+        />
+      </div>
+
+      <div className="mt-5 flex items-center justify-between">
+        <h4 className="text-sm font-semibold">Balancers</h4>
+        <button
+          type="button"
+          className={addBtn}
+          onClick={() =>
+            setBalancers((p) => [...p, { tag: '', selector: '', strategy: 'random', fallbackTag: '' }])
+          }
+        >
+          Add balancer
+        </button>
+      </div>
+      <div className="mt-2 flex flex-col gap-3">
+        {balancers.map((b, i) => (
+          <div key={i} className="rounded-xl border p-3">
+            <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
+              <TextField label="Tag" value={b.tag} onChange={(v) => patchBalancer(i, { tag: v })} />
+              <TextField
+                label="Selector (comma-separated prefixes)"
+                value={b.selector}
+                onChange={(v) => patchBalancer(i, { selector: v })}
+              />
+              <SelectField
+                label="Strategy"
+                value={b.strategy}
+                onChange={(v) => patchBalancer(i, { strategy: v as Strategy })}
+                options={STRATEGIES}
+              />
+              <TextField
+                label="Fallback tag"
+                value={b.fallbackTag}
+                onChange={(v) => patchBalancer(i, { fallbackTag: v })}
+                placeholder="optional"
+              />
+            </div>
+            <div className="mt-2 flex justify-end">
+              <button
+                type="button"
+                className={addBtn}
+                onClick={() => setBalancers((p) => p.filter((_, j) => j !== i))}
+              >
+                Remove
+              </button>
+            </div>
+          </div>
+        ))}
+      </div>
+
+      <div className="mt-5 flex items-center justify-between">
+        <h4 className="text-sm font-semibold">Rules</h4>
+        <button
+          type="button"
+          className={addBtn}
+          onClick={() =>
+            setRules((p) => [
+              ...p,
+              { domain: '', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: '' },
+            ])
+          }
+        >
+          Add rule
+        </button>
+      </div>
+      <div className="mt-2 flex flex-col gap-3">
+        {rules.map((r, i) => (
+          <div key={i} className="rounded-xl border p-3">
+            <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
+              <TextField label="Domain (comma)" value={r.domain} onChange={(v) => patchRule(i, { domain: v })} placeholder="geosite:google, example.com" />
+              <TextField label="IP (comma)" value={r.ip} onChange={(v) => patchRule(i, { ip: v })} placeholder="geoip:cn, 1.1.1.1" />
+              <TextField label="Port" value={r.port} onChange={(v) => patchRule(i, { port: v })} placeholder="443 or 1000-2000" />
+              <SelectField label="Network" value={r.network} onChange={(v) => patchRule(i, { network: v })} options={NETWORKS} />
+              <TextField label="Inbound tag (comma)" value={r.inboundTag} onChange={(v) => patchRule(i, { inboundTag: v })} placeholder="optional" />
+              <SelectField label="Target kind" value={r.targetKind} onChange={(v) => patchRule(i, { targetKind: v as 'outbound' | 'balancer' })} options={TARGET_KINDS} />
+              <TextField label="Target tag" value={r.targetTag} onChange={(v) => patchRule(i, { targetTag: v })} />
+            </div>
+            <div className="mt-2 flex justify-end">
+              <button
+                type="button"
+                className={addBtn}
+                onClick={() => setRules((p) => p.filter((_, j) => j !== i))}
+              >
+                Remove
+              </button>
+            </div>
+          </div>
+        ))}
+      </div>
+
+      <div className="mt-4">
+        <OutputBlock label="Routing block (Xray JSON)" value={buildRoutingJson(input)} />
+      </div>
+    </ToolFrame>
+  );
+}

+ 82 - 0
docs/components/tools/share-link-inspector.tsx

@@ -0,0 +1,82 @@
+'use client';
+
+import { useMemo, useState } from 'react';
+import { AlertCircle } from 'lucide-react';
+import { parseLink, type ParsedLink } from '@/lib/xray/links';
+import { ToolFrame } from './tool-frame';
+
+type Result = { ok: true; data: ParsedLink } | { ok: false; error: string } | null;
+
+export function ShareLinkInspector() {
+  const [input, setInput] = useState('');
+
+  const result: Result = useMemo(() => {
+    const value = input.trim();
+    if (!value) return null;
+    try {
+      return { ok: true, data: parseLink(value) };
+    } catch (e) {
+      return { ok: false, error: (e as Error).message };
+    }
+  }, [input]);
+
+  return (
+    <ToolFrame
+      title="Share-link inspector"
+      description="Paste a vless / vmess / trojan / ss link to decode every parameter. It is parsed entirely in your browser — nothing is sent over the network."
+      onReset={input ? () => setInput('') : undefined}
+    >
+      <textarea
+        value={input}
+        onChange={(e) => setInput(e.target.value)}
+        placeholder="vless://uuid@host:443?security=reality&pbk=...#name"
+        dir="ltr"
+        rows={3}
+        spellCheck={false}
+        className="w-full resize-y rounded-lg border bg-fd-background px-3 py-2 font-mono text-sm outline-none transition-colors focus-visible:border-fd-primary focus-visible:ring-2 focus-visible:ring-fd-ring/30"
+      />
+
+      {result && !result.ok ? (
+        <div className="mt-3 flex items-center gap-2 rounded-lg border border-red-500/40 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400">
+          <AlertCircle className="size-4 shrink-0" aria-hidden />
+          <span>{result.error}</span>
+        </div>
+      ) : null}
+
+      {result && result.ok ? <ResultTable data={result.data} /> : null}
+    </ToolFrame>
+  );
+}
+
+function ResultTable({ data }: { data: ParsedLink }) {
+  const rows: [string, string][] = [
+    ['Protocol', data.protocol],
+    ['Name', data.name],
+    ['Address', data.address],
+    ['Port', String(data.port)],
+    [data.protocol === 'trojan' ? 'Password' : 'ID / credential', data.credential],
+    ...Object.entries(data.params),
+  ];
+
+  return (
+    <div className="mt-3 overflow-hidden rounded-xl border">
+      <table className="w-full text-sm">
+        <tbody>
+          {rows.map(([key, value], i) => (
+            <tr key={`${key}-${i}`} className="border-b last:border-b-0">
+              <th
+                scope="row"
+                className="w-1/3 bg-fd-muted/40 px-3 py-2 text-start align-top font-medium text-fd-muted-foreground"
+              >
+                {key}
+              </th>
+              <td dir="ltr" className="break-all px-3 py-2 text-start font-mono">
+                {value || <span className="text-fd-muted-foreground">—</span>}
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+}

+ 46 - 0
docs/components/tools/shared/copy-button.tsx

@@ -0,0 +1,46 @@
+'use client';
+
+import { useState } from 'react';
+import { Check, Copy } from 'lucide-react';
+import { cn } from '@/lib/cn';
+
+export function CopyButton({
+  value,
+  label = 'Copy',
+  className,
+}: {
+  value: string;
+  label?: string;
+  className?: string;
+}) {
+  const [copied, setCopied] = useState(false);
+
+  async function copy() {
+    try {
+      await navigator.clipboard.writeText(value);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    } catch {
+      // Clipboard unavailable (insecure context) — ignore.
+    }
+  }
+
+  return (
+    <button
+      type="button"
+      onClick={copy}
+      aria-label={copied ? 'Copied' : label}
+      className={cn(
+        'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground focus-visible:outline-2 focus-visible:outline-fd-ring',
+        className,
+      )}
+    >
+      {copied ? (
+        <Check className="size-3.5 text-brand" aria-hidden />
+      ) : (
+        <Copy className="size-3.5" aria-hidden />
+      )}
+      <span>{copied ? 'Copied' : label}</span>
+    </button>
+  );
+}

+ 99 - 0
docs/components/tools/shared/fields.tsx

@@ -0,0 +1,99 @@
+'use client';
+
+import { useId } from 'react';
+
+const inputClass =
+  'rounded-lg border bg-fd-background px-3 py-2 text-sm outline-none transition-colors focus-visible:border-fd-primary focus-visible:ring-2 focus-visible:ring-fd-ring/30';
+
+export function TextField({
+  label,
+  value,
+  onChange,
+  placeholder,
+  hint,
+  inputMode,
+}: {
+  label: string;
+  value: string;
+  onChange: (value: string) => void;
+  placeholder?: string;
+  hint?: string;
+  inputMode?: 'numeric' | 'text';
+}) {
+  const id = useId();
+  return (
+    <div className="flex flex-col gap-1.5">
+      <label htmlFor={id} className="text-sm font-medium">
+        {label}
+      </label>
+      {/* Values are technical (hosts, links) and stay LTR even on RTL pages. */}
+      <input
+        id={id}
+        dir="ltr"
+        inputMode={inputMode}
+        value={value}
+        placeholder={placeholder}
+        onChange={(e) => onChange(e.target.value)}
+        className={inputClass}
+      />
+      {hint ? <span className="text-xs text-fd-muted-foreground">{hint}</span> : null}
+    </div>
+  );
+}
+
+export function CheckboxField({
+  label,
+  checked,
+  onChange,
+}: {
+  label: string;
+  checked: boolean;
+  onChange: (checked: boolean) => void;
+}) {
+  const id = useId();
+  return (
+    <label htmlFor={id} className="inline-flex cursor-pointer items-center gap-2 text-sm">
+      <input
+        id={id}
+        type="checkbox"
+        checked={checked}
+        onChange={(e) => onChange(e.target.checked)}
+        className="size-4 accent-fd-primary"
+      />
+      {label}
+    </label>
+  );
+}
+
+export function SelectField({
+  label,
+  value,
+  onChange,
+  options,
+}: {
+  label: string;
+  value: string;
+  onChange: (value: string) => void;
+  options: readonly string[];
+}) {
+  const id = useId();
+  return (
+    <div className="flex flex-col gap-1.5">
+      <label htmlFor={id} className="text-sm font-medium">
+        {label}
+      </label>
+      <select
+        id={id}
+        value={value}
+        onChange={(e) => onChange(e.target.value)}
+        className={inputClass}
+      >
+        {options.map((opt) => (
+          <option key={opt} value={opt}>
+            {opt}
+          </option>
+        ))}
+      </select>
+    </div>
+  );
+}

+ 31 - 0
docs/components/tools/shared/output-block.tsx

@@ -0,0 +1,31 @@
+'use client';
+
+import QRCode from 'react-qr-code';
+import { CopyButton } from './copy-button';
+
+export function OutputBlock({
+  label,
+  value,
+  qr = false,
+}: {
+  label: string;
+  value: string;
+  qr?: boolean;
+}) {
+  return (
+    <div className="overflow-hidden rounded-xl border">
+      <div className="flex items-center justify-between gap-2 border-b bg-fd-muted/40 px-3 py-2">
+        <span className="text-xs font-medium text-fd-muted-foreground">{label}</span>
+        <CopyButton value={value} />
+      </div>
+      <pre dir="ltr" className="max-h-80 overflow-auto p-3 text-start text-xs leading-relaxed">
+        <code>{value}</code>
+      </pre>
+      {qr && value ? (
+        <div className="flex justify-center border-t bg-white p-4">
+          <QRCode value={value} size={180} />
+        </div>
+      ) : null}
+    </div>
+  );
+}

+ 209 - 0
docs/components/tools/subscription-builder.tsx

@@ -0,0 +1,209 @@
+'use client';
+
+import { useState } from 'react';
+import {
+  buildSubscriptionUrls,
+  buildShareLinks,
+  buildBase64Subscription,
+  buildJsonSubscription,
+  type SubClient,
+  type SubUrlInput,
+} from '@/lib/xray/subscription';
+import type { Network, Security } from '@/lib/xray/outbounds';
+import { ToolFrame } from './tool-frame';
+import { TextField, SelectField, CheckboxField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+type ClientProtocol = 'vless' | 'vmess' | 'trojan' | 'ss';
+
+interface ClientRow {
+  protocol: ClientProtocol;
+  remark: string;
+  address: string;
+  port: string;
+  credential: string; // id (vless/vmess) or password (trojan/ss)
+  method: string; // ss
+  network: Network;
+  security: Security;
+  sni: string;
+}
+
+const PROTOCOLS: readonly ClientProtocol[] = ['vless', 'vmess', 'trojan', 'ss'];
+const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
+const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
+
+const addBtn =
+  'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
+
+const DEFAULT_CLIENTS: ClientRow[] = [
+  {
+    protocol: 'vless',
+    remark: 'HK-01',
+    address: 'a.example.com',
+    port: '443',
+    credential: '11111111-2222-3333-4444-555555555555',
+    method: '',
+    network: 'tcp',
+    security: 'reality',
+    sni: 'www.microsoft.com',
+  },
+];
+
+function toClient(r: ClientRow): SubClient {
+  const isUuid = r.protocol === 'vless' || r.protocol === 'vmess';
+  return {
+    protocol: r.protocol,
+    remark: r.remark,
+    address: r.address,
+    port: Number(r.port),
+    id: isUuid ? r.credential : undefined,
+    password: isUuid ? undefined : r.credential,
+    method: r.protocol === 'ss' ? r.method : undefined,
+    network: r.network,
+    security: r.security,
+    sni: r.sni || undefined,
+  };
+}
+
+export function SubscriptionBuilder() {
+  const [scheme, setScheme] = useState<'http' | 'https'>('https');
+  const [host, setHost] = useState('sub.example.com');
+  const [port, setPort] = useState('2096');
+  const [subPath, setSubPath] = useState('/sub/');
+  const [jsonPath, setJsonPath] = useState('/json/');
+  const [subId, setSubId] = useState('user-1');
+  const [behindProxy, setBehindProxy] = useState(false);
+  const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
+
+  function patch(i: number, p: Partial<ClientRow>) {
+    setClients((prev) => prev.map((c, j) => (i === j ? { ...c, ...p } : c)));
+  }
+
+  const urlInput: SubUrlInput = { scheme, host, port: Number(port), subPath, jsonPath, subId, behindProxy };
+  const urls = buildSubscriptionUrls(urlInput);
+  const subClients = clients.filter((c) => c.address.trim()).map(toClient);
+
+  function reset() {
+    setScheme('https');
+    setHost('sub.example.com');
+    setPort('2096');
+    setSubPath('/sub/');
+    setJsonPath('/json/');
+    setSubId('user-1');
+    setBehindProxy(false);
+    setClients(DEFAULT_CLIENTS);
+  }
+
+  return (
+    <ToolFrame
+      title="Subscription & sub-JSON builder"
+      description="Build the subscription URLs and preview both body formats — the Base64 link list and the JSON (Xray-json) config."
+      onReset={reset}
+    >
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+        <SelectField
+          label="Scheme"
+          value={scheme}
+          onChange={(v) => setScheme(v as 'http' | 'https')}
+          options={['https', 'http']}
+        />
+        <TextField label="Host" value={host} onChange={setHost} />
+        <TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
+        <TextField label="Sub ID" value={subId} onChange={setSubId} />
+        <TextField label="Sub path" value={subPath} onChange={setSubPath} />
+        <TextField label="JSON path" value={jsonPath} onChange={setJsonPath} />
+        <CheckboxField
+          label="Behind a reverse proxy (omit the port)"
+          checked={behindProxy}
+          onChange={setBehindProxy}
+        />
+      </div>
+
+      <div className="mt-4 grid grid-cols-1 gap-4">
+        <OutputBlock label="Base64 subscription URL" value={urls.base64} qr />
+        <OutputBlock label="JSON subscription URL" value={urls.json} />
+      </div>
+
+      <div className="mt-5 flex items-center justify-between">
+        <h4 className="text-sm font-semibold">Clients in this subscription</h4>
+        <button
+          type="button"
+          className={addBtn}
+          onClick={() =>
+            setClients((p) => [
+              ...p,
+              {
+                protocol: 'vless',
+                remark: '',
+                address: '',
+                port: '443',
+                credential: '',
+                method: '',
+                network: 'tcp',
+                security: 'reality',
+                sni: '',
+              },
+            ])
+          }
+        >
+          Add client
+        </button>
+      </div>
+      <div className="mt-2 flex flex-col gap-3">
+        {clients.map((c, i) => (
+          <div key={i} className="rounded-xl border p-3">
+            <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
+              <SelectField
+                label="Protocol"
+                value={c.protocol}
+                onChange={(v) => patch(i, { protocol: v as ClientProtocol })}
+                options={PROTOCOLS}
+              />
+              <TextField label="Remark" value={c.remark} onChange={(v) => patch(i, { remark: v })} />
+              <TextField label="Address" value={c.address} onChange={(v) => patch(i, { address: v })} />
+              <TextField label="Port" value={c.port} onChange={(v) => patch(i, { port: v })} inputMode="numeric" />
+              <TextField
+                label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
+                value={c.credential}
+                onChange={(v) => patch(i, { credential: v })}
+              />
+              {c.protocol === 'ss' ? (
+                <TextField label="Method" value={c.method} onChange={(v) => patch(i, { method: v })} />
+              ) : null}
+              <SelectField
+                label="Transport"
+                value={c.network}
+                onChange={(v) => patch(i, { network: v as Network })}
+                options={NETWORKS}
+              />
+              <SelectField
+                label="Security"
+                value={c.security}
+                onChange={(v) => patch(i, { security: v as Security })}
+                options={SECURITIES}
+              />
+              {c.security !== 'none' ? (
+                <TextField label="SNI" value={c.sni} onChange={(v) => patch(i, { sni: v })} />
+              ) : null}
+            </div>
+            <div className="mt-2 flex justify-end">
+              <button
+                type="button"
+                className={addBtn}
+                onClick={() => setClients((p) => p.filter((_, j) => j !== i))}
+              >
+                Remove
+              </button>
+            </div>
+          </div>
+        ))}
+      </div>
+
+      <div className="mt-4 grid grid-cols-1 gap-4">
+        <OutputBlock label="Subscription links (decoded body)" value={buildShareLinks(subClients).join('\n')} />
+        <OutputBlock label="Base64 body" value={buildBase64Subscription(subClients)} />
+        <OutputBlock label="JSON subscription (preview)" value={buildJsonSubscription(subClients)} />
+      </div>
+    </ToolFrame>
+  );
+}

+ 114 - 0
docs/components/tools/telegram-setup-helper.tsx

@@ -0,0 +1,114 @@
+'use client';
+
+import type { ReactNode } from 'react';
+import { useState } from 'react';
+import {
+  validateBotToken,
+  parseAdminIds,
+  validateRunTime,
+  telegramApiBase,
+  buildBotConfigSummary,
+} from '@/lib/xray/telegram';
+import { ToolFrame } from './tool-frame';
+import { TextField } from './shared/fields';
+import { OutputBlock } from './shared/output-block';
+
+function Status({ ok, children }: { ok: boolean; children: ReactNode }) {
+  return (
+    <p
+      className={`text-xs ${ok ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'}`}
+    >
+      {children}
+    </p>
+  );
+}
+
+export function TelegramSetupHelper() {
+  const [token, setToken] = useState('');
+  const [adminIds, setAdminIds] = useState('');
+  const [runTime, setRunTime] = useState('@daily');
+
+  const tokenV = validateBotToken(token);
+  const idsV = parseAdminIds(adminIds);
+  const cronV = validateRunTime(runTime);
+  const summary = buildBotConfigSummary({ token, adminIds, runTime });
+
+  const settingsText = [
+    `tgBotEnable = true`,
+    `tgBotToken  = ${summary.tgBotToken || '<token>'}`,
+    `tgBotChatId = ${summary.tgBotChatId || '<admin ids>'}`,
+    `tgRunTime   = ${summary.tgRunTime || '@daily'}`,
+  ].join('\n');
+
+  function reset() {
+    setToken('');
+    setAdminIds('');
+    setRunTime('@daily');
+  }
+
+  return (
+    <ToolFrame
+      title="Telegram bot setup helper"
+      description="Validate your bot token, admin IDs, and report schedule, then copy the panel settings."
+      onReset={reset}
+    >
+      <div className="grid grid-cols-1 gap-4">
+        <div>
+          <TextField
+            label="Bot token (from @BotFather)"
+            value={token}
+            onChange={setToken}
+            placeholder="123456789:AA..."
+          />
+          {token ? (
+            tokenV.valid ? (
+              <Status ok>Valid — bot id {tokenV.botId}</Status>
+            ) : (
+              <Status ok={false}>{tokenV.error}</Status>
+            )
+          ) : null}
+        </div>
+        <div>
+          <TextField
+            label="Admin chat IDs (comma-separated)"
+            value={adminIds}
+            onChange={setAdminIds}
+            placeholder="111111111, 222222222"
+          />
+          {adminIds ? (
+            idsV.invalid.length > 0 ? (
+              <Status ok={false}>Not numeric: {idsV.invalid.join(', ')}</Status>
+            ) : (
+              <Status ok>
+                {idsV.ids.length} admin id{idsV.ids.length === 1 ? '' : 's'}
+              </Status>
+            )
+          ) : null}
+        </div>
+        <div>
+          <TextField
+            label="Report schedule (tgRunTime)"
+            value={runTime}
+            onChange={setRunTime}
+            placeholder="@daily, @every 8h, or a cron expression"
+          />
+          {runTime ? (
+            cronV.valid ? (
+              <Status ok>Valid ({cronV.kind})</Status>
+            ) : (
+              <Status ok={false}>{cronV.error}</Status>
+            )
+          ) : null}
+        </div>
+      </div>
+
+      <div className="mt-4 grid grid-cols-1 gap-4">
+        <OutputBlock label="Panel settings" value={settingsText} />
+        <OutputBlock
+          label="Bot API base (keep secret)"
+          value={tokenV.valid ? telegramApiBase(token) : 'https://api.telegram.org/bot<token>'}
+        />
+      </div>
+    </ToolFrame>
+  );
+}

+ 44 - 0
docs/components/tools/tool-frame.tsx

@@ -0,0 +1,44 @@
+'use client';
+
+import { RotateCcw } from 'lucide-react';
+import type { ReactNode } from 'react';
+
+export function ToolFrame({
+  title,
+  description,
+  onReset,
+  children,
+}: {
+  title: string;
+  description?: string;
+  onReset?: () => void;
+  children: ReactNode;
+}) {
+  return (
+    <section
+      role="group"
+      aria-label={title}
+      className="not-prose my-6 overflow-hidden rounded-2xl border bg-fd-card text-fd-foreground"
+    >
+      <header className="flex items-start justify-between gap-3 border-b px-4 py-3">
+        <div>
+          <h3 className="font-semibold">{title}</h3>
+          {description ? (
+            <p className="mt-0.5 text-sm text-fd-muted-foreground">{description}</p>
+          ) : null}
+        </div>
+        {onReset ? (
+          <button
+            type="button"
+            onClick={onReset}
+            className="inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
+          >
+            <RotateCcw className="size-3.5" aria-hidden />
+            Reset
+          </button>
+        ) : null}
+      </header>
+      <div className="p-4">{children}</div>
+    </section>
+  );
+}

+ 67 - 0
docs/content/docs/en/config/clients.mdx

@@ -0,0 +1,67 @@
+---
+title: Clients
+description: Manage 3x-ui clients — credentials, traffic and expiry limits, IP limits, groups, bulk actions, external links, and online status.
+icon: Users
+---
+
+A **client** is a single user, identified by a unique **email**. In the current
+panel, clients are first-class records that can be attached to **multiple
+inbounds** at once, with per-client traffic accounting.
+
+## Client fields
+
+| Field          | Applies to            | Meaning                                                            |
+| -------------- | --------------------- | ------------------------------------------------------------------ |
+| **Email**      | all                   | Unique identifier used for accounting and lookups.                 |
+| **ID (UUID)**  | VLESS, VMess          | The client credential.                                             |
+| **Password**   | Trojan, Shadowsocks   | The client credential.                                             |
+| **Auth**       | Hysteria2             | The client credential.                                             |
+| **Flow**       | VLESS                 | XTLS flow, e.g. `xtls-rprx-vision`.                                |
+| **Limit IP**   | all                   | Max simultaneous source IPs (enforced via Fail2ban).               |
+| **Total (GB)** | all                   | Traffic quota; the client is disabled when exhausted.              |
+| **Expiry**     | all                   | Date after which the client stops working.                        |
+| **Reset**      | all                   | Auto-renew period in **days** (rolls the quota over).              |
+| **Telegram ID**| all                   | Links the client to a Telegram user for self-service/notifications.|
+| **Sub ID**     | all                   | Subscription identifier grouping this client's links.             |
+| **Group**      | all                   | Optional client group for organization and bulk filtering.        |
+| **Comment**    | all                   | Free-text note.                                                   |
+
+<Callout type="info">
+  Reaching the **traffic** or **expiry** limit disables the client; the panel can
+  restart Xray automatically when clients are auto-disabled
+  (`restartXrayOnClientDisable`, on by default).
+</Callout>
+
+## Limits and IP control
+
+- **Traffic / expiry** caps disable the client when hit; a **Reset** period
+  auto-renews the quota.
+- **Limit IP** caps simultaneous source IPs. Enforcement relies on Fail2ban —
+  see [Security](/docs/operations/security). You can view a client's recent IPs
+  and clear them from the client's actions.
+- **Online status** and **last-online** times are tracked per client (and per
+  node in multi-node setups).
+
+## Share links and external links
+
+Every client has share links and a QR code for its inbounds, plus a combined
+[subscription](/docs/config/subscription). You can also attach **external
+links** to a client — extra `vless://`, `vmess://`, `trojan://`, `ss://`,
+`hysteria2://`, or `wireguard://` links, or a remote subscription URL — so they
+appear alongside the panel-generated ones in the client's subscription.
+
+To inspect exactly what a link contains, paste it into the
+[share-link inspector](/docs/config/share-links).
+
+## Bulk actions
+
+For managing many clients at once, the panel supports bulk **create, enable,
+disable, delete, attach/detach** (to inbounds), **reset traffic**, and
+**adjust** (add days / add bytes / set flow). Maintenance actions also let you
+delete **depleted** clients (quota/expiry exhausted) and **orphaned** clients
+(not attached to any inbound).
+
+<Callout type="warn">
+  A client's share link contains its credential. Treat links and QR codes like
+  passwords, and rotate the credential if one leaks.
+</Callout>

+ 97 - 0
docs/content/docs/en/config/inbounds.mdx

@@ -0,0 +1,97 @@
+---
+title: Inbounds & Protocols
+description: Create inbounds in 3x-ui — protocols, transports, traffic reset and expiry, and fallbacks that serve multiple protocols on one port.
+icon: ArrowDownToLine
+---
+
+An **inbound** is a listener that accepts client connections on a port using a
+particular protocol and transport. Most of your day-to-day work is creating and
+managing inbounds and the clients inside them.
+
+## Create an inbound
+
+<Steps>
+
+<Step>
+### Add an inbound
+
+Open **Inbounds → Add**, give it a remark, pick a **protocol**, and choose a
+**port** and listen address.
+</Step>
+
+<Step>
+### Choose a transport and security
+
+Pick the transport (TCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, …) and the security
+layer (none, TLS, or REALITY). See [Transports](/docs/config/transports) and
+[REALITY](/docs/config/reality).
+</Step>
+
+<Step>
+### Add clients
+
+Add one or more clients, each with its own credential, limits, and share link.
+See [Clients](/docs/config/clients).
+</Step>
+
+<Step>
+### Set traffic limit, expiry, and reset
+
+Optionally cap total traffic and set an expiry date for the inbound, and choose a
+periodic **traffic reset** schedule: `never` (default), `hourly`, `daily`,
+`weekly`, or `monthly`.
+</Step>
+
+</Steps>
+
+## Supported protocols
+
+The inbound editor accepts these protocols:
+
+| Protocol               | Notes                                                                    |
+| ---------------------- | ------------------------------------------------------------------------ |
+| **VLESS**              | Lightweight; the basis for REALITY + XTLS-Vision. Recommended.           |
+| **VMess**              | Older but very widely supported by clients.                              |
+| **Trojan**             | TLS-based; supports XTLS and fallbacks.                                   |
+| **Shadowsocks**        | Includes Shadowsocks-2022 (`2022-blake3-*`) ciphers.                      |
+| **WireGuard**          | Modern tunnel.                                                           |
+| **Hysteria2**          | Selected as `hysteria`; the panel emits `hysteria2://` links.             |
+| **HTTP**               | HTTP proxy.                                                             |
+| **Mixed (SOCKS/HTTP)** | A combined SOCKS + HTTP listener.                                        |
+| **Dokodemo-door / Tunnel** | Port forwarding / traffic redirect.                                 |
+| **MTProto**            | Telegram MTProto proxy, served by a bundled `mtg` process (not Xray).    |
+
+<Callout type="info">
+  Hysteria2 isn't a separate protocol internally — it's the `hysteria` protocol
+  with the transport version set to 2, and the panel generates `hysteria2://`
+  share links for it.
+</Callout>
+
+## Fallbacks — multiple protocols on one port
+
+Fallbacks let a single TLS port (e.g. `443`) serve more than one protocol — for
+example VLESS **and** Trojan — by routing unmatched handshakes to a child
+inbound. In 3x-ui, fallbacks are managed in the panel (a master inbound's
+**Fallbacks** list) rather than hand-written into JSON.
+
+Fallbacks are available only when the master inbound is:
+
+- **VLESS** or **Trojan**,
+- on the raw **TCP** transport,
+- with **TLS** or **REALITY** security.
+
+Each fallback rule targets a child inbound and can match on `path`, `alpn`, and
+`dest`. Client share links for a fallback child are automatically rewritten to
+advertise the master's address, port, and TLS.
+
+## Not sure which to pick?
+
+Use the wizard to get a recommendation based on your goals and clients:
+
+<ProtocolWizard />
+
+<Callout type="info">
+  For censorship resistance with modern clients, **VLESS + REALITY +
+  XTLS-Vision** is the usual best choice — continue to
+  [REALITY](/docs/config/reality).
+</Callout>

+ 14 - 0
docs/content/docs/en/config/meta.json

@@ -0,0 +1,14 @@
+{
+  "title": "Configuration",
+  "icon": "Settings",
+  "pages": [
+    "panel",
+    "ssl-certificates",
+    "inbounds",
+    "reality",
+    "transports",
+    "clients",
+    "subscription",
+    "share-links"
+  ]
+}

+ 77 - 0
docs/content/docs/en/config/panel.mdx

@@ -0,0 +1,77 @@
+---
+title: Panel Settings
+description: Every 3x-ui panel setting — web server, TLS, display, security, and notifications — with defaults from the source.
+icon: SlidersHorizontal
+---
+
+**Panel Settings** controls how the panel itself is served and secured (separate
+from your inbounds and clients). Settings are stored as key/value pairs; the
+defaults below come straight from the panel source. Secrets (tokens, passwords)
+are shown only as a "set / not set" indicator and are never returned to the
+browser in full.
+
+## Web server
+
+| Setting             | Default                 | Meaning                                                                 |
+| ------------------- | ----------------------- | ----------------------------------------------------------------------- |
+| `webPort`           | `2053`                  | Panel port (1–65535). The `XUI_PORT` env var overrides it at runtime.   |
+| `webListen`         | _(all interfaces)_      | Bind the panel to a specific IP.                                        |
+| `webBasePath`       | `/`                     | URL path the panel is served under (always normalized to `/…/`).        |
+| `webCertFile` / `webKeyFile` | _(none)_       | TLS certificate + key. When both are set, the panel serves **HTTPS**.   |
+| `sessionMaxAge`     | `360`                   | Session lifetime in **minutes** (default 6 hours).                      |
+| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IPs/CIDRs whose forwarded headers (real client IP) are trusted.         |
+| `panelOutbound`     | _(none)_                | Route the panel's own egress (update checks, Telegram, geo/sub fetches) through a named Xray outbound. |
+
+After changing the port or base path, the panel URL becomes
+`http(s)://<server>:<port><web-base-path>`. You can preset the base path on first
+launch with [`XUI_INIT_WEB_BASE_PATH`](/docs/reference/env-vars).
+
+### TLS
+
+Serving the panel over HTTPS protects your credentials in transit. Either set
+`webCertFile` + `webKeyFile` — the [`x-ui` SSL menu](/docs/config/ssl-certificates)
+can obtain a Let's Encrypt certificate for you — or terminate TLS at a
+[reverse proxy](/docs/operations/reverse-proxy).
+
+<Callout type="warn">
+  Never expose the panel over plain HTTP on the public internet. Use TLS, a
+  non-default port, and a long random web base path.
+</Callout>
+
+## Display
+
+| Setting          | Default                                          | Meaning                                                       |
+| ---------------- | ------------------------------------------------ | ------------------------------------------------------------- |
+| `pageSize`       | `25`                                             | Rows per page in lists (`0` disables pagination).             |
+| `expireDiff`     | `0`                                              | Days before expiry to start warning.                          |
+| `trafficDiff`    | `0`                                              | Percent of quota remaining at which to start warning.         |
+| `remarkTemplate` | `{{INBOUND}}-{{EMAIL}}\|📊{{TRAFFIC_LEFT}}\|⏳{{DAYS_LEFT}}D` | Default client remark template (see [Share links](/docs/config/share-links#remark-template-variables)). |
+| `timeLocation`   | `Local`                                          | Time zone for stats and expiry.                               |
+| `datepicker`     | `gregorian`                                      | Calendar for date inputs (Gregorian or Jalali/Persian).       |
+
+## Security & authentication
+
+Credentials, two-factor auth, the brute-force limiter, sessions, and LDAP are
+covered in [First login](/docs/guide/first-login) and
+[Security](/docs/operations/security). In short:
+
+- Passwords are stored as **bcrypt** hashes; changing them logs out all sessions.
+- **2FA (TOTP)** can be required at login.
+- An **LDAP** fallback can authenticate users when the local password check fails.
+- API access uses **API tokens** managed under Panel Settings (see the
+  [API reference](/docs/reference/api/api-tokens)).
+
+## Notifications & subscription
+
+These have their own settings groups and pages:
+
+<Cards>
+  <Card title="Telegram bot" href="/docs/operations/telegram-bot" description="Token, chat IDs, alerts, and reports." />
+  <Card title="Subscription" href="/docs/config/subscription" description="Subscription server, formats, and paths." />
+  <Card title="Security" href="/docs/operations/security" description="2FA, IP limits, and hardening." />
+</Cards>
+
+<Callout type="info">
+  Email (SMTP) notifications are also configurable (host, port, encryption,
+  recipients) with the same event types as the Telegram bot.
+</Callout>

+ 135 - 0
docs/content/docs/en/config/reality.mdx

@@ -0,0 +1,135 @@
+---
+title: REALITY
+description: Set up a VLESS + REALITY inbound with XTLS-Vision in 3x-ui — keys, short IDs, SNI, fingerprints, and common pitfalls.
+icon: ShieldCheck
+---
+
+**REALITY** is an Xray transport security that disguises your proxy as ordinary
+traffic to a real, popular website. Unlike classic TLS, your server needs **no
+certificate of its own** — it borrows the TLS handshake of the target site
+(`dest`). Combined with the **XTLS-Vision** flow, it is fast and resistant to
+deep-packet inspection.
+
+REALITY is used with **VLESS** (and Trojan). The recommended flow is
+`xtls-rprx-vision`.
+
+## Key settings
+
+When you choose **REALITY** as the security mode on a VLESS inbound, 3x-ui
+exposes these fields:
+
+| Field                    | What it is                                                         |
+| ------------------------ | ------------------------------------------------------------------ |
+| **Dest (target)**        | A real TLS site to impersonate, e.g. `www.microsoft.com:443`.      |
+| **SNI / Server Names**   | The hostname(s) clients send; must match the target's certificate. |
+| **Public / Private key** | An **x25519** keypair. The private key stays on the server.        |
+| **Short IDs**            | Hex strings used to authenticate clients (you can have several).   |
+| **Flow**                 | Set to `xtls-rprx-vision`.                                         |
+| **Fingerprint (uTLS)**   | The client TLS fingerprint to mimic, e.g. `chrome`.                |
+
+The private key is generated with Xray's `x25519` utility (the panel can
+generate the pair for you):
+
+```bash title="generate an x25519 keypair"
+xray x25519
+```
+
+## Set it up in the panel
+
+<Steps>
+
+<Step>
+### Create a VLESS inbound
+
+Add a new inbound, choose protocol **VLESS**, and set **Security** to
+**reality**.
+</Step>
+
+<Step>
+### Choose a target (dest) and SNI
+
+Pick a reputable site that supports TLS 1.3 and HTTP/2 and is reachable from your
+server and your clients (for example `www.microsoft.com:443`). Set the server
+names / SNI to match that site's certificate.
+</Step>
+
+<Step>
+### Generate keys and short IDs
+
+Generate the x25519 keypair and one or more short IDs. Keep the **private key**
+secret; clients only ever receive the **public key**.
+</Step>
+
+<Step>
+### Set the flow and fingerprint
+
+Use the `xtls-rprx-vision` flow and a common uTLS fingerprint such as `chrome`.
+</Step>
+
+<Step>
+### Add a client and share the link
+
+Create a client, then use its share link or QR code in a compatible app
+(v2rayNG, Hiddify, Mihomo, and others).
+</Step>
+
+</Steps>
+
+## What the configuration looks like
+
+On the server, a REALITY inbound's `streamSettings` looks roughly like this:
+
+```json title="server inbound (excerpt)"
+{
+  "network": "tcp",
+  "security": "reality",
+  "realitySettings": {
+    "dest": "www.microsoft.com:443",
+    "serverNames": ["www.microsoft.com"],
+    "privateKey": "<x25519 private key>",
+    "shortIds": ["<hex short id>"],
+    "fingerprint": "chrome"
+  }
+}
+```
+
+The matching client share link carries the **public** parameters:
+
+```text title="vless:// (excerpt)"
+vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni=www.microsoft.com&fp=chrome&spx=%2F&flow=xtls-rprx-vision#my-reality
+```
+
+- `pbk` — REALITY **public** key
+- `sid` — short ID (matches one on the server)
+- `sni` — server name (matches the target's certificate)
+- `fp` — client fingerprint
+- `spx` — spiderX path
+- `flow` — `xtls-rprx-vision`
+
+## Common pitfalls
+
+<Callout type="warn">
+
+- **Bad target.** The `dest` must be a real site that supports **TLS 1.3** and
+  **HTTP/2**, is reachable, and isn't blocked in your region. Pick a site you do
+  not own and that sees lots of traffic.
+- **SNI mismatch.** The SNI / server names must match the target's real
+  certificate, or the handshake gives the disguise away.
+- **Leaked private key.** Only ever distribute the **public** key to clients.
+- **Wrong flow.** REALITY + XTLS-Vision needs `flow = xtls-rprx-vision` on both
+  the inbound client entry and the share link.
+
+</Callout>
+
+## Generate a config
+
+Use the generator below to create a fresh X25519 keypair, UUID, and short ID,
+then copy the server inbound JSON and the client share link. Everything is
+computed **in your browser** — no keys or links are sent anywhere.
+
+<RealityConfigGenerator />
+
+<Callout type="info">
+  The **private key** belongs only on your server. Share the generated
+  `vless://` link (which contains the **public** key) with clients.
+</Callout>

+ 82 - 0
docs/content/docs/en/config/share-links.mdx

@@ -0,0 +1,82 @@
+---
+title: Share Links
+description: 3x-ui share-link formats (vless, vmess, trojan, ss, hysteria2, mtproto), the remark template variables, and an in-browser link inspector.
+icon: Link
+---
+
+3x-ui generates a **share link** (and QR code) for each client. Client apps such
+as v2rayNG, Hiddify, and Mihomo import these links to configure themselves.
+
+## Link formats
+
+| Scheme         | Shape                                                           |
+| -------------- | --------------------------------------------------------------- |
+| `vless://`     | `vless://<uuid>@<host>:<port>?<params>#<remark>`                |
+| `vmess://`     | `vmess://<base64-json>` (a base64-encoded JSON object)          |
+| `trojan://`    | `trojan://<password>@<host>:<port>?<params>#<remark>`           |
+| `ss://`        | `ss://<userinfo>@<host>:<port>?<params>#<remark>` (SIP002; Shadowsocks-2022 uses percent-encoded userinfo) |
+| `hysteria2://` | `hysteria2://<auth>@<host>:<port>?<params>#<remark>`            |
+| `tg://proxy`   | `tg://proxy?server=…&port=…&secret=…` (MTProto)                 |
+
+The query parameters carry the transport and security settings — `security`,
+`sni`, `fp`, `pbk`, `sid`, `spx`, `flow`, `type`, `path`, `host`, `alpn`, and
+more.
+
+## Inspect a link
+
+Paste any share link to decode every field. Parsing happens **entirely in your
+browser** — the link is never sent over the network.
+
+<ShareLinkInspector />
+
+<Callout type="warn">
+  Share links contain everything needed to connect as a client, including the
+  client's credential. Treat them like passwords.
+</Callout>
+
+## Remark template variables
+
+The text after `#` in each link (the **remark**) is generated from a template
+you control in Panel Settings (`remarkTemplate`). The default is:
+
+```text
+{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D
+```
+
+Tokens use `{{UPPER_CASE}}` syntax. The template is split on `|` into segments;
+a segment whose only value is the unlimited marker `∞` (for `TRAFFIC_LEFT`,
+`TRAFFIC_TOTAL`, `DAYS_LEFT`, or `TIME_LEFT`) is dropped, so unlimited clients
+don't show empty decorations.
+
+### Available tokens
+
+| Token | Value |
+| ----- | ----- |
+| `{{EMAIL}}` / `{{USERNAME}}` | Client email (identifier) |
+| `{{INBOUND}}` | Inbound remark |
+| `{{HOST}}` | Host-row remark (managed hosts) |
+| `{{ID}}` / `{{SHORT_ID}}` | Client UUID / its first 8 chars |
+| `{{TELEGRAM_ID}}` · `{{SUB_ID}}` · `{{COMMENT}}` | Telegram ID, subscription ID, comment |
+| `{{STATUS}}` / `{{STATUS_EMOJI}}` | `active`/`expired`/`depleted`/`disabled` (or ✅⏳🚫) |
+| `{{DAYS_LEFT}}` / `{{TIME_LEFT}}` | Days, or `Xd Xh Xm`, remaining (`∞` if unlimited) |
+| `{{EXPIRE_DATE}}` / `{{JALALI_EXPIRE_DATE}}` / `{{EXPIRE_UNIX}}` | Expiry as Gregorian / Jalali date / Unix seconds |
+| `{{CREATED_UNIX}}` | Creation time (Unix seconds) |
+| `{{TRAFFIC_USED}}` / `{{TRAFFIC_LEFT}}` / `{{TRAFFIC_TOTAL}}` | Human-readable usage (`∞` if unlimited) |
+| `{{TRAFFIC_USED_BYTES}}` / `{{TRAFFIC_LEFT_BYTES}}` / `{{TRAFFIC_TOTAL_BYTES}}` | Same, in bytes |
+| `{{UP}}` / `{{DOWN}}` | Upload / download (human-readable) |
+| `{{RESET_DAYS}}` · `{{USAGE_PERCENTAGE}}` | Reset period (days) · used percent |
+| `{{PROTOCOL}}` / `{{TRANSPORT}}` / `{{SECURITY}}` | e.g. `VLESS` / `ws` / `REALITY` |
+
+<Callout type="info">
+  Usage tokens (traffic, days, status) appear in the subscription **body** but
+  are stripped from the display/QR view, so a shared QR doesn't leak a client's
+  remaining quota. Date tokens follow the `datepicker` setting (Gregorian or
+  Jalali).
+</Callout>
+
+## Related
+
+<Cards>
+  <Card title="REALITY" href="/docs/config/reality" description="Generate a VLESS + REALITY config and link." />
+  <Card title="Subscription" href="/docs/config/subscription" description="Serve all of a client's links from one URL." />
+</Cards>

+ 181 - 0
docs/content/docs/en/config/ssl-certificates.mdx

@@ -0,0 +1,181 @@
+---
+title: SSL Certificates
+description: Get and renew TLS certificates for the 3x-ui panel and inbounds — with the x-ui ACME menu (domain or bare IP), a Cloudflare DNS-01 wildcard, or manual Certbot.
+icon: ShieldCheck
+---
+
+A TLS certificate lets you serve the **panel** over HTTPS (so your login and API
+traffic are encrypted) and terminate TLS on **inbounds** (VLESS-TLS, Trojan,
+Shadowsocks-TLS, and friends). There are three ways to obtain one:
+
+- **The `x-ui` menu** — built-in [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment)
+  client. Easiest for a single domain or a bare IP.
+- **Cloudflare DNS-01** — also from the menu; needed for **wildcard** certs or
+  when port 80 is blocked / the server sits behind Cloudflare's proxy.
+- **Manual Certbot** — if you'd rather manage `acme.sh`/Certbot yourself.
+
+<Callout type="info">
+  If you put the panel behind Nginx or Caddy, let the proxy handle the
+  certificate instead — see [Reverse proxy](/docs/operations/reverse-proxy).
+  [REALITY](/docs/config/reality) inbounds need **no** certificate at all; they
+  borrow a real site's TLS. This page is for the panel and for classic TLS
+  inbounds.
+</Callout>
+
+## The `x-ui` SSL menu (Let's Encrypt)
+
+Run `x-ui` and choose **`20` — SSL Certificate Management**. It drives
+[acme.sh](https://github.com/acmesh-official/acme.sh) and offers:
+
+| Option                         | What it does                                                        |
+| ------------------------------ | ------------------------------------------------------------------- |
+| Get SSL (Domain)               | Issue a certificate for a domain via HTTP validation.               |
+| Get SSL for IP Address         | Issue a short-lived (6-day, auto-renewing) cert for a **bare IP**.  |
+| Revoke                         | Revoke an existing certificate.                                     |
+| Force Renew                    | Renew now, before expiry.                                           |
+| Show Existing Domains          | List certificates already on the server.                           |
+| Set Cert paths for the panel   | Point the panel's TLS at an issued cert (sets the fields for you).  |
+
+### Issue a certificate for a domain
+
+<Steps>
+
+<Step>
+### Point the domain at the server
+
+Create an `A` (and/or `AAAA`) record for your domain that resolves to this
+server's public IP. Validation fails until DNS has propagated.
+</Step>
+
+<Step>
+### Free up port 80
+
+HTTP validation needs **port 80** reachable from the internet and not already in
+use. Stop anything bound to it for the duration, and allow it through the
+[firewall](/docs/reference/ports-firewall).
+</Step>
+
+<Step>
+### Run the issuer
+
+`x-ui` → `20` → **Get SSL (Domain)**, then enter the domain. acme.sh requests
+the certificate and saves it under `/root/cert/<domain>/` as `fullchain.pem`
+(the certificate chain) and `privkey.pem` (the private key).
+</Step>
+
+<Step>
+### Wire it into the panel
+
+Choose **Set Cert paths for the panel** to fill in `webCertFile` and
+`webKeyFile` and restart the panel, or set them yourself in
+[Panel Settings](/docs/config/panel#tls). The panel serves HTTPS as soon as both
+are set.
+</Step>
+
+</Steps>
+
+### Issue a certificate for a bare IP
+
+No domain? Choose **Get SSL for IP Address** to obtain a short-lived
+certificate (valid ~6 days, renewed automatically) bound to the server's IP.
+Useful for reaching the panel over HTTPS before you've set up a domain.
+
+## Cloudflare (DNS-01 wildcard)
+
+DNS validation proves you control the domain by creating a TXT record instead of
+answering on port 80 — so it works **behind Cloudflare's proxy**, on servers
+where port 80 is blocked, and for **wildcard** certificates (`*.example.com`).
+
+Your domain's DNS must be managed by Cloudflare, and you need one of:
+
+- a **scoped API token** with the `Zone:DNS:Edit` permission (recommended), or
+- your account **email + Global API Key**.
+
+<Steps>
+
+<Step>
+### Create a scoped API token
+
+In the Cloudflare dashboard go to **My Profile → API Tokens →
+[Create Token](https://dash.cloudflare.com/profile/api-tokens)**, pick the
+**Edit zone DNS** template, scope it to the zone you're issuing for, and create
+it. Copy the token — it's shown only once.
+</Step>
+
+<Step>
+### Run the Cloudflare issuer
+
+`x-ui` → **`21` — Cloudflare SSL Certificate**. When asked, choose **`t`** for an
+API token (the default) or **`g`** for the Global API Key, then enter your
+domain (and, for the Global API Key, your account email and key). acme.sh creates
+the TXT record, validates, and cleans it up.
+</Step>
+
+<Step>
+### Point the panel at it
+
+As with the domain flow, use **Set Cert paths for the panel** (menu `20`) or set
+`webCertFile` / `webKeyFile` in [Panel Settings](/docs/config/panel#tls).
+</Step>
+
+</Steps>
+
+<Callout type="info">
+  Prefer a scoped token over the Global API Key — it only grants DNS edits on the
+  zone you choose, so a leak can't touch the rest of your Cloudflare account.
+</Callout>
+
+## Manual (Certbot)
+
+If you'd rather not use the menu, issue a certificate with Certbot's standalone
+plugin (again, this needs port 80 free and the domain resolving to the server):
+
+```bash
+apt-get install certbot -y
+certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
+certbot renew --dry-run
+```
+
+Certbot writes the certificate to `/etc/letsencrypt/live/yourdomain.com/`
+(`fullchain.pem` and `privkey.pem`). Point the panel at those two files in
+[Panel Settings](/docs/config/panel#tls), and set up renewal — `certbot renew`
+runs on a systemd timer by default.
+
+## Using the certificate
+
+- **Panel** — set `webCertFile` (the full chain) and `webKeyFile` (the private
+  key) in [Panel Settings](/docs/config/panel#tls). Both must be set for the
+  panel to switch to HTTPS. Menu option **`11` — View Current Settings** prints
+  the paths currently in use.
+- **Inbounds** — when you enable TLS on an inbound, reference the same
+  certificate and key files (or paste their contents) in the inbound's TLS
+  settings. See [Inbounds](/docs/config/inbounds) and
+  [Transports](/docs/config/transports).
+
+<Callout type="warn">
+  Certificates expire (Let's Encrypt: 90 days; IP certs: ~6 days). The menu and
+  Certbot both renew automatically, but the panel keeps reading the **files** at
+  their fixed paths — so renew **in place** rather than moving the files, and the
+  panel picks up the new cert on its next restart. **Force Renew** (menu `20`)
+  triggers a renewal on demand.
+</Callout>
+
+## Next steps
+
+<Cards>
+  <Card
+    title="Panel Settings"
+    href="/docs/config/panel#tls"
+    description="webCertFile / webKeyFile and the rest of the web-server settings."
+  />
+  <Card
+    title="Reverse proxy"
+    href="/docs/operations/reverse-proxy"
+    description="Let Nginx or Caddy terminate TLS for you instead."
+  />
+  <Card
+    title="REALITY"
+    href="/docs/config/reality"
+    description="Stealth TLS for inbounds — no certificate required."
+  />
+</Cards>

+ 86 - 0
docs/content/docs/en/config/subscription.mdx

@@ -0,0 +1,86 @@
+---
+title: Subscription
+description: Run the 3x-ui subscription server — base64/JSON/Clash formats, ports and paths, TLS, response headers, and custom templates.
+icon: Rss
+---
+
+A **subscription** is a single URL that returns all of a client's
+configurations. Client apps refresh it periodically, so when you change an
+inbound, clients pick up the change automatically. The subscription server runs
+as a **separate** server from the panel.
+
+## Enable and configure
+
+The subscription server is **on by default** (`subEnable`). Configure it in the
+panel's subscription settings:
+
+| Setting       | Default | Meaning                                                         |
+| ------------- | ------- | --------------------------------------------------------------- |
+| `subPort`     | `2096`  | Listen port (separate from the panel).                          |
+| `subListen`   | _(all)_ | Bind address.                                                   |
+| `subPath`     | `/sub/` | Base path for raw subscription URLs.                            |
+| `subDomain`   | _(none)_| Public host; if set, the server only answers for that Host.     |
+| `subCertFile` / `subKeyFile` | _(none)_ | TLS cert + key — when set, the server serves **HTTPS**. |
+| `subEncrypt`  | `true`  | Base64-encode the raw subscription body.                        |
+| `subUpdates`  | `12`    | Suggested refresh interval (hours) sent to clients.             |
+
+A subscription URL looks like:
+
+```text
+https://<sub-host>:<sub-port>/sub/<sub-id>
+```
+
+where `<sub-id>` is the client's **Sub ID**.
+
+The same Sub ID is served in several formats on different paths — the **Base64**
+list at `subPath` and the **JSON** (Xray-json) config at the JSON path. Build the
+URLs and preview both bodies here:
+
+<SubscriptionBuilder />
+
+## Output formats
+
+The **format is chosen by path**, each with its own enable toggle:
+
+| Format                | Path      | Enabled by       | Output                                              |
+| --------------------- | --------- | ---------------- | --------------------------------------------------- |
+| **Raw links**         | `/sub/`   | always (if on)   | A list of `vless://`, `vmess://`, … links (base64-encoded when `subEncrypt` is on). |
+| **JSON**              | `/json/`  | `subJsonEnable`  | Full Xray client config(s).                         |
+| **Clash / Mihomo**    | `/clash/` | `subClashEnable` | YAML profile.                                       |
+
+Only enabled inbounds using **VLESS, VMess, Trojan, Shadowsocks, or Hysteria2**
+appear in a subscription, ordered by their sub-sort index. Requesting `/sub/`
+with an `Accept: text/html` header (or `?html=1`) returns a human-readable info
+page instead of the raw body.
+
+### Base64 vs JSON
+
+The **Base64** body is just the newline-joined share links, standard-base64
+encoded (toggle with `subEncrypt`). The **JSON** body wraps each client in a
+complete Xray client config — a fixed skeleton (local mixed/HTTP inbounds, DNS,
+routing, policy) plus a `proxy` outbound pointing at the inbound. 3x-ui emits a
+**single config object for one client and an array for several**, uses the flat
+outbound `settings` form (`address`/`port`/`id`, `level: 8`), and strips
+`sockopt` from `streamSettings`.
+
+## Response headers
+
+Subscriptions return standard headers that compatible apps read:
+
+- **`Subscription-Userinfo`** — `upload`, `download`, `total` (bytes; `total=0`
+  means unlimited) and `expire` (Unix seconds).
+- **`Profile-Update-Interval`** — refresh interval in hours (`subUpdates`).
+- **`Profile-Title`**, **`Support-Url`**, **`Profile-Web-Page-Url`**,
+  **`Announce`** — optional branding shown by some clients.
+
+## Custom page templates
+
+Point `subThemeDir` at a folder containing a custom info-page template to brand
+the HTML subscription page. The per-client remark on each link is fully
+templated — see [Share links → remark variables](/docs/config/share-links#remark-template-variables).
+
+<Callout type="info">
+  Put the subscription server behind TLS (set `subCertFile`/`subKeyFile`, or a
+  [reverse proxy](/docs/operations/reverse-proxy)) so subscription contents
+  aren't exposed in transit.
+</Callout>

+ 199 - 0
docs/content/docs/en/config/transports.mdx

@@ -0,0 +1,199 @@
+---
+title: Transports & Security
+description: Every transport 3x-ui exposes — TCP, mKCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, Hysteria — with their settings, plus FinalMask obfuscation, sockopt, TLS/REALITY, XTLS-Vision, and VLESS encryption.
+icon: Network
+---
+
+A **transport** decides how packets are carried between client and server, a
+**security** layer decides how they're encrypted and disguised, and **FinalMask**
+can obfuscate what's left. The panel only offers valid combinations; this page
+lists every transport's settings and the rules the panel enforces.
+
+## Transports
+
+Pick the transport (the inbound's `network`) in the inbound/outbound form. Each
+network writes its own settings key on the wire (`tcpSettings`, `kcpSettings`, …).
+
+| Transport       | Settings key          | When to use it                                                         |
+| --------------- | --------------------- | ---------------------------------------------------------------------- |
+| **TCP (Raw)**   | `tcpSettings`         | Lowest overhead. The basis for REALITY + XTLS-Vision and fallbacks; optional HTTP/1.1 header camouflage. |
+| **mKCP**        | `kcpSettings`         | Reliable protocol over **UDP** — trades bandwidth for lower latency on lossy links. Carries no TLS/REALITY. |
+| **WebSocket**   | `wsSettings`          | Works through CDNs and HTTP reverse proxies; very compatible.          |
+| **gRPC**        | `grpcSettings`        | HTTP/2-based; multiplexes well and proxies cleanly through Nginx.      |
+| **HTTPUpgrade** | `httpupgradeSettings` | CDN-friendly HTTP/1.1 `Upgrade`; lighter than full WebSocket.          |
+| **XHTTP**       | `xhttpSettings`       | Modern stream-multiplexed HTTP transport; CDN-friendly and REALITY-capable. |
+| **Hysteria**    | `hysteriaSettings`    | QUIC-based transport — only for the **Hysteria2** protocol.            |
+
+<Callout type="info">
+  **WireGuard** and **Tunnel** (dokodemo-door) inbounds expose no transport
+  selector — their stream carries only security/sockopt. Earlier panels also
+  exposed a raw **HTTP/2 (`http`)** transport; it has been superseded by **XHTTP**
+  and is no longer selectable.
+</Callout>
+
+### TCP (Raw) — `tcpSettings`
+
+| Field                          | Default | Meaning                                                                 |
+| ------------------------------ | ------- | ----------------------------------------------------------------------- |
+| `acceptProxyProtocol`          | `false` | Accept the PROXY protocol from an upstream proxy so the real client IP is preserved. |
+| `header.type`                  | `none`  | `none`, or `http` for HTTP/1.1 camouflage.                              |
+| `header.request` / `response`  | —       | When `type: http`: method, path, version and a header map that mimic a normal HTTP exchange. |
+
+### mKCP — `kcpSettings`
+
+| Field              | Default     | Meaning                                                          |
+| ------------------ | ----------- | ---------------------------------------------------------------- |
+| `mtu`              | `1350`      | Maximum transmission unit, in bytes (576–1460).                  |
+| `tti`              | `20`        | Transmission time interval, in ms (10–100). Lower = more responsive, more overhead. |
+| `uplinkCapacity`   | `5`         | Upload bandwidth budget, in **MB/s**.                            |
+| `downlinkCapacity` | `20`        | Download bandwidth budget, in **MB/s**.                          |
+| `cwndMultiplier`   | `1`         | Congestion-window multiplier; raise to push harder on good links. |
+| `maxSendingWindow` | `2097152`   | Upper bound on in-flight packets.                               |
+
+<Callout type="info">
+  mKCP can't carry TLS or REALITY. To disguise it, add a **FinalMask** UDP mask —
+  the `mkcp-legacy` mask reproduces the classic header obfuscation that older Xray
+  stored in `kcpSettings.header`/`seed` (those fields no longer exist here).
+</Callout>
+
+### WebSocket — `wsSettings`
+
+| Field                 | Default | Meaning                                                          |
+| --------------------- | ------- | ---------------------------------------------------------------- |
+| `path`                | `/`     | Request path — route on it when several services share one host. |
+| `host`                | _(none)_| `Host` header override (useful behind a CDN).                    |
+| `headers`             | `{}`    | Extra request headers.                                           |
+| `heartbeatPeriod`     | `0`     | Seconds between keepalive pings; `0` disables them.              |
+| `acceptProxyProtocol` | `false` | Accept the PROXY protocol from an upstream.                      |
+
+### gRPC — `grpcSettings`
+
+| Field         | Default | Meaning                                                   |
+| ------------- | ------- | --------------------------------------------------------- |
+| `serviceName` | _(none)_| gRPC service path; acts like a secret route.              |
+| `authority`   | _(none)_| `:authority` pseudo-header override.                      |
+| `multiMode`   | `false` | Multiplex several streams over one connection.            |
+
+### HTTPUpgrade — `httpupgradeSettings`
+
+| Field                 | Default | Meaning                                       |
+| --------------------- | ------- | --------------------------------------------- |
+| `path`                | `/`     | Request path.                                 |
+| `host`                | _(none)_| `Host` header override.                       |
+| `headers`             | `{}`    | Extra request headers.                        |
+| `acceptProxyProtocol` | `false` | Accept the PROXY protocol from an upstream.   |
+
+HTTPUpgrade is a one-shot HTTP/1.1 `Upgrade` with no WebSocket framing — there's
+no heartbeat field.
+
+### XHTTP — `xhttpSettings`
+
+XHTTP (SplitHTTP) has a large field set; the panel fills sensible defaults. The
+ones you'll usually touch:
+
+| Field                  | Default     | Meaning                                                                |
+| ---------------------- | ----------- | ---------------------------------------------------------------------- |
+| `path`                 | `/`         | Request path.                                                          |
+| `host`                 | _(none)_    | `Host` header override.                                                |
+| `mode`                 | `auto`      | `auto`, `packet-up`, `stream-up`, or `stream-one`. `packet-up` is the most CDN-compatible; `stream-*` are lower latency. |
+| `xPaddingBytes`        | `100-1000`  | Random padding range that blurs packet sizes.                          |
+| `scMaxBufferedPosts`   | `30`        | Server-side buffer for uploaded POSTs.                                 |
+| `scStreamUpServerSecs` | `20-80`     | Stream-up server window (dash range).                                  |
+| `xmux` (`enableXmux`)  | _(off)_     | Connection multiplexing — `maxConcurrency` `16-32`, `maxConnections` `6`, … Turn on for high concurrency. |
+
+Session-ID fields (`sessionIDPlacement`, `sessionIDKey`, `sessionIDTable`,
+`sessionIDLength`) and the `scMin/MaxEachPostBytes` knobs are advanced; leave them
+empty unless you're matching a specific upstream.
+
+### Hysteria — `hysteriaSettings`
+
+Only valid when the protocol is **Hysteria2**.
+
+| Field            | Default | Meaning                                                                 |
+| ---------------- | ------- | ----------------------------------------------------------------------- |
+| `version`        | `2`     | Hysteria protocol version.                                              |
+| `auth`           | _(none)_| Shared authentication string.                                           |
+| `udpIdleTimeout` | `60`    | Seconds (2–600) before idle UDP sessions are dropped.                   |
+| `masquerade`     | —       | Disguise as an HTTP/3 server: `type` `proxy`/`file`/`string` with `url`/`dir`/`content`, plus `headers` and `statusCode`. |
+
+## FinalMask — late-layer obfuscation
+
+**FinalMask** wraps traffic **after** the transport and security layers, so it can
+disguise transports that don't carry TLS (like mKCP) or add a second skin on top of
+TLS. Masks are configured per direction:
+
+- **TCP masks** — `fragment`, `sudoku`, `header-custom`.
+- **UDP masks** — `salamander`, `mkcp-legacy`, `header-custom`, `xdns`, `xicmp`,
+  `noise`, `sudoku`, `realm`. (`mkcp-legacy` reproduces the old mKCP header
+  obfuscation.)
+- **QUIC params** — congestion control (`reno`, `bbr`, `brutal`, `force-brutal`),
+  Brutal up/down rates, `udpHop` (rotate the QUIC port across a range to dodge
+  port blocking), and receive-window tuning.
+
+FinalMask replaces the per-transport `header`/`seed` obfuscation that older Xray
+builds exposed.
+
+## sockopt — low-level socket options
+
+`sockopt` rides alongside any transport and tunes the underlying socket. The most
+useful fields:
+
+| Field                 | Default | Meaning                                                          |
+| --------------------- | ------- | ---------------------------------------------------------------- |
+| `tcpFastOpen`         | `false` | Enable TCP Fast Open.                                            |
+| `tcpcongestion`       | `bbr`   | Congestion control: `bbr`, `cubic`, or `reno`.                  |
+| `tproxy`              | `off`   | Transparent proxy mode: `off`, `redirect`, or `tproxy`.          |
+| `domainStrategy`      | `AsIs`  | How addresses resolve (`UseIP`, `ForceIPv4`, …).                 |
+| `dialerProxy`         | _(none)_| Chain this outbound's dialing through another outbound tag.      |
+| `interface`           | _(none)_| Bind to a specific network interface.                           |
+| `mark`                | `0`     | SO_MARK for policy routing (`0` = unset).                        |
+
+Numeric fields left at `0` are omitted on the wire so Xray keeps OS defaults.
+Advanced entries (`happyEyeballs`, `customSockopt[]`, keepalive timers) are
+available for special cases.
+
+## Security
+
+The security layer is one of **`none`**, **`tls`**, or **`reality`**, with these
+eligibility rules:
+
+| Security    | Eligible transports                          | Eligible protocols                                  |
+| ----------- | -------------------------------------------- | --------------------------------------------------- |
+| **TLS**     | `tcp`, `ws`, `grpc`, `httpupgrade`, `xhttp`  | VLESS, VMess, Trojan, Shadowsocks (Hysteria2 is always TLS) |
+| **REALITY** | `tcp`, `grpc`, `xhttp`                       | VLESS, Trojan                                       |
+
+mKCP and Hysteria don't take a separate TLS/REALITY layer — mKCP runs plaintext
+(obfuscate with FinalMask), and Hysteria is QUIC/TLS by design. REALITY disguises
+your server as a real TLS site and needs no certificate — see
+[REALITY](/docs/config/reality).
+
+## XTLS-Vision flow
+
+The `xtls-rprx-vision` flow is fast and DPI-resistant. It's available for
+**VLESS** when either:
+
+- the transport is raw **TCP** with **TLS** or **REALITY** security (classic
+  XTLS-Vision), or
+- the transport is **XHTTP** with VLESS encryption enabled (see below).
+
+Set the flow on the VLESS **client**, not the inbound. With classic Vision on
+TCP, the panel can also offer a **Vision seed** once a client uses the flow.
+
+## VLESS encryption (ML-KEM)
+
+VLESS supports post-quantum **encryption** (ML-KEM / `mlkem768x25519`), stored in
+the inbound's `decryption` (server) and clients' `encryption` (for link
+generation). When enabled, it unlocks the Vision flow over XHTTP. Generate the
+keys from the panel's VLESS settings.
+
+## Shadowsocks ciphers
+
+Shadowsocks inbounds support both classic ciphers and **Shadowsocks-2022**
+(method names starting with `2022-blake3-`). Most ciphers are multi-user;
+`2022-blake3-chacha20-poly1305` is single-user.
+
+<Callout type="info">
+  Transports and security must match on both ends. The client's share link
+  encodes them (`type=ws`, `security=reality`, `flow=xtls-rprx-vision`, …) —
+  decode any link with the [share-link inspector](/docs/config/share-links).
+</Callout>

+ 124 - 0
docs/content/docs/en/guide/first-login.mdx

@@ -0,0 +1,124 @@
+---
+title: First Login
+description: Find your generated 3x-ui credentials, reach the panel, enable two-factor auth, and harden it before exposing anything.
+icon: KeyRound
+---
+
+After installation, your first job is to log in and **secure the panel** before
+exposing anything else.
+
+## Reach the panel
+
+The panel is served at:
+
+```text
+http://<your-server-ip>:<port>/<web-base-path>
+```
+
+The default port is **2053** and the default base path is `/` — but a script
+install **randomly generates** the username, password, **port**, and web base
+path, so check your actual values.
+
+### Find your credentials
+
+A script install prints a credential summary when it finishes and also writes it
+to a root-only file:
+
+```bash title="/etc/x-ui/install-result.env (mode 600)"
+XUI_USERNAME=...
+XUI_PASSWORD=...
+XUI_PANEL_PORT=...
+XUI_WEB_BASE_PATH=...
+XUI_ACCESS_URL=...
+XUI_API_TOKEN=...
+XUI_DB_TYPE=sqlite
+```
+
+If you missed them, use the management tools:
+
+```bash
+x-ui              # menu → 11 (View Current Settings)
+x-ui settings     # or the one-shot form
+```
+
+For **Docker**, read the generated credentials from the container logs, or run
+`docker exec -it <container> x-ui setting -show`.
+
+<Callout type="warn">
+  If your panel still uses the default `admin` / `admin` (the panel warns when it
+  does), change it immediately — before creating any inbounds.
+</Callout>
+
+## Change credentials, port, and path
+
+A non-default port and a long, random **web base path** make the panel much
+harder to find. Change them from **Panel Settings** in the UI, or from the
+`x-ui` menu:
+
+- **7 — Reset Username & Password** (optionally disabling 2FA at the same time)
+- **8 — Reset Web Base Path** (randomizes it)
+- **10 — Change Port**
+
+Changing your username or password **logs out all existing sessions** and, if
+two-factor auth was on, disables it.
+
+## Two-factor authentication (2FA)
+
+3x-ui supports TOTP two-factor auth (compatible with Google Authenticator, Aegis,
+etc.). Enable it in **Panel Settings** — once enabled, the login page asks for a
+6-digit code in addition to your password, and turning it on forces everyone to
+log in again. You can disable it from the menu's **Reset Username & Password**
+step or with `x-ui setting -resetTwoFactor`.
+
+## Built-in login protection
+
+- **Brute-force limiter:** after **5** failed logins from the same IP/username
+  within 5 minutes, that combination is blocked for **15 minutes**.
+- **Generic errors:** the login page reports "wrong username or password" for
+  both bad credentials and bad 2FA codes, so it leaks nothing.
+- **Sessions** last `sessionMaxAge` minutes (default **360** = 6 hours) and are
+  invalidated when you change credentials.
+- **LDAP** can be enabled as an auth fallback in Panel Settings.
+
+## Essential hardening checklist
+
+<Steps>
+
+<Step>
+### Set strong, unique credentials
+
+Replace the generated (or `admin/admin`) username and password with strong values.
+</Step>
+
+<Step>
+### Use a non-default port and random base path
+
+Move the panel off `2053` and serve it under a long random path.
+</Step>
+
+<Step>
+### Enable two-factor authentication
+
+Turn on 2FA so a leaked password alone can't grant access.
+</Step>
+
+<Step>
+### Put the panel behind TLS
+
+Use a valid certificate (via the `x-ui` menu's SSL management, or a reverse
+proxy) so the panel is only reachable over HTTPS.
+</Step>
+
+<Step>
+### Restrict access with a firewall
+
+Open only the ports you actually need, and consider limiting panel access by IP.
+</Step>
+
+</Steps>
+
+<Callout type="info">
+  Want the panel on a clean domain with automatic HTTPS? See
+  [Reverse proxy](/docs/operations/reverse-proxy). For deeper hardening, see
+  [Security](/docs/operations/security).
+</Callout>

+ 71 - 0
docs/content/docs/en/guide/index.mdx

@@ -0,0 +1,71 @@
+---
+title: Meet 3x-ui
+description: A web panel for Xray-core — manage inbounds, protocols, clients, and subscriptions from your browser instead of editing JSON by hand.
+icon: Info
+---
+
+**3x-ui** is a web control panel that sits on top of
+[Xray-core](https://github.com/XTLS/Xray-core), the proxy engine that actually
+moves your traffic. Instead of writing and reloading Xray's JSON configuration
+by hand, you manage everything — inbounds, protocols, clients, certificates,
+subscriptions — from a browser dashboard.
+
+## How the pieces fit together
+
+<Mermaid
+  chart={`
+flowchart LR
+  Admin["Admin browser"] -->|HTTPS panel| Panel["3x-ui panel"]
+  Panel -->|writes config, reloads| Xray["Xray-core"]
+  Panel --- DB[("SQLite or PostgreSQL")]
+  Clients["Client apps"] -->|VLESS / VMess / Trojan / ...| Xray
+  Xray -->|proxied traffic| Internet["Internet"]
+`}
+/>
+
+- **The panel** is the management layer: it stores your configuration in a
+  database, renders the dashboard, exposes a REST API, and writes the live Xray
+  configuration.
+- **Xray-core** is the data plane: it terminates client connections on your
+  **inbounds** and forwards traffic to its destination.
+- **Client apps** (such as v2rayNG, Clash/Mihomo, Hiddify, and others) connect
+  using a share link or a subscription that the panel generates for each client.
+
+## What it gives you
+
+- A dashboard for **inbounds** across every major protocol — VLESS, VMess,
+  Trojan, Shadowsocks, WireGuard, Hysteria2, SOCKS, HTTP, and Dokodemo-door.
+- First-class **REALITY** and **XTLS-Vision** support for stealthy, fast
+  transports.
+- **Per-client** traffic quotas, expiry dates, IP limits, online status, and
+  one-click share links / QR codes.
+- **Subscriptions** in VLESS, Clash/Mihomo, and JSON formats.
+- Operational tooling: **multi-node** management, a **Telegram bot**, backups,
+  Fail2ban-based IP limiting, and a documented REST API.
+
+## Under the hood
+
+| Layer        | Technology                                   |
+| ------------ | -------------------------------------------- |
+| Backend      | Go with the Gin web framework                |
+| Frontend     | TypeScript / React                           |
+| Database     | SQLite (default) or PostgreSQL               |
+| Proxy engine | Xray-core (bundled and managed by the panel) |
+
+The default SQLite database lives at `/etc/x-ui/x-ui.db`, and the panel listens
+on port **2053** by default. Both are configurable — see
+[First login](/docs/guide/first-login) and the environment variable reference.
+
+## Who it's for
+
+3x-ui is aimed at anyone running their own Xray server: from a single personal
+VPS to operators managing many nodes and clients. If you want the power of
+Xray-core without living in JSON config files, this is for you.
+
+<Callout type="info">
+  3x-ui is an enhanced fork of the original X-UI project, adding broader protocol
+  support, improved stability, per-client traffic accounting, multi-node
+  management, and many quality-of-life features.
+</Callout>
+
+Ready to install? Continue to [Installation](/docs/guide/installation).

+ 148 - 0
docs/content/docs/en/guide/installation.mdx

@@ -0,0 +1,148 @@
+---
+title: Installation
+description: Install 3x-ui via the official script (stable, pinned, or dev-latest), unattended/cloud-init, or Docker — and choose SQLite or PostgreSQL.
+icon: Download
+---
+
+3x-ui runs on a wide range of Linux distributions — Ubuntu, Debian, Armbian,
+Fedora, CentOS, RHEL, AlmaLinux, Rocky Linux, Oracle Linux, Amazon Linux,
+Virtuozzo, Arch, Manjaro, openSUSE (Tumbleweed/Leap), Alpine — and Windows,
+across `amd64`, `386`, `arm64`, `armv7`, `armv6`, `armv5`, and `s390x`.
+
+<Callout type="warn">
+  Run the script installer as **root** (or with `sudo`). It installs a service,
+  sets up the `x-ui` management command, and enables the panel on boot.
+</Callout>
+
+<Tabs items={['Script', 'Docker', 'Manual']}>
+
+<Tab value="Script">
+
+The official script is the recommended path. During installation it generates a
+**random** username, password, and access (web base) path, sets up the service,
+and installs the `x-ui` management command.
+
+```bash title="latest stable"
+bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
+```
+
+Install a **specific version** by appending its tag:
+
+```bash title="pinned version"
+bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.0
+```
+
+Install the rolling **dev** build (the latest per-commit pre-release from `main`
+— not a stable release) by passing `dev-latest`:
+
+```bash title="rolling dev build"
+bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest
+```
+
+When it finishes, note the printed login details and run `x-ui` to open the
+[management menu](/docs/guide/update-uninstall#the-x-ui-management-menu), then
+continue to [First login](/docs/guide/first-login).
+
+</Tab>
+
+<Tab value="Docker">
+
+The default Compose setup uses SQLite. Clone the repo (or copy its
+`docker-compose.yml` and `Dockerfile`) and start it:
+
+```bash
+docker compose up -d
+```
+
+To run with the bundled **PostgreSQL** service, uncomment the two `XUI_DB_*`
+lines in `docker-compose.yml` and start with the profile:
+
+```bash
+docker compose --profile postgres up -d
+```
+
+Prefer the prebuilt image? It's published to the GitHub Container Registry. The
+image bundles Fail2ban (for [IP limits](/docs/operations/security)), which bans
+with `iptables` and therefore needs `NET_ADMIN` (and `NET_RAW` for IPv6) —
+otherwise bans are logged but never applied:
+
+```bash title="docker run"
+docker run -d \
+  --cap-add=NET_ADMIN \
+  --cap-add=NET_RAW \
+  -e XUI_ENABLE_FAIL2BAN=true \
+  -v $PWD/db/:/etc/x-ui/ \
+  -v $PWD/cert/:/root/cert/ \
+  --network=host \
+  --restart=unless-stopped \
+  --name 3x-ui \
+  ghcr.io/mhsanaei/3x-ui:latest
+```
+
+The `db/` volume holds the SQLite database (`/etc/x-ui/x-ui.db`) and `cert/`
+holds TLS certificates, so your data survives upgrades.
+
+</Tab>
+
+<Tab value="Manual">
+
+For advanced users, download a release archive for your architecture from the
+[releases page](https://github.com/MHSanaei/3x-ui/releases), extract it, and run
+the binary as a systemd service. The install script automates exactly these
+steps, so it's preferred unless you have a specific reason to install by hand.
+
+</Tab>
+
+</Tabs>
+
+## Build your install command
+
+Tailor the command to your setup:
+
+<InstallCommandBuilder />
+
+## Choose a database
+
+You pick the storage backend at install time:
+
+- **SQLite** (default) — a single file at `/etc/x-ui/x-ui.db`. Zero setup.
+- **PostgreSQL** — for high client counts or multi-node setups. The installer
+  can install it locally or use a DSN you provide.
+
+See [Database](/docs/reference/database) for details and SQLite→PostgreSQL
+migration.
+
+## Unattended / cloud-init
+
+The installer also runs **non-interactively** for automation. Set
+`XUI_NONINTERACTIVE=1` (or run with no TTY) and it installs end-to-end with zero
+prompts, generating random credentials and writing them to
+`/etc/x-ui/install-result.env`:
+
+```bash
+XUI_NONINTERACTIVE=1 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
+```
+
+The repo's [`deploy/`](https://github.com/MHSanaei/3x-ui/tree/main/deploy)
+directory has ready-made **cloud-init** user-data for unattended installs on any
+cloud (Hetzner, AWS, DigitalOcean, Vultr, GCP, Azure, Oracle).
+
+## Next steps
+
+<Cards>
+  <Card
+    title="First login"
+    href="/docs/guide/first-login"
+    description="Reach the panel and secure it."
+  />
+  <Card
+    title="Update & uninstall"
+    href="/docs/guide/update-uninstall"
+    description="The x-ui menu, updates, and removal."
+  />
+  <Card
+    title="REALITY"
+    href="/docs/config/reality"
+    description="Configure your first stealthy inbound."
+  />
+</Cards>

+ 5 - 0
docs/content/docs/en/guide/meta.json

@@ -0,0 +1,5 @@
+{
+  "title": "Getting Started",
+  "icon": "Rocket",
+  "pages": ["index", "installation", "first-login", "update-uninstall"]
+}

+ 99 - 0
docs/content/docs/en/guide/update-uninstall.mdx

@@ -0,0 +1,99 @@
+---
+title: Update & Uninstall
+description: Manage 3x-ui with the x-ui menu and CLI — update (stable, dev, or legacy), change settings, and uninstall cleanly.
+icon: RefreshCw
+---
+
+After a script install, the `x-ui` command is your control center. Run it with
+no arguments for the interactive menu, or pass a subcommand for a one-shot action.
+
+```bash
+x-ui
+```
+
+## The `x-ui` management menu
+
+The menu (items `0`–`28`) shows the panel/Xray status at the top, then:
+
+| #     | Item                                   | What it does                                              |
+| ----- | -------------------------------------- | --------------------------------------------------------- |
+| 1     | Install                                | (Re)install from the remote script                        |
+| 2     | Update                                 | Update to the latest **stable** release                   |
+| 3     | Update to Dev Channel (latest commit)  | Update to the rolling `dev-latest` build                  |
+| 4     | Update Menu                            | Update just the `x-ui` menu script                        |
+| 5     | Legacy Version                         | Install a specific older version (prompts for a tag)      |
+| 6     | Uninstall                              | Remove 3x-ui (see below)                                  |
+| 7     | Reset Username & Password              | Set new credentials; optionally disable 2FA               |
+| 8     | Reset Web Base Path                    | Randomize the web base path                               |
+| 9     | Reset Settings                         | Reset panel settings (your account is preserved)          |
+| 10    | Change Port                            | Change the panel port                                     |
+| 11    | View Current Settings                  | Show username, port, web base path, cert paths            |
+| 12–14 | Start / Stop / Restart                 | Control the panel service                                 |
+| 15    | Restart Xray                           | Reload only Xray-core                                     |
+| 16    | Check Status                           | Service status                                            |
+| 17    | Logs Management                        | View debug logs / clear logs                              |
+| 18–19 | Enable / Disable Autostart             | Toggle start-on-boot                                      |
+| 20    | SSL Certificate Management             | Let's Encrypt (domain or IP), custom paths, renew/revoke  |
+| 21    | Cloudflare SSL Certificate             | DNS-01 wildcard cert via Cloudflare                       |
+| 22    | IP Limit Management                    | Fail2ban-based per-client IP limits                       |
+| 23    | Firewall Management                    | `ufw` install and port rules                              |
+| 24    | SSH Port Forwarding Management         | Bind the panel to localhost and tunnel over SSH           |
+| 25    | PostgreSQL Management                  | Install/migrate/manage PostgreSQL                         |
+| 26    | Enable BBR                             | Toggle the BBR congestion-control sysctl                  |
+| 27    | Update Geo Files                       | Update geoip/geosite data (Loyalsoldier, IR, RU)          |
+| 28    | Speedtest by Ookla                     | Run an Ookla speed test                                   |
+| 0     | Exit                                   | —                                                         |
+
+Some of these have their own pages: [SSL certificates](/docs/config/ssl-certificates)
+(items 20–21), [Security](/docs/operations/security) (IP limits, firewall),
+[Reverse proxy](/docs/operations/reverse-proxy) and [Panel settings](/docs/config/panel)
+(TLS), and [Database](/docs/reference/database) (PostgreSQL).
+
+## CLI subcommands
+
+For scripts and quick actions, `x-ui` also takes a subcommand directly:
+
+| Command                    | Action                                              |
+| -------------------------- | --------------------------------------------------- |
+| `x-ui start` / `stop` / `restart` | Control the service                          |
+| `x-ui restart-xray`        | Reload only Xray-core                               |
+| `x-ui status`              | Show status                                         |
+| `x-ui settings`            | Show current settings                               |
+| `x-ui enable` / `disable`  | Toggle autostart on boot                            |
+| `x-ui log`                 | Tail the debug log                                  |
+| `x-ui banlog`              | Show Fail2ban ban log                               |
+| `x-ui update`              | Update to the latest stable release                 |
+| `x-ui update-dev`          | Update to the rolling `dev-latest` build            |
+| `x-ui legacy`              | Install a specific older version (prompts)          |
+| `x-ui update-all-geofiles` | Update all geo files, restart if changed            |
+| `x-ui migrate-db --dsn …`  | Migrate SQLite → PostgreSQL (see [Database](/docs/reference/database)) |
+| `x-ui install` / `uninstall` | Install / uninstall                               |
+
+## Updating
+
+- **Stable:** menu option **2** or `x-ui update`. Re-running the install script
+  also updates in place.
+- **Dev channel:** menu option **3** or `x-ui update-dev` — the rolling
+  `dev-latest` per-commit build (not a stable release).
+- **A specific older version:** menu option **5** (Legacy Version).
+
+Updating preserves your database and settings. Take a
+[backup](/docs/operations/backup-restore) before a major-version jump.
+
+<Callout type="info">
+  Docker users update differently — pull the new image and recreate the
+  container (`docker compose pull && docker compose up -d`) rather than using the
+  `x-ui` update commands.
+</Callout>
+
+## Uninstalling
+
+Menu option **6** or `x-ui uninstall`. It stops and disables the service, removes
+the service unit, and deletes `/etc/x-ui/` and the install folder. If the panel
+used a locally-installed PostgreSQL, it offers to purge that too (a separate,
+irreversible confirmation).
+
+<Callout type="warn">
+  Uninstalling removes the database (`/etc/x-ui/x-ui.db`) and your configuration.
+  [Back up](/docs/operations/backup-restore) first if you might need it.
+</Callout>

+ 64 - 0
docs/content/docs/en/help/contributing.mdx

@@ -0,0 +1,64 @@
+---
+title: Contributing
+description: How to contribute to 3x-ui and to translate this documentation.
+icon: GitPullRequestArrow
+---
+
+3x-ui is community-driven. Contributions to the panel and to these docs are
+welcome.
+
+## Support the developer
+
+3x-ui is free and open source, built and maintained in the open. If it's useful
+to you, consider supporting continued development:
+
+- **Donate** at [donate.sanaei.dev](https://donate.sanaei.dev/) — the page shows
+  current funding **goals and targets** you can help reach.
+- **Star** the [repository](https://github.com/MHSanaei/3x-ui) and share the
+  project.
+- **Join** the Telegram channel [@XrayUI](https://t.me/XrayUI) to follow news and
+  help others.
+
+## Contribute to 3x-ui
+
+- Read the project's `CONTRIBUTING.md` in the
+  [repository](https://github.com/MHSanaei/3x-ui).
+- Open issues for bugs and feature requests with clear reproduction steps.
+- Use Conventional Commits and keep pull requests focused.
+
+## Translate the documentation
+
+This site is built for translation. Content lives under `content/docs/<locale>/`
+(`en`, `fa`, `ru`, `zh`), and untranslated pages **fall back to English**, so you
+can translate incrementally.
+
+<Steps>
+
+<Step>
+### Copy a page
+
+Copy a page from `content/docs/en/...` to the same path under your locale, e.g.
+`content/docs/fa/guide/installation.mdx`.
+</Step>
+
+<Step>
+### Translate the prose only
+
+Translate the body and the frontmatter `title`/`description`. **Do not** translate
+code, commands, environment variable names, protocol names, or share links.
+</Step>
+
+<Step>
+### Mind direction
+
+Persian (`fa`) renders right-to-left. Keep code blocks and links left-to-right
+(the layout already handles this), and check the page in both directions.
+</Step>
+
+</Steps>
+
+<Callout type="info">
+  Missing a page in your locale is fine — it falls back to English rather than
+  404ing. Translate the highest-traffic pages first (installation, first login,
+  REALITY).
+</Callout>

+ 47 - 0
docs/content/docs/en/help/faq.mdx

@@ -0,0 +1,47 @@
+---
+title: FAQ
+description: Frequently asked questions about 3x-ui — licensing, supported systems, databases, and clients.
+icon: CircleQuestionMark
+---
+
+## Is 3x-ui free and open source?
+
+Yes. 3x-ui is open source under the GPL-3.0 license. The source is on
+[GitHub](https://github.com/MHSanaei/3x-ui).
+
+## What systems does it run on?
+
+Most major Linux distributions (Ubuntu, Debian, CentOS/RHEL and derivatives,
+Fedora, Arch, Alpine, and more) across `amd64`, `arm64`, and other architectures.
+It also runs as a Docker container. See [Installation](/docs/guide/installation).
+
+## SQLite or PostgreSQL?
+
+SQLite is the default and is fine for most deployments. PostgreSQL is available
+for larger setups via `XUI_DB_TYPE=postgres` and `XUI_DB_DSN` — see the
+[environment variables](/docs/reference/env-vars).
+
+## Which client apps work with it?
+
+Any Xray-compatible client — for example v2rayNG, Hiddify, and Clash/Mihomo.
+Import a client's share link or QR code, or use a
+[subscription](/docs/config/subscription).
+
+## How is 3x-ui different from x-ui?
+
+3x-ui is an enhanced fork of the original X-UI project. It adds broader protocol
+support, improved stability, per-client traffic accounting, multi-node
+management, a 13-language UI, and many quality-of-life features.
+
+## How do I update?
+
+Re-run the install script (it updates in place), or pull the new Docker image.
+See [Installation](/docs/guide/installation) and the
+[releases page](https://github.com/MHSanaei/3x-ui/releases).
+
+## Where can I get help?
+
+Join the official Telegram channel [@XrayUI](https://t.me/XrayUI) for
+announcements and community support, or open an issue on
+[GitHub](https://github.com/MHSanaei/3x-ui/issues). For common problems, start
+with [Troubleshooting](/docs/help/troubleshooting).

+ 5 - 0
docs/content/docs/en/help/meta.json

@@ -0,0 +1,5 @@
+{
+  "title": "Help",
+  "icon": "LifeBuoy",
+  "pages": ["troubleshooting", "faq", "migration", "contributing"]
+}

+ 52 - 0
docs/content/docs/en/help/migration.mdx

@@ -0,0 +1,52 @@
+---
+title: Migration
+description: Migrate to 3x-ui from x-ui, between servers, or between 3x-ui versions.
+icon: ArrowRightLeft
+---
+
+## Between servers
+
+Moving to a new server is a database move:
+
+<Steps>
+
+<Step>
+### Back up the old server
+
+Copy the database (default `/etc/x-ui/x-ui.db`) and your certificates. See
+[Backup & restore](/docs/operations/backup-restore).
+</Step>
+
+<Step>
+### Install 3x-ui on the new server
+
+Use the same install method and a compatible version. See
+[Installation](/docs/guide/installation).
+</Step>
+
+<Step>
+### Restore the database
+
+Stop the panel, put the database in place, restore certificates, and start the
+panel. Update any IP/domain-specific settings.
+</Step>
+
+</Steps>
+
+## Between 3x-ui versions
+
+Within the same backend, upgrading is usually just re-running the installer or
+pulling a new image — the panel migrates its own database. Across **major**
+versions, read the [release notes](https://github.com/MHSanaei/3x-ui/releases)
+first and take a backup.
+
+## From x-ui
+
+Importing an older x-ui panel's database directly into 3x-ui is **not
+supported** — the schemas differ. Install 3x-ui fresh and recreate
+inbounds/clients, exporting share links from the old panel as you go.
+
+<Callout type="warn">
+  Always take a backup before any migration, and keep the old server running
+  until you've verified the new one.
+</Callout>

+ 42 - 0
docs/content/docs/en/help/troubleshooting.mdx

@@ -0,0 +1,42 @@
+---
+title: Troubleshooting
+description: Fix common 3x-ui problems — the panel won't start, 502 errors, certificate issues, and clients that can't connect.
+icon: Wrench
+---
+
+A checklist for the most common issues. When in doubt, raise the log level
+(`XUI_LOG_LEVEL=debug`) and check the panel and Xray logs.
+
+## The panel won't start
+
+- Check the service status and logs (`x-ui` menu → status/logs).
+- Make sure the **panel port isn't already in use** by another service.
+- Verify the database path is writable (default `/etc/x-ui/x-ui.db`).
+
+## 502 / panel unreachable behind a proxy
+
+- Confirm the panel is actually listening on the upstream port (e.g. `2053`).
+- Ensure your [reverse proxy](/docs/operations/reverse-proxy) passes WebSocket
+  upgrade headers and points at the correct port and **web base path**.
+- Check the firewall isn't blocking the proxy → panel connection.
+
+## Certificate problems
+
+- The domain's DNS must point at the server before issuing a certificate.
+- Ports 80/443 must be reachable for HTTP/TLS validation (or use DNS validation).
+- For REALITY, remember it needs **no certificate** — the issue is usually a bad
+  `dest`/SNI instead (see [REALITY pitfalls](/docs/config/reality)).
+
+## A client can't connect
+
+- Decode the client's link with the
+  [share-link inspector](/docs/config/share-links) and confirm every parameter.
+- Check the **transport and security match** on both ends.
+- Make sure the client hasn't hit its **traffic, expiry, or IP limit**.
+- Confirm the inbound port is open in the firewall.
+
+<Callout type="info">
+  Still stuck? Search the
+  [GitHub issues](https://github.com/MHSanaei/3x-ui/issues) — your symptom has
+  probably been seen before.
+</Callout>

+ 54 - 0
docs/content/docs/en/index.mdx

@@ -0,0 +1,54 @@
+---
+title: 3x-ui Documentation
+description: Official documentation for 3x-ui — an advanced web panel for managing Xray-core servers, proxies, clients, and subscriptions.
+icon: House
+---
+
+**3x-ui** is an open-source web panel for deploying and managing
+[Xray-core](https://github.com/XTLS/Xray-core) servers. It gives you a modern
+dashboard for inbounds, protocols, clients, traffic accounting, subscriptions,
+and more — without hand-editing Xray JSON on the command line.
+
+This site is the official documentation and a set of **interactive, in-browser
+tools** (config generators) that run entirely on your device — no data ever
+leaves the page.
+
+## Start here
+
+<Cards>
+  <Card
+    title="What is 3x-ui?"
+    href="/docs/guide"
+    description="The big picture: Xray-core, the panel, and who it's for."
+  />
+  <Card
+    title="Installation"
+    href="/docs/guide/installation"
+    description="Install via script, Docker, or manually."
+  />
+  <Card
+    title="First login"
+    href="/docs/guide/first-login"
+    description="Reach the panel and harden it before anything else."
+  />
+  <Card
+    title="REALITY"
+    href="/docs/config/reality"
+    description="Set up VLESS + REALITY with XTLS-Vision."
+  />
+</Cards>
+
+## Highlights
+
+- **Every major protocol** — VLESS, VMess, Trojan, Shadowsocks, WireGuard,
+  Hysteria2, SOCKS, HTTP, and Dokodemo-door.
+- **REALITY & XTLS-Vision** — modern, censorship-resistant transports.
+- **Per-client controls** — traffic quotas, expiry dates, IP limits, share
+  links, and QR codes.
+- **Subscriptions** — VLESS, Clash/Mihomo, and JSON formats.
+- **Operations** — multi-node management, Telegram bot, backups, and a REST API.
+
+<Callout type="info">
+  New to Xray? Read [What is 3x-ui?](/docs/guide) first — it explains how the panel, Xray-core, and
+  your client apps fit together.
+</Callout>

+ 3 - 0
docs/content/docs/en/meta.json

@@ -0,0 +1,3 @@
+{
+  "pages": ["index", "guide", "config", "operations", "reference", "help"]
+}

+ 57 - 0
docs/content/docs/en/operations/backup-restore.mdx

@@ -0,0 +1,57 @@
+---
+title: Backup & Restore
+description: Back up and restore your 3x-ui database and certificates, manually or via the Telegram bot.
+icon: DatabaseBackup
+---
+
+Your entire configuration — inbounds, clients, settings — lives in the panel's
+database. Back it up regularly so you can recover or migrate.
+
+## What to back up
+
+- **The database** — SQLite at `/etc/x-ui/x-ui.db` by default (or your
+  PostgreSQL database if you use that backend).
+- **Certificates** — anything under `/root/cert/` (or wherever you store TLS
+  certs).
+
+## Manual backup
+
+You can download a backup from the panel's overview, or copy the database file
+directly from the server:
+
+```bash title="copy the SQLite database"
+cp /etc/x-ui/x-ui.db /root/x-ui-backup-$(date +%F).db
+```
+
+To restore, stop the panel, put the database back in place, and start it again.
+
+<Callout type="warn">
+  Restore a backup onto the **same major version** it came from when possible.
+  Across major upgrades, let the panel run its migrations rather than forcing an
+  old schema.
+</Callout>
+
+## Telegram backup
+
+If you've configured the [Telegram bot](/docs/operations/telegram-bot), enable
+**`tgBotBackup`** to attach a backup to the periodic report (on the `tgRunTime`
+schedule, default daily). The bot sends both the **database** and the **Xray
+`config.json`** to your admin chat, so you always have an off-server copy. Admins
+can also request a backup on demand from the bot's menu.
+
+## SQLite dump / restore
+
+The `x-ui migrate-db` command converts the SQLite database to and from a plain
+SQL text dump (handy for inspection or transferring between machines):
+
+```bash
+x-ui migrate-db --dump /root/x-ui.sql       # SQLite -> SQL text
+x-ui migrate-db --restore /root/x-ui.sql     # SQL text -> SQLite
+```
+
+To move to PostgreSQL instead, see [Database](/docs/reference/database).
+
+<Callout type="info">
+  Whatever method you use, store backups **off the server** and test a restore
+  occasionally — an untested backup isn't a backup.
+</Callout>

+ 12 - 0
docs/content/docs/en/operations/meta.json

@@ -0,0 +1,12 @@
+{
+  "title": "Operations",
+  "icon": "ServerCog",
+  "pages": [
+    "reverse-proxy",
+    "multi-node",
+    "outbounds-routing",
+    "backup-restore",
+    "telegram-bot",
+    "security"
+  ]
+}

+ 97 - 0
docs/content/docs/en/operations/multi-node.mdx

@@ -0,0 +1,97 @@
+---
+title: Multi-node & Managed Hosts
+description: Manage multiple 3x-ui panels from one master, with API-token or mTLS trust, heartbeats, and per-inbound host overrides for subscriptions.
+icon: Boxes
+---
+
+3x-ui can manage **multiple servers** from a single master panel, and override
+how each inbound is advertised in subscriptions with **managed hosts**.
+
+## Nodes
+
+A **node** is another 3x-ui panel that your master panel manages over the node's
+API. The master polls each node and shows its status, versions, CPU/memory,
+uptime, and traffic in one place.
+
+### Add a node
+
+Provide the node's connection details:
+
+| Field             | Notes                                                                 |
+| ----------------- | --------------------------------------------------------------------- |
+| **Name**          | Unique label (e.g. `de-fra-1`).                                        |
+| **Scheme**        | `https` (default) or `http`.                                          |
+| **Address / Port**| The node panel's host and port.                                       |
+| **Base path**     | The node's web base path.                                             |
+| **API token**     | A Bearer token created on the node (not needed in mTLS mode).         |
+| **TLS verify**    | `verify` (default), `skip`, `pin` (pin a cert SHA-256), or `mtls`.    |
+| **Inbound sync**  | `all` inbounds, or `selected` by tag.                                 |
+| **Outbound tag**  | Optionally reach the node **through** a named outbound (egress bridge).|
+
+The master verifies reachability when you add or test a node. It then sends a
+**heartbeat** every few seconds, updating the node's status (`online` / `offline`)
+and emitting `node.up` / `node.down` events (see the
+[Telegram bot](/docs/operations/telegram-bot)).
+
+<Callout type="info">
+  Nodes are identified by a stable per-panel GUID, so a node keeps its identity
+  across restarts. A node can itself manage further nodes — the master surfaces
+  those as read-only **transitive** sub-nodes (Node 1 → Node 2 → Node 3).
+</Callout>
+
+### Mutual TLS (mTLS) between master and node
+
+For the strongest trust, use `tlsVerifyMode = mtls` (requires `https`):
+
+<Steps>
+
+<Step>
+### Get the master's CA
+
+On the master, fetch its node-auth CA certificate (the CA private key never
+leaves the panel).
+</Step>
+
+<Step>
+### Trust it on the node
+
+Paste that CA into the node's "trusted CA" setting. It takes effect on the node's
+next restart.
+</Step>
+
+<Step>
+### Switch the node to mTLS
+
+Set the node's TLS verify mode to `mtls`. The master now presents a client
+certificate instead of an API token.
+</Step>
+
+</Steps>
+
+## Managed hosts
+
+A **managed host** is an override endpoint attached to an inbound. At
+subscription time, each enabled host renders an additional share link / proxy
+with its own address, port, TLS, SNI, host header, path, and more — superseding
+the older "external proxy" list. Use them to:
+
+- front an inbound through a **CDN** (Cloudflare) with a different address/SNI,
+- advertise **multiple domains** or per-region endpoints for one inbound,
+- tweak ALPN, fingerprint, ECH, or mux per endpoint.
+
+Each host has a remark (which supports the same
+[template variables](/docs/config/share-links#remark-template-variables)), an
+enable toggle, a sort order, and can be **excluded from specific subscription
+formats** or **scoped to specific nodes**.
+
+<Callout type="info">
+  Hosts whose address/port point at a CDN let you keep the real server address
+  private while clients connect through the CDN edge.
+</Callout>
+
+## Related
+
+<Cards>
+  <Card title="Outbounds & routing" href="/docs/operations/outbounds-routing" description="WARP, NordVPN, outbound subscriptions, and routing." />
+  <Card title="Subscription" href="/docs/config/subscription" description="How hosts shape subscription output." />
+</Cards>

+ 157 - 0
docs/content/docs/en/operations/outbounds-routing.mdx

@@ -0,0 +1,157 @@
+---
+title: Outbounds & Routing
+description: Shape egress in 3x-ui — WARP and NordVPN outbounds, outbound subscriptions (server pools), routing rules, and load balancers.
+icon: Route
+---
+
+Inbounds accept clients; **outbounds** decide where their traffic goes next.
+3x-ui can route traffic through Cloudflare WARP, NordVPN, or arbitrary outbound
+pools imported from a subscription, and select between them with routing rules
+and balancers.
+
+## Editing outbounds & routing
+
+Outbounds, routing rules, balancers, DNS, and logging all live in the **Xray
+configuration** (the config template you edit under Xray Settings). There's no
+separate per-rule UI — you edit the JSON, and the panel reloads Xray. The panel
+also offers an **outbound connectivity test** and a **route test** (ask the
+running core which outbound a given destination would use).
+
+## Build an outbound
+
+Every outbound is a JSON object with up to four parts: a **`tag`** (referenced by
+routing rules and balancers), a **`protocol`**, protocol-specific **`settings`**,
+and — for proxy protocols — **`streamSettings`** that must match the remote
+inbound's transport and security. Two outbounds are almost always present:
+
+- **`freedom`** sends traffic straight to its destination — the default egress.
+  Optionally set a `domainStrategy` (e.g. `UseIP`) to control how hostnames resolve.
+- **`blackhole`** drops traffic. Route unwanted destinations (ads, torrents) here.
+
+```json title="freedom + blackhole"
+{
+  "outbounds": [
+    { "tag": "direct", "protocol": "freedom", "settings": {} },
+    { "tag": "block", "protocol": "blackhole", "settings": {} }
+  ]
+}
+```
+
+A **proxy** outbound (VLESS, VMess, Trojan, Shadowsocks) forwards to another
+server — handy for chaining or sending select traffic abroad. Mind the wire
+shapes 3x-ui uses: **VLESS is the flat form** (`address`/`port`/`id`/`flow`/
+`encryption`), **VMess uses `settings.vnext[]`**, and **Trojan/Shadowsocks use
+`settings.servers[]`**. The `streamSettings` must mirror the destination's
+[transport and security](/docs/config/transports).
+
+Assemble any outbound below and paste the JSON into **Xray Settings → Outbounds**:
+
+<OutboundGenerator />
+
+## Cloudflare WARP
+
+WARP lets your server egress through Cloudflare's network. 3x-ui can register a
+WARP account for you and wire it into a WireGuard outbound tagged **`warp`**:
+
+<Steps>
+
+<Step>
+### Add a `warp`-tagged outbound
+
+Create a WireGuard outbound with the tag `warp` in your Xray config.
+</Step>
+
+<Step>
+### Register WARP
+
+From the panel's WARP controls, register an account. 3x-ui fills the outbound's
+keys, addresses, reserved bytes, and peer endpoint automatically.
+</Step>
+
+<Step>
+### (Optional) auto-rotate the IP
+
+Set a WARP update interval (in **days**) to periodically rotate the WARP IP. A
+free license can also be applied.
+</Step>
+
+</Steps>
+
+Route the traffic you want (for example specific domains) to the `warp` outbound
+with a routing rule.
+
+## NordVPN
+
+3x-ui can fetch NordVPN (NordLynx/WireGuard) credentials from an access token (or
+accept a private key directly) and list countries/servers, so you can build a
+NordVPN outbound.
+
+## Outbound subscriptions (server pools)
+
+An **outbound subscription** imports a remote share-link subscription and injects
+its servers as **outbounds** into the running Xray config — without touching your
+saved template. This is the recommended way to subscribe to a *pool* of servers.
+
+| Field            | Default | Meaning                                                        |
+| ---------------- | ------- | -------------------------------------------------------------- |
+| `url`            | —       | The remote subscription URL (SSRF-guarded).                    |
+| `tagPrefix`      | auto    | Prefix for generated outbound tags (e.g. `hk-`); blank = `subN-`. |
+| `updateInterval` | `600`   | Refresh interval in **seconds**.                               |
+| `prepend`        | `false` | Place these outbounds before your manual ones.                 |
+| `priority`       | `0`     | Merge order (lower first).                                     |
+
+Imported outbounds get **stable tags**: the same server keeps the same tag across
+refreshes, so exact-tag routing/balancer selectors stay pinned — while
+prefix/wildcard selectors (e.g. `hk-*`) automatically pick up new servers as the
+pool changes. Supported link schemes: `vmess`, `vless`, `trojan`, `ss`,
+`hysteria2` (`hy2`), and `wireguard` (`wg`). The panel refreshes enabled
+subscriptions on a timer and reloads Xray when something changes.
+
+## Routing rules
+
+**Routing rules** decide which outbound (or balancer) each connection uses. Each
+rule is a `field`-type matcher: set any of `domain`, `ip`, `port`, `network`,
+`protocol`, `inboundTag`, `sourceIP`, … and point it at an **`outboundTag`** or a
+**`balancerTag`**. Rules are evaluated **top-to-bottom — the first match wins**, so
+put specific rules above general ones.
+
+```json title="route ads to blackhole, private IPs direct"
+{
+  "routing": {
+    "domainStrategy": "IPIfNonMatch",
+    "rules": [
+      { "type": "field", "domain": ["geosite:category-ads-all"], "outboundTag": "block" },
+      { "type": "field", "ip": ["geoip:private"], "outboundTag": "direct" }
+    ]
+  }
+}
+```
+
+## Balancers
+
+A **balancer** groups outbounds by a **selector** (tag prefixes, including the
+wildcard pools from outbound subscriptions) and spreads or fails traffic over them
+with a **strategy**:
+
+| Strategy      | Picks…                                          | Needs a monitor          |
+| ------------- | ----------------------------------------------- | ------------------------ |
+| `random`      | a random member per connection                  | no                       |
+| `roundRobin`  | members in rotation                             | no                       |
+| `leastPing`   | the lowest-latency member                       | **`observatory`**        |
+| `leastLoad`   | the most stable member by sampled load          | **`burstObservatory`**   |
+
+Reference a balancer from a rule via `balancerTag`. `leastPing` and `leastLoad`
+need a health monitor, which Xray places at the **top level** of the config
+(`observatory` / `burstObservatory`, **not** inside `routing`). The panel can
+report balancer status and **override** a balancer to a specific outbound for
+testing.
+
+Build the routing block — rules, balancers, and the matching observatory — here:
+
+<RoutingBuilder />
+
+<Callout type="warn">
+  Outbounds that reach external services are fetched with SSRF protection — by
+  default private/internal addresses are blocked unless you explicitly allow them
+  per source.
+</Callout>

+ 51 - 0
docs/content/docs/en/operations/reverse-proxy.mdx

@@ -0,0 +1,51 @@
+---
+title: Reverse Proxy
+description: Put the 3x-ui panel and subscription behind Nginx or Caddy with Let's Encrypt TLS.
+icon: Waypoints
+---
+
+A reverse proxy lets you serve the panel and subscription on a clean domain with
+automatic HTTPS, and hide the real ports behind ports 80/443. Prefer to let the
+panel terminate TLS directly? Get a certificate with the
+[`x-ui` SSL menu](/docs/config/ssl-certificates) instead.
+
+## Generate a config
+
+<ReverseProxyGenerator />
+
+<Callout type="info">
+  The panel uses WebSockets for live updates, so the proxy must pass the
+  `Upgrade`/`Connection` headers (the Nginx output above already does). Caddy
+  handles WebSocket upgrades automatically.
+</Callout>
+
+## Nginx + certificate
+
+With Nginx, obtain a certificate with `certbot` (or `acme.sh`) and reference it
+in the server block:
+
+```bash title="certbot"
+certbot certonly --nginx -d panel.example.com
+```
+
+Reload Nginx after installing the certificate, and set up automatic renewal
+(`certbot renew` runs on a timer by default).
+
+## Caddy
+
+Caddy obtains and renews certificates for you — point a Caddyfile at the panel
+and it just works:
+
+```text title="Caddyfile"
+panel.example.com {
+    reverse_proxy 127.0.0.1:2053
+}
+```
+
+## Tips
+
+- Keep the panel's **web base path** even behind a proxy; defense in depth.
+- If you terminate TLS at the proxy, you may want `XUI_SKIP_HSTS=true` on the
+  panel — see the [environment variables reference](/docs/reference/env-vars).
+- Proxy the [subscription](/docs/config/subscription) server too, so its contents
+  are served over HTTPS.

+ 66 - 0
docs/content/docs/en/operations/security.mdx

@@ -0,0 +1,66 @@
+---
+title: Security
+description: Harden 3x-ui — panel auth and 2FA, Fail2ban IP limits, firewall rules, BBR, and staying current.
+icon: ShieldCheck
+---
+
+A proxy panel is a high-value target. A few layers of hardening go a long way.
+
+## Panel hardening
+
+- Strong, unique credentials and **two-factor auth** (TOTP).
+- A non-default panel port and a long, random web base path.
+- TLS on the panel (directly or via a [reverse proxy](/docs/operations/reverse-proxy)).
+- The built-in **login limiter** blocks an IP/username after 5 failed attempts
+  in 5 minutes (for 15 minutes), and the panel can route its own egress through
+  an outbound.
+
+See [First login](/docs/guide/first-login) for the full checklist.
+
+## Fail2ban & IP limits
+
+Set an **IP limit** per client (see [Clients](/docs/config/clients)) to cap the
+number of simultaneous source IPs. Enforcement is handled by **Fail2ban**, which
+3x-ui installs and configures for you (enabled by default on script installs, and
+on Docker via `XUI_ENABLE_FAIL2BAN=true`).
+
+Manage it from the `x-ui` menu (**22 — IP Limit Management**): install/configure,
+change the ban duration (default **30 minutes**), ban/unban an IP, view ban logs,
+and check status. Under the hood:
+
+- The jail is named **`3x-ipl`**; ban logs live at `/var/log/x-ui/3xipl.log` and
+  `/var/log/x-ui/3xipl-banned.log` (also via `x-ui banlog`).
+- Bans cover all TCP/UDP **except** your SSH and panel ports, so a ban can't lock
+  you out of the server or panel.
+
+<Callout type="warn">
+  On Docker, Fail2ban bans with `iptables`, which needs the `NET_ADMIN` (and
+  `NET_RAW`) capability — `docker-compose.yml` grants them. With a bare
+  `docker run`, add `--cap-add=NET_ADMIN --cap-add=NET_RAW` or bans are logged
+  but never applied.
+</Callout>
+
+## Firewall
+
+Open only the ports you actually use: SSH, the panel port, the subscription
+port, and your inbound ports. The `x-ui` menu (**23 — Firewall Management**) wraps
+`ufw`, or generate rules here:
+
+<FirewallRulesGenerator />
+
+<Callout type="warn">
+  Make sure SSH stays allowed before enabling a default-deny firewall, or you can
+  lock yourself out. Test with a second session open.
+</Callout>
+
+## Network tuning (BBR)
+
+The `x-ui` menu (**26 — Enable BBR**) toggles Google's BBR congestion control
+(`net.ipv4.tcp_congestion_control = bbr`, `net.core.default_qdisc = fq`), which
+often improves throughput on congested links.
+
+## Keep current
+
+Update 3x-ui and Xray-core regularly — security fixes land in new releases. Watch
+the [releases page](https://github.com/MHSanaei/3x-ui/releases) and see
+[Update & uninstall](/docs/guide/update-uninstall).

+ 112 - 0
docs/content/docs/en/operations/telegram-bot.mdx

@@ -0,0 +1,112 @@
+---
+title: Telegram Bot
+description: Connect a Telegram bot to 3x-ui for commands, periodic reports, event alerts (login, CPU, node up/down), backups, and client self-service.
+icon: Send
+---
+
+3x-ui can drive a Telegram bot for monitoring, alerts, backups, and remote
+management. Admins get full control; regular users (linked by Telegram ID) can
+check their own usage and links.
+
+<Callout type="info">
+  Looking for news and community support? Join the official Telegram channel
+  [@XrayUI](https://t.me/XrayUI). That's separate from the bot below, which you
+  run yourself to manage your own panel.
+</Callout>
+
+## Set it up
+
+<Steps>
+
+<Step>
+### Create a bot
+
+Message [@BotFather](https://t.me/BotFather), send `/newbot`, and copy the **bot
+token**.
+</Step>
+
+<Step>
+### Find your Telegram ID
+
+Get your numeric Telegram user ID (the bot's own `/id` command reports it once
+connected). This is your **admin** ID.
+</Step>
+
+<Step>
+### Configure the panel
+
+In Panel Settings, enable the Telegram bot and set the **token** and **admin chat
+ID(s)** (comma-separated). Save, then message your bot.
+</Step>
+
+</Steps>
+
+Validate your token, admin IDs, and report schedule before pasting them into the
+panel:
+
+<TelegramSetupHelper />
+
+## Commands
+
+These appear in the Telegram command menu: `/start`, `/help`, `/status`, `/id`.
+Additional commands:
+
+| Command            | Who    | Action                                                       |
+| ------------------ | ------ | ------------------------------------------------------------ |
+| `/start`, `/help`  | anyone | Greeting and the menu of inline buttons                      |
+| `/status`          | anyone | Confirm the bot is alive                                     |
+| `/id`              | anyone | Show your Telegram numeric ID                                |
+| `/usage <arg>`     | both   | Admins search clients; users look up their own usage         |
+| `/inbound <remark>`| admin  | Show an inbound's details                                    |
+| `/restart`         | admin  | Restart Xray                                                 |
+
+Admins also get inline-button flows for server usage, sorted traffic reports,
+resetting traffic, DB backups, ban logs, listing inbounds/clients, online
+clients, "depleting soon", and a full **add-client** wizard. Regular users get
+buttons for their own usage, subscription links, individual links, and QR codes.
+
+## Reports & alerts
+
+- **Periodic report** — on the `tgRunTime` schedule (default `@daily`), the bot
+  sends admins server usage (host, versions, uptime, load, memory, online
+  clients, traffic), a list of exhausted/expiring clients, and — if
+  `tgBotBackup` is on — a database + Xray config backup. Clients linked by
+  Telegram ID get their own expiry/quota warnings.
+- **Event alerts** — selected by `tgEnabledEvents` (default `login.attempt,cpu.high`):
+
+  | Event           | When                                                    |
+  | --------------- | ------------------------------------------------------- |
+  | `login.attempt` | A panel login succeeds or fails (with IP and username)  |
+  | `cpu.high`      | CPU exceeds `tgCpu` percent (default 80)                |
+  | `memory.high`   | Memory exceeds `tgMemory` percent (default 80)          |
+  | `xray.crash`    | Xray-core crashes                                       |
+  | `outbound.down` / `outbound.up` | An outbound goes down / recovers        |
+  | `node.down` / `node.up`         | A node goes offline / comes back        |
+
+The warning lead times come from `expireDiff` (days before expiry) and
+`trafficDiff` (GB of quota remaining); both default to `0` (off).
+
+## Settings
+
+| Setting        | Default                    | Meaning                                        |
+| -------------- | -------------------------- | ---------------------------------------------- |
+| `tgBotEnable`  | `false`                    | Master on/off.                                 |
+| `tgBotToken`   | _(secret)_                 | Bot API token.                                 |
+| `tgBotChatId`  | _(none)_                   | Comma-separated **admin** Telegram IDs.        |
+| `tgBotProxy`   | _(none)_                   | `socks5://`, `http://`, or `https://` proxy.   |
+| `tgBotAPIServer` | _(default)_              | Custom Telegram Bot API server.                |
+| `tgRunTime`    | `@daily`                   | Report schedule (cron / `@daily` / `@every …`).|
+| `tgBotBackup`  | `false`                    | Attach a DB backup to the periodic report.     |
+| `tgCpu` / `tgMemory` | `80` / `80`          | CPU / memory alert thresholds (percent).       |
+| `tgLang`       | `en-US`                    | Bot language.                                  |
+| `tgEnabledEvents` | `login.attempt,cpu.high`| Which events to deliver.                       |
+
+<Callout type="warn">
+  The bot token controls your bot — keep it secret and only add **trusted** admin
+  chat IDs. Login alerts never include passwords.
+</Callout>
+
+<Callout type="info">
+  Email (SMTP) notifications mirror the same events (`smtpEnabledEvents`) if you'd
+  rather receive alerts by email — configure SMTP in Panel Settings.
+</Callout>

+ 79 - 0
docs/content/docs/en/reference/api/api-tokens.mdx

@@ -0,0 +1,79 @@
+---
+title: API Tokens
+description: >-
+  Manage Bearer tokens used for programmatic auth (bots, central panels acting
+  on this node, CI). Each token has a unique name and an enabled flag — disable
+  to revoke without deleting, delete to revoke permanently. Tokens are stored as
+  SHA-256 hashes and the plaintext is returned only once, in the create response
+  — it cannot be retrieved afterwards, so copy it then. Send one as
+  <code>Authorization: Bearer &lt;token&gt;</code> on any /panel/api/* request —
+  the token is a full-admin credential.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        List every API token, enabled or not. The token value is never returned
+        — only metadata.
+      url: >-
+        #list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
+    - depth: 2
+      title: >-
+        Mint a new API token. Name must be unique and 1-64 characters; the token
+        string is server-generated and returned only in this response — it is
+        stored hashed and cannot be retrieved later.
+      url: >-
+        #mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
+    - depth: 2
+      title: >-
+        Permanently delete a token. Any caller using it stops authenticating
+        immediately.
+      url: >-
+        #permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
+    - depth: 2
+      title: >-
+        Toggle a token enabled/disabled without deleting it. Disabled tokens are
+        rejected by checkAPIAuth on the next request.
+      url: >-
+        #toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
+  structuredData:
+    headings:
+      - content: >-
+          List every API token, enabled or not. The token value is never
+          returned — only metadata.
+        id: >-
+          list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
+      - content: >-
+          Mint a new API token. Name must be unique and 1-64 characters; the
+          token string is server-generated and returned only in this response —
+          it is stored hashed and cannot be retrieved later.
+        id: >-
+          mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
+      - content: >-
+          Permanently delete a token. Any caller using it stops authenticating
+          immediately.
+        id: >-
+          permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
+      - content: >-
+          Toggle a token enabled/disabled without deleting it. Disabled tokens
+          are rejected by checkAPIAuth on the next request.
+        id: >-
+          toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/setting/apiTokens","method":"get"},{"path":"/panel/api/setting/apiTokens/create","method":"post"},{"path":"/panel/api/setting/apiTokens/delete/{id}","method":"post"},{"path":"/panel/api/setting/apiTokens/setEnabled/{id}","method":"post"}]} showTitle />
+    </>
+  );
+}

+ 74 - 0
docs/content/docs/en/reference/api/authentication.mdx

@@ -0,0 +1,74 @@
+---
+title: Authentication
+description: >-
+  Two authentication modes are supported. UI sessions use a cookie set by the
+  login endpoint. Programmatic clients (bots, scripts, remote panels)
+  authenticate with a Bearer token taken from Settings → Security → API Token.
+  Both work for every endpoint under /panel/api/*.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        Authenticate with username + password and receive a session cookie.
+        Required before any cookie-based API call.
+      url: >-
+        #authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
+    - depth: 2
+      title: Clear the session cookie. Requires the CSRF header for browser sessions.
+      url: '#clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions'
+    - depth: 2
+      title: >-
+        Mint a CSRF token for the current session. The SPA replays it in the
+        X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
+        this — the middleware short-circuits CSRF for authenticated API
+        requests.
+      url: >-
+        #mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests
+    - depth: 2
+      title: >-
+        Returns whether 2FA is enabled on the panel — used by the login page to
+        decide whether to show the OTP field.
+      url: >-
+        #returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
+  structuredData:
+    headings:
+      - content: >-
+          Authenticate with username + password and receive a session cookie.
+          Required before any cookie-based API call.
+        id: >-
+          authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
+      - content: >-
+          Clear the session cookie. Requires the CSRF header for browser
+          sessions.
+        id: clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions
+      - content: >-
+          Mint a CSRF token for the current session. The SPA replays it in the
+          X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
+          this — the middleware short-circuits CSRF for authenticated API
+          requests.
+        id: >-
+          mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests
+      - content: >-
+          Returns whether 2FA is enabled on the panel — used by the login page
+          to decide whether to show the OTP field.
+        id: >-
+          returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/login","method":"post"},{"path":"/logout","method":"post"},{"path":"/csrf-token","method":"get"},{"path":"/getTwoFactorEnable","method":"post"}]} showTitle />
+    </>
+  );
+}

+ 37 - 0
docs/content/docs/en/reference/api/backup.mdx

@@ -0,0 +1,37 @@
+---
+title: Backup
+description: Operations that interact with the configured Telegram bot.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        Send a fresh DB backup to every Telegram chat configured as an admin
+        recipient. No body, no params.
+      url: >-
+        #send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
+  structuredData:
+    headings:
+      - content: >-
+          Send a fresh DB backup to every Telegram chat configured as an admin
+          recipient. No body, no params.
+        id: >-
+          send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/backuptotgbot","method":"post"}]} showTitle />
+    </>
+  );
+}

Різницю між файлами не показано, бо вона завелика
+ 623 - 0
docs/content/docs/en/reference/api/clients.mdx


+ 108 - 0
docs/content/docs/en/reference/api/hosts.mdx

@@ -0,0 +1,108 @@
+---
+title: Hosts
+description: >-
+  Per-inbound override endpoints. Each enabled host renders one extra
+  subscription link/proxy with its own address/port/TLS, superseding the legacy
+  externalProxy array. All endpoints under /panel/api/hosts.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        List every host across all inbounds, grouped by inbound then ordered by
+        sort order.
+      url: >-
+        #list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
+    - depth: 2
+      title: Fetch a single host by ID.
+      url: '#fetch-a-single-host-by-id'
+    - depth: 2
+      title: Fetch one inbound's hosts, ordered by sort order then id.
+      url: '#fetch-one-inbounds-hosts-ordered-by-sort-order-then-id'
+    - depth: 2
+      title: Distinct, sorted set of tags used across all hosts.
+      url: '#distinct-sorted-set-of-tags-used-across-all-hosts'
+    - depth: 2
+      title: >-
+        Create a host on an inbound. inboundId and remark are required; security
+        defaults to "same" (inherit the inbound).
+      url: >-
+        #create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
+    - depth: 2
+      title: >-
+        Replace a host’s content. The inbound and sort order are immutable here
+        (use /reorder for ordering).
+      url: >-
+        #replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
+    - depth: 2
+      title: Delete a host.
+      url: '#delete-a-host'
+    - depth: 2
+      title: >-
+        Enable or disable a single host (disabled hosts are skipped in
+        subscriptions).
+      url: >-
+        #enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
+    - depth: 2
+      title: Set host sort order by the position of each id in the array.
+      url: '#set-host-sort-order-by-the-position-of-each-id-in-the-array'
+    - depth: 2
+      title: Enable or disable many hosts in one call.
+      url: '#enable-or-disable-many-hosts-in-one-call'
+    - depth: 2
+      title: Delete many hosts in one call.
+      url: '#delete-many-hosts-in-one-call'
+  structuredData:
+    headings:
+      - content: >-
+          List every host across all inbounds, grouped by inbound then ordered
+          by sort order.
+        id: >-
+          list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
+      - content: Fetch a single host by ID.
+        id: fetch-a-single-host-by-id
+      - content: Fetch one inbound's hosts, ordered by sort order then id.
+        id: fetch-one-inbounds-hosts-ordered-by-sort-order-then-id
+      - content: Distinct, sorted set of tags used across all hosts.
+        id: distinct-sorted-set-of-tags-used-across-all-hosts
+      - content: >-
+          Create a host on an inbound. inboundId and remark are required;
+          security defaults to "same" (inherit the inbound).
+        id: >-
+          create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
+      - content: >-
+          Replace a host’s content. The inbound and sort order are immutable
+          here (use /reorder for ordering).
+        id: >-
+          replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
+      - content: Delete a host.
+        id: delete-a-host
+      - content: >-
+          Enable or disable a single host (disabled hosts are skipped in
+          subscriptions).
+        id: >-
+          enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
+      - content: Set host sort order by the position of each id in the array.
+        id: set-host-sort-order-by-the-position-of-each-id-in-the-array
+      - content: Enable or disable many hosts in one call.
+        id: enable-or-disable-many-hosts-in-one-call
+      - content: Delete many hosts in one call.
+        id: delete-many-hosts-in-one-call
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{id}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{id}","method":"post"},{"path":"/panel/api/hosts/del/{id}","method":"post"},{"path":"/panel/api/hosts/setEnable/{id}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
+    </>
+  );
+}

+ 242 - 0
docs/content/docs/en/reference/api/inbounds.mdx

@@ -0,0 +1,242 @@
+---
+title: Inbounds
+description: >-
+  Manage inbound configurations and their clients. All endpoints live under
+  /panel/api/inbounds and require a logged-in session or Bearer token.
+  Link-generating endpoints honour forwarded headers only when the request comes
+  from a configured trusted proxy.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        List every inbound owned by the authenticated user, including each
+        inbound’s clientStats traffic counters. settings, streamSettings, and
+        sniffing are returned as nested JSON objects (no escaped strings);
+        legacy callers that send them back as JSON-encoded strings are still
+        accepted on write.
+      url: >-
+        #list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write
+    - depth: 2
+      title: >-
+        Same shape as /list but with settings.clients[] stripped down to {email,
+        enable, comment} and ClientStats not enriched with UUID/SubId. Use this
+        for list pages; fetch /get/:id when you need the full per-client payload
+        (uuid, password, flow, ...).
+      url: >-
+        #same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-
+    - depth: 2
+      title: >-
+        Lightweight picker projection of the authenticated user’s inbounds.
+        Returns id, remark, tag, protocol, port, a server-computed
+        tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
+        XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
+        Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the
+        client UI to generate a valid Shadowsocks 2022 PSK). Use this for
+        dropdowns and attach pickers — it skips settings, streamSettings, and
+        clientStats so the payload stays small even on panels with thousands of
+        clients.
+      url: >-
+        #lightweight-picker-projection-of-the-authenticated-users-inbounds-returns-id-remark-tag-protocol-port-a-server-computed-tlsflowcapable-flag-true-for-vless-on-tcp-with-tls-or-reality-or-on-xhttp-with-vless-encryption--vlessenc-enabled-and-ssmethod-the-shadowsocks-cipher-empty-for-non-shadowsocks-inbounds--used-by-the-client-ui-to-generate-a-valid-shadowsocks-2022-psk-use-this-for-dropdowns-and-attach-pickers--it-skips-settings-streamsettings-and-clientstats-so-the-payload-stays-small-even-on-panels-with-thousands-of-clients
+    - depth: 2
+      title: Fetch a single inbound by numeric ID.
+      url: '#fetch-a-single-inbound-by-numeric-id'
+    - depth: 2
+      title: >-
+        Create a new inbound. Send the full inbound payload (protocol, port,
+        settings, streamSettings, sniffing, remark, expiryTime, total, enable).
+        settings, streamSettings, and sniffing may be sent as nested JSON
+        objects (preferred) or as JSON-encoded strings (legacy).
+      url: >-
+        #create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy
+    - depth: 2
+      title: Delete an inbound by ID. Also removes its associated client stats rows.
+      url: '#delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows'
+    - depth: 2
+      title: >-
+        Delete many inbounds in one call. Processes the list sequentially;
+        failures are reported per id and the rest still proceed. Restarts xray
+        at most once.
+      url: >-
+        #delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once
+    - depth: 2
+      title: >-
+        Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
+        inbounds with thousands of clients — prefer /setEnable for enable-only
+        flips.
+      url: >-
+        #replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
+    - depth: 2
+      title: >-
+        Toggle only the enable flag without serialising the whole settings JSON.
+        Recommended for UI switches on large inbounds.
+      url: >-
+        #toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds
+    - depth: 2
+      title: >-
+        Zero out upload + download counters for a single inbound. Does not touch
+        per-client counters.
+      url: >-
+        #zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
+    - depth: 2
+      title: >-
+        Remove every client attached to a single inbound while keeping the
+        inbound itself. Collects emails from settings.clients[] and feeds them
+        into the optimized bulk-delete path (runtime user removal + traffic-row
+        cleanup + SyncInbound). Destructive and cannot be undone.
+      url: >-
+        #remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone
+    - depth: 2
+      title: >-
+        Reset upload + download counters on every inbound. Destructive —
+        accounting history is lost.
+      url: >-
+        #reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
+    - depth: 2
+      title: >-
+        Bulk-import an inbound from a JSON blob (e.g. one exported via the UI).
+        The body uses form encoding with a single "data" field.
+      url: >-
+        #bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field
+    - depth: 2
+      title: >-
+        Receive a master panel's aggregated per-client usage, keyed by the
+        master's GUID. Stored in a side table used only for the UI display
+        overlay and local quota enforcement — never folded into the local
+        counters that masters poll, so delta accounting stays intact. Called
+        panel-to-panel by the node traffic sync job.
+      url: >-
+        #receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job
+    - depth: 2
+      title: >-
+        List the fallback rules attached to a master VLESS/Trojan TCP-TLS
+        inbound. Each rule links one child inbound (the dest) to optional
+        SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
+        inbound's listen+port is used.
+      url: >-
+        #list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used
+    - depth: 2
+      title: >-
+        Replace the entire fallback list for a master inbound. Body is JSON.
+        Triggers an Xray restart.
+      url: >-
+        #replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
+  structuredData:
+    headings:
+      - content: >-
+          List every inbound owned by the authenticated user, including each
+          inbound’s clientStats traffic counters. settings, streamSettings, and
+          sniffing are returned as nested JSON objects (no escaped strings);
+          legacy callers that send them back as JSON-encoded strings are still
+          accepted on write.
+        id: >-
+          list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write
+      - content: >-
+          Same shape as /list but with settings.clients[] stripped down to
+          {email, enable, comment} and ClientStats not enriched with UUID/SubId.
+          Use this for list pages; fetch /get/:id when you need the full
+          per-client payload (uuid, password, flow, ...).
+        id: >-
+          same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-
+      - content: >-
+          Lightweight picker projection of the authenticated user’s inbounds.
+          Returns id, remark, tag, protocol, port, a server-computed
+          tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
+          XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
+          Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the
+          client UI to generate a valid Shadowsocks 2022 PSK). Use this for
+          dropdowns and attach pickers — it skips settings, streamSettings, and
+          clientStats so the payload stays small even on panels with thousands
+          of clients.
+        id: >-
+          lightweight-picker-projection-of-the-authenticated-users-inbounds-returns-id-remark-tag-protocol-port-a-server-computed-tlsflowcapable-flag-true-for-vless-on-tcp-with-tls-or-reality-or-on-xhttp-with-vless-encryption--vlessenc-enabled-and-ssmethod-the-shadowsocks-cipher-empty-for-non-shadowsocks-inbounds--used-by-the-client-ui-to-generate-a-valid-shadowsocks-2022-psk-use-this-for-dropdowns-and-attach-pickers--it-skips-settings-streamsettings-and-clientstats-so-the-payload-stays-small-even-on-panels-with-thousands-of-clients
+      - content: Fetch a single inbound by numeric ID.
+        id: fetch-a-single-inbound-by-numeric-id
+      - content: >-
+          Create a new inbound. Send the full inbound payload (protocol, port,
+          settings, streamSettings, sniffing, remark, expiryTime, total,
+          enable). settings, streamSettings, and sniffing may be sent as nested
+          JSON objects (preferred) or as JSON-encoded strings (legacy).
+        id: >-
+          create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy
+      - content: >-
+          Delete an inbound by ID. Also removes its associated client stats
+          rows.
+        id: delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows
+      - content: >-
+          Delete many inbounds in one call. Processes the list sequentially;
+          failures are reported per id and the rest still proceed. Restarts xray
+          at most once.
+        id: >-
+          delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once
+      - content: >-
+          Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
+          inbounds with thousands of clients — prefer /setEnable for enable-only
+          flips.
+        id: >-
+          replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
+      - content: >-
+          Toggle only the enable flag without serialising the whole settings
+          JSON. Recommended for UI switches on large inbounds.
+        id: >-
+          toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds
+      - content: >-
+          Zero out upload + download counters for a single inbound. Does not
+          touch per-client counters.
+        id: >-
+          zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
+      - content: >-
+          Remove every client attached to a single inbound while keeping the
+          inbound itself. Collects emails from settings.clients[] and feeds them
+          into the optimized bulk-delete path (runtime user removal +
+          traffic-row cleanup + SyncInbound). Destructive and cannot be undone.
+        id: >-
+          remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone
+      - content: >-
+          Reset upload + download counters on every inbound. Destructive —
+          accounting history is lost.
+        id: >-
+          reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
+      - content: >-
+          Bulk-import an inbound from a JSON blob (e.g. one exported via the
+          UI). The body uses form encoding with a single "data" field.
+        id: >-
+          bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field
+      - content: >-
+          Receive a master panel's aggregated per-client usage, keyed by the
+          master's GUID. Stored in a side table used only for the UI display
+          overlay and local quota enforcement — never folded into the local
+          counters that masters poll, so delta accounting stays intact. Called
+          panel-to-panel by the node traffic sync job.
+        id: >-
+          receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job
+      - content: >-
+          List the fallback rules attached to a master VLESS/Trojan TCP-TLS
+          inbound. Each rule links one child inbound (the dest) to optional
+          SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
+          inbound's listen+port is used.
+        id: >-
+          list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used
+      - content: >-
+          Replace the entire fallback list for a master inbound. Body is JSON.
+          Triggers an Xray restart.
+        id: >-
+          replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/inbounds/list","method":"get"},{"path":"/panel/api/inbounds/list/slim","method":"get"},{"path":"/panel/api/inbounds/options","method":"get"},{"path":"/panel/api/inbounds/get/{id}","method":"get"},{"path":"/panel/api/inbounds/add","method":"post"},{"path":"/panel/api/inbounds/del/{id}","method":"post"},{"path":"/panel/api/inbounds/bulkDel","method":"post"},{"path":"/panel/api/inbounds/update/{id}","method":"post"},{"path":"/panel/api/inbounds/setEnable/{id}","method":"post"},{"path":"/panel/api/inbounds/{id}/resetTraffic","method":"post"},{"path":"/panel/api/inbounds/{id}/delAllClients","method":"post"},{"path":"/panel/api/inbounds/resetAllTraffics","method":"post"},{"path":"/panel/api/inbounds/import","method":"post"},{"path":"/panel/api/inbounds/pushClientTraffics","method":"post"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"get"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"post"}]} showTitle />
+    </>
+  );
+}

+ 40 - 0
docs/content/docs/en/reference/api/index.mdx

@@ -0,0 +1,40 @@
+---
+title: API Reference
+description: The 3x-ui panel REST API — authentication, inbounds, clients, server stats, and more, generated from the OpenAPI spec.
+icon: Webhook
+---
+
+The 3x-ui panel exposes a REST API for automation. These pages are generated from
+the panel's OpenAPI specification, so they always match the documented schema.
+
+## Authentication
+
+Requests authenticate with either a **session cookie** (from `POST /login`) or a
+**Bearer token**:
+
+```text
+Authorization: Bearer <token>
+```
+
+Create API tokens in the panel; they are full-admin credentials, so store them
+securely. See [API Tokens](/docs/reference/api/api-tokens).
+
+Build an authenticated request for any endpoint below:
+
+<ApiRequestBuilder />
+
+## Browse by area
+
+<Cards>
+  <Card title="Authentication" href="/docs/reference/api/authentication" />
+  <Card title="Inbounds" href="/docs/reference/api/inbounds" />
+  <Card title="Clients" href="/docs/reference/api/clients" />
+  <Card title="Server" href="/docs/reference/api/server" />
+  <Card title="Settings" href="/docs/reference/api/settings" />
+  <Card title="Xray Settings" href="/docs/reference/api/xray-settings" />
+</Cards>
+
+<Callout type="info">
+  This reference is regenerated from `public/openapi.json` with `pnpm gen:api`.
+  Don't edit the generated tag pages by hand.
+</Callout>

+ 19 - 0
docs/content/docs/en/reference/api/meta.json

@@ -0,0 +1,19 @@
+{
+  "title": "API Reference",
+  "icon": "Webhook",
+  "pages": [
+    "index",
+    "authentication",
+    "api-tokens",
+    "inbounds",
+    "clients",
+    "server",
+    "settings",
+    "xray-settings",
+    "subscription-server",
+    "hosts",
+    "nodes",
+    "backup",
+    "websocket"
+  ]
+}

+ 183 - 0
docs/content/docs/en/reference/api/nodes.mdx

@@ -0,0 +1,183 @@
+---
+title: Nodes
+description: >-
+  Manage remote 3x-ui panels acting as nodes for a central panel. All endpoints
+  under /panel/api/nodes.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        List every configured node with its connection details, health, and last
+        heartbeat patch.
+      url: >-
+        #list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
+    - depth: 2
+      title: >-
+        This panel's node-auth CA certificate (public, PEM) to paste into a
+        node's mTLS trust setting. Lazily mints the CA and the master client
+        cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
+      url: >-
+        #this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node
+    - depth: 2
+      title: >-
+        Set the CA certificate this panel trusts for incoming node-API client
+        certificates (this panel acting as a node). Paste the managing panel's
+        CA (from nodes/mtls/ca). An empty caCert disables it. A non-empty value
+        must be a PEM certificate. Applied on the next panel restart.
+      url: >-
+        #set-the-ca-certificate-this-panel-trusts-for-incoming-node-api-client-certificates-this-panel-acting-as-a-node-paste-the-managing-panels-ca-from-nodesmtlsca-an-empty-cacert-disables-it-a-non-empty-value-must-be-a-pem-certificate-applied-on-the-next-panel-restart
+    - depth: 2
+      title: Fetch a single node by ID.
+      url: '#fetch-a-single-node-by-id'
+    - depth: 2
+      title: >-
+        Fetch a node's own web TLS certificate/key file paths (proxied to the
+        node). Used by the inbound form's "Set Cert from Panel" so a
+        node-assigned inbound gets paths that exist on the node, not the central
+        panel.
+      url: >-
+        #fetch-a-nodes-own-web-tls-certificatekey-file-paths-proxied-to-the-node-used-by-the-inbound-forms-set-cert-from-panel-so-a-node-assigned-inbound-gets-paths-that-exist-on-the-node-not-the-central-panel
+    - depth: 2
+      title: >-
+        Register a new remote node. Provide its URL, apiToken, and optional
+        remark / allowPrivateAddress flag.
+      url: >-
+        #register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag
+    - depth: 2
+      title: Replace a node’s connection details. Same body shape as /add.
+      url: '#replace-a-nodes-connection-details-same-body-shape-as-add'
+    - depth: 2
+      title: Delete a node. Inbounds bound to it are not auto-migrated.
+      url: '#delete-a-node-inbounds-bound-to-it-are-not-auto-migrated'
+    - depth: 2
+      title: Pause or resume traffic sync with this node.
+      url: '#pause-or-resume-traffic-sync-with-this-node'
+    - depth: 2
+      title: >-
+        Probe a node without saving it. Uses the body as connection details and
+        returns the same heartbeat snapshot a registered node would have.
+      url: >-
+        #probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have
+    - depth: 2
+      title: >-
+        Connect to the node over HTTPS without verifying its certificate and
+        return the leaf certificate's SHA-256 (base64). Used by the Add/Edit
+        Node dialog to fetch and pin a self-signed certificate. Uses the same
+        body as /test.
+      url: >-
+        #connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test
+    - depth: 2
+      title: >-
+        Use unsaved node connection details to list the remote inbounds
+        available for selective import.
+      url: >-
+        #use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
+    - depth: 2
+      title: Probe an existing node, updating its cached health state.
+      url: '#probe-an-existing-node-updating-its-cached-health-state'
+    - depth: 2
+      title: >-
+        Trigger the official panel self-updater on each given node (downloads
+        the latest release and restarts). Only enabled, online nodes are
+        updated; offline/disabled ones are reported as skipped. Set "dev": true
+        to move the nodes to the rolling per-commit dev channel instead of the
+        latest stable release. Returns a per-node result list.
+      url: >-
+        #trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list
+    - depth: 2
+      title: >-
+        Aggregated metric history for a node — same shape as /server/history,
+        scoped to one node.
+      url: >-
+        #aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
+  structuredData:
+    headings:
+      - content: >-
+          List every configured node with its connection details, health, and
+          last heartbeat patch.
+        id: >-
+          list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
+      - content: >-
+          This panel's node-auth CA certificate (public, PEM) to paste into a
+          node's mTLS trust setting. Lazily mints the CA and the master client
+          cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
+        id: >-
+          this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node
+      - content: >-
+          Set the CA certificate this panel trusts for incoming node-API client
+          certificates (this panel acting as a node). Paste the managing panel's
+          CA (from nodes/mtls/ca). An empty caCert disables it. A non-empty
+          value must be a PEM certificate. Applied on the next panel restart.
+        id: >-
+          set-the-ca-certificate-this-panel-trusts-for-incoming-node-api-client-certificates-this-panel-acting-as-a-node-paste-the-managing-panels-ca-from-nodesmtlsca-an-empty-cacert-disables-it-a-non-empty-value-must-be-a-pem-certificate-applied-on-the-next-panel-restart
+      - content: Fetch a single node by ID.
+        id: fetch-a-single-node-by-id
+      - content: >-
+          Fetch a node's own web TLS certificate/key file paths (proxied to the
+          node). Used by the inbound form's "Set Cert from Panel" so a
+          node-assigned inbound gets paths that exist on the node, not the
+          central panel.
+        id: >-
+          fetch-a-nodes-own-web-tls-certificatekey-file-paths-proxied-to-the-node-used-by-the-inbound-forms-set-cert-from-panel-so-a-node-assigned-inbound-gets-paths-that-exist-on-the-node-not-the-central-panel
+      - content: >-
+          Register a new remote node. Provide its URL, apiToken, and optional
+          remark / allowPrivateAddress flag.
+        id: >-
+          register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag
+      - content: Replace a node’s connection details. Same body shape as /add.
+        id: replace-a-nodes-connection-details-same-body-shape-as-add
+      - content: Delete a node. Inbounds bound to it are not auto-migrated.
+        id: delete-a-node-inbounds-bound-to-it-are-not-auto-migrated
+      - content: Pause or resume traffic sync with this node.
+        id: pause-or-resume-traffic-sync-with-this-node
+      - content: >-
+          Probe a node without saving it. Uses the body as connection details
+          and returns the same heartbeat snapshot a registered node would have.
+        id: >-
+          probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have
+      - content: >-
+          Connect to the node over HTTPS without verifying its certificate and
+          return the leaf certificate's SHA-256 (base64). Used by the Add/Edit
+          Node dialog to fetch and pin a self-signed certificate. Uses the same
+          body as /test.
+        id: >-
+          connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test
+      - content: >-
+          Use unsaved node connection details to list the remote inbounds
+          available for selective import.
+        id: >-
+          use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
+      - content: Probe an existing node, updating its cached health state.
+        id: probe-an-existing-node-updating-its-cached-health-state
+      - content: >-
+          Trigger the official panel self-updater on each given node (downloads
+          the latest release and restarts). Only enabled, online nodes are
+          updated; offline/disabled ones are reported as skipped. Set "dev":
+          true to move the nodes to the rolling per-commit dev channel instead
+          of the latest stable release. Returns a per-node result list.
+        id: >-
+          trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list
+      - content: >-
+          Aggregated metric history for a node — same shape as /server/history,
+          scoped to one node.
+        id: >-
+          aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/nodes/list","method":"get"},{"path":"/panel/api/nodes/mtls/ca","method":"post"},{"path":"/panel/api/nodes/mtls/trustCA","method":"post"},{"path":"/panel/api/nodes/get/{id}","method":"get"},{"path":"/panel/api/nodes/webCert/{id}","method":"get"},{"path":"/panel/api/nodes/add","method":"post"},{"path":"/panel/api/nodes/update/{id}","method":"post"},{"path":"/panel/api/nodes/del/{id}","method":"post"},{"path":"/panel/api/nodes/setEnable/{id}","method":"post"},{"path":"/panel/api/nodes/test","method":"post"},{"path":"/panel/api/nodes/certFingerprint","method":"post"},{"path":"/panel/api/nodes/inbounds","method":"post"},{"path":"/panel/api/nodes/probe/{id}","method":"post"},{"path":"/panel/api/nodes/updatePanel","method":"post"},{"path":"/panel/api/nodes/history/{id}/{metric}/{bucket}","method":"get"}]} showTitle />
+    </>
+  );
+}

Різницю між файлами не показано, бо вона завелика
+ 377 - 0
docs/content/docs/en/reference/api/server.mdx


+ 122 - 0
docs/content/docs/en/reference/api/settings.mdx

@@ -0,0 +1,122 @@
+---
+title: Settings
+description: >-
+  Panel configuration and user credentials. All endpoints live under
+  /panel/api/setting and require a logged-in session or Bearer token.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        Return every panel setting: web server, Telegram bot, subscription,
+        security, LDAP. The full JSON blob that the Settings page edits.
+      url: >-
+        #return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
+    - depth: 2
+      title: >-
+        Return the computed default settings based on the request host. Useful
+        to preview what a fresh install would use.
+      url: >-
+        #return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
+    - depth: 2
+      title: >-
+        Persist every setting at once. The body mirrors the shape returned by
+        /all. Invalid values (bad ports, missing cert pairs, etc.) are rejected
+        before write.
+      url: >-
+        #persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write
+    - depth: 2
+      title: >-
+        Change the panel admin username and password. Requires the current
+        credentials for verification. The session is refreshed with the new
+        values on success.
+      url: >-
+        #change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
+    - depth: 2
+      title: >-
+        Restart the entire 3x-ui process after a 3-second grace period. The
+        connection drops immediately; the panel comes back online ~5-10 seconds
+        later.
+      url: >-
+        #restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later
+    - depth: 2
+      title: >-
+        Test SMTP connection with stage-by-stage reporting (connect, auth,
+        send). Returns structured result with stage and message.
+      url: >-
+        #test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
+    - depth: 2
+      title: >-
+        Test Telegram bot connection by sending a test message to the configured
+        chat.
+      url: >-
+        #test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
+    - depth: 2
+      title: >-
+        Return the built-in default Xray JSON config template that ships with
+        this panel version.
+      url: >-
+        #return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
+  structuredData:
+    headings:
+      - content: >-
+          Return every panel setting: web server, Telegram bot, subscription,
+          security, LDAP. The full JSON blob that the Settings page edits.
+        id: >-
+          return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
+      - content: >-
+          Return the computed default settings based on the request host. Useful
+          to preview what a fresh install would use.
+        id: >-
+          return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
+      - content: >-
+          Persist every setting at once. The body mirrors the shape returned by
+          /all. Invalid values (bad ports, missing cert pairs, etc.) are
+          rejected before write.
+        id: >-
+          persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write
+      - content: >-
+          Change the panel admin username and password. Requires the current
+          credentials for verification. The session is refreshed with the new
+          values on success.
+        id: >-
+          change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
+      - content: >-
+          Restart the entire 3x-ui process after a 3-second grace period. The
+          connection drops immediately; the panel comes back online ~5-10
+          seconds later.
+        id: >-
+          restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later
+      - content: >-
+          Test SMTP connection with stage-by-stage reporting (connect, auth,
+          send). Returns structured result with stage and message.
+        id: >-
+          test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
+      - content: >-
+          Test Telegram bot connection by sending a test message to the
+          configured chat.
+        id: >-
+          test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
+      - content: >-
+          Return the built-in default Xray JSON config template that ships with
+          this panel version.
+        id: >-
+          return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/setting/all","method":"post"},{"path":"/panel/api/setting/defaultSettings","method":"post"},{"path":"/panel/api/setting/update","method":"post"},{"path":"/panel/api/setting/updateUser","method":"post"},{"path":"/panel/api/setting/restartPanel","method":"post"},{"path":"/panel/api/setting/testSmtp","method":"post"},{"path":"/panel/api/setting/testTgBot","method":"post"},{"path":"/panel/api/setting/getDefaultJsonConfig","method":"get"}]} showTitle />
+    </>
+  );
+}

+ 72 - 0
docs/content/docs/en/reference/api/subscription-server.mdx

@@ -0,0 +1,72 @@
+---
+title: Subscription Server
+description: >-
+  A separate HTTP/HTTPS server that serves proxy subscription links (standard,
+  JSON, and Clash) to clients. The server listens on its own port (default
+  10882) and is configured in Settings → Subscription. Paths are configurable;
+  defaults are shown below. All subscription endpoints set response headers for
+  client apps to read traffic/expiry info.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        Return base64-encoded subscription links for all enabled clients
+        matching the subscription ID. When the request has an Accept: text/html
+        header or ?html=1, renders a styled info page instead. Default path:
+        /sub/:subid.
+      url: >-
+        #return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+    - depth: 2
+      title: >-
+        Return subscription as a JSON array of proxy configs (one per enabled
+        client). Only when JSON subscription is enabled in settings. Default
+        path: /json/:subid.
+      url: >-
+        #return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+    - depth: 2
+      title: >-
+        Return subscription as a Clash/Mihomo-compatible YAML config, including
+        configured global Clash routing rules. Only when Clash subscription is
+        enabled in settings. Default path: /clash/:subid.
+      url: >-
+        #return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+  structuredData:
+    headings:
+      - content: >-
+          Return base64-encoded subscription links for all enabled clients
+          matching the subscription ID. When the request has an Accept:
+          text/html header or ?html=1, renders a styled info page instead.
+          Default path: /sub/:subid.
+        id: >-
+          return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+      - content: >-
+          Return subscription as a JSON array of proxy configs (one per enabled
+          client). Only when JSON subscription is enabled in settings. Default
+          path: /json/:subid.
+        id: >-
+          return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+      - content: >-
+          Return subscription as a Clash/Mihomo-compatible YAML config,
+          including configured global Clash routing rules. Only when Clash
+          subscription is enabled in settings. Default path: /clash/:subid.
+        id: >-
+          return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/{subPath}{subid}","method":"get"},{"path":"/{jsonPath}{subid}","method":"get"},{"path":"/{clashPath}{subid}","method":"get"}]} showTitle />
+    </>
+  );
+}

+ 46 - 0
docs/content/docs/en/reference/api/websocket.mdx

@@ -0,0 +1,46 @@
+---
+title: WebSocket
+description: >-
+  Real-time status updates via WebSocket. Connect once at
+  <code>ws://<panel>/ws</code> to receive a stream of JSON messages without
+  polling. Requires an authenticated session cookie (Bearer token auth is not
+  supported). Each message has a <code>type</code> field that identifies the
+  payload shape.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        Upgrade an HTTP connection to a WebSocket. Requires an authenticated
+        session cookie (Bearer token auth is not supported here). Returns 101
+        Switching Protocols on success. The server then pushes JSON messages
+        described below.
+      url: >-
+        #upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below
+  structuredData:
+    headings:
+      - content: >-
+          Upgrade an HTTP connection to a WebSocket. Requires an authenticated
+          session cookie (Bearer token auth is not supported here). Returns 101
+          Switching Protocols on success. The server then pushes JSON messages
+          described below.
+        id: >-
+          upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/ws","method":"get"}]} showTitle />
+    </>
+  );
+}

+ 257 - 0
docs/content/docs/en/reference/api/xray-settings.mdx

@@ -0,0 +1,257 @@
+---
+title: Xray Settings
+description: >-
+  Xray configuration template, outbound management, Warp/Nord integration, and
+  config testing. All endpoints under /panel/api/xray.
+full: true
+_openapi:
+  preload:
+    - ./public/openapi.json
+  toc:
+    - depth: 2
+      title: >-
+        Return the Xray config template (JSON string), available inbound tags,
+        client reverse tags, and the configured outbound test URL in one
+        response.
+      url: >-
+        #return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
+    - depth: 2
+      title: >-
+        Return the built-in default Xray config shipped with the panel
+        (identical to /panel/api/setting/getDefaultJsonConfig).
+      url: >-
+        #return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
+    - depth: 2
+      title: >-
+        Return traffic statistics for every outbound. Each outbound shows
+        up/down/total counters.
+      url: >-
+        #return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
+    - depth: 2
+      title: >-
+        Return the most recent Xray process stdout/stderr output. Useful to
+        check for startup errors or runtime warnings.
+      url: >-
+        #return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
+    - depth: 2
+      title: >-
+        Save the Xray JSON config template and optionally the outbound test URL.
+        Both are sent as form fields.
+      url: >-
+        #save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
+    - depth: 2
+      title: >-
+        Manage Cloudflare Warp integration. The action parameter selects the
+        operation.
+      url: >-
+        #manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
+    - depth: 2
+      title: Manage NordVPN integration. The action parameter selects the operation.
+      url: '#manage-nordvpn-integration-the-action-parameter-selects-the-operation'
+    - depth: 2
+      title: Reset traffic counters for a specific outbound by tag.
+      url: '#reset-traffic-counters-for-a-specific-outbound-by-tag'
+    - depth: 2
+      title: >-
+        Test an outbound configuration. Sends the outbound JSON (required),
+        optionally all outbounds (to resolve sockopt.dialerProxy dependencies),
+        and a mode flag.
+      url: >-
+        #test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
+    - depth: 2
+      title: >-
+        Test a batch of outbounds (max 50) through one shared temp xray
+        instance. Returns an array of results in input order, each with the
+        outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
+        breakdown.
+      url: >-
+        #test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown
+    - depth: 2
+      title: >-
+        Live state of routing balancers in the running core
+        (RoutingService.GetBalancerInfo): current override and the targets the
+        strategy prefers. Returns a map keyed by balancer tag.
+      url: >-
+        #live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag
+    - depth: 2
+      title: >-
+        Force a balancer in the running core to always pick one outbound
+        (RoutingService.OverrideBalancerTarget). Applied live without a restart;
+        cleared automatically when Xray restarts.
+      url: >-
+        #force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
+    - depth: 2
+      title: >-
+        Ask the running core which outbound its router would pick for a
+        synthetic connection (RoutingService.TestRoute). No traffic is sent.
+      url: >-
+        #ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
+    - depth: 2
+      title: >-
+        List all outbound subscriptions (remote URLs that supply additional
+        outbounds), newest first.
+      url: >-
+        #list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
+    - depth: 2
+      title: >-
+        Create an outbound subscription. The URL is fetched, parsed into
+        outbounds with stable tags, and merged additively into the running Xray
+        config.
+      url: >-
+        #create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
+    - depth: 2
+      title: >-
+        Update an existing outbound subscription by id. Accepts the same form
+        fields as create.
+      url: >-
+        #update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
+    - depth: 2
+      title: Delete an outbound subscription by id.
+      url: '#delete-an-outbound-subscription-by-id'
+    - depth: 2
+      title: >-
+        Delete an outbound subscription by id (POST alias of DELETE for
+        axios-friendly clients).
+      url: >-
+        #delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
+    - depth: 2
+      title: >-
+        Force an immediate re-fetch of the subscription and return the parsed
+        outbounds. Signals Xray to reload.
+      url: >-
+        #force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
+    - depth: 2
+      title: >-
+        Reorder a subscription one step up or down in priority (controls its
+        position in the merged outbounds).
+      url: >-
+        #reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
+    - depth: 2
+      title: >-
+        Preview a subscription URL: fetch and parse it into outbounds without
+        persisting anything.
+      url: >-
+        #preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
+  structuredData:
+    headings:
+      - content: >-
+          Return the Xray config template (JSON string), available inbound tags,
+          client reverse tags, and the configured outbound test URL in one
+          response.
+        id: >-
+          return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
+      - content: >-
+          Return the built-in default Xray config shipped with the panel
+          (identical to /panel/api/setting/getDefaultJsonConfig).
+        id: >-
+          return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
+      - content: >-
+          Return traffic statistics for every outbound. Each outbound shows
+          up/down/total counters.
+        id: >-
+          return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
+      - content: >-
+          Return the most recent Xray process stdout/stderr output. Useful to
+          check for startup errors or runtime warnings.
+        id: >-
+          return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
+      - content: >-
+          Save the Xray JSON config template and optionally the outbound test
+          URL. Both are sent as form fields.
+        id: >-
+          save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
+      - content: >-
+          Manage Cloudflare Warp integration. The action parameter selects the
+          operation.
+        id: >-
+          manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
+      - content: >-
+          Manage NordVPN integration. The action parameter selects the
+          operation.
+        id: manage-nordvpn-integration-the-action-parameter-selects-the-operation
+      - content: Reset traffic counters for a specific outbound by tag.
+        id: reset-traffic-counters-for-a-specific-outbound-by-tag
+      - content: >-
+          Test an outbound configuration. Sends the outbound JSON (required),
+          optionally all outbounds (to resolve sockopt.dialerProxy
+          dependencies), and a mode flag.
+        id: >-
+          test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
+      - content: >-
+          Test a batch of outbounds (max 50) through one shared temp xray
+          instance. Returns an array of results in input order, each with the
+          outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
+          breakdown.
+        id: >-
+          test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown
+      - content: >-
+          Live state of routing balancers in the running core
+          (RoutingService.GetBalancerInfo): current override and the targets the
+          strategy prefers. Returns a map keyed by balancer tag.
+        id: >-
+          live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag
+      - content: >-
+          Force a balancer in the running core to always pick one outbound
+          (RoutingService.OverrideBalancerTarget). Applied live without a
+          restart; cleared automatically when Xray restarts.
+        id: >-
+          force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
+      - content: >-
+          Ask the running core which outbound its router would pick for a
+          synthetic connection (RoutingService.TestRoute). No traffic is sent.
+        id: >-
+          ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
+      - content: >-
+          List all outbound subscriptions (remote URLs that supply additional
+          outbounds), newest first.
+        id: >-
+          list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
+      - content: >-
+          Create an outbound subscription. The URL is fetched, parsed into
+          outbounds with stable tags, and merged additively into the running
+          Xray config.
+        id: >-
+          create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
+      - content: >-
+          Update an existing outbound subscription by id. Accepts the same form
+          fields as create.
+        id: >-
+          update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
+      - content: Delete an outbound subscription by id.
+        id: delete-an-outbound-subscription-by-id
+      - content: >-
+          Delete an outbound subscription by id (POST alias of DELETE for
+          axios-friendly clients).
+        id: >-
+          delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
+      - content: >-
+          Force an immediate re-fetch of the subscription and return the parsed
+          outbounds. Signals Xray to reload.
+        id: >-
+          force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
+      - content: >-
+          Reorder a subscription one step up or down in priority (controls its
+          position in the merged outbounds).
+        id: >-
+          reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
+      - content: >-
+          Preview a subscription URL: fetch and parse it into outbounds without
+          persisting anything.
+        id: >-
+          preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
+    contents: []
+---
+
+{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+
+export default function Layout(props) {
+  const { APIPage, OpenAPIPage } = props.components ?? {};
+  // "APIPage" is the old name from v10, this allows both for backward compatibility
+  const Comp = OpenAPIPage ?? APIPage;
+  return (
+    <>
+      {props.children}
+      <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/xray/","method":"post"},{"path":"/panel/api/xray/getDefaultJsonConfig","method":"get"},{"path":"/panel/api/xray/getOutboundsTraffic","method":"get"},{"path":"/panel/api/xray/getXrayResult","method":"get"},{"path":"/panel/api/xray/update","method":"post"},{"path":"/panel/api/xray/warp/{action}","method":"post"},{"path":"/panel/api/xray/nord/{action}","method":"post"},{"path":"/panel/api/xray/resetOutboundsTraffic","method":"post"},{"path":"/panel/api/xray/testOutbound","method":"post"},{"path":"/panel/api/xray/testOutbounds","method":"post"},{"path":"/panel/api/xray/balancerStatus","method":"post"},{"path":"/panel/api/xray/balancerOverride","method":"post"},{"path":"/panel/api/xray/routeTest","method":"post"},{"path":"/panel/api/xray/outbound-subs","method":"get"},{"path":"/panel/api/xray/outbound-subs","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"delete"},{"path":"/panel/api/xray/outbound-subs/{id}/del","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/refresh","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/move","method":"post"},{"path":"/panel/api/xray/outbound-subs/parse","method":"post"}]} showTitle />
+    </>
+  );
+}

+ 64 - 0
docs/content/docs/en/reference/database.mdx

@@ -0,0 +1,64 @@
+---
+title: Database
+description: 3x-ui storage backends — SQLite (default) and PostgreSQL — the database path, connection pool, and SQLite-to-PostgreSQL migration.
+icon: Database
+---
+
+3x-ui stores everything — inbounds, clients, settings — in a database. You choose
+the backend at install time; both are first-class.
+
+## SQLite (default)
+
+A single file at `/etc/x-ui/x-ui.db`. Zero setup, ideal for small and medium
+deployments. The folder is configurable with
+[`XUI_DB_FOLDER`](/docs/reference/env-vars#database) (on Windows it defaults next
+to the binary).
+
+## PostgreSQL
+
+Recommended for high client counts or multi-node setups. The installer can
+install PostgreSQL locally for you, or accept a DSN to an existing server. At
+runtime the backend is selected via environment variables, which the installer
+writes to `/etc/default/x-ui`:
+
+```bash title="/etc/default/x-ui"
+XUI_DB_TYPE=postgres
+XUI_DB_DSN=postgres://xui:[email protected]:5432/xui?sslmode=disable
+```
+
+Tune the connection pool with `XUI_DB_MAX_OPEN_CONNS` and
+`XUI_DB_MAX_IDLE_CONNS`.
+
+### Docker
+
+`docker compose up -d` keeps using SQLite. To run with the bundled PostgreSQL
+service, uncomment the two `XUI_DB_*` lines in `docker-compose.yml` and start
+with the profile:
+
+```bash
+docker compose --profile postgres up -d
+```
+
+## Migrate SQLite → PostgreSQL
+
+Move an existing SQLite install to PostgreSQL with the built-in command:
+
+```bash
+x-ui migrate-db --dsn "postgres://xui:[email protected]:5432/xui?sslmode=disable"
+```
+
+Then set `XUI_DB_TYPE` and `XUI_DB_DSN` in `/etc/default/x-ui` and restart:
+
+```bash
+systemctl restart x-ui
+```
+
+<Callout type="info">
+  The source SQLite file is left untouched — remove it manually only after you've
+  verified the new backend works.
+</Callout>
+
+## Backups
+
+Whichever backend you use, back it up regularly — see
+[Backup & restore](/docs/operations/backup-restore).

+ 84 - 0
docs/content/docs/en/reference/env-vars.mdx

@@ -0,0 +1,84 @@
+---
+title: Environment Variables
+description: Complete reference for 3x-ui's XUI_* environment variables — database, panel, logging, memory, and the tunnel health monitor.
+icon: Variable
+---
+
+3x-ui reads its runtime configuration from `XUI_*` environment variables. On a
+script install the installer writes them to the service environment file
+(`/etc/default/x-ui`, or `/etc/conf.d/x-ui` / `/etc/sysconfig/x-ui` depending on
+the distro); for Docker you set them in `docker-compose.yml` or `docker run -e`.
+Defaults are sensible — set only what you need to change, then restart:
+`systemctl restart x-ui`.
+
+## Database
+
+| Variable                 | Default     | Description                                                          |
+| ------------------------ | ----------- | -------------------------------------------------------------------- |
+| `XUI_DB_TYPE`            | `sqlite`    | Backend: `sqlite`, or `postgres` (also accepts `postgresql` / `pg`). |
+| `XUI_DB_FOLDER`          | `/etc/x-ui` | Folder for the SQLite database file (`x-ui.db`).                     |
+| `XUI_DB_DSN`             | —           | PostgreSQL connection string (used when `XUI_DB_TYPE=postgres`).     |
+| `XUI_DB_MAX_OPEN_CONNS`  | —           | Max open connections in the PostgreSQL pool.                         |
+| `XUI_DB_MAX_IDLE_CONNS`  | —           | Max idle connections in the PostgreSQL pool.                         |
+
+The default SQLite database path is `/etc/x-ui/x-ui.db`. See
+[Database](/docs/reference/database) for the SQLite ↔ PostgreSQL details.
+
+## Panel
+
+| Variable                 | Default | Description                                                              |
+| ------------------------ | ------- | ------------------------------------------------------------------------ |
+| `XUI_PORT`               | —       | Override the panel port (1–65535). Takes precedence over the stored setting. |
+| `XUI_INIT_WEB_BASE_PATH` | `/`     | Initial web base path on **first** launch (e.g. `/panel`).               |
+| `XUI_ENABLE_FAIL2BAN`    | `true`  | Enable Fail2ban-based IP-limit enforcement.                              |
+| `XUI_SKIP_HSTS`          | `false` | Skip the HSTS header — set `true` when TLS is terminated by a reverse proxy. |
+
+## Logging & binaries
+
+| Variable         | Default          | Description                                                 |
+| ---------------- | ---------------- | ----------------------------------------------------------- |
+| `XUI_LOG_LEVEL`  | `info`           | `debug`, `info`, `notice`, `warning`, or `error`.           |
+| `XUI_DEBUG`      | `false`          | Debug mode (forces log level to `debug`).                   |
+| `XUI_LOG_FOLDER` | `/var/log/x-ui`  | Log output directory.                                       |
+| `XUI_BIN_FOLDER` | `bin`            | Folder for the Xray-core binary and geosite/geoip files.    |
+
+## Memory & profiling
+
+The panel keeps memory low via `GOGC` and periodic releases. These are advanced
+knobs — leave them unset unless you're tuning a constrained host.
+
+| Variable                      | Default | Description                                                       |
+| ----------------------------- | ------- | ----------------------------------------------------------------- |
+| `XUI_GOGC`                    | —       | Go GC target percentage; lower = less RAM, slightly more CPU.     |
+| `XUI_MEMORY_RELEASE_INTERVAL` | —       | Minutes between `FreeOSMemory` calls; `0` disables.               |
+| `XUI_MEMORY_LIMIT`            | —       | Go soft memory limit in **MiB**.                                  |
+| `GOMEMLIMIT`                  | —       | Go-syntax soft limit (e.g. `400MiB`); takes precedence over above.|
+| `XUI_PPROF`                   | `false` | Expose pprof profiling on `127.0.0.1:6060`.                       |
+
+## Xray
+
+| Variable                 | Default | Description           |
+| ------------------------ | ------- | --------------------- |
+| `XRAY_VMESS_AEAD_FORCED` | `false` | Force VMess AEAD.     |
+
+## Tunnel health monitor
+
+Optional watchdog: it probes a URL (optionally **through** a local Xray inbound)
+and restarts Xray after repeated failures. A restart drops all connected
+clients, so enable it deliberately.
+
+| Variable                      | Default                                      | Description                                                       |
+| ----------------------------- | -------------------------------------------- | ----------------------------------------------------------------- |
+| `XUI_TUNNEL_HEALTH_MONITOR`   | `false`                                      | Enable the monitor.                                               |
+| `XUI_TUNNEL_HEALTH_PROXY`     | —                                            | Proxy to send the probe through, e.g. `socks5://127.0.0.1:1080`. Empty = only checks host connectivity. |
+| `XUI_TUNNEL_HEALTH_URL`       | `https://www.cloudflare.com/cdn-cgi/trace`   | URL to probe.                                                     |
+| `XUI_TUNNEL_HEALTH_INTERVAL`  | `30s`                                        | Interval between probes.                                         |
+| `XUI_TUNNEL_HEALTH_TIMEOUT`   | `10s`                                        | Per-probe timeout.                                               |
+| `XUI_TUNNEL_HEALTH_FAILURES`  | `3`                                          | Consecutive failures before a restart.                          |
+| `XUI_TUNNEL_HEALTH_COOLDOWN`  | `5m`                                         | Minimum delay between restarts.                                 |
+
+## Unattended install
+
+| Variable             | Description                                                                                  |
+| -------------------- | -------------------------------------------------------------------------------------------- |
+| `XUI_NONINTERACTIVE` | Set to `1` (or run with no TTY) to install with zero prompts; generated credentials are written to `/etc/x-ui/install-result.env`. See [Installation](/docs/guide/installation#unattended--cloud-init). |

+ 5 - 0
docs/content/docs/en/reference/meta.json

@@ -0,0 +1,5 @@
+{
+  "title": "Reference",
+  "icon": "BookMarked",
+  "pages": ["env-vars", "database", "ports-firewall", "api"]
+}

+ 36 - 0
docs/content/docs/en/reference/ports-firewall.mdx

@@ -0,0 +1,36 @@
+---
+title: Ports & Firewall
+description: The ports 3x-ui uses by default and ready-made ufw / nftables rules to open them.
+icon: Network
+---
+
+Open only the ports you actually use. Below are the common ones and a generator
+for `ufw` and `nftables` rules.
+
+## Common ports
+
+| Port (default) | Purpose                                   |
+| -------------- | ----------------------------------------- |
+| `22`           | SSH (keep this open!).                     |
+| `2053`         | Panel (configurable).                     |
+| `2096`         | Subscription server (if separate).        |
+| `443`          | A common inbound port (TLS / REALITY).    |
+| `80` / `443`   | Reverse proxy (if you run one).           |
+
+Your actual inbound ports depend on the inbounds you create.
+
+## Generate firewall rules
+
+<FirewallRulesGenerator />
+
+<Callout type="warn">
+  Always keep SSH allowed before enabling a default-deny policy, and test from a
+  second session so you don't lock yourself out.
+</Callout>
+
+## Related
+
+<Cards>
+  <Card title="Security" href="/docs/operations/security" description="Fail2ban, IP limits, and hardening." />
+  <Card title="Environment variables" href="/docs/reference/env-vars" description="XUI_PORT and friends." />
+</Cards>

+ 67 - 0
docs/content/docs/fa/config/clients.mdx

@@ -0,0 +1,67 @@
+---
+title: کلاینت‌ها
+description: مدیریت کلاینت‌های 3x-ui — اعتبارنامه‌ها، محدودیت ترافیک و انقضا، محدودیت IP، گروه‌ها، اقدامات گروهی، لینک‌های خارجی و وضعیت آنلاین.
+icon: Users
+---
+
+یک **کلاینت** یک کاربر منفرد است که با یک **ایمیل** یکتا شناسایی می‌شود. در پنل
+فعلی، کلاینت‌ها رکوردهای درجه‌یک هستند که می‌توانند هم‌زمان به **چندین ورودی**
+متصل شوند و حساب‌داری ترافیک به‌صورت جداگانه برای هر کلاینت انجام می‌شود.
+
+## فیلدهای کلاینت
+
+| فیلد           | اعمال بر               | معنی                                                               |
+| -------------- | --------------------- | ------------------------------------------------------------------ |
+| **Email**      | همه                   | شناسه‌ی یکتا که برای حساب‌داری و جست‌وجوها استفاده می‌شود.          |
+| **ID (UUID)**  | VLESS, VMess          | اعتبارنامه‌ی کلاینت.                                                |
+| **Password**   | Trojan, Shadowsocks   | اعتبارنامه‌ی کلاینت.                                                |
+| **Auth**       | Hysteria2             | اعتبارنامه‌ی کلاینت.                                                |
+| **Flow**       | VLESS                 | جریان XTLS، برای مثال `xtls-rprx-vision`.                          |
+| **Limit IP**   | همه                   | بیشینه‌ی تعداد IPهای مبدأ هم‌زمان (با Fail2ban اعمال می‌شود).       |
+| **Total (GB)** | همه                   | سهمیه‌ی ترافیک؛ هنگام اتمام، کلاینت غیرفعال می‌شود.                |
+| **Expiry**     | همه                   | تاریخی که پس از آن کلاینت از کار می‌افتد.                          |
+| **Reset**      | همه                   | دوره‌ی تمدید خودکار به **روز** (سهمیه را از نو می‌چرخاند).          |
+| **Telegram ID**| همه                   | کلاینت را به یک کاربر Telegram برای سلف‌سرویس/اعلان‌ها پیوند می‌دهد.|
+| **Sub ID**     | همه                   | شناسه‌ی اشتراک که لینک‌های این کلاینت را گروه‌بندی می‌کند.          |
+| **Group**      | همه                   | گروه اختیاری کلاینت برای سازمان‌دهی و فیلترکردن گروهی.             |
+| **Comment**    | همه                   | یادداشت متنی آزاد.                                                 |
+
+<Callout type="info">
+  رسیدن به محدودیت **ترافیک** یا **انقضا** کلاینت را غیرفعال می‌کند؛ پنل می‌تواند
+  هنگام غیرفعال‌شدن خودکار کلاینت‌ها، Xray را به‌صورت خودکار راه‌اندازی مجدد کند
+  (`restartXrayOnClientDisable`، به‌صورت پیش‌فرض فعال).
+</Callout>
+
+## محدودیت‌ها و کنترل IP
+
+- سقف‌های **ترافیک / انقضا** هنگام رسیدن، کلاینت را غیرفعال می‌کنند؛ یک دوره‌ی
+  **Reset** سهمیه را به‌صورت خودکار تمدید می‌کند.
+- **Limit IP** تعداد IPهای مبدأ هم‌زمان را محدود می‌کند. اعمال آن به Fail2ban متکی
+  است — به [امنیت](/docs/operations/security) مراجعه کنید. می‌توانید IPهای اخیر یک
+  کلاینت را مشاهده کرده و آن‌ها را از بخش اقدامات کلاینت پاک کنید.
+- **وضعیت آنلاین** و زمان‌های **آخرین‌بار آنلاین** برای هر کلاینت (و در پیکربندی‌های
+  چندنودی برای هر نود) ثبت می‌شوند.
+
+## لینک‌های اشتراک‌گذاری و لینک‌های خارجی
+
+هر کلاینت برای ورودی‌هایش لینک‌های اشتراک‌گذاری و یک کد QR دارد، به‌علاوه‌ی یک
+[اشتراک](/docs/config/subscription) ترکیبی. همچنین می‌توانید **لینک‌های خارجی** به
+یک کلاینت متصل کنید — لینک‌های اضافی `vless://`، `vmess://`، `trojan://`، `ss://`،
+`hysteria2://` یا `wireguard://`، یا یک URL اشتراک از راه دور — تا در کنار
+لینک‌های تولیدشده توسط پنل، در اشتراک کلاینت نمایش داده شوند.
+
+برای بررسی دقیق محتویات یک لینک، آن را در
+[بازرس لینک اشتراک‌گذاری](/docs/config/share-links) جای‌گذاری کنید.
+
+## اقدامات گروهی
+
+برای مدیریت هم‌زمان تعداد زیادی کلاینت، پنل از اقدامات گروهی **ساخت، فعال‌سازی،
+غیرفعال‌سازی، حذف، اتصال/قطع اتصال** (به ورودی‌ها)، **بازنشانی ترافیک** و
+**تنظیم** (افزودن روز / افزودن بایت / تعیین flow) پشتیبانی می‌کند. اقدامات
+نگه‌داری همچنین به شما امکان می‌دهند کلاینت‌های **تمام‌شده** (سهمیه/انقضای پایان‌یافته)
+و کلاینت‌های **یتیم** (متصل‌نشده به هیچ ورودی) را حذف کنید.
+
+<Callout type="warn">
+  لینک اشتراک‌گذاری یک کلاینت حاوی اعتبارنامه‌ی آن است. با لینک‌ها و کدهای QR مانند
+  رمز عبور رفتار کنید و در صورت نشت یکی از آن‌ها، اعتبارنامه را تعویض کنید.
+</Callout>

+ 97 - 0
docs/content/docs/fa/config/inbounds.mdx

@@ -0,0 +1,97 @@
+---
+title: ورودی‌ها و پروتکل‌ها
+description: ساخت ورودی‌ها در 3x-ui — پروتکل‌ها، انتقال‌ها، بازنشانی و انقضای ترافیک، و fallbackهایی که چند پروتکل را روی یک پورت سرویس می‌دهند.
+icon: ArrowDownToLine
+---
+
+یک **ورودی (inbound)** شنونده‌ای است که اتصال‌های کلاینت را روی یک پورت با
+پروتکل و انتقال مشخصی می‌پذیرد. بیشتر کارهای روزمره شما ساخت و مدیریت ورودی‌ها و
+کلاینت‌های درون آن‌هاست.
+
+## ساخت یک ورودی
+
+<Steps>
+
+<Step>
+### افزودن یک ورودی
+
+به **Inbounds → Add** بروید، یک توضیح (remark) برای آن بگذارید، یک **پروتکل**
+انتخاب کنید، و یک **پورت** و آدرس شنود انتخاب کنید.
+</Step>
+
+<Step>
+### انتخاب انتقال و امنیت
+
+انتقال (TCP، WebSocket، gRPC، HTTPUpgrade، XHTTP، …) و لایه امنیتی (هیچ‌کدام،
+TLS یا REALITY) را انتخاب کنید. به [انتقال‌ها](/docs/config/transports) و
+[REALITY](/docs/config/reality) مراجعه کنید.
+</Step>
+
+<Step>
+### افزودن کلاینت‌ها
+
+یک یا چند کلاینت اضافه کنید که هر کدام اعتبارنامه، محدودیت‌ها و لینک اشتراک
+خودش را دارد. به [کلاینت‌ها](/docs/config/clients) مراجعه کنید.
+</Step>
+
+<Step>
+### تنظیم محدودیت ترافیک، انقضا و بازنشانی
+
+به‌صورت اختیاری می‌توانید کل ترافیک را محدود کنید و یک تاریخ انقضا برای ورودی
+تعیین کنید، و یک زمان‌بندی **بازنشانی ترافیک** دوره‌ای انتخاب کنید: `never`
+(پیش‌فرض)، `hourly`، `daily`، `weekly` یا `monthly`.
+</Step>
+
+</Steps>
+
+## پروتکل‌های پشتیبانی‌شده
+
+ویرایشگر ورودی این پروتکل‌ها را می‌پذیرد:
+
+| پروتکل                 | توضیحات                                                                  |
+| ---------------------- | ------------------------------------------------------------------------ |
+| **VLESS**              | سبک؛ پایه‌ی REALITY + XTLS-Vision. توصیه‌شده.                            |
+| **VMess**              | قدیمی‌تر اما با پشتیبانی بسیار گسترده در کلاینت‌ها.                       |
+| **Trojan**             | مبتنی بر TLS؛ از XTLS و fallback پشتیبانی می‌کند.                         |
+| **Shadowsocks**        | شامل رمزهای Shadowsocks-2022 (`2022-blake3-*`).                          |
+| **WireGuard**          | تونل مدرن.                                                               |
+| **Hysteria2**          | با عنوان `hysteria` انتخاب می‌شود؛ پنل لینک‌های `hysteria2://` تولید می‌کند. |
+| **HTTP**               | پراکسی HTTP.                                                             |
+| **Mixed (SOCKS/HTTP)** | یک شنونده ترکیبی SOCKS + HTTP.                                            |
+| **Dokodemo-door / Tunnel** | فورواردینگ پورت / هدایت ترافیک.                                      |
+| **MTProto**            | پراکسی MTProto تلگرام که توسط یک فرایند همراه `mtg` سرویس می‌شود (نه Xray). |
+
+<Callout type="info">
+  Hysteria2 در سطح داخلی یک پروتکل جداگانه نیست — همان پروتکل `hysteria` است که
+  نسخه انتقال آن روی ۲ تنظیم شده، و پنل برای آن لینک‌های اشتراک `hysteria2://`
+  تولید می‌کند.
+</Callout>
+
+## Fallbackها — چند پروتکل روی یک پورت
+
+Fallbackها به یک پورت TLS واحد (مثلاً `443`) اجازه می‌دهند بیش از یک پروتکل را
+سرویس دهد — برای مثال VLESS **و** Trojan — با هدایت دست‌دادن‌های (handshake)
+ناهماهنگ به یک ورودی فرزند. در 3x-ui، fallbackها در پنل مدیریت می‌شوند (لیست
+**Fallbacks** یک ورودی اصلی) به‌جای آنکه به‌صورت دستی در JSON نوشته شوند.
+
+Fallbackها تنها زمانی در دسترس‌اند که ورودی اصلی این‌گونه باشد:
+
+- **VLESS** یا **Trojan**،
+- روی انتقال خام **TCP**،
+- با امنیت **TLS** یا **REALITY**.
+
+هر قاعده fallback یک ورودی فرزند را هدف قرار می‌دهد و می‌تواند بر اساس `path`،
+`alpn` و `dest` تطبیق یابد. لینک‌های اشتراک کلاینت برای یک فرزند fallback
+به‌صورت خودکار بازنویسی می‌شوند تا آدرس، پورت و TLS ورودی اصلی را اعلام کنند.
+
+## مطمئن نیستید کدام را انتخاب کنید؟
+
+از این جادوگر استفاده کنید تا بر اساس اهداف و کلاینت‌هایتان یک پیشنهاد دریافت کنید:
+
+<ProtocolWizard />
+
+<Callout type="info">
+  برای مقاومت در برابر سانسور با کلاینت‌های مدرن، **VLESS + REALITY +
+  XTLS-Vision** معمولاً بهترین انتخاب است — به
+  [REALITY](/docs/config/reality) ادامه دهید.
+</Callout>

+ 14 - 0
docs/content/docs/fa/config/meta.json

@@ -0,0 +1,14 @@
+{
+  "title": "پیکربندی",
+  "icon": "Settings",
+  "pages": [
+    "panel",
+    "ssl-certificates",
+    "inbounds",
+    "reality",
+    "transports",
+    "clients",
+    "subscription",
+    "share-links"
+  ]
+}

+ 77 - 0
docs/content/docs/fa/config/panel.mdx

@@ -0,0 +1,77 @@
+---
+title: تنظیمات پنل
+description: تمام تنظیمات پنل 3x-ui — وب‌سرور، TLS، نمایش، امنیت و اعلان‌ها — همراه با مقادیر پیش‌فرض برگرفته از سورس.
+icon: SlidersHorizontal
+---
+
+**تنظیمات پنل** نحوه‌ی ارائه و ایمن‌سازی خودِ پنل را کنترل می‌کند (جدا از ورودی‌ها
+و کلاینت‌های شما). تنظیمات به‌صورت جفت‌های کلید/مقدار ذخیره می‌شوند؛ مقادیر
+پیش‌فرض زیر مستقیماً از سورس پنل گرفته شده‌اند. اطلاعات محرمانه (توکن‌ها، گذرواژه‌ها)
+فقط با نشانگرِ «تنظیم‌شده / تنظیم‌نشده» نمایش داده می‌شوند و هرگز به‌صورت کامل به
+مرورگر بازگردانده نمی‌شوند.
+
+## وب‌سرور
+
+| Setting             | Default                 | Meaning                                                                 |
+| ------------------- | ----------------------- | ----------------------------------------------------------------------- |
+| `webPort`           | `2053`                  | پورت پنل (۱ تا ۶۵۵۳۵). متغیر محیطی `XUI_PORT` در زمان اجرا آن را بازنویسی می‌کند. |
+| `webListen`         | _(همه‌ی واسط‌ها)_      | پنل را به یک IP مشخص مقید کنید.                                        |
+| `webBasePath`       | `/`                     | مسیر URLی که پنل زیر آن ارائه می‌شود (همیشه به شکل `/…/` نرمال‌سازی می‌شود).        |
+| `webCertFile` / `webKeyFile` | _(هیچ‌کدام)_   | گواهی + کلید TLS. وقتی هر دو تنظیم شوند، پنل با **HTTPS** ارائه می‌شود.   |
+| `sessionMaxAge`     | `360`                   | طول عمر نشست بر حسب **دقیقه** (پیش‌فرض ۶ ساعت).                      |
+| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128`  | IPها/CIDRهایی که هدرهای فورواردشده‌شان (IP واقعی کلاینت) مورد اعتماد است.         |
+| `panelOutbound`     | _(هیچ‌کدام)_                | مسیریابی خروجیِ خود پنل (بررسی به‌روزرسانی‌ها، Telegram، واکشی geo/sub) از طریق یک خروجی Xray با نام مشخص. |
+
+پس از تغییر پورت یا مسیر پایه، آدرس پنل به‌صورت
+`http(s)://<server>:<port><web-base-path>` درمی‌آید. می‌توانید مسیر پایه را در
+نخستین اجرا با [`XUI_INIT_WEB_BASE_PATH`](/docs/reference/env-vars) از پیش تعیین کنید.
+
+### TLS
+
+ارائه‌ی پنل روی HTTPS از اطلاعات احراز هویت شما در حین انتقال محافظت می‌کند. یا
+`webCertFile` + `webKeyFile` را تنظیم کنید — [منوی SSL در `x-ui`](/docs/config/ssl-certificates)
+می‌تواند یک گواهی Let's Encrypt برای شما تهیه کند — یا TLS را روی یک
+[پروکسی معکوس](/docs/operations/reverse-proxy) خاتمه دهید.
+
+<Callout type="warn">
+  هرگز پنل را روی اینترنت عمومی با HTTP ساده در دسترس قرار ندهید. از TLS، یک پورت
+  غیرپیش‌فرض، و یک مسیر پایه‌ی وب تصادفیِ بلند استفاده کنید.
+</Callout>
+
+## نمایش
+
+| Setting          | Default                                          | Meaning                                                       |
+| ---------------- | ------------------------------------------------ | ------------------------------------------------------------- |
+| `pageSize`       | `25`                                             | تعداد ردیف در هر صفحه از فهرست‌ها (`0` صفحه‌بندی را غیرفعال می‌کند).             |
+| `expireDiff`     | `0`                                              | تعداد روز پیش از انقضا برای شروع هشدار.                          |
+| `trafficDiff`    | `0`                                              | درصد باقی‌مانده از سهمیه که در آن هشدار آغاز می‌شود.         |
+| `remarkTemplate` | `{{INBOUND}}-{{EMAIL}}\|📊{{TRAFFIC_LEFT}}\|⏳{{DAYS_LEFT}}D` | الگوی پیش‌فرض یادداشت کلاینت (نگاه کنید به [لینک‌های اشتراک‌گذاری](/docs/config/share-links#remark-template-variables)). |
+| `timeLocation`   | `Local`                                          | منطقه‌ی زمانی برای آمار و انقضا.                               |
+| `datepicker`     | `gregorian`                                      | تقویم برای ورودی‌های تاریخ (میلادی یا جلالی/شمسی).       |
+
+## امنیت و احراز هویت
+
+اطلاعات احراز هویت، احراز هویت دومرحله‌ای، محدودکننده‌ی حملات جستجوی فراگیر،
+نشست‌ها و LDAP در [نخستین ورود](/docs/guide/first-login) و
+[امنیت](/docs/operations/security) پوشش داده شده‌اند. به‌طور خلاصه:
+
+- گذرواژه‌ها به‌صورت هش‌های **bcrypt** ذخیره می‌شوند؛ تغییر آن‌ها همه‌ی نشست‌ها را از سیستم خارج می‌کند.
+- **۲FA (TOTP)** می‌تواند در زمان ورود الزامی شود.
+- یک سازوکار جایگزین **LDAP** می‌تواند زمانی که بررسی گذرواژه‌ی محلی ناموفق باشد، کاربران را احراز هویت کند.
+- دسترسی API از **توکن‌های API** استفاده می‌کند که زیر تنظیمات پنل مدیریت می‌شوند (نگاه کنید به
+  [مرجع API](/docs/reference/api/api-tokens)).
+
+## اعلان‌ها و اشتراک
+
+این موارد گروه‌های تنظیمات و صفحه‌های مخصوص خود را دارند:
+
+<Cards>
+  <Card title="ربات Telegram" href="/docs/operations/telegram-bot" description="توکن، شناسه‌های چت، هشدارها و گزارش‌ها." />
+  <Card title="اشتراک" href="/docs/config/subscription" description="سرور اشتراک، قالب‌ها و مسیرها." />
+  <Card title="امنیت" href="/docs/operations/security" description="۲FA، محدودیت‌های IP و سخت‌سازی." />
+</Cards>
+
+<Callout type="info">
+  اعلان‌های ایمیل (SMTP) نیز قابل پیکربندی‌اند (میزبان، پورت، رمزنگاری،
+  گیرندگان) با همان انواع رویدادهای ربات Telegram.
+</Callout>

+ 135 - 0
docs/content/docs/fa/config/reality.mdx

@@ -0,0 +1,135 @@
+---
+title: REALITY
+description: راه‌اندازی یک ورودی VLESS + REALITY همراه با XTLS-Vision در 3x-ui — کلیدها، short IDها، SNI، اثرانگشت‌ها و اشتباهات رایج.
+icon: ShieldCheck
+---
+
+**REALITY** یک لایه‌ی امنیتی انتقال در Xray است که پروکسی شما را به‌شکل ترافیک
+عادی به یک وب‌سایت واقعی و پرطرفدار جلوه می‌دهد. برخلاف TLS کلاسیک، سرور شما به
+**هیچ گواهی اختصاصی** نیاز ندارد — دست‌دهی (handshake) TLS سایت مقصد (`dest`) را
+قرض می‌گیرد. در ترکیب با جریان **XTLS-Vision**، سریع است و در برابر بازرسی عمیق
+بسته‌ها (DPI) مقاومت می‌کند.
+
+REALITY همراه با **VLESS** (و Trojan) به‌کار می‌رود. جریان توصیه‌شده
+`xtls-rprx-vision` است.
+
+## تنظیمات کلیدی
+
+وقتی **REALITY** را به‌عنوان حالت امنیتی روی یک ورودی VLESS انتخاب می‌کنید، 3x-ui
+این فیلدها را نمایش می‌دهد:
+
+| فیلد                     | چیست                                                               |
+| ------------------------ | ------------------------------------------------------------------ |
+| **Dest (target)**        | یک سایت TLS واقعی برای جعل هویت، برای مثال `www.microsoft.com:443`. |
+| **SNI / Server Names**   | نام میزبانی که کلاینت‌ها ارسال می‌کنند؛ باید با گواهی مقصد بخواند.  |
+| **Public / Private key** | یک جفت‌کلید **x25519**. کلید خصوصی روی سرور باقی می‌ماند.           |
+| **Short IDs**            | رشته‌های Hex برای احراز هویت کلاینت‌ها (می‌توانید چندتا داشته باشید).|
+| **Flow**                 | روی `xtls-rprx-vision` تنظیم شود.                                  |
+| **Fingerprint (uTLS)**   | اثرانگشت TLS کلاینت برای تقلید، برای مثال `chrome`.                 |
+
+کلید خصوصی با ابزار `x25519` از Xray تولید می‌شود (پنل می‌تواند این جفت‌کلید را
+برای شما بسازد):
+
+```bash title="generate an x25519 keypair"
+xray x25519
+```
+
+## راه‌اندازی در پنل
+
+<Steps>
+
+<Step>
+### ساخت یک ورودی VLESS
+
+یک ورودی جدید اضافه کنید، پروتکل **VLESS** را انتخاب کنید و **Security** را روی
+**reality** تنظیم کنید.
+</Step>
+
+<Step>
+### انتخاب مقصد (dest) و SNI
+
+سایتی معتبر که از TLS 1.3 و HTTP/2 پشتیبانی می‌کند و هم از سرور و هم از کلاینت‌های
+شما در دسترس است انتخاب کنید (برای مثال `www.microsoft.com:443`). نام سرورها / SNI
+را طوری تنظیم کنید که با گواهی آن سایت بخواند.
+</Step>
+
+<Step>
+### تولید کلیدها و short IDها
+
+جفت‌کلید x25519 و یک یا چند short ID تولید کنید. **کلید خصوصی** را محرمانه نگه
+دارید؛ کلاینت‌ها فقط **کلید عمومی** را دریافت می‌کنند.
+</Step>
+
+<Step>
+### تنظیم جریان و اثرانگشت
+
+از جریان `xtls-rprx-vision` و یک اثرانگشت رایج uTLS مانند `chrome` استفاده کنید.
+</Step>
+
+<Step>
+### افزودن کلاینت و اشتراک‌گذاری لینک
+
+یک کلاینت بسازید، سپس از لینک اشتراک‌گذاری یا کد QR آن در یک برنامه‌ی سازگار
+(v2rayNG، Hiddify، Mihomo و دیگران) استفاده کنید.
+</Step>
+
+</Steps>
+
+## پیکربندی چه شکلی است
+
+روی سرور، `streamSettings` یک ورودی REALITY تقریباً به این شکل است:
+
+```json title="server inbound (excerpt)"
+{
+  "network": "tcp",
+  "security": "reality",
+  "realitySettings": {
+    "dest": "www.microsoft.com:443",
+    "serverNames": ["www.microsoft.com"],
+    "privateKey": "<x25519 private key>",
+    "shortIds": ["<hex short id>"],
+    "fingerprint": "chrome"
+  }
+}
+```
+
+لینک اشتراک‌گذاری متناظر کلاینت، پارامترهای **عمومی** را حمل می‌کند:
+
+```text title="vless:// (excerpt)"
+vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni=www.microsoft.com&fp=chrome&spx=%2F&flow=xtls-rprx-vision#my-reality
+```
+
+- `pbk` — کلید **عمومی** REALITY
+- `sid` — short ID (با یکی از موارد روی سرور می‌خواند)
+- `sni` — نام سرور (با گواهی مقصد می‌خواند)
+- `fp` — اثرانگشت کلاینت
+- `spx` — مسیر spiderX
+- `flow` — `xtls-rprx-vision`
+
+## اشتباهات رایج
+
+<Callout type="warn">
+
+- **مقصد نامناسب.** مقدار `dest` باید یک سایت واقعی باشد که از **TLS 1.3** و
+  **HTTP/2** پشتیبانی می‌کند، در دسترس است و در منطقه‌ی شما مسدود نیست. سایتی را
+  انتخاب کنید که مالک آن نیستید و ترافیک زیادی دارد.
+- **عدم تطابق SNI.** مقدار SNI / نام سرورها باید با گواهی واقعی مقصد بخواند، وگرنه
+  دست‌دهی، استتار را لو می‌دهد.
+- **نشت کلید خصوصی.** فقط و فقط **کلید عمومی** را میان کلاینت‌ها توزیع کنید.
+- **جریان نادرست.** REALITY + XTLS-Vision به `flow = xtls-rprx-vision` هم در ورودیِ
+  مدخل کلاینت و هم در لینک اشتراک‌گذاری نیاز دارد.
+
+</Callout>
+
+## تولید یک پیکربندی
+
+از تولیدکننده‌ی زیر برای ساخت یک جفت‌کلید تازه‌ی X25519، UUID و short ID استفاده
+کنید، سپس JSON ورودی سرور و لینک اشتراک‌گذاری کلاینت را کپی کنید. همه‌چیز **در
+مرورگر شما** محاسبه می‌شود — هیچ کلید یا لینکی به جایی ارسال نمی‌شود.
+
+<RealityConfigGenerator />
+
+<Callout type="info">
+  **کلید خصوصی** فقط به سرور شما تعلق دارد. لینک تولیدشده‌ی `vless://` (که حاوی
+  **کلید عمومی** است) را با کلاینت‌ها به اشتراک بگذارید.
+</Callout>

+ 81 - 0
docs/content/docs/fa/config/share-links.mdx

@@ -0,0 +1,81 @@
+---
+title: لینک‌های اشتراک‌گذاری
+description: قالب‌های لینک اشتراک‌گذاری 3x-ui (vless، vmess، trojan، ss، hysteria2، mtproto)، متغیرهای قالب توضیحات، و بازرس لینک درون‌مرورگری.
+icon: Link
+---
+
+3x-ui برای هر کلاینت یک **لینک اشتراک‌گذاری** (و کد QR) تولید می‌کند. برنامه‌های کلاینت
+مانند v2rayNG، Hiddify و Mihomo این لینک‌ها را برای پیکربندی خودشان وارد می‌کنند.
+
+## قالب‌های لینک
+
+| Scheme         | ساختار                                                          |
+| -------------- | --------------------------------------------------------------- |
+| `vless://`     | `vless://<uuid>@<host>:<port>?<params>#<remark>`                |
+| `vmess://`     | `vmess://<base64-json>` (یک شیء JSON کدشده با base64)           |
+| `trojan://`    | `trojan://<password>@<host>:<port>?<params>#<remark>`           |
+| `ss://`        | `ss://<userinfo>@<host>:<port>?<params>#<remark>` (SIP002؛ Shadowsocks-2022 از userinfo کدگذاری‌شده با درصد استفاده می‌کند) |
+| `hysteria2://` | `hysteria2://<auth>@<host>:<port>?<params>#<remark>`            |
+| `tg://proxy`   | `tg://proxy?server=…&port=…&secret=…` (MTProto)                 |
+
+پارامترهای کوئری تنظیمات انتقال و امنیت را حمل می‌کنند — `security`،
+`sni`، `fp`، `pbk`، `sid`، `spx`، `flow`، `type`، `path`، `host`، `alpn` و
+بیشتر.
+
+## بازرسی یک لینک
+
+هر لینک اشتراک‌گذاری را بچسبانید تا تک‌تک فیلدها رمزگشایی شوند. تجزیه **به‌طور کامل در
+مرورگر شما** انجام می‌شود — لینک هرگز روی شبکه ارسال نمی‌شود.
+
+<ShareLinkInspector />
+
+<Callout type="warn">
+  لینک‌های اشتراک‌گذاری همه‌ی آنچه را برای اتصال به‌عنوان یک کلاینت لازم است در بر دارند، از جمله
+  اعتبارنامه‌ی کلاینت. با آن‌ها مانند رمز عبور رفتار کنید.
+</Callout>
+
+## متغیرهای قالب توضیحات
+
+متنی که در هر لینک پس از `#` می‌آید (**توضیحات** یا remark) از یک قالب تولید می‌شود
+که شما در تنظیمات پنل کنترل می‌کنید (`remarkTemplate`). مقدار پیش‌فرض این است:
+
+```text
+{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D
+```
+
+توکن‌ها از نحو `{{UPPER_CASE}}` استفاده می‌کنند. قالب بر اساس `|` به بخش‌هایی تقسیم می‌شود؛
+بخشی که تنها مقدارش نشانگر نامحدود `∞` باشد (برای `TRAFFIC_LEFT`،
+`TRAFFIC_TOTAL`، `DAYS_LEFT` یا `TIME_LEFT`) حذف می‌شود، تا کلاینت‌های نامحدود
+تزئینات خالی نشان ندهند.
+
+### توکن‌های در دسترس
+
+| توکن | مقدار |
+| ----- | ----- |
+| `{{EMAIL}}` / `{{USERNAME}}` | ایمیل کلاینت (شناسه) |
+| `{{INBOUND}}` | توضیحات اینباند |
+| `{{HOST}}` | توضیحات ردیف هاست (هاست‌های مدیریت‌شده) |
+| `{{ID}}` / `{{SHORT_ID}}` | UUID کلاینت / ۸ کاراکتر نخست آن |
+| `{{TELEGRAM_ID}}` · `{{SUB_ID}}` · `{{COMMENT}}` | شناسه Telegram، شناسه اشتراک، توضیح |
+| `{{STATUS}}` / `{{STATUS_EMOJI}}` | `active`/`expired`/`depleted`/`disabled` (یا ✅⏳🚫) |
+| `{{DAYS_LEFT}}` / `{{TIME_LEFT}}` | روزهای باقی‌مانده، یا `Xd Xh Xm` (`∞` در صورت نامحدود بودن) |
+| `{{EXPIRE_DATE}}` / `{{JALALI_EXPIRE_DATE}}` / `{{EXPIRE_UNIX}}` | تاریخ انقضا به‌صورت میلادی / شمسی / ثانیه‌های Unix |
+| `{{CREATED_UNIX}}` | زمان ایجاد (ثانیه‌های Unix) |
+| `{{TRAFFIC_USED}}` / `{{TRAFFIC_LEFT}}` / `{{TRAFFIC_TOTAL}}` | مصرف خوانا برای انسان (`∞` در صورت نامحدود بودن) |
+| `{{TRAFFIC_USED_BYTES}}` / `{{TRAFFIC_LEFT_BYTES}}` / `{{TRAFFIC_TOTAL_BYTES}}` | همان، بر حسب بایت |
+| `{{UP}}` / `{{DOWN}}` | آپلود / دانلود (خوانا برای انسان) |
+| `{{RESET_DAYS}}` · `{{USAGE_PERCENTAGE}}` | دوره بازنشانی (روز) · درصد مصرف‌شده |
+| `{{PROTOCOL}}` / `{{TRANSPORT}}` / `{{SECURITY}}` | مثلاً `VLESS` / `ws` / `REALITY` |
+
+<Callout type="info">
+  توکن‌های مصرف (ترافیک، روزها، وضعیت) در **بدنه**ی اشتراک ظاهر می‌شوند اما
+  از نمای نمایش/QR حذف می‌گردند، تا یک QR اشتراک‌گذاری‌شده سهمیه‌ی باقی‌مانده‌ی یک کلاینت
+  را افشا نکند. توکن‌های تاریخ از تنظیم `datepicker` پیروی می‌کنند (میلادی یا شمسی).
+</Callout>
+
+## مرتبط
+
+<Cards>
+  <Card title="REALITY" href="/docs/config/reality" description="یک پیکربندی و لینک VLESS + REALITY بسازید." />
+  <Card title="اشتراک" href="/docs/config/subscription" description="همه‌ی لینک‌های یک کلاینت را از یک URL ارائه دهید." />
+</Cards>

Деякі файли не було показано, через те що забагато файлів було змінено