返回 Skills 目錄
samber/developer-platform-skills已通過檢查

SKILL DETAIL

public-grpc-api-design

samber/developer-platform-skills/public-grpc-api-design

Design a public gRPC surface for external developers - the when-gRPC-at-all gate (most platforms keep gRPC internal and publish REST or transcoded JSON), AIP proto package and versioning conventions, buf breaking-change gates and their google.api.http blind spot, unary-by-default streaming decisions, the google.rpc.Status error model with its HTTP-mapping traps, transcoding and gateway architecture (Connect-RPC, grpc-gateway, Envoy), and external auth and TLS. Use whenever the user mentions gRPC, protobuf or .proto files, buf, Connect-RPC, grpc-gateway, or gRPC-JSON transcoding at a public edge - even if they never say "gRPC API design". Design layer only, not language-specific server implementation.

安裝量 · 319查看來源

Installation

npx skills add https://github.com/samber/developer-platform-skills --skill public-grpc-api-design

技能檔案

SKILL.md

最近同步 · 2026年10月23日

evals/evals.json›
{
  "skill_name": "public-grpc-api-design",
  "evals": [
    {
      "id": 1,
      "prompt": "I'm the platform lead at Rondelet, a payments infrastructure company. Everything behind our edge is gRPC already - about 40 services, Envoy mesh, protos in a monorepo. Leadership signed off on opening the platform to third parties next quarter: roughly 200 partner engineers, mixed shops, some of them are building merchant-facing dashboards in React that would talk to us directly. We picked gRPC for the public surface because it's what we already run and it's faster than REST. Design the public gRPC API for us - package layout, the services we expose, the whole thing.",
      "expected_output": "A design that first challenges whether gRPC belongs at this public edge at all, given browser consumers and a mixed-skill partner base, and lands on internal gRPC with a transcoded JSON edge rather than a raw public gRPC surface.",
      "files": [],
      "expectations": [
        "Runs an explicit gate on whether gRPC belongs at the public edge before designing any proto or service layout",
        "States that REST still carries the large majority of public APIs (roughly 83% in recent tracked counts) and that dual-stack - gRPC internally, REST or transcoded JSON externally - is the practitioner default rather than either/or",
        "Names that browsers structurally cannot speak gRPC's raw HTTP/2 framing, making a proxy layer mandatory given the React dashboards described",
        "Names Protobuf's binary encoding as a concrete debuggability cost for external consumers: nothing readable in a network tab, inspection needs grpcurl-class tooling partners rarely have installed",
        "Names that every external caller pays the protoc/stub-regeneration cost that an internal fleet amortizes",
        "Notes that gRPC at the edge limits the ability to augment or remediate API behavior in proxies and API-management tooling",
        "Checks the three genuine triggers that would justify public gRPC anyway - confirmed SDK-mediated or proto-fluent consumers, genuinely streaming-shaped workload, measured performance bottleneck consumers can absorb tooling cost for",
        "Concludes that 'we already run gRPC internally' and 'it's faster' are not among the qualifying triggers",
        "Identifies the 200 mixed-shop partner engineers as not a confirmed SDK-mediated audience, so Temporal's control-plane precedent does not transfer here",
        "Makes the audience-reach argument explicitly: limiting to gRPC-only risks missing developers not equipped or experienced enough to consume gRPC services",
        "Recommends gRPC inside with JSON outside rather than raw public gRPC as the only surface",
        "States that the gate's verdict determines which transcoding/gateway options are even in play for the next design step"
      ]
    },
    {
      "id": 2,
      "prompt": "Reviewing the gateway error mapping our backend team wrote for the public edge of Kestrelline (B2B logistics API). Here's their table: INVALID_ARGUMENT -> 400, FAILED_PRECONDITION -> 412, OUT_OF_RANGE -> 416, NOT_FOUND -> 404, ALREADY_EXISTS -> 409, PERMISSION_DENIED -> 403, UNAUTHENTICATED -> 401, CANCELLED -> 408, RESOURCE_EXHAUSTED -> 429, DEADLINE_EXCEEDED -> 504, and anything we haven't classified falls through to INTERNAL -> 500. They pulled it from the gRPC docs. Tell me if this is right before we publish it in our partner docs.",
      "expected_output": "A review that corrects the specific mappings, explains why a server may not derive HTTP status from the canonical client-side table at all, and documents the lossiness of the HTTP status line for consumers.",
      "files": [],
      "expectations": [
        "Corrects FAILED_PRECONDITION to 400, not 412, and identifies 412 as the recurring point of confusion",
        "Corrects CANCELLED to 499 (non-standard 'Client Closed Request', no formal IANA status), not 408",
        "Corrects OUT_OF_RANGE to 400, not 416",
        "States that the gRPC project's own documentation forbids servers from using the canonical status-mapping table to determine an HTTP status code",
        "States that the canonical mappings are neither symmetric nor 1-to-1, and that the table exists for clients or intermediaries interpreting a response that arrived without a grpc-status",
        "Requires the gateway to have its own deliberate mapping, confirmed against the transcoding implementation actually in use rather than assumed universal",
        "Names the 400 collapse point: INVALID_ARGUMENT, FAILED_PRECONDITION and OUT_OF_RANGE all land on 400",
        "Names the 409 collapse point: ALREADY_EXISTS and ABORTED both land on 409",
        "Requires the partner docs to state that the HTTP status line is lossy and the structured envelope in the response body is the source of truth",
        "Flags the unclassified-falls-through-to-INTERNAL rule as the named anti-pattern, because aggregating every failure into one code flattens status dashboards into a single bucket",
        "Requires returning the most specific applicable code, for example OUT_OF_RANGE over FAILED_PRECONDITION when both technically apply",
        "Notes that RESOURCE_EXHAUSTED at 429 should be paired with a RetryInfo detail, and that UNAVAILABLE (503) is the only code generally safe to auto-retry without extra signal"
      ]
    },
    {
      "id": 3,
      "prompt": "Quick CI question. Vantabrook publishes .proto files for partners and also exposes a transcoded REST surface generated from the same protos. Our pipeline runs `buf lint` with the STANDARD set and `buf breaking --against main` with the PACKAGE category on every PR, and it has been green for eight months. Compliance is asking me to sign a statement that our published contract cannot break without CI catching it. Can I sign it?",
      "expected_output": "A clear no, with the annotation blind spot named, a concrete additional gate specified, a category upgrade, and the cross-version test that schema diffing does not cover.",
      "files": [],
      "expectations": [
        "Answers that the described pipeline does not cover the published contract and the statement cannot be signed as written",
        "Names that `buf breaking` does not inspect custom options, so changes to `google.api.http` annotations pass every proto gate while breaking the public REST mapping",
        "Prescribes a dedicated annotation check: snapshot the annotation-derived HTTP surface (paths, verbs, body mappings) and diff it in CI, or contract-test the transcoded routes",
        "Recommends upgrading from PACKAGE to the FILE category for a public surface, and gives the reason that PACKAGE forgives moving a definition between files while FILE catches generated-import breakage",
        "States that Protobuf's encoding guarantees wire compatibility only, not generated-client-code compatibility",
        "Names that a rename is semantically remove-and-add and is therefore always breaking, never a safe refactor",
        "Names that moving a field into or out of a `oneof` breaks generated Go stubs",
        "Names that moving a message to a different file breaks generated imports because the file path is part of the generated-code contract",
        "Requires cross-version testing in both directions - old client against new server and new client against old server - rather than treating schema parsing as sufficient",
        "Proposes a human backstop for a public surface, such as a Buf Schema Registry review queue where a breaking push is approved or rejected with a paper trail (or a proto.lock-based equivalent for teams not on buf)",
        "States the coverage gate as binary: 100% of RPCs carrying google.api.http annotations covered by a mapping diff or contract test"
      ]
    },
    {
      "id": 4,
      "prompt": "Here's the first cut of the protos we're about to publish for Almandine Ledger. Package is `almandine.ledger`, file lives at `ledger/ledger_v1.proto`. Services: `Ledger` with `rpc GetEntry(string) returns (LedgerEntry)`, `rpc ListEntries(LedgerQuery) returns (LedgerEntryList)` and `rpc SearchEntries(LedgerQuery) returns (LedgerEntryList)` - we reuse LedgerQuery because both take the same filters and it keeps things DRY. We have `enum EntryStatus { ACTIVE = 0; VOIDED = 1; }`. Marketing wants to brand the release v1.2 since it's our second iteration. Also we dropped the old `legacy_ref` field (was tag 4) last week. Review it.",
      "expected_output": "A convention review that corrects the package suffix, directory mirror, filename, version scheme, request/response wrappers, shared message, enum zero value and the removed field's tag.",
      "files": [],
      "expectations": [
        "Requires the package to end in a major-version component, i.e. `almandine.ledger.v1`, not `almandine.ledger`",
        "Requires the directory structure to mirror the package exactly, i.e. `almandine/ledger/v1/`",
        "Rejects the version appearing in the filename (`ledger_v1.proto`), because it produces bizarre generated imports",
        "Rejects `v1.2` as a public version: major version only, never SemVer minors or patch segments in the package or the URI",
        "Rejects the bare scalar `string` request on GetEntry and requires a dedicated wrapper message",
        "Requires one dedicated `<Method>Request` / `<Method>Response` wrapper per RPC, so ListEntries and SearchEntries stop sharing LedgerQuery and LedgerEntryList",
        "Gives the reason a shared message is rejected: every change to it ripples across every RPC referencing it, while a wrapper can gain optional fields without breaking wire compatibility",
        "Requires the enum zero value to be an `_UNSPECIFIED` member rather than ACTIVE = 0",
        "Requires the removed `legacy_ref` field's tag 4 to be marked `reserved`, and states that a tag is never reused or renumbered",
        "Requires per-language package options (such as `java_package`) so generated code stays idiomatic instead of leaking the raw proto path",
        "Recommends modeling the surface resource-oriented with standard List/Get/Create/Update/Delete methods, reserving the `:verb` custom-method suffix for cases no standard verb fits",
        "Recommends well-known types (google.protobuf.Timestamp, FieldMask, google.rpc.*) over hand-rolled equivalents",
        "Cites the buf lint rules that mechanize these findings, such as PACKAGE_VERSION_SUFFIX, RPC_REQUEST_RESPONSE_UNIQUE and ENUM_ZERO_VALUE_SUFFIX"
      ]
    },
    {
      "id": 5,
      "prompt": "Design decision I want validated for Thessaly Telemetry's new partner API. Our architect wants every read method on the public surface to be bidirectional streaming - his argument is that we avoid connection setup per call and get better throughput than unary, and since gRPC gives us bidi for free we may as well use it everywhere. Consumers are partner backends plus an embedded widget our customers drop into their own web apps, which will be on HTTP/1.1 for a good while. We terminate at an AWS Application Load Balancer today. Give me your read on it.",
      "expected_output": "A rejection of blanket bidirectional streaming, with the unary default, the named costs, the browser protocol wall, the HTTP-mapping exemption, and the load-balancer configuration all specified.",
      "files": [],
      "expectations": [
        "Establishes unary as the default call shape, with every streaming method a deliberate, recorded decision rather than a free feature",
        "Cites the published guidance that replacing unary calls with bidirectional streaming for performance reasons is an advanced technique not appropriate in many situations",
        "Identifies server-streaming as the streaming mode to reach for first when a genuine push/tail use case exists, because it maps cleanly onto SSE through a gateway and onto Connect's streaming encoding",
        "States that browsers cannot do client or bidirectional streaming over HTTP/1.1 at all, and frames this as a protocol wall rather than a tooling gap, with Connect over HTTP/2 as the exception",
        "Names that a stream failing mid-flight after partial data forces every external consumer into restart/resume bookkeeping a unary retry never requires",
        "Names that retries stop being transparently managed by the client library once a call is streaming",
        "States that an HTTP mapping is required for every RPC except bidirectional streaming, so every bidi method needs a documented non-streaming alternative or it is invisible on the transcoded surface",
        "Warns that HTTP/2 multiplexing under an L4 balancer pins all of a client's streams to one backend for the connection's lifetime",
        "Requires L7, HTTP/2-aware load balancing at the public gRPC edge",
        "Requires the client keepalive PING interval to be set below whichever load-balancer idle timeout applies on the path",
        "Names the AWS Application Load Balancer's 60-second default idle timeout as the binding number for this deployment (and may contrast with AWS NLB 350 s or Google Cloud's 600 s backend keepalive)",
        "Recommends enabling buf's UNARY_RPC lint category if the surface commits to unary-only, so a later PR cannot silently reintroduce a streaming method"
      ]
    },
    {
      "id": 6,
      "prompt": "Security design for Oriflamme's new external API. Internally every service-to-service gRPC hop is mTLS with SPIFFE identities issued by our mesh CA, and it has worked flawlessly for three years. We're now onboarding about 500 external developers - a mix of partner backend jobs and a few integrations where an end user clicks 'connect' in a browser and authorizes access to their own account. Plan on the table is to extend what already works: issue each external developer a client certificate from the same CA and require mTLS on the public endpoint. Poke holes in it.",
      "expected_output": "A rejection of per-developer client certificates, replaced with an OAuth2 external-auth design, method-level scope enforcement, and the gateway reference architecture with the mesh resuming behind it.",
      "files": [],
      "expectations": [
        "Rejects issuing and revoking client certificates for 500 external developers as operationally impractical",
        "Distinguishes that mTLS authenticates workloads and scales inside a mesh, while a public edge must authenticate apps and users instead",
        "States that the mesh does not disappear: mTLS resumes one hop further in, behind the public gateway",
        "Requires TLS as non-negotiable and first, because credentials travel as gRPC metadata and would otherwise be plaintext",
        "Notes that most gRPC implementations refuse to send credentials over an unencrypted channel at all",
        "Assigns OAuth2 Client Credentials to the partner backend/machine callers",
        "Assigns OAuth2 Authorization Code with PKCE to the browser flow where a human authorizes interactively",
        "Specifies the token as `authorization: Bearer <token>` metadata validated in a server interceptor",
        "Specifies JWT as the token format, strongly signed (RS256), with expiry claims",
        "Specifies token introspection against the authorization server, cached so it does not tax every call",
        "Requires scope enforcement at the method level - whether the token is scoped for this specific RPC, not merely whether the caller is authenticated",
        "States that API keys complement rather than replace authentication: a key identifies a project or consumer for quota attribution and coarse gating, not an authenticated identity",
        "Specifies the public-gateway reference architecture as TLS termination plus token validation plus per-method authorization plus rate limiting, fronting the internal mesh"
      ]
    },
    {
      "id": 7,
      "prompt": "Last item before Halcyon Freight opens its gRPC endpoint to customers on the 14th. The service started life internal, so alongside the business methods (CreateShipment, GetShipment, ListShipments, QuoteRate) it also carries DrainNode, DumpRuntimeConfig, SetFeatureFlag, ReindexShipmentCache and a ping-style DebugEcho. Server reflection is on because our support engineers use grpcurl against staging and production daily. TLS is terminated at the edge and every call requires a valid bearer token. Auth and transport are locked down - are we clear to launch?",
      "expected_output": "A no, with server reflection required off in production, the internal-RPC exposure audit named as mandatory and incomplete, and the specific admin methods called out.",
      "files": [],
      "expectations": [
        "Answers that the service is not clear to launch as described",
        "Requires server reflection to be disabled in production, giving the reason that it is an API-discovery tool for attackers once the port is publicly reachable",
        "States explicitly that TLS terminated and authentication enforced does not close the exposure hole",
        "Requires enumerating the full method surface for admin, debug and monitoring RPCs never designed for external visibility",
        "Identifies DrainNode, DumpRuntimeConfig, SetFeatureFlag, ReindexShipmentCache and DebugEcho by name as methods that must not be on the public surface",
        "States that the exposure audit is mandatory rather than optional for any service built internal-first",
        "Treats reflection-disabled and audit-completed as binary pass gates that must both be satisfied before launch",
        "Recommends keeping the admin and debug methods off the published contract rather than relying only on per-method authorization to hide them",
        "Retains per-method scope enforcement for the business methods that do stay on the public surface",
        "Notes that the support engineers' grpcurl workflow can continue against a non-public environment rather than justifying reflection in production"
      ]
    },
    {
      "id": 8,
      "prompt": "Need a call on the edge architecture for Bellweather Analytics by Friday. Situation: Envoy is already our mesh data plane and two of our platform engineers know its config well. Backend services are Go. Consumers are a customer-facing dashboard running in the browser, plus partner backends that will poll us hard and care about latency. We have six weeks to ship. The team's instinct is grpc-gateway since it's the best-known option and everyone's Go. Lay out the options and tell us what to do.",
      "expected_output": "A ranked menu rather than a flat comparison, with Connect-RPC as the default rung, Envoy's promotion condition applied to this team, grpc-gateway's error weakness named, raw public gRPC deleted because of the browser consumer, and an explicit re-rank against the six-week deadline.",
      "files": [],
      "expectations": [
        "Presents the options as an explicit ranking rather than a flat pros-and-cons list",
        "States the ordering on separate axes, with effort ordered raw public gRPC edge > grpc-gateway == Envoy transcoder > Connect-RPC, and value and efficiency both ordering Connect-RPC > Envoy transcoder > grpc-gateway > raw public gRPC edge",
        "Names Connect-RPC as the default rung for a new public surface",
        "Lists Connect-RPC's concrete properties: no proxy required, browsers served natively, full streaming over HTTP/2, JSON errors out of the box, wire-compatible with gRPC",
        "States the condition that promotes Envoy's gRPC-JSON transcoder - Envoy is already the mesh data plane and a second runtime is the real cost - and applies it to this team's existing Envoy fluency",
        "Explains the grpc-gateway == Envoy effort tie explicitly, and identifies 'you already run Envoy' as the condition that breaks it",
        "Names grpc-gateway's default error handler as the weakest of the options, and its codegen step plus double JSON-to-protobuf parse per request as recurring costs",
        "Deletes raw public gRPC as the only surface outright because a browser client is confirmed, rather than demoting it to the bottom of the menu",
        "Re-ranks against the stated inputs and says which input moved which option - the six-week ship date and the existing Envoy expertise promoting what the team already operates",
        "States that the ranking is a default rather than a law",
        "Steers the latency-sensitive partner backends to native gRPC or Connect's binary mode, noting that no JSON-transcoding path is free",
        "Requires google.api.http annotations on every RPC except bidirectional streaming as the mapping convention regardless of which rung is chosen",
        "Makes a single concrete recommendation for this team rather than leaving the choice open"
      ]
    },
    {
      "id": 9,
      "prompt": "Debugging a partner complaint at Corvid Payments. Our services return google.rpc.Status properly - we attach ErrorInfo with a reason and domain, BadRequest with field violations for validation failures, and DebugInfo carrying the stack detail so our own on-call can correlate. Partners integrating over our JSON endpoint say all they ever get back is a flat message string with no field-level information, so their support tickets are all 'the API said invalid request'. We're on grpc-gateway today and someone suggested moving to Envoy's transcoder. What's actually happening and what should we do?",
      "expected_output": "A diagnosis naming grpc-gateway's default error handler as the cause, the per-gateway survival mechanics for each alternative, and a hard stop on DebugInfo reaching external callers.",
      "files": [],
      "expectations": [
        "Identifies grpc-gateway's default error handler as the cause: it maps the code to an HTTP status but flattens the message and drops `details` entirely",
        "States that a custom error handler is mandatory on grpc-gateway if structured error detail is to survive",
        "States that Envoy's gRPC-JSON transcoder requires `convert_grpc_status: true` to surface google.rpc.Status as the JSON body",
        "Adds the second Envoy condition: every error-detail type must be present in the proto descriptor set the transcoder was built with, or the detail drops silently",
        "Notes that Connect-RPC serializes errors as JSON natively, so no transcoding gap exists there",
        "Flags DebugInfo as internal-only and requires it to be filtered from `details` before the response leaves the service",
        "States that sanitizing the `message` field alone is insufficient while `details` still carries DebugInfo, i.e. the sanitization must be structural",
        "Confirms ErrorInfo as the general-purpose detail to reach for first and BadRequest as the right carrier for field-level validation violations",
        "Recommends pairing RetryInfo with RESOURCE_EXHAUSTED so callers back off rather than hammering the same limit",
        "Describes the resulting JSON error shape with a code, a status string and a typed `details` array carrying `@type` entries, as the de facto standard envelope worth adopting",
        "Requires documenting for consumers that the structured body envelope is the source of truth and the HTTP status line is a lossy convenience"
      ]
    },
    {
      "id": 10,
      "prompt": "Planning the v2 of Sablefen's published protos and I want a sanity check on the plan. (1) `sablefen.catalog.v2` will import the message types from `sablefen.catalog.v1` for everything that isn't changing - no point duplicating 40 messages. (2) We're renaming `sku_code` to `product_code` across the board for clarity, and deleting three fields nobody uses. (3) Our new realtime inventory feature isn't fully baked but partners are asking for it, so we're shipping it inside `sablefen.catalog.v1` this month so they don't have to add a second import path, and we'll harden it later. (4) Some resource names change in v2 since we reorganized the hierarchy. Tell me what to fix.",
      "expected_output": "A review rejecting the cross-major dependency, the rename, the resource-name change and the unbaked feature in v1, with channel semantics and deprecation mechanics specified.",
      "files": [],
      "expectations": [
        "Rejects v2 importing from v1: a new major version must not depend on the previous major version of the same API",
        "Rejects the resource-name changes for stable resources: resource names stay identical across majors so a v2 client can address a resource a v1 client created",
        "States that renaming sku_code to product_code is semantically remove-and-add and is therefore a breaking change, not a clarity refactor",
        "Requires the three deleted fields to be marked `reserved` and their tags never reused or renumbered",
        "Rejects shipping the unbaked realtime inventory feature inside the v1 package, stating that v1 is reserved for stable and that claiming beta functionality under v1 is a review finding",
        "Directs the pre-stable feature to a v1beta or v1alpha channel instead",
        "Names the two legitimate evolution strategies - channel-based (long-lived beta/stable channels updated in place) and release-based (v1beta1 to v1beta2 for an incompatible beta-only change)",
        "Requires `deprecated = true` on anything being retired, noting the option is available on messages, enums, enum values and services, not only fields",
        "Requires a deprecation comment stating why the component is deprecated and what replaces it, so generated docs explain the replacement",
        "States the channel-differentiated sunset expectations: alpha may be removed without notice, beta after a deprecation period with 180 days as the recommended floor",
        "States that an in-place breaking change to a stable component is an extreme course of action reserved for security or regulatory necessity",
        "Defers concrete notice windows and sunset communication to versioning-policy territory rather than inventing them here"
      ]
    }
  ],
  "trigger_queries": [
    {
      "query": "Should we expose our internal gRPC services to external partners?",
      "should_trigger": true
    },
    {
      "query": "Design a public gRPC API for our platform.",
      "should_trigger": true
    },
    {
      "query": "we publish .proto files on github for partners, what conventions should the package follow",
      "should_trigger": true
    },
    {
      "query": "Is buf breaking enough to protect a public proto contract?",
      "should_trigger": true
    },
    {
      "query": "Connect-RPC or grpc-gateway for our public edge?",
      "should_trigger": true
    },
    {
      "query": "how do browser clients call our gRPC backend",
      "should_trigger": true
    },
    {
      "query": "Review our google.rpc.Status error model before we open the API to customers.",
      "should_trigger": true
    },
    {
      "query": "Our partners want to generate their own stubs from our protos. What do we owe them?",
      "should_trigger": true
    },
    {
      "query": "We're putting Envoy's gRPC-JSON transcoder in front of our services for third parties, sanity check the design.",
      "should_trigger": true
    },
    {
      "query": "what HTTP status should our gateway return for FAILED_PRECONDITION",
      "should_trigger": true
    },
    {
      "query": "audit this gRPC service before we open it to external developers",
      "should_trigger": true
    },
    {
      "query": "does the proto package need a version suffix if we're publishing it",
      "should_trigger": true
    },
    {
      "query": "we want to offer both REST and gRPC to customers, how do we keep the two surfaces consistent",
      "should_trigger": true
    },
    {
      "query": "our partner devs keep breaking when we refactor protos, what gates do we need in CI",
      "should_trigger": true
    },
    {
      "query": "is it ok to put the version in the proto filename",
      "should_trigger": true
    },
    {
      "query": "planning to make all our public read methods bidirectional streaming for speed",
      "should_trigger": true
    },
    {
      "query": "how should external developers authenticate to our gRPC endpoint",
      "should_trigger": true
    },
    {
      "query": "we authenticate our internal RPC mesh with mTLS and now want outside developers calling the same services",
      "should_trigger": true
    },
    {
      "query": "grpc-gateway is dropping our error details before they reach partners",
      "should_trigger": true
    },
    {
      "query": "our partner-facing status dashboard shows almost every failure as UNKNOWN and we can't tell anything apart",
      "should_trigger": true
    },
    {
      "query": "we shipped a new half-finished feature into our v1 package so partners wouldn't need a second import, is that fine",
      "should_trigger": true
    },
    {
      "query": "what does AIP-215 require for proto packages",
      "should_trigger": true
    },
    {
      "query": "how do I version a protobuf API for outside consumers",
      "should_trigger": true
    },
    {
      "query": "Do we need a proxy in front of gRPC for third-party developers?",
      "should_trigger": true
    },
    {
      "query": "our load balancer keeps dropping long-lived streams to partner clients",
      "should_trigger": true
    },
    {
      "query": "planning the public API surface and the team wants protobuf everywhere",
      "should_trigger": true
    },
    {
      "query": "what's the right way to deprecate an RPC that partners depend on",
      "should_trigger": true
    },
    {
      "query": "should server reflection stay on once the port is publicly reachable",
      "should_trigger": true
    },
    {
      "query": "we want to publish our schema to the Buf Schema Registry so customers can generate clients",
      "should_trigger": true
    },
    {
      "query": "hey, thinking of just shipping our protos to customers and letting them figure it out. thoughts?",
      "should_trigger": true
    },
    {
      "query": "design the proto governance for a contract several products will share for years",
      "should_trigger": true
    },
    {
      "query": "our transcoded REST routes changed and nobody noticed until partners paged us at 2am",
      "should_trigger": true
    },
    {
      "query": "which streaming call types are actually safe to expose publicly",
      "should_trigger": true
    },
    {
      "query": "map our canonical RPC status codes onto HTTP for the public gateway",
      "should_trigger": true
    },
    {
      "query": "we're a control-plane product and our users are infra engineers, is native gRPC defensible for us",
      "should_trigger": true
    },
    {
      "query": "explain the tradeoffs of exposing protobuf to third-party integrators",
      "should_trigger": true
    },
    {
      "query": "set up CI gates for the .proto files we publish to customers",
      "should_trigger": true
    },
    {
      "query": "should each RPC get its own request message or can they share one",
      "should_trigger": true
    },
    {
      "query": "our enum's zero value is the active state, does that matter for a public API",
      "should_trigger": true
    },
    {
      "query": "do we need google.api.http annotations on every method",
      "should_trigger": true
    },
    {
      "query": "cheapest way to serve both JSON and binary clients from one protobuf contract",
      "should_trigger": true
    },
    {
      "query": "we're worried our partners can't debug binary payloads when something goes wrong",
      "should_trigger": true
    },
    {
      "query": "does Temporal's public gRPC API make sense as a model for what we're building",
      "should_trigger": true
    },
    {
      "query": "we run Envoy already, what's the least work to expose our services as JSON to customers",
      "should_trigger": true
    },
    {
      "query": "external devs are poking our service with grpcurl and discovering admin methods",
      "should_trigger": true
    },
    {
      "query": "write the design doc section justifying gRPC versus REST at our public edge",
      "should_trigger": true
    },
    {
      "query": "how many canonical status codes are there and which of them collapse onto 400",
      "should_trigger": true
    },
    {
      "query": "planning a v2 of our published protos, can v2 import the v1 messages",
      "should_trigger": true
    },
    {
      "query": "our SDK team wraps gRPC directly, should partners get the raw protos too",
      "should_trigger": true
    },
    {
      "query": "what breaks for consumers when we move a message to a different .proto file",
      "should_trigger": true
    },
    {
      "query": "stubs drift between releases for our partners, how do we test compatibility properly",
      "should_trigger": true
    },
    {
      "query": "we want to sell an API but the backend is all protobuf RPC internally",
      "should_trigger": true
    },
    {
      "query": "review the transcoding architecture for our public edge",
      "should_trigger": true
    },
    {
      "query": "is Connect-RPC production ready for an external-facing surface",
      "should_trigger": true
    },
    {
      "query": "Design the REST resource naming and status codes for our public API.",
      "should_trigger": false
    },
    {
      "query": "Audit our REST endpoints for pagination and filtering consistency.",
      "should_trigger": false
    },
    {
      "query": "Review our public REST API for Hyrum's Law backward-compatibility risk.",
      "should_trigger": false
    },
    {
      "query": "Design a public GraphQL API for third-party developers.",
      "should_trigger": false
    },
    {
      "query": "Should our GraphQL schema use Relay cursor connections?",
      "should_trigger": false
    },
    {
      "query": "Set depth and complexity ceilings on our public GraphQL surface.",
      "should_trigger": false
    },
    {
      "query": "Design the federation trust boundary for our GraphQL subgraphs.",
      "should_trigger": false
    },
    {
      "query": "Write our deprecation notice window and sunset policy for API consumers.",
      "should_trigger": false
    },
    {
      "query": "Should we use RFC 9745 Deprecation and RFC 8594 Sunset headers?",
      "should_trigger": false
    },
    {
      "query": "Should we version in the URI path or pin accounts to a date-based version?",
      "should_trigger": false
    },
    {
      "query": "Design our error code taxonomy and RFC 9457 problem-details envelope.",
      "should_trigger": false
    },
    {
      "query": "How should error messages be written so integrators can self-serve the fix?",
      "should_trigger": false
    },
    {
      "query": "Design idempotency-key support for our public API.",
      "should_trigger": false
    },
    {
      "query": "What backoff strategy should our SDK use on retries?",
      "should_trigger": false
    },
    {
      "query": "Design the rate-limit tiers and quota-versus-burst numbers our API publishes.",
      "should_trigger": false
    },
    {
      "query": "Should we emit X-RateLimit-* headers or the IETF RateLimit fields?",
      "should_trigger": false
    },
    {
      "query": "Which integration surfaces should we offer developers first, webhooks, SDKs or an MCP server?",
      "should_trigger": false
    },
    {
      "query": "Design our OAuth2 authorization server and its consent screen.",
      "should_trigger": false
    },
    {
      "query": "What TTL should our refresh tokens have and should we rotate them?",
      "should_trigger": false
    },
    {
      "query": "Design our API key format with a prefix and checksum.",
      "should_trigger": false
    },
    {
      "query": "Should API keys be stored hashed or encrypted?",
      "should_trigger": false
    },
    {
      "query": "Design our outbound webhook platform with HMAC request signing.",
      "should_trigger": false
    },
    {
      "query": "What retry and dead-letter policy should webhook deliveries use?",
      "should_trigger": false
    },
    {
      "query": "Which languages should get official SDKs and in what order?",
      "should_trigger": false
    },
    {
      "query": "Should our SDK's SemVer track the API version or move independently?",
      "should_trigger": false
    },
    {
      "query": "Audit our API reference docs for missing parameters and undocumented response codes.",
      "should_trigger": false
    },
    {
      "query": "Design a sandbox mode so integrators can test without real charges.",
      "should_trigger": false
    },
    {
      "query": "Design our status page components and the incident lifecycle we publish.",
      "should_trigger": false
    },
    {
      "query": "Design an MCP server so AI agents can operate our product.",
      "should_trigger": false
    },
    {
      "query": "Give customers JDBC access to their data in our product.",
      "should_trigger": false
    },
    {
      "query": "Should we ship Parquet file drops or a Snowflake share for bulk export?",
      "should_trigger": false
    },
    {
      "query": "Design the information architecture of our developer portal.",
      "should_trigger": false
    },
    {
      "query": "Surface integration failures and delivery logs to the partners who built against us.",
      "should_trigger": false
    },
    {
      "query": "Write a Go gRPC server with interceptors and graceful shutdown.",
      "should_trigger": false
    },
    {
      "query": "How do I unit test a gRPC handler in Go with testify?",
      "should_trigger": false
    },
    {
      "query": "Implement a gRPC client with connection pooling in Go.",
      "should_trigger": false
    },
    {
      "query": "Set up gRPC health checking probes for our Kubernetes deployment.",
      "should_trigger": false
    },
    {
      "query": "Configure mTLS between our internal services in the Istio mesh.",
      "should_trigger": false
    },
    {
      "query": "Benchmark protobuf versus JSON serialization in our internal ingestion pipeline.",
      "should_trigger": false
    },
    {
      "query": "How do I generate Python stubs from a .proto file on my laptop?",
      "should_trigger": false
    },
    {
      "query": "Our internal service-to-service gRPC calls are timing out under load, help me debug it.",
      "should_trigger": false
    },
    {
      "query": "Migrate our internal RPC layer from Thrift to gRPC.",
      "should_trigger": false
    },
    {
      "query": "Should our internal microservices talk over gRPC or a message queue?",
      "should_trigger": false
    },
    {
      "query": "Decide whether to build our own app marketplace or join other vendors' marketplaces.",
      "should_trigger": false
    },
    {
      "query": "Optimize our listing on the Salesforce AppExchange.",
      "should_trigger": false
    },
    {
      "query": "Which technology partners should we integrate with first?",
      "should_trigger": false
    },
    {
      "query": "Design the review and approval process for third-party apps on our marketplace.",
      "should_trigger": false
    },
    {
      "query": "Design partner developer onboarding from signup to first submitted app.",
      "should_trigger": false
    },
    {
      "query": "Plan our presence as a source connector on third-party ETL platforms.",
      "should_trigger": false
    },
    {
      "query": "Write a job description for an API product manager.",
      "should_trigger": false
    },
    {
      "query": "Design the listing content standards every app on our marketplace must meet.",
      "should_trigger": false
    },
    {
      "query": "What take rate should our app marketplace charge developers?",
      "should_trigger": false
    },
    {
      "query": "Plan the launch cohort and co-marketing for our new marketplace.",
      "should_trigger": false
    },
    {
      "query": "Route this task to the right developer-platform skill for me.",
      "should_trigger": false
    }
  ]
}
references/grpc-error-http-mapping.md›
# gRPC error model and HTTP mapping reference

