How NexorPanel is built
NexorPanel is a self-contained hosting control panel: its own interface, its own API, its own accounts, its own storage and its own agent on the machine. Nothing is borrowed from another program.
Panel and agent
browser ──HTTPS──► nexor-api (Go) ──── panel.db (SQLite): accounts,
React/TS SPA │ sites, log
│
└──HMAC──► nexor-agent on the machine
│
├─ site folders, system users
├─ web server configuration + reload
└─ metrics, services, files, logs
The panel stores what the server should look like. It does not walk the machine, does not know its paths and does not run programs on it. Its data file answers one question: “how should things be”.
The agent brings the machine to that state. It knows its machine, its folders and its services — and nothing about the panel except what the panel sent: a domain name, a mailbox name, an “enabled” flag. A path never arrives in a request; the agent takes its folders from its own configuration, written by the machine's administrator, not by the panel.
The main property follows from this: whoever gets into the panel does not get the machine. They get the right to ask the agent for what the agent was configured to do — and nothing beyond that. The one named exception is the Terminal section, which runs a typed command on the machine; it is off by default and is switched on by a person on the machine, with NEXOR_AGENT_CONSOLE=1 in the agent's settings.
Components
| Program | What it does |
|---|---|
nexor-api | The panel: the HTTP API, sessions, accounts, its own storage, and the built interface. One static Go binary under systemd, behind nginx. |
nexor-api doctor | Checks a live installation using the same environment as the service: data file, agent settings, session key, request origin, trusted proxies. Changes nothing; says what exactly to fix. |
nexor-agent | The helper on each managed machine: site folders, system users, web server, PHP pools, certificates, mail, DNS, databases, backups, metrics, services. Accepts only HMAC-signed requests from the panel. |
nexor-hook | An optional webhook receiver: a git push becomes a server update. |
| Web interface | A React + TypeScript single-page app. It is an ordinary client of the API and has no other path into the panel — so anything the interface can do, the API can do too. |
Source layout
| Folder | Contents |
|---|---|
apps/api | Go backend: the panel's API, storage, sessions, agent, doctor |
apps/web | React + TypeScript frontend: shell, dashboard, lists, sign-in; end-to-end tests with Playwright and axe |
deploy | systemd units, configuration examples, nginx, the webhook receiver, installation guide |
packaging | .deb packages of the panel and of the agent: control files, postinst / prerm / postrm |
docs | architecture, API, roadmap, mockups and screenshots |
Backend layers:
cmd/nexor-api entry point, configuration, mode selection, graceful shutdown cmd/nexor-agent the agent on the managed machine cmd/nexor-hook the webhook receiver internal/config configuration from the environment internal/native own storage: the data file, accounts, sites internal/agent the agent and the panel's client to it internal/store what the panel needs from a data source: interface and implementations internal/session sessions, HttpOnly / SameSite=Strict cookies internal/httpapi routes, middleware, JSON responses and error codes
internal/store is the boundary: everything the panel can do is described there as an interface. Own storage, an external engine and fixtures all plug into the same place, and no screen knows which one is behind it.
On the server
| Path | What |
|---|---|
/usr/local/bin/nexor-api | the panel binary, run as the system user nexor |
/usr/local/bin/nexor-agent | the agent binary, run as root |
/usr/local/share/nexorpanel/web | the built interface (NEXOR_WEB_ROOT) |
/etc/nexorpanel/nexor-api.env | panel settings, chmod 600; the package creates it with a generated NEXOR_SESSION_KEY |
/etc/nexorpanel/nexor-agent.env | agent settings: listen address, shared secret, services, folders |
/var/lib/nexorpanel/panel.db | all of the panel's own data (NEXOR_DATA) |
nexor-api.service, nexor-agent.service | systemd units |
The panel listens on 127.0.0.1:8080 and the agent on 127.0.0.1:8462 by default; nginx or another TLS terminator sits in front of the panel. One panel can manage several machines: each one runs its own agent, and the panel is told about them in NEXOR_AGENTS.
Run modes
The mode is not a separate setting; it follows from the others.
| Mode | When | What it does |
|---|---|---|
| Native | nothing set — the default | own storage, own agent |
| External engine | NEXOR_ENGINE_URL is set | works through an external execution engine over its API; kept for installations that already run one |
| Fixtures | NEXOR_MOCK=1 | demonstration data; manages nothing, useful to look at the screens |
Order of operations
Record first, machine second. If writing the record fails, nothing is left on disk that the panel does not know about. The worst case is a record without a folder: it is visible in the list and can be applied again or deleted.
Deleting goes the other way: first removed from the machine, then forgotten. Deleting a site removes its configuration but does not touch its files — configuration is recreated in a minute, files are not.
Every section is being rewritten onto the panel's own code one by one. Until a section is ready, it answers “not connected yet” rather than an empty list, because an empty list reads as “there is nothing”.
Accounts and roles
- Roles: administrator (everything, including servers, plans, firewall and logs), reseller (their own clients and their objects) and client (only their own sites, mail, DNS, databases).
- A client sees only their own objects, and the query guarantees it, not a filter over the response. Someone else's object and a missing one both answer
404. - Passwords are bcrypt hashes. A disabled account says “disabled” only to someone who knows the password.
- Sessions are sealed cookies (AES-256-GCM,
HttpOnly,Secure,SameSite=Strict), rechecked on every request: a disabled account, a changed password or a removed role ends the session immediately. - Second factor — TOTP codes with recovery codes.
- Programs use API keys, not passwords: one key per task, read-only if needed, with an expiry, revocable on its own. See API keys.
- The first administrator is created on the first start. The password comes from
NEXOR_ADMIN_PASSWORDor is generated and written to the log once.
Storage
One SQLite file next to the panel: accounts, sites, the panel's log, second-factor bindings, API keys and revoked sessions. There is no separate database server — a panel that needs another program to start is no longer self-contained. A backup is a copy of the file, and on an empty machine with just the binary the panel comes up whole.
The schema is applied on every start, and every step is idempotent, so there are no schema versions to drift apart. Copies of the panel can also be sent to S3-compatible storage (NEXOR_S3_*), encrypted beforehand with NEXOR_BACKUP_KEY.
Configuration
Everything is configured with environment variables in the .env files above. The most important ones:
| Variable | Meaning |
|---|---|
NEXOR_ADDR | where the panel listens, e.g. 127.0.0.1:8080 |
NEXOR_PUBLIC_ORIGIN | the origin the panel is served from; state-changing requests from anywhere else are refused |
NEXOR_SESSION_KEY | 32 bytes in base64 that seal session cookies; without it every restart signs everyone out |
NEXOR_SESSION_TTL | session lifetime, e.g. 8h |
NEXOR_DATA | the data file |
NEXOR_WEB_ROOT | the built interface; empty if nginx serves it |
NEXOR_ENTRANCE | secret entrance: a path segment under which the whole panel lives |
NEXOR_TRUSTED_PROXIES | proxies whose X-Forwarded-For is believed |
NEXOR_AGENTS | the machines and their agents |
NEXOR_S3_*, NEXOR_BACKUP_KEY | off-site storage for panel copies and its encryption key |
NEXOR_NOTIFY_URL, NEXOR_NOTIFY_TOKEN | where notifications are sent outside the panel |
NEXOR_IMAP, NEXOR_SMTP | mail servers for webmail |
NEXOR_AGENT_LISTEN, NEXOR_AGENT_SECRET | agent side: listen address and the shared HMAC secret |
NEXOR_AGENT_SERVICES | agent side: which systemd services the panel may start, stop and restart |
NEXOR_AGENT_CONSOLE | agent side: 1 enables the Terminal section |
Run nexor-api doctor after changing them.
Security rules
- State-changing requests are only
POST,PUTandDELETE, with anOrigincheck. - Administrative sections are closed by a separate
adminOnlylayer, independent of storage ownership checks. - The agent verifies an HMAC signature with a one-time nonce on every request; the secret is in a root-only file and is never sent anywhere.
- The agent runs only programs from its own list, with arguments it builds itself. There is no shell anywhere: a semicolon in a name is a semicolon, not a second command.
- Sign-in is rate-limited: 5 attempts a minute per address and login, plus a limit per address.
- File paths never leave the site's folder, including through symbolic links. Deleted files go to the site's
.nexor-trashfor 30 days. - A secret entrance (
NEXOR_ENTRANCE) hides the sign-in form from scanners: everything else gets an identical empty 404. - The service sets its own security headers — content security policy without inline scripts, frame ban,
nosniff,Referrer-Policy. - The panel never stores database passwords; phpMyAdmin is opened with a half-hour account created for that one visit.
Stack
| Layer | Choice |
|---|---|
| Backend | Go, one static binary under systemd; HTTP on the standard library, no web framework |
| Storage | SQLite in one file (modernc.org/sqlite, no cgo) |
| Passwords | bcrypt |
| Frontend | React 19 + TypeScript + Vite; own components on design tokens, own SVG charts |
| Languages | English, Russian, Ukrainian, Belarusian, German, Polish — the API answers in the language of Accept-Language |
| Checks | gofmt, go vet, go test -race; type checks and dictionary completeness; installer tests; end-to-end tests with WCAG 2.1 AA checks in both themes |