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 ajaque_sessioncookie carrying a signed session token; the cookie isHttpOnly, 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 thejaque_sessioncookie 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 missingstartorend, 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-tokenis set and the request carries no bearer token, or the token doesn't match; or, with-auth-configset, no session cookie or bearer token resolves to an identity.permission_denied: with-auth-configset, the resolved identity's role doesn't meet the RPC's requirement (operatorforCommandService,viewerforQueryService).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.