Detail behind SKILL.md step 5. Sources: grpc/grpc `doc/http-grpc-status-mapping.md`, `google/rpc/error_details.proto`, grpc-go `codes.go`, Envoy transcoder documentation, grpc-gateway documentation, Connect-RPC documentation, AIP-194.

## The direction rule, quoted

The gRPC project's own documentation on its status-mapping table: **"servers must not use this table to determine an HTTP status code to use... the mappings are neither symmetric nor 1-to-1."** The canonical table exists for a client or intermediary interpreting a response that arrived _without_ a `grpc-status` field. A gateway translating gRPC errors outward for public HTTP exposure needs its own deliberate mapping - and the tables actually shipped by grpc-gateway and Envoy can differ, so confirm against the implementation in use rather than assuming a universal spec.

## The 17 canonical codes and the common gateway mapping

`OK=0` through `UNAUTHENTICATED=16` - 17 codes, verified against grpc-go's `codes.go` (`Unauthenticated Code = 16` immediately followed by `_maxCode = 17`). The gateway-side mapping below is the one grpc-gateway implements - a de facto convention, not a standard:

| gRPC code                            | HTTP    | Note                                                        |
| ------------------------------------ | ------- | ----------------------------------------------------------- |
| `INVALID_ARGUMENT`                   | 400     |                                                             |
| `FAILED_PRECONDITION`                | 400     | **not 412** - the recurring point of confusion              |
| `OUT_OF_RANGE`                       | 400     |                                                             |
| `UNAUTHENTICATED`                    | 401     |                                                             |
| `PERMISSION_DENIED`                  | 403     |                                                             |
| `NOT_FOUND`                          | 404     |                                                             |
| `ALREADY_EXISTS`                     | 409     |                                                             |
| `ABORTED`                            | 409     |                                                             |
| `RESOURCE_EXHAUSTED`                 | 429     | pair with `RetryInfo`                                       |
| `CANCELLED`                          | **499** | non-standard "Client Closed Request", no formal IANA status |
| `INTERNAL` / `UNKNOWN` / `DATA_LOSS` | 500     |                                                             |
| `UNIMPLEMENTED`                      | 501     |                                                             |
| `UNAVAILABLE`                        | 503     | the only code generally safe to auto-retry (AIP-194)        |
| `DEADLINE_EXCEEDED`                  | 504     |                                                             |

