[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐Ÿซ– Status page with uptime monitoring & API monitoring as code ๐Ÿซ– openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
openstatus apps server CONNECTRPC_SPEC.md
14 kB

ConnectRPC API Specification #

Overview #

This document specifies the implementation of a ConnectRPC API for OpenStatus server. ConnectRPC will be used for new features only while the existing REST API remains for current functionality.

Architecture Decisions #

Transport & Protocol #

  • Protocol: Connect protocol only (HTTP/1.1 compatible)
  • Streaming: Unary calls only (request-response, no streaming)
  • Mounting: Same port as REST, mounted at /rpc/* path prefix on the existing Hono app

Schema Management #

  • Approach: Schema-first with .proto files
  • Tooling: Buf (buf.yaml, buf.gen.yaml)
  • Location: packages/proto (shared package for monorepo consumption)
  • Package naming: openstatus.<domain>.v1 (e.g., openstatus.monitor.v1)

Code Generation Targets #

  • TypeScript (@bufbuild/protobuf + @connectrpc/connect)
  • Go (for potential backend service consumers)

Authentication & Authorization #

Supported Methods #

Both authentication methods resolve to the same workspace context:

  1. API Key (existing system)
    • Header: x-openstatus-key
    • Formats: os_[32-char-hex] (custom) or Unkey keys
    • Super admin: sa_ prefix

Workspace Context #

  • Workspace ID inferred from authenticated credentials
  • Super-admin override: Tokens with sa_ prefix can specify target workspace via x-workspace-id metadata header

Error Handling #

Error Model #

Use ConnectRPC error codes with Google ErrorInfo for structured details:

// Error codes used: NOT_FOUND, INVALID_ARGUMENT, PERMISSION_DENIED,
// UNAUTHENTICATED, RESOURCE_EXHAUSTED, INTERNAL, UNAVAILABLE

// Include ErrorInfo details:
// - domain: "openstatus.com"
// - reason: Machine-readable error reason (e.g., "MONITOR_NOT_FOUND")
// - metadata: Additional context (requestId, resourceId, etc.)

Mapping to Existing Errors #

Reuse OpenStatusApiError codes, map to ConnectRPC equivalents in interceptor.


First Service: Monitor Management #

Service Definition #

syntax = "proto3";

package openstatus.monitor.v1;

import "buf/validate/validate.proto";
import "google/protobuf/timestamp.proto";

// MonitorService provides CRUD and operational commands for monitors.
service MonitorService {
  // CreateMonitor creates a new monitor in the workspace.
  rpc CreateMonitor(CreateMonitorRequest) returns (CreateMonitorResponse);

  // GetMonitor retrieves a single monitor by ID.
  rpc GetMonitor(GetMonitorRequest) returns (GetMonitorResponse);

  // ListMonitors returns a paginated list of monitors.
  rpc ListMonitors(ListMonitorsRequest) returns (ListMonitorsResponse);

  // DeleteMonitor removes a monitor.
  rpc DeleteMonitor(DeleteMonitorRequest) returns (DeleteMonitorResponse);

  // TriggerMonitor initiates an immediate check.
  rpc TriggerMonitor(TriggerMonitorRequest) returns (TriggerMonitorResponse);

}

Monitor Type Modeling #

Separate message types for each monitor kind:

// HttpMonitor configuration for HTTP/HTTPS endpoint monitoring.
message HttpMonitor {
  // The URL to monitor (required).
  string url = 1 [(buf.validate.field).string.uri = true];

  // HTTP method to use.
  HttpMethod method = 2;

  // Request headers to include.
  map<string, string> headers = 3;

  // Request body for POST/PUT/PATCH.
  optional string body = 4;

  // Timeout in milliseconds (default: 30000).
  int32 timeout_ms = 5 [(buf.validate.field).int32 = {gte: 1000, lte: 60000}];

  // Assertions to validate the response.
  repeated HttpAssertion assertions = 6;
}

// TcpMonitor configuration for TCP connection monitoring.
message TcpMonitor {
  // Host to connect to (required).
  string host = 1 [(buf.validate.field).string.min_len = 1];

  // Port number (required).
  int32 port = 2 [(buf.validate.field).int32 = {gte: 1, lte: 65535}];

  // Timeout in milliseconds.
  int32 timeout_ms = 3;
}

// DnsMonitor configuration for DNS record monitoring.
message DnsMonitor {
  // Domain name to query (required).
  string domain = 1 [(buf.validate.field).string.hostname = true];

  // DNS record type to check.
  DnsRecordType record_type = 2;

  // Expected values for the record.
  repeated string expected_values = 3;
}

Pagination #

Offset-based pagination for list operations (page_token is the numeric offset):

message ListMonitorsRequest {
  // Maximum number of monitors to return (default: 50, max: 100).
  int32 page_size = 1 [(buf.validate.field).int32 = {gte: 1, lte: 100}];

  // Token from previous response for pagination.
  optional string page_token = 2;

  // Filter by monitor status.
  optional MonitorStatus status_filter = 3;

  // Filter by monitor type.
  optional MonitorType type_filter = 4;
}

message ListMonitorsResponse {
  // The monitors in this page.
  repeated Monitor monitors = 1;

  // Token for retrieving the next page, empty if no more results.
  string next_page_token = 2;

  // Total count of monitors matching the filter.
  int32 total_count = 3;
}

Validation #

Approach #

Use protovalidate (Buf ecosystem) for request validation:

  • Validation rules defined in proto annotations
  • Runs before handler via interceptor
  • Returns INVALID_ARGUMENT with field-level details on failure

Example Annotations #

message CreateMonitorRequest {
  string name = 1 [(buf.validate.field).string = {min_len: 1, max_len: 256}];
  string description = 2 [(buf.validate.field).string.max_len = 1024];
  int32 periodicity = 3 [(buf.validate.field).int32 = {in: [60, 300, 600, 1800, 3600]}];
  repeated string regions = 4 [(buf.validate.field).repeated = {min_items: 1, max_items: 35}];
}

Code Organization #

Shared Service Layer #

Both REST and RPC handlers call the same business logic:

packages/proto/                    # Shared proto definitions
โ”œโ”€โ”€ buf.yaml                       # Buf configuration
โ”œโ”€โ”€ buf.gen.yaml                   # Code generation config
โ”œโ”€โ”€ openstatus/
โ”‚   โ””โ”€โ”€ monitor/
โ”‚       โ””โ”€โ”€ v1/
โ”‚           โ”œโ”€โ”€ monitor.proto      # Message definitions
โ”‚           โ””โ”€โ”€ service.proto      # Service definition
โ””โ”€โ”€ gen/                           # Generated code
    โ”œโ”€โ”€ ts/                        # TypeScript output
    โ””โ”€โ”€ go/                        # Go output

apps/server/src/
โ”œโ”€โ”€ services/                      # Shared business logic (NEW)
โ”‚   โ””โ”€โ”€ monitor/
โ”‚       โ”œโ”€โ”€ create.ts
โ”‚       โ”œโ”€โ”€ get.ts
โ”‚       โ”œโ”€โ”€ list.ts
โ”‚       โ”œโ”€โ”€ update.ts
โ”‚       โ”œโ”€โ”€ delete.ts
โ”‚       โ””โ”€โ”€ operations.ts          # trigger, pause, resume
โ”œโ”€โ”€ routes/
โ”‚   โ””โ”€โ”€ v1/                        # REST handlers (existing)
โ”‚       โ””โ”€โ”€ monitors/
โ””โ”€โ”€ rpc/                           # ConnectRPC handlers (NEW)
    โ”œโ”€โ”€ index.ts                   # Mount point
    โ”œโ”€โ”€ interceptors/
    โ”‚   โ”œโ”€โ”€ auth.ts                # Auth interceptor
    โ”‚   โ”œโ”€โ”€ logging.ts             # Request logging
    โ”‚   โ””โ”€โ”€ error.ts               # Error mapping
    โ””โ”€โ”€ handlers/
        โ””โ”€โ”€ monitor.ts             # MonitorService implementation

Handler Pattern #

// apps/server/src/rpc/handlers/monitor.ts
import type { ConnectRouter } from "@connectrpc/connect";
import { MonitorService } from "@openstatus/proto/gen/ts/openstatus/monitor/v1/service_connect";
import * as monitorService from "../../services/monitor";

export default (router: ConnectRouter) =>
  router.service(MonitorService, {
    async createMonitor(req, ctx) {
      const workspace = ctx.values.get(workspaceKey);
      return await monitorService.create(workspace, req);
    },
    // ... other methods
  });

Interceptors #

Authentication Interceptor #

// Extracts and validates auth from headers
// Sets workspace context for downstream handlers
// Supports both API key and Bearer token

Logging Interceptor #

// Integrates with existing LogTape setup
// Logs: method, duration, status, workspace, requestId

Error Interceptor #

// Maps internal errors to ConnectRPC codes
// Attaches ErrorInfo details
// Reports to Sentry (filtered for client errors)

Observability #

Logging #

  • Integrate with existing LogTape JSON logging
  • Log fields: rpc.method, rpc.status_code, duration_ms, workspace_id, request_id

Error Tracking #

  • Sentry integration via interceptor
  • Filter client errors (INVALID_ARGUMENT, NOT_FOUND, etc.)
  • Include request context in error reports

Rate Limiting #

Use existing infrastructure:

  • Hono middleware / upstream proxy handles rate limiting
  • No RPC-specific rate limiting interceptors needed

Testing Strategy #

Unit Tests #

  • Test handlers directly with mocked service layer
  • Test interceptors in isolation
  • Test proto validation rules

Integration Tests #

  • Spin up real server instance
  • Use generated TypeScript client to make RPC calls
  • Test full request lifecycle including auth

Test File Structure #

apps/server/src/rpc/
โ”œโ”€โ”€ __tests__/
โ”‚   โ”œโ”€โ”€ handlers/
โ”‚   โ”‚   โ””โ”€โ”€ monitor.test.ts        # Handler unit tests
โ”‚   โ”œโ”€โ”€ interceptors/
โ”‚   โ”‚   โ””โ”€โ”€ auth.test.ts           # Interceptor tests
โ”‚   โ””โ”€โ”€ integration/
โ”‚       โ””โ”€โ”€ monitor.integration.ts # Full flow tests

Additional Considerations #

Health Check Endpoint #

Add a simple Health service for load balancer probes at /rpc:

service HealthService {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}

message HealthCheckRequest {}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
  }
  ServingStatus status = 1;
}

Request ID Propagation #

  • Generate x-request-id in logging interceptor if not present in request headers
  • Propagate request ID to all downstream services and log entries
  • Include request ID in error responses for debugging

Go Code Generation #

  • Defer Go codegen until there are concrete Go service consumers
  • Reduces maintenance burden and build complexity initially
  • Can be enabled later by adding Go target to buf.gen.yaml

Proto Dependency Pinning #

  • Use buf.lock to pin versions of:
    • buf.build/bufbuild/protovalidate
    • buf.build/googleapis/googleapis (if using google.protobuf types)
  • Run buf mod update to generate/update lock file

Configuration Details #

CORS Handling #

  • /rpc endpoint should inherit existing CORS configuration from Hono app
  • If different CORS rules needed, configure via Hono middleware before mounting RPC routes
  • Connect protocol uses standard HTTP methods (POST), no special CORS requirements

Content-Type Support #

Enable both JSON and binary formats for flexibility:

  • application/json - Human-readable, easier debugging, slightly larger payloads
  • application/proto - Binary format, smaller payloads, better performance
  • Connect clients auto-negotiate based on Content-Type header

Deadline/Timeout Propagation #

  • Client-specified timeouts via connect-timeout-ms header
  • Server interceptor should:
    • Read timeout from request metadata
    • Create context with deadline
    • Cancel operations if deadline exceeded
    • Return DEADLINE_EXCEEDED error code on timeout

Dependencies #

New Packages (packages/proto) #

{
  "devDependencies": {
    "@bufbuild/buf": "latest",
    "@bufbuild/protoc-gen-es": "latest",
    "@connectrpc/protoc-gen-connect-es": "latest"
  },
  "dependencies": {
    "@bufbuild/protobuf": "^2.0.0",
    "@bufbuild/protobuf-conformance": "^2.0.0"
  }
}

Server App Additions #

{
  "dependencies": {
    "@connectrpc/connect": "^2.0.0",
    "@connectrpc/connect-node": "^2.0.0",
    "@bufbuild/protovalidate": "^0.3.0",
    "@openstatus/proto": "workspace:*"
  }
}

Migration & Rollout #

Phase 1: Foundation #

  1. Create packages/proto with Buf setup
  2. Define monitor service proto
  3. Generate TypeScript and Go clients
  4. Add protovalidate annotations

Phase 2: Server Integration #

  1. Add ConnectRPC dependencies to server
  2. Implement interceptors (auth, logging, error)
  3. Mount RPC routes at /rpc on Hono app
  4. Extract shared service layer from REST handlers

Phase 3: Handler Implementation #

  1. Implement MonitorService handlers
  2. Write unit tests
  3. Write integration tests
  4. Internal testing

Phase 4: Release #

  1. Documentation
  2. Client SDK examples
  3. Gradual rollout via feature flag (optional)

Open Questions (Resolved) #

Question Decision
REST replacement or parallel? New features only
Transport protocol Connect protocol only
Streaming Unary only
Schema approach Schema-first (.proto)
Auth mechanism Both API key + JWT
Proto location Shared package
Tooling Buf
Error details With ErrorInfo
Code sharing Shared service layer
Client targets TypeScript + Go
Validation protovalidate
Type modeling Separate messages
Port strategy Same port, /rpc prefix
Pagination Offset-based
Rate limiting Existing infrastructure
Operations style Separate methods
Observability Sentry + LogTape
Testing Unit + Integration
Health check Yes, HealthService
Request ID Generated + propagated
Go codegen Deferred
CORS Inherit from Hono
Content-Type JSON + Binary
Timeouts connect-timeout-ms header

References #

Future work #

  • Implement additional services and procedure:

    // PauseMonitor suspends monitoring. rpc PauseMonitor(PauseMonitorRequest) returns (PauseMonitorResponse);

    // ResumeMonitor resumes a paused monitor. rpc ResumeMonitor(ResumeMonitorRequest) returns (ResumeMonitorResponse);

    // UpdateMonitor modifies an existing monitor. rpc UpdateMonitor(UpdateMonitorRequest) returns (UpdateMonitorResponse);