All your VPN subscriptions. One local proxy. Zero reconfiguration.
Self-hosted control panel that merges nodes from every provider you have
and always keeps the fastest one active.
You have more than one VPN provider — a paid subscription here, a backup vless:// link there,
an AmneziaWG config from a friend, an encrypted happ:// link from another service. Each comes
in its own format, needs its own client app, and when the active server degrades you switch
everything by hand, on every device.
submerge turns that pile of subscriptions into a single, self-managing local proxy.
- Paste anything — a subscription URL, a
vless:///happ://link, a WireGuard.conf. The type is detected automatically. - submerge merges all nodes into one list, feeds them to the battle-tested mihomo engine, and races them by latency.
- Your devices connect once — to
127.0.0.1:7890(SOCKS5 + HTTP). Which provider and which server is behind that port is submerge's job, not yours. Point a router at it and the whole network benefits.
Providers come and go, quotas run out, servers degrade — the proxy endpoint never changes.
- 🔌 Every source format through one input field — auto-detected on paste:
- subscription URLs: clash-yaml, base64, v2ray/sing-box JSON (+ quota, expiry and refresh-interval metadata from provider headers);
- single links:
vless://,vmess://,trojan://,ss://,hysteria2://,tuic://; - encrypted
happ://links — decoded by the official Happ binary, so it survives provider key rotation (ADR-0001); - WireGuard / AmneziaWG
.conffiles (drag & drop) and Amneziavpn://links; - client deep-links (Clash, sing-box, incy, …) — the wrapped URL is extracted.
- ⚡ Automatic node selection with real policies, not just a dumb url-test: latency race with a switch tolerance (no flapping), sticky IP mode that holds a node while it's healthy, priority node mode — plus a visible decision history explaining every switch. Manual mode one click away.
- 📈 Live dashboard — latency and traffic streamed over SSE in real time, per-node ping, latency history, nodes grouped by subscription with same-name nodes collapsed into auto url-test groups.
- 🔐 Sane security defaults — everything binds to
127.0.0.1; optional single-admin password (argon2id sessions) for internet-facing deploys; per-source X-Hwid for device-bound providers (ADR-0002). - 📦 One
docker compose up— prebuilt multiarch (amd64/arm64) images from GHCR, SQLite persistence, health checks, LAN-access toggle and engine restart from the UI.
flowchart LR
subgraph you["Your network"]
DEV["Devices / router /<br/>any SOCKS5-HTTP client"]
end
subgraph stack["docker compose"]
SUB["<b>submerge</b><br/>web UI + control plane<br/>:3000"]
MI["<b>mihomo</b> engine<br/>proxy :7890"]
HD["<b>happ-decoder</b><br/>official Happ binary"]
end
SRC["Subscriptions · vless:// ·<br/>happ:// · .conf files"] --> SUB
SUB -- "generates config.yaml,<br/>controls via REST API" --> MI
SUB -. "happ:// links only" .-> HD
DEV --> MI --> INET(("Internet<br/>via best node"))
- submerge — the app itself (React SPA + tRPC/SSE server in one container): ingests sources, generates the mihomo config, hot-reloads the engine, streams live node status.
- mihomo — the tunneling engine (Clash core) that actually moves the traffic: VLESS Reality, Hysteria2, WireGuard and friends, health checks, url-test groups.
- happ-decoder — the official Happ desktop binary running headless (Xvfb + mitmproxy)
as an on-demand
happ://decoding service.
More detail: docs/architecture.md.
Sources — paste, drop a .conf, done |
Settings — selection policies, decision history |
|---|---|
![]() |
![]() |
git clone https://github.com/gentslava/submerge && cd submerge
docker compose up -d- Open http://127.0.0.1:3000 and paste a subscription /
vless:///happ://— the type is detected automatically. - Point your apps (or router) at the proxy —
127.0.0.1:7890, SOCKS5 and HTTP on the same port:
curl --proxy socks5h://127.0.0.1:7890 https://api.ipify.org # prints the node's IPNo .env is needed for localhost — the compose defaults just work.
Note: the
happ-decoderimage bundles the official Happ desktop + Qt, so it is a few hundred MB — the first pull takes a moment.
The panel is designed to sit behind a TLS reverse-proxy (Traefik / Caddy / Dokploy). Copy the template and set the essentials:
cp .env.example .env| Variable | Default | Purpose |
|---|---|---|
ADMIN_PASSWORD |
(empty — auth off) | Admin password. Required before exposing the panel: while empty, anyone can use it. |
COOKIE_SECURE |
false |
Set true behind HTTPS, otherwise the browser drops the session cookie. Don't leave blank. |
SUBMERGE_BIND |
127.0.0.1 |
Host address for port 3000. Keep loopback and front it with TLS; 0.0.0.0 only for deliberate direct exposure. |
MIHOMO_SECRET |
poc |
Secret for the internal mihomo REST API — change it from the default. |
On Linux hosts the submerge container runs as uid 999 and writes the shared mihomo
config into the bind-mounted ./mihomo — make it writable: chown -R 999:999 mihomo
(automatic on Docker Desktop).
The runtime mihomo/config.yaml contains your nodes and is git-ignored — never commit it.
Node 24 LTS · pnpm workspaces · strict TypeScript · Biome · Vitest.
pnpm install
pnpm dev:infra # isolated mihomo :7890/:9090 + decoder :8080
pnpm dev:server # TypeScript API on the default :3000
pnpm dev:web # Vite/HMR on :5173; /trpc → :3000
pnpm test # vitest
pnpm typecheck # tsc -b --noEmit
pnpm lint # biome ci .Run the three development commands in separate terminals. The development sidecars use
their own Compose project and .local-run/mihomo directory; the standard ports make a
conflicting stack fail loudly instead of connecting to the wrong controller. The server
keeps its normal :3000 default; override both processes when needed, for example
PORT=3100 pnpm dev:server and
VITE_PROXY_TARGET=http://127.0.0.1:3100 pnpm dev:web.
Monorepo layout: packages/shared (Zod domain schemas — the single
front↔back contract), packages/server (tRPC, Drizzle + SQLite, SSE,
service clients), packages/web (React SPA, Indigo Console design system).
See CONTRIBUTING.md and AGENTS.md.
- Specs: docs/specs/ · Plans: docs/plans/ · Decisions: docs/adr/ · Design system: docs/design-system.md
The sidebar already hints at it: Traffic, Connections, Routing (multi-channel: several active channels with per-rule routing), Logs and Diagnostics screens are on the way.
MIT. This project is not affiliated with Happ or mihomo.