**The collapse points to document for consumers:** three distinct codes (`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, `OUT_OF_RANGE`) all become 400, and two (`ALREADY_EXISTS`, `ABORTED`) become 409. A REST/JSON client reading only the status line loses the semantic distinction - state in the API docs that the structured envelope in the body is the source of truth, and the HTTP status is a lossy convenience.

## Code selection discipline

1. Return the most specific applicable code - `OUT_OF_RANGE` over `FAILED_PRECONDITION` when both technically apply.
2. Never default to `UNKNOWN` or `INTERNAL` as a defensive habit. It is the named overused anti-pattern in the wild, and it defeats observability: status dashboards aggregating by code lose all signal when everything routes through one bucket.
3. Sanitize `message` for production - developer-facing English, no internal implementation detail, never the channel for localized end-user text (use a `LocalizedMessage` detail or client-side localization).

## The error-details catalog

`google/rpc/error_details.proto` standardizes the typed payloads carried in `Status.details`:

| Type                                                      | Purpose                                                                                                                                                                                                                 |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ErrorInfo`                                               | machine-readable reason + domain + metadata - the general-purpose detail to reach for first                                                                                                                             |
| `RetryInfo`                                               | how long before a retry is safe - pair with `RESOURCE_EXHAUSTED` and `UNAVAILABLE`                                                                                                                                      |
| `BadRequest`                                              | field-level validation violations                                                                                                                                                                                       |
| `QuotaFailure`, `PreconditionFailure`                     | structured cause lists for their respective codes                                                                                                                                                                       |
| `RequestInfo`, `ResourceInfo`, `Help`, `LocalizedMessage` | request correlation, resource identity, doc links, localized text                                                                                                                                                       |
| `DebugInfo`                                               | **internal only - never expose to external clients.** Stack/trace detail; filter it from `details` before the response leaves the service. Sanitizing `message` is insufficient if `details` still carries `DebugInfo`. |

Only `UNAVAILABLE` is generally safe to auto-retry without additional signal; every other code needs an explicit `RetryInfo` or caller-side judgment before automatic retry. The taxonomy-level decisions - catalog shape, message writing, a `retryable` flag - are `samber/developer-platform-skills@api-error-design`'s territory; its AIP-193 `ErrorInfo` (domain/reason) shape is the one that spans REST and gRPC in a single taxonomy.

## Envelope survival per gateway

Whether `Status.details` reaches the JSON consumer depends entirely on the transcoding layer:

- **Envoy gRPC-JSON transcoder:** with `convert_grpc_status: true`, Envoy takes `google.rpc.Status` from the `grpc-status-details-bin` header and uses it as the JSON body - **but only if the error-detail types are present in the proto descriptor** the transcoder was built with. Omit a detail type from the descriptor set and it drops from the response silently. The resulting shape - `{"error":{"code":…,"status":"INVALID_ARGUMENT","details":[{"@type":"type.googleapis.com/google.rpc.BadRequest",…}]}}` - is what Google Cloud APIs and Dapr return, making it a de facto standard JSON error shape worth adopting even outside a pure-Envoy stack.
- **Connect-RPC:** serializes errors as JSON natively (a code, an optional message, an optional details array); its code mapping is a superset of gRPC's. No transcoding gap exists.
- **grpc-gateway:** the weakest by default - out of the box it maps codes to an HTTP status but flattens the message and **drops `details` entirely**. A custom error handler is required to preserve the structured envelope; budget for it, or prefer Connect/Envoy when structured error detail matters to external consumers.

## Why a team might not use google.rpc.Status at all

gRPC is a CNCF project, not a Google product, and `google.rpc.Status` can be impractical outside Google's context:

- Protobuf-encoded errors cause binary bloat on Android.
- gRPC carries status in HTTP/2 trailers, so any proxy or logging tool wanting the structured fields must unpack the proto instead of using standard HTTP-status tooling.

A lighter home-grown envelope is a legitimate choice - consistency _within_ one system matters more than universal standardization across systems. If taken, the mapping-direction rule and the collapse points above still apply unchanged.
references/proto-governance-reference.md›
# Proto governance reference

Detail behind SKILL.md steps 2-3. Sources: google.aip.dev (AIP-121, 127, 180, 181, 185, 191, 192, 215), buf.build documentation, and the buf breaking/lint rule sets.

## AIP file and package rules (step 2 detail)

**Package ↔ directory mirror (AIP-191, AIP-215):**

| Rule                          | Correct                                           | Violation                                                |
| ----------------------------- | ------------------------------------------------- | -------------------------------------------------------- |
| Package ends in major version | `example.library.v1`                              | `example.library`                                        |
| Directory mirrors package     | `example/library/v1/book.proto`                   | `example/library/book_v1.proto`                          |
| No version in filename        | `book.proto` inside `v1/`                         | `v1.proto`, `library_v1beta1.proto`                      |
| Per-language options set      | `option java_package = "com.example.library.v1";` | option omitted, raw proto path leaks into generated code |

The api-linter's `proto-package` check fails a directory with a version segment whose package misses it, and vice versa.

**File content ordering (AIP-191):** file-level order, blank line between blocks:

1. Copyright/license notice.
2. `syntax` statement.
3. `package` statement.
4. Imports (alphabetical).
5. File-level options.
6. Service definitions.

Within a file:

- Standard methods before custom methods.
- Parent resources before child resources.
- Request/response messages follow their methods in the same order.

**Resource orientation (AIP-121, 131, 132, 136):** model the API as named resources in a collection hierarchy, exposed through the standard methods List, Get, Create, Update, Delete. A custom method uses the `:verb` suffix on the transcoded surface (`POST /v1/videos:process`) and is justified only when no standard method fits. Reference another resource by its resource name (AIP-122), never by embedding another API's message type inline - inlining tangles the generated client's dependency graph across API boundaries.

**Version semantics (AIP-185):** major version only - `v1`, never `v1.0` or `v1.4.2`; Google APIs expose no minor/patch segment. `v1` is reserved for the stable channel; `v1beta1`/`v1alpha1` are pre-stable channels. Mirror the version as the first URI path segment of the transcoded surface.

## buf lint rules that operationalize the conventions

The STANDARD lint set turns the AIP style rules into CI checks - cite the rule name in review findings:

| Rule                                                       | Enforces                                                                                                                                                                      |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PACKAGE_VERSION_SUFFIX`                                   | last package component matches `v\d+`, `v\d+beta\d*`, etc.                                                                                                                    |
| `SERVICE_SUFFIX`                                           | service names end in `Service`                                                                                                                                                |
| `RPC_REQUEST_STANDARD_NAME` / `RPC_RESPONSE_STANDARD_NAME` | each RPC has `<Method>Request` / `<Method>Response` wrappers                                                                                                                  |
| `RPC_REQUEST_RESPONSE_UNIQUE`                              | no message shared between RPCs - "sharing a Protobuf message between RPCs means every change to that message ripples across every RPC that references it" (buf's own wording) |
| `ENUM_ZERO_VALUE_SUFFIX`                                   | every enum zero value ends in `_UNSPECIFIED`                                                                                                                                  |
| `UNARY_RPC` (opt-in category)                              | forbids streaming RPCs - enable once the surface commits to unary-only (SKILL.md step 4)                                                                                      |

## buf breaking mechanics (step 3 detail)

`buf breaking` diffs the current schema against a reference (Git branch/tag, BSR module, tarball, or Buf image) and fails on incompatible changes. Buf ships over 40 lint rules and 50 breaking-change rules in opinionated default sets; CI wiring is `buf breaking --against <ref>` per PR via the official `bufbuild/buf-action`.

**Four rule categories, strictest first** (a stricter category implies every looser one):

1. **FILE** - generated-code breakage per file; the default, and the right choice for a public surface. Matters for languages with file-specific generated imports (C++, Python).
2. **PACKAGE** - generated-code breakage per package; forgives moving a definition between files within one package.
3. **WIRE_JSON** - binary wire format or JSON encoding breakage.
4. **WIRE** - binary wire format only.

**AIP-180 traps the tooling catches that reviewers miss:**

- An existing component must not be removed within a major version.
- "Renaming a component is semantically equivalent to remove and add" - a rename is always breaking, never a safe refactor.
- Moving a field into or out of a `oneof` breaks generated Go stubs specifically.
- Moving a message to a different file breaks generated imports - the file path is part of the generated-code contract, which is why teams reorganizing protos "for readability" break consumers.

**The blind spot, restated precisely:** breaking-change detection does not work on changes to custom options like `google.api.http`. The transcoding annotations defining the public HTTP mapping are not compatibility-checked at all - the dedicated annotation check in SKILL.md step 3.3 is the only gate covering them.

**BSR review flow:** with instance-wide breaking-change enforcement on, a non-compliant push enters a review queue where repository owners approve or reject before it lands - a paper trail of schema evolution, plus notifications and a safe revert path. Direct Protobuf analogue of Confluent Schema Registry's compatibility modes for Avro/Kafka. Non-buf alternative: Salesforce's `proto-backwards-compat-maven-plugin`, checking against a committed `proto.lock` file.

## Version evolution and deprecation

**Two legitimate evolution strategies (AIP-185):** channel-based (long-lived `v1beta1`/`v1` channels updated in place) and release-based (increment `v1beta1` → `v1beta2` for an incompatible beta-only change). Constraints either way: a new major version must not depend on a previous major version of the same API, and stable resources keep identical resource names across majors so a v2 client can address a resource a v1 client created.

**Deprecation mechanics (AIP-181, AIP-192):** the `deprecated = true` option is available on messages, enums, enum values, and services (since protobuf 2.6.0), not just fields - Java emits `@Deprecated`, C++ clang-tidy warns on use. AIP-192 additionally requires a deprecation comment on every deprecated component stating why and what to use instead, so generated docs explain the replacement rather than just flagging staleness.

**Channel-differentiated sunset expectations:**

- Alpha may be removed without notice.
- Beta may be removed after a deprecation period with 180 days as the recommended floor.
- An in-place breaking change to a _stable_ component is "an extreme course of action" reserved for security or regulatory necessity, never routine.

Concrete notice windows and sunset communication for your own program are `samber/developer-platform-skills@api-versioning-policy`'s territory; these channel semantics are what that policy plugs into for gRPC.
references/public-grpc-case-studies.md›
# Public gRPC case studies and transcoding mechanics

Detail behind SKILL.md steps 1, 4, and 6. Sources: Google Cloud engineering blog ("Understanding gRPC, OpenAPI and REST"), Google Apigee blog ("From gRPC to RESTful APIs", 2024), Square engineering (2015), Dropbox engineering (Nginx→Envoy migration), Cloudflare blog and product docs, Temporal.io documentation, Buf announcements ("Connect: A better gRPC"; CNCF acceptance), Microsoft gRPC-vs-HTTP comparison, Google Cloud and AWS load-balancer documentation. Each entry keeps its precision caveat - do not cite beyond what the caveat allows.

## Case studies, with precision caveats

**Google Cloud - the archetype of gRPC-plus-transcoding.** External Google Cloud APIs are dual-published: a REST/JSON facade plus native gRPC, with SDKs shipping actual gRPC stubs where latency matters (Bigtable, Spanner, Pub/Sub, Firestore) and REST for the rest. Two distinct quotable arguments from Google itself:

- Reach (Apigee, 2024 - use when deciding _whether_ to transcode): "not all developers are equipped or experienced enough to consume gRPC services. API providers would therefore risk missing out on a potential audience by limiting themselves to exposing gRPC services only."
- Control loss (Google Cloud engineering blog - use when weighing the operational cost of gRPC at the edge): adopting gRPC in place of OpenAPI/REST gives "much more limited opportunity to augment or remediate the API's behaviors in proxies, especially API management tools."

**Square - dual-stack, stated with its caveat.** Square's 2015 engineering post recounts evaluating open-sourcing its internal Protobuf-based RPC library, then choosing to get involved with gRPC instead - establishing gRPC/Protobuf as Square's internal contract layer (payments-fraud, accounting, dispute-resolution flows). Separately, Square's public developer API is REST/JSON (HTTPS + JSON bodies, a `Square-Version` header, cursor pagination) - "because partner developers expect that."

**Caveat:** the crisp "gRPC inside, REST outside" framing combines two separate primary sources; no single Square sentence asserts both halves, and secondary claims circulating about Square (e.g. a "35% fraud-decision tail-latency improvement") could not be verified against any Square primary source - never repeat them as company-stated fact. Netflix and Lyft are also _reported_ to run the same split in the same source family, but only Square's product-surface detail was independently verifiable - name them only as "also reported", never with Square's level of detail.

**Dropbox - internal gRPC, public gRPC as a stated aspiration only.** Dropbox runs gRPC internally via an in-house framework named Courier. Its Nginx→Envoy migration writeup notes internal and private-external APIs "gradually migrating from REST to gRPC" and muses about eventually offering "public gRPC APIs… transcoding our existing REST-based APIs right on the Edge" - an aspiration, not a shipped state; the public surface remained REST-fronted as of that post.

**Cloudflare - both practitioner and vendor.** gRPC for internal Kubernetes pod-to-pod communication, while separately shipping gRPC proxying as a customer-facing product (WAF understanding of gRPC semantics, HTTP/2 to origin) so customers can run their own public gRPC APIs behind Cloudflare. Cite it as the named answer to "what actually terminates TLS and enforces policy in front of our raw gRPC edge".

**Temporal.io - the genuine public-gRPC counter-example.** Temporal's Cloud Ops API is "an open source, public HTTP API and gRPC API", with proto files published on GitHub and the Buf Schema Registry for client-library generation; the server frontend API is gRPC wrapped directly by official SDKs. It works because the consumer base is control-plane/infrastructure engineers already fluent in generated-SDK tooling - exactly SKILL.md's SDK-mediated trigger. Note that even Temporal dual-publishes an HTTP API alongside the gRPC one.

**Bottom line by audience** (near-verbatim from the 2026 source consensus):

- Broad third-party developer platforms (payments, SaaS partner APIs, anything browser-adjacent): REST/JSON, optionally Protobuf-defined and transcoded, remains the right default in 2026.
- Infrastructure/control-plane APIs whose users are engineers wiring up SDKs: native gRPC or Connect-RPC is defensible and increasingly the more attractive choice.

## Transcoding-option mechanics table

The menu SKILL.md step 6 ranks; this table carries the per-option mechanics. gRPC-Web appears here for completeness - Connect serves the gRPC-Web protocol itself, which is why it does not need its own rung in the menu.

| Option                     | What it is                                                        | Streaming                                                    | Browser path                      | Proxy needed?                                  | Operating burden                                                                                                                                |
| -------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Connect-RPC                | Buf's slim framework; one server speaks Connect + gRPC + gRPC-Web | Full, incl. bidi over HTTP/2; server-streaming over HTTP/1.1 | Native via connect-web - no proxy | No                                             | Lowest: "no custom HTTP implementation, no new name resolution or load balancing… any package that works with net/http also works with Connect" |
| Envoy gRPC-JSON transcoder | Envoy HTTP filter driven by proto descriptors; no codegen         | Server-streaming (array semantics)                           | Via REST/JSON (any client)        | Yes - Envoy, often already the mesh data plane | Config-heavy, no generated code; the most common approach in Kubernetes environments; descriptor set must be kept in sync                       |
| grpc-gateway               | Codegen'd Go reverse proxy from `google.api.http` annotations     | Server-streaming via chunked/SSE; limited                    | Via REST/JSON                     | Runs as a separate proxy/handler               | Regenerate on every proto change; double JSON↔protobuf parse per request; default error handler drops `details`                                 |
| gRPC-Web                   | Official browser answer; needs a translating proxy                | Server-streaming only; no client/bidi                        | Yes (generated JS/TS client)      | Yes (Envoy or equivalent)                      | Frontend regenerates stubs; historically poor TypeScript ergonomics                                                                             |
| Raw public gRPC            | Native gRPC exposed at the edge                                   | Full                                                         | None - browsers cannot connect    | gRPC-aware L7 edge (LB, WAF, TLS)              | Consumer-side Protobuf tooling mandatory; full exposure audit; API-management tooling coverage limited                                          |

**Performance honesty across the table:**

- grpc-gateway pays a double parse (JSON→protobuf→JSON) on every request.
- Envoy's transcoder similarly adds JSON parse/serialize overhead.
- gRPC-Web sends binary end-to-end and avoids gateway-side parsing.

No JSON-transcoding path is free - steer performance-critical consumers to native gRPC or Connect's binary mode.

**Connect-RPC adoption facts, dated:** Connect-RPC was accepted to the CNCF Sandbox on April 13, 2024 (Buf's announcement followed on June 4, 2024). Named production adopters per Buf: CrowdStrike, PlanetScale, RedPanda, Chick-fil-A, Bluesky, Dropbox. It is wire-compatible with gRPC, validated against Google's own interoperability test suite.

Buf's launch framing of plain grpc-go - "130 thousand lines of hand-written code… nearly a hundred configuration options, and bespoke name resolution and load balancing" - against Connect's premise that "conceptually, the gRPC protocol is HTTP with Protobuf-encoded bodies". Import-path footnote for any example code: the Go package moved from `github.com/bufbuild/connect-go` to `connectrpc.com/connect` at v1.0 (2023); the packages are identical apart from the import path.

## Load-balancer keepalive and idle-timeout numbers

The streaming/load-balancing rule in SKILL.md step 4, with the vendor numbers that make it actionable rather than "configure keepalive appropriately":

| Balancer                             | Idle/keepalive timeout                                                                                       |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| Google Cloud external Application LB | 600 s backend keepalive - Google states the backend value "is fixed" (client-side default 610 s, adjustable) |
| AWS Classic / Application LB         | 60 s idle timeout (default)                                                                                  |
| AWS Network LB (TCP flows)           | 350 s (default)                                                                                              |

**The actionable rule:** set the gRPC client's HTTP/2 keepalive PING interval _below_ whichever load balancer idle timeout applies on the path, or the balancer silently drops the connection before the client-side keepalive ever fires. These are vendor-documented defaults as of 2026 - reverify against current vendor documentation before quoting in a design doc, since defaults change.
SKILL.md›
---
name: public-grpc-api-design
description: Design a public gRPC surface for external developers - the when-gRPC-at-all gate (most platforms keep gRPC internal and publish REST or transcoded JSON), AIP proto package and versioning conventions, buf breaking-change gates and their google.api.http blind spot, unary-by-default streaming decisions, the google.rpc.Status error model with its HTTP-mapping traps, transcoding and gateway architecture (Connect-RPC, grpc-gateway, Envoy), and external auth and TLS. Use whenever the user mentions gRPC, protobuf or .proto files, buf, Connect-RPC, grpc-gateway, or gRPC-JSON transcoding at a public edge - even if they never say "gRPC API design". Design layer only, not language-specific server implementation.
license: MIT
metadata:
  author: Samuel Berthe
  version: "1.0.1"
---

# Public gRPC API Design

You are a public gRPC surface designer. Decide whether gRPC belongs at a platform's public edge at all, and - when it does - design the proto governance, streaming posture, error model, transcoding architecture, and external auth that let developers you have never met consume it safely.

The framing question is never "how do we design the best public gRPC API" but "does this workload belong on public gRPC, and through what edge". Sibling `samber/developer-platform-skills@api-integration-surface-strategy` partially owns that umbrella decision; re-ask it here anyway, because teams arrive having assumed "public gRPC" without validating it.

**Memory (advised):** When memory lives in a file, consider using `developer-platform-context.md`; if a different memory system is in use, rely on that instead. The file is an advisory reference, not a mandatory requirement. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.

## Clarifying questions

Ask these before designing anything; each answer changes a later step. Batch them - this is a tactical design task, not a strategy interview.

1. Does gRPC already run internally, or is this greenfield? If internal: the service definition likely contains admin/debug/monitoring RPCs never designed for external eyes - that is what makes step 7's closing exposure audit mandatory rather than optional.
2. Who are the external consumers: engineers wiring up your generated SDKs (control-plane/infrastructure audience), raw-proto integrators generating their own stubs, or browser-based clients? (drives the step 1 gate and the step 6 menu)
3. Do browser clients need to call this surface directly? Browsers structurally cannot speak gRPC's HTTP/2 framing - a yes deletes raw public gRPC from step 6's menu.
4. Does a public REST surface already exist, and must the gRPC surface stay consistent with it (same resources, same versioning story)?
5. Is any workload genuinely streaming-shaped (push, tail, telemetry ingestion), or is "streaming because gRPC has it" the only driver? (drives step 4)
6. Re-rank inputs: by when must the surface ship, is this a one-off exposure or a contract several products will share for years, and what is the effort ceiling (team hours, new runtimes you may operate, appetite for consumer-visible migration)? (re-ranks step 6's menu)

If your harness has persistent memory, store the design's settled decisions (the gate's verdict and its trigger, proto package and version scheme, streaming posture, chosen transcoding rung with its promotion condition, auth flow per caller type) so later runs - a new RPC, a proto review, the docs pass - start from the design instead of re-deriving it.

## Consumer types

The split that changes the design is how the consumer meets the contract:

- **SDK-mediated consumers** - engineers using your published, generated client libraries (Temporal's model: protos on GitHub and a schema registry, SDKs wrapping gRPC directly). The one audience for which raw public gRPC is a proven fit; they absorb stub regeneration as normal tooling.
- **Raw-proto integrators** - third parties generating their own stubs from your protos. They inherit every breaking change directly, need `protoc`/buf fluency you cannot assume, and debug binary payloads with tooling most do not have installed. Proto governance (steps 2-3) is designed for them.
- **Browser and generic HTTP clients** - cannot speak gRPC's raw HTTP/2 framing at all, so a proxy layer is mandatory the moment one of them is a consumer. They reach the surface only through transcoding, gRPC-Web, or Connect - step 6 exists for them.

Design for the least-equipped consumer type actually present. A surface only the SDK-mediated audience can use silently excludes the other two - that is a business-reach decision, not a technical default; make it consciously in step 1.

## Workflow

1. Run the when-gRPC gate - decide what the public edge actually speaks.
2. Set proto package and versioning conventions.
3. Install breaking-change gates - buf plus the annotation blind-spot check.
4. Decide the streaming posture, unary by default.
5. Design the error model on `google.rpc.Status`.
6. Choose the transcoding/gateway architecture (ranked menu).
7. Design external auth and TLS, and audit the surface for exposure.

Each step has a section below, in order.

## 1. The when-gRPC gate

Practitioner consensus favors dual-stack, not either/or: gRPC for internal service-to-service traffic where you control both ends, REST or transcoded JSON at the edge where you do not. REST still carries the large majority of public APIs per tracked estimates (roughly 83% in the most recent count). See [references/public-grpc-case-studies.md](references/public-grpc-case-studies.md) for the sourced detail and the precision caveats on each:

- Google dual-publishes: a REST facade plus native gRPC stubs where latency matters.
- Square, Dropbox, and Cloudflare run gRPC internally while their public developer surfaces stay REST-fronted.

Why REST wins the public edge by default - concrete mechanisms, not preference:

- Browsers cannot speak gRPC's raw HTTP/2 framing; a proxy layer is mandatory the moment any consumer is browser-based.
- Protobuf is binary: nothing to read in a network tab, and traffic inspection needs `grpcurl`-class tooling third parties rarely have.
- Every external caller pays the `protoc`/stub-regeneration cost that an internal fleet amortizes.
- Google Cloud's own engineering blog names the operational cost: gRPC in place of REST/OpenAPI leaves "much more limited opportunity to augment or remediate the API's behaviors in proxies, especially API management tools".
- gRPC services built internal-first carry admin and debug RPCs never audited for public discovery.

The genuine triggers that justify public gRPC anyway - all three worth checking, any one may suffice:

- Consumers are confirmed SDK-mediated or proto-fluent (question 2). Temporal.io is the clean named precedent: its Cloud Ops API is "an open source, public HTTP API and gRPC API", viable precisely because its audience is control-plane engineers already fluent in generated-SDK tooling - and even Temporal ships an HTTP API alongside.
- The workload is genuinely streaming-shaped, where REST would need polling or a separate WebSocket layer (validate against step 4's costs first).
- Performance is the measured bottleneck and consumers can absorb the client-tooling cost. Binary Protobuf is commonly cited at 3-11x smaller and 8-12x faster to serialize than JSON - vendor/blog benchmarks, directional only, never cite as exact.

When the gate says "gRPC inside, JSON outside", Google's Apigee team supplies the reach argument to put in the design doc: "not all developers are equipped or experienced enough to consume gRPC services. API providers would therefore risk missing out on a potential audience by limiting themselves to exposing gRPC services only." The gate's output feeds step 6: it decides which rungs of the transcoding menu are even in play.

## 2. Proto package and versioning conventions

A public proto package becomes part of every generated client's import path - convention mistakes here are breaking changes once external consumers depend on them. Google's AIP corpus is the closest thing gRPC has to a canonical public style guide; adopt it rather than inventing house rules:

- One proto package per API, ending in a major-version component (`example.library.v1`) - AIP-215.
- Directory structure mirrors the package exactly (`example/library/v1/`); never put the version in a filename (`v1.proto` produces bizarre generated imports) - AIP-191.
- Major version only - `v1`, never `v1.0` or SemVer minors - encoded in the package and mirrored as the first URI segment of the transcoded surface. `v1` means stable; `v1beta`/`v1alpha` are the pre-stable channels, and claiming beta functionality under `v1` is the review finding to flag - AIP-185.
- Model the surface resource-oriented (AIP-121): named resources, standard List/Get/Create/Update/Delete methods, custom methods only where a standard verb genuinely fails, with the `:verb` suffix.
- One dedicated `<Method>Request`/`<Method>Response` wrapper message per RPC, never a bare scalar or a shared message - a shared message ripples every field change across every RPC referencing it, and a wrapper can always gain optional fields without breaking wire compatibility.
- Field discipline as absolutes for a public surface:
  - Never reuse or renumber a tag.
  - `reserved` every removed field.
  - Every enum zero value is `_UNSPECIFIED`.
  - Prefer well-known types (`google.protobuf.Timestamp`, `FieldMask`, `google.rpc.*`) over hand-rolled equivalents.
- Set per-language package options (`java_package` and friends) so generated code stays idiomatic instead of leaking the raw proto path.

Boundary: sibling `samber/developer-platform-skills@api-versioning-policy` owns notice windows, sunset communication, and governance generics across paradigms. This skill owns how a version is encoded and enforced gRPC-natively - the package-suffix rule above and step 3's gates. A new major version must not depend on the previous one, and stable resources keep identical resource names across majors so a v2 client can address what a v1 client created.

See [references/proto-governance-reference.md](references/proto-governance-reference.md) for file-content ordering, the buf lint rule names that operationalize each convention, channel-based vs release-based version evolution, and deprecation mechanics.

## 3. Breaking-change gates

Protobuf's encoding only guarantees wire compatibility, not client-code compatibility (AIP-180's distinction) - so the gate must be tooling, not reviewer vigilance:

1. Run `buf breaking --against <main>` on every PR as a hard CI gate, plus `buf lint` with the STANDARD set. Default to the strictest rule category (FILE) for a public surface; it catches generated-import breakage that package-level checks forgive.
2. Know the traps the tooling catches that human reviewers miss:
   - A rename is semantically remove-and-add (always breaking).
   - Moving a field into or out of a `oneof` breaks generated Go stubs.
   - Moving a message to a different file breaks generated imports - the file path is part of the contract.
3. **The blind spot buf leaves open:** `buf breaking` does not inspect custom options like `google.api.http`, so a team can break its REST-facing URL/verb mapping while every proto gate stays green. Add a dedicated check: snapshot the annotation-derived HTTP surface (paths, verbs, body mappings) and diff it in CI, or contract-test the transcoded routes - a named failure mode, not a theoretical one.
4. Test both directions across a version boundary: old client against new server and new client against old server - "does the schema parse" is not "does the cross-version RPC succeed".
5. Promote to a Buf Schema Registry review flow once public surface area justifies a human backstop: a breaking push enters an approve/reject queue with a paper trail, the proto-native equivalent of a review-board gate. Teams not on buf can use Salesforce's `proto-backwards-compat-maven-plugin` against a checked-in `proto.lock`.

Deprecation is proto-native too: `deprecated = true` (available on messages, enums, enum values, and services) plus an AIP-192 comment saying why and what replaces it. Channel expectations differ sharply:

- Alpha may vanish without notice.
- Beta may be removed after a recommended 180 days deprecated.
- An in-place breaking change to a stable component is "an extreme course of action" reserved for security or regulatory necessity.

The notice windows themselves are `samber/developer-platform-skills@api-versioning-policy`'s territory.

## 4. Streaming posture: unary by default

gRPC's four call types are not equally priced on a public surface. Expose unary RPCs by default; every streaming method is a deliberate, recorded decision with its costs accepted - never a free feature.

- **Server-streaming** is the one streaming mode to reach for first when a push/tail use case is real: it maps cleanly onto SSE through a gateway and onto Connect's streaming encoding. Notifications, log tailing, progressive results.
- **Client- and bidirectional streaming** carry named costs for external consumers: Microsoft's own guidance warns that "replacing unary calls with bidirectional streaming for performance reasons is an advanced technique and is not appropriate in many situations".
  - A stream fails mid-flight after partial data, so every external SDK consumer needs restart/resume bookkeeping a unary retry never requires.
  - Retries stop being transparently client-library-managed.
  - Browsers cannot do client or bidi streaming over HTTP/1.1 at all - a protocol wall, not a tooling gap (Connect over HTTP/2 is the exception).
- Load balancing degrades under streaming: HTTP/2 multiplexing pins all of a client's streams to one backend for the connection's life under an L4 balancer. A public gRPC edge needs L7, HTTP/2-aware balancing, and client keepalive intervals set below the load balancer's idle timeout - concrete vendor timeout numbers in [references/public-grpc-case-studies.md](references/public-grpc-case-studies.md).
- AIP-127 requires an HTTP mapping for every RPC _except_ bidirectional streaming, which HTTP/1.1 cannot carry - so every bidi method also needs a documented non-streaming alternative, or it is invisible to the transcoded surface.

Enforce the decision, don't just document it: when the surface commits to unary-only, enable buf's `UNARY_RPC` lint category so a future PR cannot silently introduce a streaming method the gateway, SDKs, or transport cannot carry. Sibling `samber/developer-platform-skills@api-idempotency-retry` owns retry mechanics, deadline propagation, and backoff generics; this step only decides which call shapes the public contract offers.

## 5. Error model: google.rpc.Status mechanics

Sibling `samber/developer-platform-skills@api-error-design` owns taxonomy philosophy - code catalogs, message writing, retryability signaling. This step owns the gRPC-native mechanics and the two traps that bite public surfaces:

1. Use `google.rpc.Status`:
   - `code`: one of the 17 canonical codes.
   - `message`: developer-facing English, never localized user text.
   - `details`: typed extras from `google/rpc/error_details.proto` (`ErrorInfo` first, `BadRequest` for field violations, `RetryInfo` paired with `RESOURCE_EXHAUSTED`).
2. Return the most specific applicable code (`OUT_OF_RANGE` over `FAILED_PRECONDITION` when both apply). Defaulting everything to `UNKNOWN`/`INTERNAL` as a defensive habit is the named anti-pattern: it flattens every status dashboard into one bucket.
3. Sanitize outbound errors structurally, not just prose: `DebugInfo` is documented as internal-only - filter it from `details` before the response leaves; sanitizing `message` alone is insufficient.
4. **Trap one - the mapping table's direction.** The gRPC project's own documentation is explicit: "servers must not use this table to determine an HTTP status code to use... the mappings are neither symmetric nor 1-to-1." The canonical gRPC↔HTTP table exists for clients interpreting responses that arrived without a `grpc-status`; a gateway translating errors outward needs its own deliberate mapping, confirmed against the transcoding implementation actually in use.
5. **Trap two - HTTP collapses distinctions.** In the common gateway mapping, three codes (`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, `OUT_OF_RANGE`) all land on 400, and `CANCELLED` maps to the non-standard 499. Document for consumers that the HTTP status line is lossy and the structured envelope in the body is the source of truth.
6. Verify the envelope survives your gateway:
   - Envoy needs `convert_grpc_status: true` _and_ every detail type present in the proto descriptor (missing types drop silently).
   - Connect serializes errors as JSON natively.
   - grpc-gateway's default handler flattens the message and drops `details` entirely - a custom error handler is mandatory there if structured detail matters.

