CommandService and QueryService are the ConnectRPC API on -listen: one port, three wire formats, a write service and a read service kept strictly apart. This is the API the dashboard itself calls, so nothing reachable through it is a second-class integration.

1. One port, three protocols

jaque speaks ConnectRPC: plain JSON over HTTP/1.1 (curl-friendly, no generated client needed), gRPC, and gRPC-Web, all on the same port as the dashboard and /metrics (-listen).

curl -s -X POST http://127.0.0.1:8080/jaque.v1.QueryService/ListStatus \
  -H 'Content-Type: application/json' -d '{}'

2. CommandService: writes

Fifteen RPCs, one per command variant: ProcessCheckResult, ProcessCheckResults (batch, all-or-nothing validation, in-order enqueue), AcknowledgeProblem, RemoveAcknowledgement, ScheduleDowntime, DeleteDowntime, SendCustomNotification, SetProgramNotifications, SetObjectNotifications, SetContactNotifications, SetProgramToggle, SetObjectToggle, ScheduleCheck, AddComment and DeleteComment. AcknowledgeProblem, RemoveAcknowledgement, ScheduleDowntime, SendCustomNotification, SetObjectNotifications, SetObjectToggle and ScheduleCheck all take a Target, which is either one object or every object a label selector matches, the same mechanism the dashboard's bulk actions use. An object target is a oneof of host, service or process, each its own message (HostRef, ServiceRef, ProcessRef). The variant picked is the kind; there is no inference from shape: {"object":{"host":{"name":"gw"}}} addresses a host, {"object":{"service":{"host":"nas","name":"disk"}}} addresses a service, and {"object":{"process":{"name":"checkout"}}} addresses a business process.

A success response means the command was enqueued. The engine resolves commands asynchronously at its own pace, so a success response does not mean the command has been applied yet. A submitted check result must carry a status other than STATUS_PENDING: an unset status is refused with InvalidArgument rather than recorded as UNKNOWN.

Command requests carry no realm field. Addressing an aggregator (a process started with -realms-config) uses the aggregator's own command service instead: its requests wrap the realm API's requests together with the realm that owns the target, and the aggregator forwards the inner request to the realm that owns it, which records the aggregator's service identity as the author.

-api-token (empty by default, meaning open) protects writes only:

curl -s -X POST http://127.0.0.1:8080/jaque.v1.CommandService/AcknowledgeProblem \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"target":{"object":{"host":{"name":"gw"}}},"sticky":true,"notify":true,"author":"ops","comment":"known issue"}'

notify sends an ACKNOWLEDGEMENT notification to the object's current escalation level; the dashboard always sets it.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.CommandService/ScheduleDowntime \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"target":{"object":{"host":{"name":"gw"}}},"start":"2026-09-20T02:00:00Z","end":"2026-09-20T04:00:00Z","author":"ops","comment":"maintenance","flexible":{"duration":"1800s"}}'

ScheduleDowntime's request carries a kind oneof: unset schedules a fixed downtime, flexible (a duration) schedules a flexible one, and triggered (a triggered_by downtime ID) schedules a triggered one. See Downtimes for what each kind does once scheduled.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.CommandService/SendCustomNotification \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"target":{"object":{"host":{"name":"gw"}}},"author":"ops","comment":"rolling restart in progress"}'

SendCustomNotification sends a CUSTOM notification to the object's current escalation level with the given author and comment, in any object state; a downtime still silences it.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.CommandService/SetObjectNotifications \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"target":{"object":{"host":{"name":"gw"}}},"enabled":false,"author":"ops"}'
curl -s -X POST http://127.0.0.1:8080/jaque.v1.CommandService/ScheduleCheck \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"target":{"object":{"host":{"name":"gw"}}}}'

The next check runs now and the regular cadence is kept.

curl -s -X POST http://127.0.0.1:8080/jaque.v1.CommandService/AddComment \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"target":{"object":{"host":{"name":"gw"}}},"author":"ops","text":"network is looking at this, ticket 4521"}'

The response carries the commentId the server minted, and DeleteComment takes that id. AddComment accepts a single object target; a selector is rejected.

3. QueryService: reads, no -api-token required

