An open-source web console for OpenWrt routers, in the spirit of UniFi Network: one place to see what the network is doing and change it.
Zeeli Controller runs on GL.iNet firmware today and is tested on a GL-BE9300 (Flint 3). A Zeeli image built on OpenWrt is planned.
Most people already own a capable router. What they lack is a console they can trust: one that is fast and clear, and that tells them the truth about whether a change worked. Zeeli Controller is that console. It talks only to the router on your network. It has no vendor cloud, no account and no telemetry. When you save a setting, the console reads it back from the router and reports success only if the router kept the value.
All screenshots come from the demo router (pnpm dev:demo).
The console's pages map onto these sections. The full list lives in apps/console/src/app/nav.ts.
- Overview: a dashboard with live uplink state and per-device rates, plus an activity timeline.
- Topology: a zoomable map of the path from the internet, through the router, to each device.
- Clients: every device on the network. Rename, block, forget or rate-limit them.
- Insights: traffic by device and app, and DNS queries from AdGuard Home.
- Network: internet (WAN, repeater, PPPoE, static), WiFi, networks and VLANs, Ethernet ports, DHCP and leases, routing, multi-WAN, mesh, QoS, SQM, IPv6, IGMP snooping, NAT acceleration, EdgeRouter.
- Security: firewall rules, port forwarding, DNS and filtering (encrypted DNS, rebinding protection), content filter, access control, remote management.
- Family: parental control with schedules and pause.
- VPN: tunnels with policy routing, WireGuard and OpenVPN client profiles, WireGuard and OpenVPN servers, Tailscale, ZeroTier.
- Apps: an app store for built-in and OpenWrt package apps, with settings pages for AdGuard Home, Tor, dynamic DNS, Bark alerts and app detection, plus raw package management.
- System: device health, firmware, schedules, time zone, logs, admin access, operating mode, factory reset.
A command palette (Cmd+K or Ctrl+K) finds any page. Light and dark themes are both supported.
You need Node 24 (see .nvmrc) and pnpm 11.
pnpm installpnpm dev:demoOpen http://localhost:4318. The demo runs an in-memory router in your browser with a seeded home network. Every page works, and writes change the demo state. The password is demo.
pnpm devThe dev server proxies /rpc, /cgi-bin, /upload, /download and /control to the router, so the console behaves as if it were served from the router itself. Sign in with the router's admin password.
| Variable | Default | Purpose |
|---|---|---|
ROUTER_URL |
https://192.168.8.1 |
The router the dev proxy forwards API calls to. The router's self-signed certificate is accepted. |
VERIFY_SSH |
unset | When set, the dev server answers the console's reads by running them on the router over SSH with your key, and refuses every write. Useful for checking pages against live data without signing in. Dev only, never part of a build. |
ROUTER_SSH |
root@192.168.8.1 |
The SSH target used when VERIFY_SSH is set. |
ROUTER_URL=https://192.168.1.1 pnpm dev
VERIFY_SSH=1 ROUTER_SSH=root@192.168.1.1 pnpm devpnpm buildThe console builds to apps/console/dist as a static bundle with relative paths, so it can be served from a subpath on the router.
From an SSH session on the router (ssh root@192.168.8.1):
uclient-fetch -qO /tmp/zeeli-update https://github.com/mannobeats/zeeli-controller/releases/latest/download/zeeli-update && sh /tmp/zeeli-update install latestThis downloads the latest release, checks its signature against the project's release key and installs it at https://192.168.8.1/zeeli/. GL.iNet's own panel, LuCI and SSH keep working beside it. Sign in with the router's admin password; the session is shared with the stock panel.
It installs four things:
- the console in
/www/zeeli - the updater in
/usr/libexec/zeeli-update - the Zeeli agent: the router-side API the console uses where the stock firmware's own API is too limited. Its files are listed in
/usr/share/zeeli/manifest; the source is intools/router/agent. The console reaches it through the firmware's own/rpcwith the same admin session, so it needs no extra sign-in or permissions. - one permission file,
tools/router/zeeli-controller.acl.json. It lets the admin session see whether services are running and start or stop them, start a one-test iperf3 server, and send Wake on LAN packets. Nothing else.
To remove everything:
ssh root@192.168.8.1 /usr/libexec/zeeli-update removeA firmware upgrade that doesn't keep settings removes these; install again afterwards.
The console checks GitHub for a newer release every few hours. When one is out, the top bar shows Update to x.y.z, and System → Firmware lists what changed with an Install button. The router downloads the release itself and refuses any download that isn't signed with the release key.
ROUTER_SSH=root@192.168.8.1 pnpm deploy:routerThis builds the working copy and installs it over SSH (key access required), the same way an update does but without the signature. It shows as a development build, which can install the latest release from System → Firmware.
- On GitHub, open Actions → Release → Run workflow and pick which part of the version goes up: patch (
0.1.0 → 0.1.1), minor (0.1.1 → 0.2.0) or major. - The workflow runs every quality gate, builds the console with the new version, packages and signs it, and publishes release
vX.Y.Zwith the commit list since the last release as its notes.
Routers find it within a few hours, or straight away with Check again on the Firmware page.
A release carries three files: zeeli-controller.tar.gz (the console, the updater and the permission file), its signature zeeli-controller.tar.gz.sig, and zeeli-update for first installs.
Releases are signed with an Ed25519 key in usign format, the format OpenWrt uses for its own packages. The public key is in tools/router/zeeli-update, and the secret key is the ZEELI_SIGNING_KEY repository secret. To make a new key:
node tools/release/sign.mjs keygen release # writes release.pub and release.sec
gh secret set ZEELI_SIGNING_KEY < release.secThen put the contents of release.pub in zeeli-update and publish a release with the old key still in the secret, so routers trust the new updater. Only after that, set the secret to the new key. Keep release.sec somewhere safe and out of the repository.
apps/console React interface: pages, components, queries
|
packages/router-api Typed domain API: clients, wifi, networks, vpn, ...
|
Transport gl (GL.iNet: /rpc, LuCI ubus, AdGuard Home, file upload/download)
mock (in-memory router for the demo and tests)
- The console never builds a request itself. It calls the domain API, for example
api.clients.list()orapi.dns.config.set(...), and renders the result through TanStack Query. router-apiturns raw firmware answers into typed domain objects and raises one error type,RouterError, with messages written for people.- A
Transportis the only seam to the router. The GL.iNet transport and the mock implement the same interface, and a plain-OpenWrt transport will too, so screens do not change. - Writes are verified. Firmware often answers "ok" to a value it silently ignores, so a success response alone is not trusted.
writeAndVerifywrites, reads the setting back, and throws anotKepterror if the value did not stick. Settings pages usesettings()get/set pairs, which do this for every value they send. One-shot actions (rename, forget, reboot) refetch the data they touch, so the screen always shows what the router reports.
docs/ARCHITECTURE.md covers the code layout in depth and explains how to add a page or an app.
pnpm checkThis runs every gate in order. CI runs the same gates on every push and pull request.
| Command | What it checks |
|---|---|
pnpm lint |
Biome lint and formatting |
pnpm typecheck |
TypeScript across all packages |
pnpm test |
Vitest unit tests |
pnpm doctor |
react-doctor checks on the console |
pnpm fallow |
Unused code, duplication and dependency hygiene |
pnpm build |
Production build |
The release workflow runs pnpm check before it publishes anything.
- A plain-OpenWrt transport, so the console runs on stock OpenWrt as well as GL.iNet firmware.
- A Zeeli firmware image built on OpenWrt, with the console as its interface.
- Screenshots and a hosted demo.
See CONTRIBUTING.md. To report a security issue, follow SECURITY.md instead of opening a public issue.



