Skip to content

gateway.v1

Device-facing and internal RPCs, plus the JSON WebSocket frames exchanged between the agent and the realtime Durable Object. Source: platform/nexus/proto/gateway/v1/.

File Contents
device.proto GatewayEnrollmentService, GatewayDeviceService
internal.proto GatewayInternalService
realtime.proto RealtimePushService
types.proto Shared messages: DeviceSummary, StateReportRequest and its parts, HeartbeatRequest, log entries

Services

Service Caller → server Auth
GatewayEnrollmentService agent → nexus none; the bootstrap token in the request is the credential
GatewayDeviceService agent → nexus Authorization: Bearer <api_token> (DeviceTokenInterceptor)
GatewayInternalService realtime DO (and infrastructure) → nexus X-Internal-Key (InternalKeyInterceptor, constant-time compare against NEXUS_INTERNAL_API_KEY)
RealtimePushService nexus → realtime Worker X-Internal-Key

GatewayEnrollmentService

RPC Purpose
Enroll First boot. Presents a bootstrap token and hardware facts; nexus creates a holding device row and returns {gateway_id, api_token}. The token is stored hashed.

GatewayDeviceService

The interceptor resolves the bearer token to a device; every RPC acts on that device only, never on an id from the request.

RPC Purpose
GetDesiredState Desired config version and desired agent version in one call. The agent reconciles both on connect; pushes only make it reconcile sooner. See OTA.
GetConfig Fetch the desired config.
ReportConfigStatus Outcome of an apply: applied, failed, rolled_back.
UploadLogs Batched log lines (pf, system, daemon, …).
ReportCommandReply The private reply to a command pushed with a reply id (access-code issue/revoke). Travels this channel so it is never mirrored to viewers or logged by the DO.
WhoAmI Resolve the device id from the token. Used after a claim/replace has moved the token onto another device row.
RequestCertificate Send a CSR for the gateway's own names; nexus proves the names over DNS (ACME) and returns the chain. The private key stays on the device.
RequestRelay Ask for a relay assignment (ordered list, tunnel address, peer details). Answered from the authenticated identity.

GatewayInternalService

RPC Called when
ValidateGatewayToken Gateway WebSocket upgrade (?token=). Binds the socket to a device id.
ValidateViewerTicket Viewer upgrade (?role=viewer&ticket=). Redeems a ticket from IssueViewerTicket; binds the socket to a user and org.
ValidateShellTicket Operator upgrade (?role=operator&ticket=). Redeems a shell ticket; a viewer ticket never validates here.
ReportShellSessionEnd An operator socket closed; closes the shell_sessions row.
Heartbeat heartbeat frame (every 30 s). Updates devices.last_seen_at, appends a gateway_metrics row.
StateReport state_report frame (every 5 min and on carrier change). Upserts gateway_states, appends a gateway_block_samples row.
ConfigReport config_report frame. Same meaning as ReportConfigStatus, arriving via the DO.
ReportAgentUpdate agent_update frame. Upserts agent_update_status.
UploadInternalLogs log_batch frame.
DeviceOffline The gateway socket closed. Marks the device offline at once.
RegisterRelayToken Infrastructure registers a relay's bootstrap token before the relay first enrols.

RealtimePushService

RPC Purpose
Push Deliver an ApplyConfigPayload or a CommandPayload to one device's DO, which writes it onto the gateway socket. PushResponse.status is ok (command acknowledged), sent (written, nothing to acknowledge), no_ack (command not acknowledged within 3 s) or offline (no gateway socket).

See Nexus → Realtime integration.

WebSocket frames

Frames on wss://{realtime}/gateway/{device_id} are JSON, not protobuf. The TypeScript definitions are platform/realtime/src/messages.ts; the agent's Go structs are in edge/gateway/internal/connector/messages.go.

Field names are camelCase

uptimeSeconds, not uptime_s; exitCode, not exit_code. The JSON tags in the agent's structs and messages.ts are authoritative, not the proto field names.

Agent → DO

type GatewayClientMessage =
    | { type: "heartbeat";      uptimeSeconds?, loadAverage1m?, memoryFreeBytes?,
                                memoryTotalBytes?, cpuUsagePercent?,
                                cpuTemperatureCelsius?, timestamp? }
    | { type: "state_report";   /* fields directly on the message, no envelope */ }
    | { type: "config_report";  version?, status?, configHash?, error?, appliedAt? }
    | { type: "agent_update";   phase?, targetVersion?, fromVersion?, error? }
    | { type: "log_batch";      source?, entries?: { timestamp?, line }[] }
    | { type: "command_result"; commandId?, exitCode?, stdout?, stderr? }
  • Every frame except the shell frames is broadcast verbatim to viewers.
  • heartbeat, state_report, config_report, agent_update and log_batch become the matching GatewayInternalService RPC. The state_report mapping is platform/realtime/src/state-report-mapping.ts; a field it does not name is dropped. See State reporting.
  • command_result is not forwarded to nexus. Its arrival acknowledges the pending Push with the same commandId; viewers receive it in the broadcast.
  • agent_update.phase is one of checking, downloading, verifying, installing, restarting, confirmed, failed.

DO → Agent

type GatewayServerMessage =
    | { type: "apply_config"; version: number; configHash: string; config: string /* base64 JSON */ }
    | { type: "command";      id: string; commandType: string; payload?: string /* base64 */ }

Produced from the PushRequest oneof in platform/realtime/src/realtime-push.ts.

Shell frames

shell_input, shell_resize, shell_output, shell_close (base64 payloads inside JSON) are relayed between the operator socket and the gateway socket only. They never reach nexus or viewers. An operator socket that sends any other frame type is closed.

Command kinds

commandType Notes
ping, traceroute Diagnostics; payload carries the target. Result as command_result.
reload, sync Acknowledged only.
confirm_config Cancels the rollback timer. The version is in the command id, parsed as an integer.
set_hostname Persists to /etc/myname and applies it.
reboot, restart_agent Asynchronous: acknowledged, then acted on.
update_agent Asynchronous; progress arrives as agent_update frames.
reenroll Enrol against the target environment first; drop the current identity only on success.
relay_reassess Make the relay keeper ask for an assignment again.
issue_code, revoke_code Published-service access codes. The reply goes over ReportCommandReply; the socket only gets a bodiless acknowledgement.

StateReport payload

The messages are in types.proto. What the .proto does not tell you:

  • On the wire the JSON is camelCase (see above).
  • PFStatus carries block counters (loggingBlockRules, loggingBlockPackets, loggingBlockEvaluations, silentBlockRules, silentBlockPackets), so "nothing was blocked" can be told apart from "blocks happen but their log is not arriving".
  • NetworkInterface carries WireGuard and uplink state: public key, listen port, per-peer detail, and uplinkState (carrying / standby / down).
  • Some fields encode an absence on purpose; see State reporting.
  • int64 fields arrive as strings once the report has been through Connect-JSON. nexus decodes the stored blob with that in mind (portal_state_int64_test.go).