Friend: 'BFF' API for Client Apps
Overview
We call our internal API Friend, because it is implements something somewhat similar to the Backend for Frontend (BFF) pattern, where RPCs are coupled to their intended frontend UI purpose.
Friend is a publicly accesible RPC (Connect) API located at rpc.pivot.app,
intended to be the single HTTP surface for first-party client applications (i.e.
our apps) to interact with the backend via queries and mutations.
Friend is a Go web service that uses connect-go, which only exposes the
Connect protocol, not gRPC or gRPC-web, because Friend is consumed by clients
running React (in the browser, via React Native, and via our Tauri desktop app).
Pilot provides lightweight websocket subscriptions via Dealer instances, which creates a simple and separated approach for server-to-client messaging and client-to-client pub/sub.
Rest is designed for public access by customers/third-party developers. Friend is not; it's just for our own 'internal' use between our backend and client applications.
A key principle we apply in Friend is delegated control. Handler should be short and simply when there is an underlying service that provides the data or handles the mutation.
Why Connect RPC?
Using Protobuf for types and connect-es for consuming APIs from React and
React Native is unconventional, but here are the key reasons we do this:
- GraphQL is complicated and can be hard to tune. We are running effectively a single app (query and mutation logic is shared between the web app (Expo), desktop, and React Native) and can build entity and screen specific RPCs faster and more performantly than we can a generic all-encompassing GraphQL schema that handles all past, current, and future query permutations.
- TypeScript has tRPC but that requires TypeScript on both sides. With Connect we have cross platform support for clients/servers in Go, TypeScript, and Swift/Kotlin, giving us a simple RPC experience with the cross-platform typesafety that GraphQL is praised for.
- Connect integrates with React Query (Tanstack Query) for a simple client story that makes offline friendly queries and mutations easy with its caching and persistence story. With GraphQL client library caching, our cache would be tightly integrated with the GraphQL layer.
As Buf says in their Connect docs (opens in a new tab):
Unlike a hand-written REST service, you didn't need to design a URL hierarchy, hand-write request and response structs, manage your own marshaling, or parse typed values out of query parameters. More importantly, your users got an idiomatic, type-safe client without any extra work on your part.
For more details on data fetching, see this article.
API
Friend's Role vs. Underlying Services
Friend is responsible for rate limiting each client (which it delegates to
Cloudflare's WAF), authentication via JWT (shared access token secret with
Visa), and basic authorization at the RPC level (e.g., which RPCs require
authentication). Detailed validation and userId-based authorization should be
delegated to the underlying service. That is to say, wherever possible Friend
should stay out of each domain as much as possible.
Friend RPCs
We divide Friend's RPCs into queries and mutations, though that is not meaningful in Protobuf technically, just a way we name RPCs (get, create, update, delete, etc.) for clarity. Those queries and mutations are then grouped together into services.
There are three types of services:
- Entity – In general, there are
Getqueries for each entity. Friend simply fronts the underlying internal service, such as Facebox, Blockhead, and Messenger which store this data and handle authZ. This is similar for mutations; many entities haveCreateandUpdatemutations. - Base – Some queries are fundamental to loading the app itself and not directly entity-based nor specifically support a piece of the UI.
- UI – While we try to avoid writing UI-specific queries for all screens and components to optimize for granuler data fetching and caching, sometimes it is necessary.
The full Friend API list (opens in a new tab) includes each method/RPC/query/mutation grouped by the Protobuf service it exists within.
NATS
Publication
N/A
Consumption
N/A
Databases
N/A - Friend itself does not persist data. It uses mutation RPCs that call out to other services and is entirely stateless itself.
Temporal Workflows
N/A
Deployment
- Friend runs as an ECS service, via a replica set that scales up and down with demand.
- ECS registers each Friend instance with the AWS Application Load Balancer (ALB).
Observability
The Friend service is instrumented at each RPC handler to push traces to Axiom via OpenTelemetry, which gives us visibility into what RPCs are being used as well as any services bottlenecking Friend.
Security
- Rate limiting at the firewall level via Cloudflare.
- AWS ALB only accepts traffic from Cloudflare.
- Security group on Friend ensures that Friend is only accessible via AWS ALB.
- Validation of JWTs issued by Visa service via shared secret between Friend and Visa.