# Yacd-meta: a static web dashboard for the Clash controller API

> Yacd-meta is a single page dashboard for the Clash external-controller API, built with React, Vite and bun, that you point at a controller with query parameters and host as static files. It ships as an nginx container image, and the README is unusually candid about why the public demo is served over plain HTTP and where the controller secret ends up.

**MetaCubeX/Yacd-meta** — Yet Another Clash Dashboard

- Repository: https://github.com/MetaCubeX/Yacd-meta
- Website: http://yacd.metacubex.one
- Stars: 795 · Forks: 234
- Language: TypeScript
- License: not declared
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/metacubex-yacd-meta

## What the dashboard actually talks to

Yacd-meta is the third iteration of a project whose name says it plainly: yet another Clash dashboard. Clash itself is a proxy client, and it exposes a controller API, configured through the external-controller setting, which a front end can call to see connections, rules and traffic.

This dashboard is a static single page application. The important consequence is that it is not a server-side application with a database and a session: the built assets are files, and everything the page knows comes from the browser talking to your controller. That makes deployment trivial and makes the security model the controller's own, which is a point worth returning to later.

The project's lineage shows in the README, which describes the tool as yet another Clash dashboard in the tradition of the original clash-dashboard. The current repository is the MetaCubeX rewrite, the name suffix meta marking the modernised version, and it has been restructured around a modern toolchain: TypeScript, React with the Vite build, bun as the package manager, and a container image that serves the built assets with nginx.

The audience is a self-hosted user who wants to see what their proxy is doing without reading log files, and who is comfortable pointing a browser at a local controller.

## Pointing the page at a controller

The connection is made with query parameters on the page URL, which is what allows a single static build to serve every user without any configuration file on the server.

The supported parameters are listed in the README. The hostname is the host part of the external-controller setting, and the port is the port part of it. The secret is the Clash API secret, which the README notes is the secret value in your config.yaml. The theme chooses the interface colour scheme, with dark, light and auto as the options, and the title sets a custom title for the webpage, which is displayed in the browser tab.

So a page loaded with a hostname, a port and a secret will connect to that controller and show its state, and adding a theme and a title changes only the presentation.

For the container image there is a related default, since the Dockerfile sets an environment variable named YACD_DEFAULT_BACKEND to http://127.0.0.1:9090. That is the address the bundled nginx configuration uses when a request arrives without the parameters in the URL, which is what makes the image usable against a controller on the same host without editing anything.

One consequence of the design deserves to be stated plainly. The secret is part of a URL. URLs are recorded in browser history, appear in the address bar, and can end up in server logs, which means the API secret of your proxy controller is travelling in a string that is not designed to be secret. It also means that sharing a configured link shares access to the controller.

## Why the public demo is not on HTTPS

The README explains the hosting decision, and the reasoning is a browser rule rather than a shortcut.

The site at yacd.metacubex.one is served with HTTP rather than HTTPS, and the stated reason is that many browsers block requests to HTTP resources from an HTTPS website. In other words, hosting the page on HTTPS while the controller API it calls is plain HTTP would break the page, so the page is served over HTTP to match the local controller it is meant to talk to.

For anyone who is not comfortable with that, the README offers the alternative: download the zip of the gh-pages branch, unzip it and serve the static files with a web server such as Nginx. That is the same content, hosted by you, and it is the arrangement to use if you want the page on HTTPS and can reach your controller over HTTPS as well.

There is one more small operational note in the same section. Installing twemoji is recommended so that emoji display better on Windows, which is a rendering detail rather than a security one, and it is the kind of thing that only shows up once you are on a Windows machine looking at a panel full of country flags.

## Running the development server with bun

The development workflow uses bun, and the README shows the three commands that matter.

Installing the dependencies is the first step, and bun is the tool that does it:

```sh
# install dependencies
# you may install bun from https://bun.sh
bun install

# start the dev server
# then go to http://127.0.0.1:3000
bun start


# build optimized assets
# ready to deploy assets will be in the directory `public`
bun run build
```

The dev server runs on port 3000 on the loopback interface, and the build writes optimised assets into a directory named public, which is the directory that gets deployed. The package.json records the same scripts in npm terms, with dev and start both running vite, build running vite build and serve running vite preview.

The lockfile is bun.lock and the package manager is pinned to bun 1.4 in the package metadata, so the dependency versions are reproducible for anyone using the same tool. The repository also carries a postcss configuration, a vite configuration in the form of vite.config.mts, a browserslist configuration for the browser targets, and an oxlint configuration for linting the sources, with the lint script running oxlint over src and the formatter running prettier across the TypeScript and SCSS sources.

## The stack and what each dependency is for

The dependency list is long enough that it tells you the design, and it splits cleanly into four groups.

For interface primitives, the project uses Radix UI for the dialog, switch, tabs and tooltip components, with class names assembled by clsx and the base layer normalised by modern-normalize. Icons come from react-feather, and the fonts are self-hosted through the @fontsource packages for Inter and Roboto Mono rather than loaded from a font CDN.

For data, the project uses TanStack Query for fetching and caching, and TanStack Table for the tabular views, with the data itself coming from the controller API. Charting is Chart.js with react-chartjs-2 and the date-fns adapter. Virtualised lists use react-window, which is what keeps a connection list with thousands of rows usable in a browser, and drag and drop reordering uses the @hello-pangea/dnd package, a maintained fork of the original react-beautiful-dnd.

