api/*.js files — the legacy pattern is deprecated and being removed.
This guide walks through adding a new RPC to an existing service and adding an entirely new service.
Important: After modifying any.protofile, you must runmake generatebefore building or pushing. The generated TypeScript files insrc/generated/are checked into the repo and must stay in sync with the proto definitions. CI does not run generation yet — this is your responsibility until we add it to the pipeline (see #200).
Prerequisites
You need Go 1.21+ and Node.js 18+ installed. Everything else is installed automatically:- buf — proto linting, dependency management, and code generation orchestrator
- protoc-gen-ts-client — generates TypeScript client classes (from sebuf)
- protoc-gen-ts-server — generates TypeScript server handler interfaces (from sebuf)
- protoc-gen-openapiv3 — generates OpenAPI v3 specs (from sebuf)
- npm dependencies — all Node.js packages
src/generated/client/{domain}/v1/service_client.ts— typed fetch client for the frontendsrc/generated/server/{domain}/v1/service_server.ts— handler interface + route factory for the backenddocs/api/{Domain}Service.openapi.yaml+.json— OpenAPI v3 documentation
Adding an RPC to an existing service
Example: addingGetEarthquakeDetails to SeismologyService.
1. Define the request/response messages
Createproto/worldmonitor/seismology/v1/get_earthquake_details.proto:
2. Add the RPC to the service definition
Editproto/worldmonitor/seismology/v1/service.proto:
3. Lint and generate
npx tsc --noEmit will fail because the handler doesn’t implement the new method yet. This is by design — the compiler enforces the contract.
4. Implement the handler
Createserver/worldmonitor/seismology/v1/get-earthquake-details.ts:
5. Wire it into the handler re-export
Editserver/worldmonitor/seismology/v1/handler.ts:
6. Verify
createSeismologyServiceRoutes() picks up the new RPC automatically — no changes needed to api/[[...path]].ts or vite.config.ts.
7. Check the generated docs
Opendocs/api/SeismologyService.openapi.yaml — the new endpoint should appear with all validation constraints from your proto annotations.
Adding a new service
Example: adding aSanctionsService.
1. Create the proto directory
2. Define entity messages
Createproto/worldmonitor/sanctions/v1/sanctions_entry.proto:
3. Define request/response messages
Createproto/worldmonitor/sanctions/v1/list_sanctions.proto:
4. Define the service
Createproto/worldmonitor/sanctions/v1/service.proto:
5. Generate
6. Implement the handler
Create the handler directory and files:server/worldmonitor/sanctions/v1/list-sanctions.ts:
server/worldmonitor/sanctions/v1/handler.ts:
7. Register the service in the gateway
Editapi/[[...path]].js — add the import and mount the routes:
8. Register in the Vite dev server
Editvite.config.ts — add the lazy import and route mount inside the sebufApiPlugin() function. Follow the existing pattern (search for any other service to see the exact locations).
9. Create the frontend service wrapper
Createsrc/services/sanctions.ts:
10. Verify
Proto conventions
These conventions are enforced across the codebase. Follow them for consistency.File naming
- One file per message type:
earthquake.proto,sanctions_entry.proto - One file per RPC pair:
list_earthquakes.proto,get_earthquake_details.proto - Service definition:
service.proto - Use
snake_casefor file names and field names
Time fields
Always useint64 with Unix epoch milliseconds. Never use google.protobuf.Timestamp.
Always add the INT64_ENCODING_NUMBER annotation so TypeScript gets number instead of string:
Validation annotations
Importbuf/validate/validate.proto and annotate fields at the proto level. These constraints flow through to the generated OpenAPI spec automatically.
Common patterns:
Shared core types
Reuse these instead of redefining:| Type | Import | Use for |
|---|---|---|
GeoCoordinates | worldmonitor/core/v1/geo.proto | Any lat/lon location (has built-in -90/90 and -180/180 bounds) |
BoundingBox | worldmonitor/core/v1/geo.proto | Spatial filtering |
TimeRange | worldmonitor/core/v1/time.proto | Time-based filtering (has INT64_ENCODING_NUMBER) |
PaginationRequest | worldmonitor/core/v1/pagination.proto | Request pagination (has page_size 1-100 constraint) |
PaginationResponse | worldmonitor/core/v1/pagination.proto | Response pagination metadata |
Comments
buf lint enforces comments on all messages, fields, services, RPCs, and enum values. Every proto element must have a// comment. This is not optional — buf lint will fail without them.
Route paths
- Service base path:
/api/{domain}/v1 - RPC path:
/{verb}-{noun}in kebab-case (e.g.,/list-earthquakes,/get-vessel-snapshot)
Handler typing
Always type the handler function against the generated interface using indexed access:Client construction
Always pass{ fetch: fetch.bind(globalThis) } when creating clients:
fetch.bind(globalThis) is required for Tauri compatibility.
Generated documentation
Every time you runmake generate, OpenAPI v3 specs are generated for each service:
docs/api/{Domain}Service.openapi.yaml— human-readable YAMLdocs/api/{Domain}Service.openapi.json— machine-readable JSON
- All endpoints with request/response schemas
- Validation constraints from
buf.validateannotations (min/max, required fields, ranges) - Field descriptions from proto comments
- Error response schemas (400 validation errors, 500 server errors)
