Hosted Hermes is a single Next.js application serving both the dashboard UI (server-rendered React + client components) and the REST API (route handlers). It manages Hermes agent instances that run as containers on a Docker host.
┌──────────────┐ session cookie ┌───────────────────────────────┐
│ Browser │ ────────────────────────▶ │ Dashboard pages (RSC) │
└──────────────┘ │ /dashboard/* │
├───────────────────────────────┤
┌──────────────┐ Bearer hhk_… │ Internal API /api/* │
│ API clients │ ────────────────────────▶ │ External API /api/v1/* │
└──────────────┘ ├───────────────────────────────┤
│ ProvisionService │
│ ├─ Store (file-backed) │
│ └─ Provisioner (driver) │
└────────────┬──────────────────┘
│ Engine API
┌──────▼──────┐
│ Docker │
│ containers │
└─────────────┘
All dashboard state — users, sessions, API keys, agent configs, instances,
metric samples — behind the Store interface:
MemoryStore— in-process Maps; used by tests.FileStore— wrapsMemoryStore, debounce-persists a JSON snapshot toHH_STORE_PATH. Suitable for single-node deployments. Swap in a database implementation ofStorefor HA.
The singleton (getStore()) seeds the admin user on first boot.
Provisioner is the runtime backend abstraction:
| Driver | Purpose |
|---|---|
DockerProvisioner |
Docker Engine API over unix socket or TCP via node:http — zero external deps |
MockProvisioner |
In-process simulation for dev/CI: lifecycle, logs, stats |
ProvisionService orchestrates: port allocation from HH_PORT_RANGE_*,
container spec construction (buildAgentEnv injects HERMES_* vars), status
reconciliation (syncInstances inspects containers and samples stats), and
error recording on the instance record.
Two layers, deliberately independent:
- Dashboard sessions — scrypt password hashes, opaque session tokens in
the store,
HttpOnly+SameSite=Laxcookie (hh_session).middleware.tsgates/dashboard/*on cookie presence; the layout and API handlers validate the session in the Node.js runtime. - API keys —
hhk_<random>keys, stored as SHA-256 hashes only (prefix kept for identification).authenticateApiKey(req, scopes)checks theAuthorization: Bearerheader and enforces scopes;adminimplies all. Used by/api/v1/*. Plaintext is returned exactly once at creation.
The auth layer is scaffolding: sessions/users live behind Store, so adding
SSO/OIDC (e.g. an OAuth provider in front of /api/auth/*) only requires
replacing the login flow — session issuance and authorization stay the same.
/api/auth/*— login/logout/session/api/instances*— CRUD +action(start/stop/restart) +logs+stats/api/agents*— agent config CRUD/api/keys*— API key create/list/revoke (hashes never returned)/api/health— unauthenticated liveness/api/v1/*— external, API-key-authenticated provisioning API (see API.md)
Server components read the store directly; client components (StatsPanel,
LogsViewer, MonitoringTable) poll the internal API for live updates.
- User — id, email, scrypt hash, role (
admin/operator/viewer) - Session — token, userId, expiry
- ApiKey — name, prefix, sha256 hash, scopes, revokedAt
- AgentConfig — model, systemPrompt, tools, env, resources, image
- Instance — name, status (
pending → provisioning → running | stopped | failed | deleting), containerId, hostPort, error - InstanceStats — cpu %, memory, net rx/tx samples (capped at 512/instance)
Provision: POST /api/instances → validate → ProvisionService.createInstance
→ store record (provisioning) → provisioner.create(spec) → start() →
running. Failure → record failed + error.
Monitor: /api/instances list or /stats → syncInstances() →
inspect() each container, sample stats() for running ones, record.
External API: Authorization: Bearer hhk_… → authenticateApiKey →
scope check → same service layer as the dashboard.