Only `UNAVAILABLE` is generally safe to auto-retry without extra signal (AIP-194); everything else needs an explicit `RetryInfo` or caller judgment. The full 17-code table, gateway mapping, details catalog, and per-gateway survival mechanics are in [references/grpc-error-http-mapping.md](references/grpc-error-http-mapping.md).

## 6. Transcoding and gateway architecture

The design decision this step owns is what edge serves non-gRPC consumers and how the mapping is expressed - AIP-127's `google.api.http` annotation is the convention every implementation converged on ("APIs must provide HTTP definitions for each RPC except bi-directional streaming RPCs"). The menu, ranked:

- effort: `raw public gRPC edge > grpc-gateway == Envoy transcoder > Connect-RPC`
- value: `Connect-RPC > Envoy transcoder > grpc-gateway > raw public gRPC edge`
- efficiency: `Connect-RPC > Envoy transcoder > grpc-gateway > raw public gRPC edge`

Effort here is what the team operates and regenerates; value is audience reached per proto contract, with error and streaming fidelity preserved. The `grpc-gateway == Envoy` effort tie is genuine: regenerating a Go proxy on every proto change and keeping a descriptor set plus filter config in sync are different work at the same order of magnitude - and "you already run Envoy" is exactly the condition that breaks it.

- **Default rung: Connect-RPC.** One server speaks Connect, gRPC, and gRPC-Web:
  - No proxy required.
  - Browsers served natively.
  - Full streaming over HTTP/2.
  - Errors are JSON out of the box.
  - Wire-compatible with gRPC (validated against Google's interop suite).

  Entered CNCF Sandbox in April 2024, already in production at CrowdStrike, PlanetScale, RedPanda, Chick-fil-A, Bluesky, and Dropbox. Buf's framing when launching it: "conceptually, the gRPC protocol is HTTP with Protobuf-encoded bodies" - Connect collapses the pick-a-gateway decision for most new surfaces.

- **Promote Envoy's gRPC-JSON transcoder** when Envoy is already your mesh data plane and a second runtime is the real cost - descriptor-driven, no generated glue code, the common approach in Kubernetes environments. Not because Envoy is inferior; "you already operate this" is a legitimate reason.
- **Keep grpc-gateway** where an existing Go investment already carries it with a custom error handler; it pays a codegen step plus a double JSON↔protobuf parse on every request, and its default error handling is the weakest of the three (step 5.6).
- **Raw public gRPC edge is the starved option:** highest fidelity (binary end-to-end, full streaming, zero transcoding loss) and highest effort (a gRPC-aware edge with L7 balancing, WAF, and TLS termination - Cloudflare ships this as a product - plus a full internal-RPC exposure audit and every consumer bearing Protobuf tooling). It loses every efficiency round; the one condition that promotes it anyway is step 1's Temporal trigger, a confirmed SDK-mediated, control-plane audience. Even then, dual-publish an HTTP surface alongside, as Temporal does.
- **Ruled out, not demoted:** raw public gRPC as the _only_ surface when question 3 said browser clients call directly. Browsers structurally cannot speak gRPC framing; keeping the option parked at the bottom of the menu silently reappears as scope. Delete it and choose among the three above.

This ranking is a default, not a law. Re-rank against question 6 and what you know about the team:

- An Envoy-fluent platform team gets the transcoder nearly free.
- A hard ship date promotes whatever the team already operates.
- A contract several products will share for years weighs error-fidelity (value axis) over this quarter's effort.

Performance-critical consumers should be steered to native gRPC or Connect's binary mode regardless of rung - every JSON-transcoding path pays a real parse cost.

Mechanics table (streaming support, browser path, proxy requirements per option) and sourced quotes: [references/public-grpc-case-studies.md](references/public-grpc-case-studies.md).

## 7. External auth, TLS, and the exposure audit

The internal-auth instinct is the wrong one: mTLS authenticates _workloads_ and scales fine inside a mesh, but issuing and revoking client certificates for developers you have never met is operationally impractical. The public edge authenticates apps and users instead - the mesh does not disappear; it starts one hop further in. Order of operations:

1. TLS first, non-negotiable: credentials travel as gRPC metadata, plaintext without transport security; most implementations refuse to send credentials over an unencrypted channel at all.
2. OAuth2 flow by caller type: Client Credentials for machine/service callers, Authorization Code + PKCE where a human authenticates interactively. Token as `authorization: Bearer <token>` metadata, validated in a server interceptor.
3. Token mechanics:
   - JWT as token format, strongly signed (RS256), with expiry claims.
   - Token introspection against the authorization server, cached so it does not tax every call.
   - An established identity provider over a hand-rolled issuer once external caller count justifies it.
4. Enforce scopes at the method level - not "is this caller authenticated" but "is this token scoped for this RPC".
5. API keys complement, never replace, authentication (Google's own Endpoints distinction): a key identifies a project/consumer for quota attribution and coarse gating, not an authenticated identity.
6. Rate limiting signals over-quota with `RESOURCE_EXHAUSTED` (429 once transcoded) paired with a `RetryInfo` detail so callers back off instead of hammering the same limit.
7. The exposure audit, mandatory for any internal-first service: disable server reflection in production (it is an API-discovery tool for attackers once the port is public), then enumerate the full method surface for admin, debug, and monitoring RPCs that were never designed for external visibility. TLS and auth switched on do not close this hole.

The reference architecture to diagram: TLS termination + token validation + per-method authZ + rate limiting at the public gateway (Apigee, Cloudflare, Envoy are named vendor instances), fronting the internal mesh where mTLS resumes.

## Failure modes

Anti-pattern checklist - each is a direct audit finding:

- The server or gateway derives HTTP status codes from the client-side gRPC↔HTTP mapping table the gRPC project explicitly forbids servers to use.
- `buf breaking` green while a `google.api.http` annotation change broke the public REST mapping - the blind spot with no dedicated check.
- grpc-gateway in production with the default error handler, silently dropping every `details` payload.
- Server reflection enabled on a publicly reachable service.
- Two RPCs sharing a request or response message; a bare scalar as a request type.
- A version number in a proto filename, or a directory that does not mirror the package.
- A streaming method added to a browser-facing surface with no non-streaming alternative and no recorded tradeoff decision.
- Everything failing as `UNKNOWN` or `INTERNAL`; `DebugInfo` reaching external callers.
- An L4 balancer in front of streaming RPCs, pinning every client's streams to one backend.
- Client keepalive interval longer than the load balancer's idle timeout, so the balancer drops connections before keepalive fires.
- Beta functionality shipped inside a `v1` package.

## Measurement

Pass gates, self-set from the sourced rules above (no industry-standard thresholds exist for this surface; each gate is binary and auditable):

- Breaking-change gate: zero breaking proto changes reach the default branch outside a new major version - `buf breaking` (FILE category) green in CI on 100% of merges.
- Annotation coverage: 100% of RPCs carrying `google.api.http` annotations covered by a dedicated mapping diff or contract test, because buf does not check them.
- Streaming discipline: `UNARY_RPC` lint enabled on a unary-only surface, or every streaming method paired with a recorded accepted-tradeoff decision and (for bidi) a non-streaming alternative.
- Exposure: reflection disabled in production and the internal-RPC audit completed before launch - both binary.

Iterate the design until all four pass. Afterwards, watch as trends (not thresholds): the share of support tickets caused by transcoded-error confusion, and cross-version compatibility test results per release.

## Invocation examples

- "Our platform team wants to expose our internal gRPC services to partners - design the public surface and tell us whether that's even the right move."
- "Design proto package and versioning conventions for a public API we'll publish protos for, with CI gates against breaking changes."
- "We're putting Connect-RPC in front of our gRPC backend for browser clients - review the error model and the transcoding architecture."
- "Audit this gRPC service before we open it to external developers."

## References

- [references/proto-governance-reference.md](references/proto-governance-reference.md) - AIP file/package rules in detail, buf lint rule names, AIP-180 compatibility traps, channel vs release versioning, deprecation mechanics.
- [references/grpc-error-http-mapping.md](references/grpc-error-http-mapping.md) - the 17 canonical codes, the full gateway HTTP mapping with its collapse points, the error-details catalog, and per-gateway envelope-survival mechanics.
- [references/public-grpc-case-studies.md](references/public-grpc-case-studies.md) - Google, Square, Dropbox, Cloudflare, Temporal case studies with precision caveats; the transcoding-option mechanics table; Connect-RPC adoption facts; load-balancer keepalive numbers.

See also, same collection:

- `samber/developer-platform-skills@api-integration-surface-strategy` - the umbrella decision of which surfaces to offer at all; this skill re-validates its gRPC branch and executes it.
- `samber/developer-platform-skills@public-api-design-review` - whole-surface consistency review for the REST/JSON side; it hands gRPC-specific design here.
- `samber/developer-platform-skills@public-graphql-api-design` - the structural analogue for a public GraphQL surface; same gate-first shape, different paradigm.
- `samber/developer-platform-skills@api-versioning-policy` - notice windows, sunset communication, and governance generics; this skill owns only the gRPC-native version encoding and gates.
- `samber/developer-platform-skills@api-error-design` - error taxonomy philosophy, message writing, retryability signaling; this skill owns only `google.rpc.Status` mechanics and the HTTP-mapping traps.
- `samber/developer-platform-skills@api-idempotency-retry` - client retry mechanics, deadline propagation, and backoff generics consumed by step 4's streaming decision.