For state, the project uses Jotai, whose atom model fits a dashboard where many unrelated values update independently. Small utilities fill the gaps: history for routing, invariant for assertions, memoize-one to avoid recomputing an unchanged value, and use-asset for loading assets in a component-oriented way.

The result is a fairly conventional modern React stack, and the notable choice is the split between Jotai for local state and TanStack Query for anything that comes from the controller, which keeps the polling and refetching logic out of the components.

## The container image serves the assets with nginx

The Dockerfile is a two-stage build, and the shape of it is the reason the container is small.

The builder stage starts from the bun image on Alpine, installs the frozen lockfile, copies the source and runs the production build. It then removes the source maps from the output, with a comment in the Dockerfile that people like small images, and tolerates the removal failing if there are no maps.

The runtime stage is nginx on Alpine. It installs a bundled configuration file, clears out the default nginx html directory, and copies the built assets across from the builder. The environment variable holding the default backend address is set on the final image, the entrypoint script is added, and that script is the container command.

So the running container is a web server serving static files, with a small shell script in front of it that presumably rewrites requests for the default backend. There is no application server and no runtime dependency on bun in the final image, which is what keeps it small.

The repository also holds the docker directory with the nginx configuration, a Dockerfile, the entrypoint script and a .dockerignore, and the release process is handled by release-please with a manifest and a configuration file, with the changelog kept in CHANGELOG.md.

## Where Yacd-meta is the wrong tool

The first limitation is the security model, and it comes from the design rather than from a bug. Because the page is static and talks to the controller from the browser, the controller API has to be reachable from wherever the browser is, and the secret has to be handed to that browser in the URL. A controller exposed on a public interface is a control panel for your proxy sitting behind a query string, and the README's advice to self-host the static files is a better answer than the hosted demo.

The second is scale. The dashboard addresses one controller at a time. There is no fleet view, no aggregation, no history and no per-user access model, so a site with twenty clients is twenty controllers, twenty URLs and twenty sets of secrets to distribute.

The third is the scope of what it does. It is a view onto the controller API. It does not manage configuration, edit rules, rotate the secret or handle subscriptions, and it stores nothing, so it cannot tell you what happened yesterday.

The fourth is the licensing inconsistency, which is a small thing but a real one. The package.json declares the licence as MIT, and the project is publicly usable under that reading, yet the repository records no recognised licence and there is no licence file among the top-level entries. Anyone redistributing the built assets inside a product should get that confirmed rather than infer it from a metadata field.

## Maintenance, releases and the Chinese README

The project is not archived and the last push was on 2026-10-11. The release history shows a busy few weeks: v0.4.0 on 2026-09-09, v0.5.0 on 2026-09-28 and v0.6.0 on 2026-09-30, which is the pattern of a rewrite that is still finding its shape, with minor versions landing close together.

The tooling is set up for that pace. Releases are cut by release-please from conventional commits, with a manifest recording the current version and a configuration file driving the release process, and the changelog is maintained in the repository. Linting uses oxlint rather than the eslint toolchain, formatting is prettier with a plugin that sorts imports, and type checking is a separate tsc pass with noEmit, so the three checks are independent scripts a contributor can run one at a time.

The repository also carries a CLAUDE.md for assistant tooling and a Chinese README alongside the English one, which is a reasonable pairing for a project of this origin. The base language of the codebase is TypeScript, with SCSS for styling, and the sources live in src with the static material in assets.

The package is marked as private in the package metadata, which reflects that it is not published to a registry as a library. It is an application you build and host, and the README's development instructions are the only supported way to produce it.

## Conclusion

Adopt Yacd-meta if you run a Clash client and want a readable view of its connections and rules from a browser, and you are willing to host the static files yourself. Do not adopt it as a way to manage a fleet of clients, since it talks to one controller at a time and holds no state of its own, and do not point it at a controller exposed without care, because the API secret travels in the URL. Verify first that your controller's external-controller is reachable from the browser with the right CORS behaviour, that the secret you pass is one you would be willing to have in browser history, and that the version you deploy matches the one the query parameter names in its documentation expect.

## FAQ

### How do I point Yacd-meta at my Clash controller?

With query parameters on the page URL: hostname and port for the host and port of the external-controller setting, and secret for the Clash API secret from your config.yaml. The theme and title parameters control the colour scheme and the browser tab title.

### Why is the public Yacd-meta demo not served over HTTPS?

Because many browsers block requests to HTTP resources from an HTTPS website, and the page needs to reach a controller that is usually on plain HTTP locally. The README suggests downloading the gh-pages zip and serving the static files yourself with a web server such as Nginx instead.

### How do I run the Yacd-meta development server?

Run bun install to install the dependencies and then bun start, which starts the Vite dev server on http://127.0.0.1:3000. The production build is bun run build, and the deployable assets land in the public directory.

### What does the Yacd-meta container image contain?

A two-stage build that compiles the assets with bun on Alpine and then serves them from nginx on Alpine, with a bundled nginx configuration, an entrypoint script and the YACD_DEFAULT_BACKEND environment variable pointing at http://127.0.0.1:9090 as the default controller address.

## Sources

- [Issues](https://github.com/MetaCubeX/Yacd-meta/issues)
- [MetaCubeX/Yacd-meta on GitHub](https://github.com/MetaCubeX/Yacd-meta)
- [Project website](http://yacd.metacubex.one)
- [README](https://github.com/MetaCubeX/Yacd-meta/blob/master/README.md)
- [Releases](https://github.com/MetaCubeX/Yacd-meta/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/metacubex-yacd-meta
