5.2 Choosing an Interface
Every boundary between two systems needs a way to talk. The standard is chosen by who is talking and what they need, not by taste: a web app, another service, a partner who wants to choose the data, a screen that must update live, or a system that must be told something happened. This page fixes which standard serves each case, why, and the rules each one follows.
5.2.1 Which Interface to Use¶
| Who talks, and what they need | Standard | Example |
|---|---|---|
| Our web apps and installed clients call a service | REST over HTTPS, with JSON | The Organization Dashboard loads a farm's animals; the farm sync client commits a sync |
| One of our services calls another and waits for the answer | gRPC | ai-gateway asks farm what an Organization has purchased |
| An external or internal actor needs to choose which data to read and how to combine it | GraphQL | An insurer's system reads the herds it covers, with the fields it needs |
| An open screen must show a change the moment it happens | WebSocket | Sync progress and new alerts appear on the Organization Dashboard |
| Our services must react to something that happened | Events on Pub/Sub | calving.registered updates a herd's statistics |
| An agent needs facts or actions from a system, as the user who is talking to it | MCP (JSON-RPC over HTTPS) to the system's MCP server | MooChat asks farm for an animal's last calvings |
| A system outside Moonitoring must react to something that happened | Pub/Sub subscriptions: push to an HTTPS address, or pull | A data issue opens, and the farm's management system is called |
Requests that need an answer now are synchronous (REST, gRPC, GraphQL). Facts that others may react to later are asynchronous (events, inside the platform and outside it): the sender does not wait and does not need to know who listens. When a caller does not need the result to continue, an event is preferred over a call, so that one system being down does not stop another.
5.2.2 REST for Web Apps and Clients¶
Every browser, phone and desktop platform speaks HTTP and JSON, and so do the load balancer, the CDN, Identity-Aware Proxy and every debugging tool. REST is therefore the interface of everything our users run: the Organization Dashboard, the Console, the farm sync client and future mobile apps.
- Resources are named after the business, in the plural, as in the data model:
/animals/v1/farms/{farm_id}/animals. - Methods and status codes keep their HTTP meaning:
GETnever changes anything,404means the resource does not exist,409a conflict with its current state. - The contract is an OpenAPI document, written in this documentation before the code and shown on the page of its system (see Contracts Live Here).
- The major version is in the path, after the domain (see Every API Is Versioned).
- A request that creates something carries an idempotency key, so that a client retrying after a lost answer, like the sync client on a farm's connection, never creates it twice. A retry with the same key and the same body gets the first answer again; the same key with another body is a 422, and a retry while the first request is still running is a 409. Keys belong to the caller: to its token of
access, to an installation token by its hash, or to a named anonymous scope such asanonymous:enrolmentfor a call that carries no token (decided 2026-11-05). - Long lists are paged with a cursor, never by offset, so that a page stays correct while data is added. A page filtered on something computed may hold fewer items than the limit and still have a next cursor: the client follows the cursor, not the count.
- The document a service serves is self-contained: every
$reftocommon/v1(Problem, paging, idempotency) is bundled into it, so a generated client needs nothing else.
5.2.3 gRPC Between Services¶
When one of our services calls another, both sides are ours, and gRPC gives what matters between them:
- Smaller and faster messages. Protocol Buffers are binary: smaller than JSON and quicker to read and write. HTTP/2 carries many calls at once over one connection.
- One contract, generated code. The
.protofile defines the calls and messages, and the client and server code is generated from it in Rust and Python, so the two sides cannot disagree on a field. - Deadlines and streaming built in. A call states how long it may take, and the deadline travels to every call it triggers; a call can also stream a long result instead of loading it whole.
gRPC is never exposed to web apps: browsers cannot speak it without a translating proxy, which is why our users get REST.
- The
.protofiles are the contract, kept in this documentation underproto/(see Contracts Live Here), in a package named after the system, the domain and the version, such asmoonitoring.farm.animals.v1. - A field number is never reused or renumbered. Fields are added; a removed field's number is marked
reserved. - Every call sets a deadline. A call without one can hold its caller forever.
- Services authenticate each other with their own service identities, never with a shared secret: the caller sends a Google-signed ID token, and the callee's Cloud Run service lets in only the identities given the invoker role.
- The audience of that ID token is the callee's fixed Cloud Run URL,
https://<service>-<project-number>.europe-west8.run.app(decided 2026-11-12), the same rule as for Pub/Sub push and Cloud Scheduler: for examplehttps://access-backend-274716978006.europe-west8.run.app. The callee accepts only that audience; the caller addresses the callee, and fetches its JWKS where it has one, at the same URL. Both are literal values in the stage manifests, bound by infra. A caller that still uses the hashed*.a.run.appform moves to the fixed URL in the same release in which the callee opens its gRPC audience. - App servers are never gRPC callers. The servers of the Console, the Organization Dashboard and the Account app call systems over REST with the person's token; their identities are allowed to invoke, but they are never in a system's list of gRPC or internal callers.
- The list of callers is authentication only; every RPC checks the caller for its own operation (decided 2026-11-11). Being let in proves which identity calls, not what it may do: each RPC accepts only the callers named for it and returns only the data that caller needs, so
StreamSnapshotofaccessgives staff roles only to the backends that serve staff operations, andRegisterServiceAccountanswers onlyfarm. Each.protonames, on every RPC, who may call it and what it sees (x-moonitoring-callerson the RPC when it differs from the service's list); a caller outside it getsPERMISSION_DENIED. When the call acts for a user, the user'saccesstoken travels with it, so the callee checks that user's capabilities itself (see Identity & Access). - The trace context travels with every call, as described in Traces.
5.2.4 GraphQL for Flexible Reads¶
REST answers the questions its designers foresaw, one resource at a time. GraphQL lets the caller ask for exactly the fields and relations it needs in one request, which is what an external actor such as an insurer, a partner or an internal tool needs when its questions are not known in advance. Our own web app screens keep REST: their questions are known, and REST is simpler to cache and to secure.
The same freedom has costs that the rules must contain:
- The schema is the contract, one per system, served at
/graphql/v1. Within a version fields are only added; old ones are marked@deprecated, and removing one makes a new version. - GraphQL is for reading. Changes go through the REST or gRPC operations of the product that owns the data, where the business rules live.
- Every query is limited in depth, cost and time, so that one query cannot exhaust the database.
- Authorization is checked on every type and field through the same use cases as REST, never by querying tables around the domain.
- Related records are loaded in batches, so that a list of 100 animals with their farms does not become 101 database queries.
5.2.5 WebSocket for Live Updates¶
When a screen must show a change the moment it happens, asking the server again and again wastes requests and still arrives late. A WebSocket keeps one connection open, and the server pushes each change as it occurs: the progress of a sync, a new alert, an updated status.
- A socket belongs to a domain's API and shares its version:
/<domain>/v1/live. - The socket carries updates, not the state. The screen loads its initial data through REST, and each message says what changed; a client that missed messages reloads through REST.
- The connection is authenticated when it opens, with the same token as REST, and again on every reconnection.
- Clients reconnect automatically, with growing delays. Cloud Run closes any connection after at most 60 minutes, and an instance can stop at any time.
- Updates reach every instance through Pub/Sub. A user may be connected to any instance of the service, so each instance listens to the events and forwards those that concern its own connections.
- A connection is closed when its screen is not visible. An open connection keeps an instance running, and an instance running is paid for.
5.2.6 MCP for Agent Tools¶
An agent reaches a system through that system's MCP server, the Model Context Protocol: JSON-RPC over HTTPS, in which the server lists its tools and the agent runtime calls them with the token of the user who is talking (decided 2026-10-31, D14). The MCP server is a component of its system and calls the system's own use cases, so a tool obeys the same authorization as the REST operation behind it.
- Every tool is declared in the system's catalog (the
CatalogServiceevery system implements): its name, namespaced like a capability (farm.animals.search), the capabilities and entitlement features it requires, its data class and its risk (read, write, dangerous).moochatoffers the model only the tools the sender may use. - A tool that writes is never run on the model's word alone: the user confirms, and the confirmation is the message that authorizes the call.
- The MCP server never holds a model key and never reads another system's data; it is one more interface of its system, versioned with it.
5.2.7 Events and Webhooks¶
An event states that something happened, such as calving.registered or organization.entitlement.changed. Inside Moonitoring, events travel on Pub/Sub; to systems outside Moonitoring, they are delivered by Pub/Sub too, through a subscription the Organization registers in the Webhooks domain of access: a push subscription that calls an HTTPS address the customer owns, or a pull subscription that the customer's own Google Cloud service account reads.
Rules for every event:
- Names are facts in the past tense, with their version, written as
<entity>.<what happened>.v<major>, such ascalving.registered.v1. - Events are published through the outbox, in the same transaction as the change they report, so that no change is saved without its event and no event is sent for a change that was rolled back. The backend publishes its outbox rows right after the commit, and
<system>-relay, called by Cloud Scheduler every minute, publishes whatever is left, so nothing waits for an always-on worker and no event is lost when an instance stops (decided 2026-11-05, see Running on Demand). Consumers receive events by Pub/Sub push. - Delivery is at least once. Every event has a unique ID, and every receiver ignores an ID it has already processed.
- Events only gain fields; a breaking change is a new version of the event, published alongside the old one.
Additional rules for deliveries outside our network, which Pub/Sub makes for us (decided 2026-10-30, D9):
- Only events marked as deliverable outside leave the platform. The relay of the system that owns an event publishes it to the topic
platform.external.v1as well as to its own topic, so no central forwarder needs to know every system's events (decided 2026-11-05); every registration is a subscription on it. The subscription's filter keeps only its Organization's events, so isolation is enforced by Pub/Sub before anything is sent; a message transform on the subscription then drops the event types the Organization did not choose (decided 2026-11-05), because a filter is limited to 256 characters and cannot list them. A new event version therefore never reaches a receiver that has not asked for it. - A push delivery is signed by Google: an OIDC token for the platform's delivery service account, which the receiver checks by signature, issuer, audience and service account e-mail with any JWT library. There is no shared secret to rotate, and no fixed sender address to allow: push calls come from Google's ranges, not from our Cloud NAT.
- A pull subscription belongs to the customer's own Google service account, granted the subscriber role on it, for partners on Google Cloud: no endpoint, no certificate, acknowledgements and at-least-once delivery as inside the platform.
- The payload is thin: the event name with its version, its ID, its time and the IDs it concerns. The receiver reads the details through our API with its own credentials, so personal data never reaches an address that was mistyped or taken over.
- Failed deliveries are retried by Pub/Sub with growing delays, then sent to the shared dead-letter topic
platform.external.dead.v1, which Pub/Sub marks with the subscription they came from;accesskeeps them as failed deliveries for 30 days, which the owner can see and redeliver. After 24 hours of continuous failures the subscription is paused and its owner told: the failures kept arriving and Cloud Monitoring counted no successful push in that time. Pub/Sub keeps the events for seven days while it is paused. - Push addresses must be public HTTPS addresses with a valid certificate, verified before the subscription becomes active:
accessposts asubscription.verification.v1event signed like a delivery, with a random challenge, and the address must answer 200 with the same challenge within ten seconds; never our own network or a private address.
5.2.8 Systems and Domains¶
Interfaces are organized the same way as the systems: each system has its own section in Interfaces, its own host names and its own business domains.
- A business domain is a group of related resources with one meaning, such as Animals or Observations, following the groups of the data model. Each domain of a system is one API, with its own contract and its own version.
- A domain is a path prefix on its system's host, followed by its version:
https://farm.moonitoring.com/animals/v1/animals/{animal_id}. The host names of every environment follow the convention in the Introduction. - The domains of each system are listed in the system's introduction, and each domain has its own page with its versions and its contract. A domain never moves between systems without a new version of its API.
5.2.9 Every API Is Versioned¶
Every interface carries a major version from its first release, so that a change that breaks callers can always be published next to the old one. Changes that do not break callers, such as a new field or a new endpoint, stay in the same version.
| Interface | Where the version is | Example |
|---|---|---|
| REST | In the path, after the domain | /animals/v1/... |
| gRPC | In the package name | moonitoring.farm.animals.v1 |
| GraphQL | In the path of the system's graph | /graphql/v1 |
| WebSocket | In the path, like the domain's REST API | /animals/v1/live |
| Events, inside and outside | At the end of the event name | calving.registered.v1 |
- A new major version is served alongside the old one until no caller uses the old one (see Releases & Versioning).
- A version that will be removed is announced in its contract (OpenAPI
deprecated, GraphQL@deprecated), and REST responses of that version carry aDeprecationheader with aSunsetdate. - The API version is not the release version. A service release
1.4.0may serveanimals/v1andanimals/v2at the same time.
5.2.10 Contracts Live Here¶
OpenAPI contracts are written in this documentation, before the code that implements them, so that a change of interface is reviewed as a change of documentation first. Each version of each domain is one file, next to the page of its domain:
docs/05-interfaces/
openapi-template.yaml the starting point for a new domain
common/v1/schemas.yaml shared types, errors, paging and security, referred to by every API
common/v1/events.yaml the envelope and delivery guarantees every event shares
proto/ the gRPC contracts, one package per domain: moonitoring/<system>/<domain>/v1/<domain>.proto
farm/
introduction.md the system: host names, a short description of each domain, who calls it, events
events/v1/asyncapi.yaml the events the system publishes and consumes, version 1
domains/
animals.md the domain: its versions, operations, gRPC and live updates, and its contract
animals/
v1/openapi.yaml the contract of the Farm Animals API, version 1
- Events are described in an AsyncAPI document per system (
<system>/events/v1/asyncapi.yaml): producer, consumers, payload, delivery guarantees and whether an Organization may receive the event outside the platform. gRPC contracts live underproto/, in the folder their package names, so thatbuf lintchecks them; a domain page shows its.protonext to its OpenAPI. - A system implements its contracts as written. Each repository keeps a pinned copy of the contracts it serves and calls, taken from a commit of this documentation;
tonic-buildgenerates the gRPC code, and a test checks that the REST API the system serves matches its OpenAPI document. A change found while coding is made here first, and a domain loses its "proposal" mark with its first release. -
Every operation and event names the requirements it serves and the capability it checks, in the
x-moonitoring-*extensions, from which the Traceability Matrix and the System Dependency Map are generated withuv run docs-interfaces. -
A new domain starts from
openapi-template.yaml, copied to<system>/domains/<domain>/v1/openapi.yaml, with a page<system>/domains/<domain>.mdnext to it. - Errors use the Problem Details format (RFC 9457,
application/problem+json), and paging, idempotency keys and authentication come fromcommon/v1/schemas.yaml, so every API answers the same way. - A new major version is a new folder (
v2/) next to the old one; the old file stays until the version is removed. - Each domain page shows its contract with the
swagger-uitag, such as<swagger-ui src="animals/v1/openapi.yaml"/>.
The documentation only displays contracts: it never sends requests to a live API. A service checks in its build that it serves the contract published here.