ListStatus (current state of one host, one service, or the whole fleet, optionally narrowed by a label selector), ListHistory (recent transitions for one object or the global feed), ListObjects (every configured object plus its labels), ListContacts, ListViews, ListSinks (the configured perfdata sinks: name, type, input and selector), and GetServerInfo (the running binary's version, the release-stamped vX.Y.Z, or "dev" for anything else, plus auth_enabled, whether the process was started with -auth-config, and read_only, whether the process was started with -read-only). Every read comes from the projection, never from the engine or the event log directly, so reads never block on write throughput. Every object row says what it is: kind is OBJECT_KIND_HOST, OBJECT_KIND_SERVICE or OBJECT_KIND_PROCESS on ListStatus and ListObjects answers (and the -snapshot-out artifact carries the same field as "kind": "host" | "service" | "process"). Every object row also carries its acknowledgement and downtime detail, the object's comments, and the names of its check and notification periods. Each Downtime in that detail carries a kind oneof naming which of the four downtime kinds it is: fixed, flexible, triggered or recurring, the last for a downtime declared in config rather than scheduled. start and end are always the window as scheduled, or the current or next occurrence for a recurring downtime; flexible and triggered also carry active_from/active_until, unset while the downtime is still pending. Unlike CommandService, reads never require -api-token: the bearer token protects writes only. -auth-config, described below, is the one thing that does gate reads.

Status and object rows also carry a realm field, and ListStatus answers carry a realms list (name, reachable, last_seen per realm). Both are set only when the answering process is an aggregator (started with -realms-config) and empty on a single deployment: the aggregator tags every row with the realm it came from, and the realms list says which realms answered this request and when each was last reached. An unreachable realm keeps its last-seen rows and reports reachable: false instead of vanishing from the answer.

With -read-only set, CommandService is not mounted at all: every request to it, whatever the RPC, answers 404. QueryService and AuthService are unaffected. See Security.

4. AuthService: login, opt-in

AuthService exists only when the process was started with -auth-config; without it the route is not mounted at all, and CommandService and QueryService stay exactly as sections 2 and 3 describe. With one configured, both services additionally require a resolved identity: CommandService needs at least the operator role, QueryService needs at least viewer. See Security, section 3, for what -auth-config's document looks like and what the roles mean.

Three RPCs:

  • Login(user, password) verifies a password against the auth document's identities and, on success, sets a jaque_session cookie carrying a signed session token; the cookie is HttpOnly, so no script on the page can read it. The response body carries the caller's name and role, never the session token itself.
  • Logout() clears the jaque_session cookie and always succeeds.
  • WhoAmI() resolves the caller's own credentials and returns its name and role, or fails with an unauthenticated error if none resolve.

A browser carries the jaque_session cookie automatically once Login sets it; every subsequent CommandService and QueryService call is resolved through it. A service identity skips Login entirely and sends Authorization: Bearer <token> on every call instead, the same header -api-token uses, but checked against the identity's own token rather than a single shared one:

curl -s -X POST http://127.0.0.1:8080/jaque.v1.QueryService/ListStatus \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <the service identity token>' \
  -d '{}'

A request carrying neither a valid cookie nor a valid bearer token is turned away with an unauthenticated error before it reaches the RPC.

5. This is the dashboard's own API

Every action in Dashboard, acknowledge, downtime, the status table, history, is a call to one of these RPCs. There is no separate, richer internal API the UI gets and scripts don't.

6. Errors

Every failure, across CommandService, QueryService and AuthService, comes back as a Connect transport error, never as an ok/error field inside the response body. The codes actually returned:

  • invalid_argument: the request itself is malformed: a batch with no results, a downtime missing start or end, a target whose service set names no host, an unparseable field.
  • not_found: the named panel or object doesn't exist (QueryPanel, ListObjectMetrics, QueryObjectMetric).
  • failed_precondition: the request is well-formed but the config it needs isn't there: a panel's datasource isn't declared, or no default datasource is configured for an object-metric query.
  • unauthenticated: -api-token is set and the request carries no bearer token, or the token doesn't match; or, with -auth-config set, no session cookie or bearer token resolves to an identity.
  • permission_denied: with -auth-config set, the resolved identity's role doesn't meet the RPC's requirement (operator for CommandService, viewer for QueryService).
  • unavailable: a panel or object-metric query reached a configured datasource, but the datasource itself failed to answer.
  • internal: a failure on jaque's own side unrelated to the caller's input, such as resolving a datasource that already passed every other check.

7. Security considerations

-api-token, when set, is checked with a constant-time comparison so a response-time difference cannot leak how much of the token matched; when unset every CommandService write is open to anyone who can reach -listen. QueryService is never gated by -api-token (see section 3), though it is gated by role when -auth-config is set, see section 4. Absent -auth-config, it should not carry anything an unauthenticated caller should not see: ListSinks and ListContacts omit sink URLs, headers and contact target payloads (webhook URLs, command lines) from their responses, because those can carry credentials. AcknowledgeProblem and ScheduleDowntime's author/comment fields are operator-supplied free text with no server-side validation beyond the RPC's type, the same posture the event log carries them into, see Event log schema, section 4. There is no transport encryption at this layer: run behind a reverse proxy or a network boundary you control if -listen is reachable beyond a trusted network.