Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions packages/documentation/content/case-studies/buffer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: "How Buffer Built Its Public GraphQL API on Top of Its Internal Schema"
excerpt:
"Buffer is a social media workspace that lets you plan, publish, and analyze posts and comments
across 10+ social networks. After closing its public REST API back in 2019, Buffer has been
rebuilding its public offering on GraphQL."
category: Social Media
date: 2026-07-22
authors:
- name: Joe Birch
position: Staff Engineer & API Lead
avatar: https://avatars.githubusercontent.com/u/3879281?v=4
---

import { ContactButton } from "@hive/design-system/contact-us";

import { DocsIcon, LargeCallout } from "#components/large-callout";
import { Lede } from "#components/lede";

<Lede>
[Buffer](https://buffer.com/) is a social media workspace that lets you plan,
publish, and analyze posts and comments across 10+ social networks including
LinkedIn, Facebook, Instagram, Bluesky, and several others. The company used
to have a public REST API, but closed it back in 2019, and has since been deep
in migrating everything internally to GraphQL, and the [Public
API](https://buffer.com/api) has been the next step on that journey. After
over 45K API clients and 3.6M posts being created through it, Buffer is
continuing to build on its Public API to position its API as the product.
</Lede>

[![How to Build on Buffer's New API](./buffer_youtube_thumbnail.jpg)](https://www.youtube.com/watch?v=lgB0HMkbx34)

Buffer wasn't building this from scratch, as there was already an established internal GraphQL API powering its own apps. The work was really about getting that existing API ready to safely expose to the public, and a lot of the decisions that followed came from opening up something that had only ever been built with an internal audience in mind, without it becoming a separate thing to maintain.

## Exposing part of a schema without splitting it in two

In the long term Buffer wants the API to be public by default, but the team were not in a position to open everything up on day one. So to start with, they needed to expose a smaller subset of the schema while rolling out API usage and getting a feel for how it was being used in practice. The harder constraint was that they didn't want to maintain separate schemas for public and internal use, as keeping two in sync would have been a constant source of synchronization and rework. This meant that the public API needed to be a view onto the existing schema rather than a copy of it.

Buffer uses a single schema for its API, and while each domain lives in its own GraphQL files internally, these are merged together so that clients can access everything in one place. To decide what gets exposed, the team created their own custom directive, `@public`, which controls what external clients are able to access and also determines what shows up in Buffer's external tooling. So rather than keeping a separate list of what's public somewhere, the schema itself is the source of truth and anything without `@public` simply stays internal. This gives the team flexibility in what they expose, so they can open up a whole type or only certain fields within it.

While Hive offers [schema contracts](https://the-guild.dev/graphql/hive/docs/schema-registry/contracts) for this kind of public and internal split, contracts are currently supported for federated projects only. Buffer runs a single, non-federated schema, so they weren't a fit here. On top of that, Buffer needed more than filtering a schema into subsets, as the same mechanism also had to carry required scopes, which the custom directive handles in one place. It's an area the team would happily revisit if contracts grow to cover non-federated graphs.

## Granting the right access to the right clients

There are two different types of API access Buffer needed to support: personal access, where someone uses the API to interact with their own Buffer account, and third-party integrations that perform actions on behalf of other people's Buffer accounts. Supporting both meant being clear about how access is granted and scoped, so that an integration acting for other people works differently to someone using the API against their own account.

Because of this, `@public` also requires scopes to be defined, which means that when an integration wants to perform an action it can request only the scopes it needs for what it's actually trying to do, keeping access tied closely to intent. Tying scopes to the same directive that decides what's public keeps the two concerns together, so exposing something and defining how it can be accessed happen in the same place rather than drifting apart over time.

## Shipping to the API while features are still moving

Changing something internally is very different from changing something that's been exposed externally, because internally a change is Buffer's to make and they can update everything that depends on it, but once something is public and developers are building on it, it can't be changed as freely, and there isn't the same line of communication with consumers that the team has internally. The risk was that a strict public contract would slow down shipping, or that features would land in the product long before they reached the API.

To handle this, Buffer uses `@experimental`, `@preview` and `@stable` directives to signal how far along each part of the API is, which lets the team expose new operations early and build in public, so developers can start working with something while it's still evolving and Buffer can be clear about how stable it is and what they can rely on. This means functionality can go out through the API alongside features landing in the product, while the team keeps room to iterate without a strict contract locked in from day one. Since opening the API they've already added new operations around features such as Ideas and Insights, with more coming soon.

## Building the developer portal and tooling

A public API needs somewhere for developers to explore it and keep up with how it changes. Buffer looked at existing solutions first, but couldn't find anything customizable that also worked well with GraphQL, so the developer portal was built from scratch.

The most complex part was building Buffer's own documentation and changelog generator. With a schema contract defined, GraphQL opens up a lot of possibilities for automating change management, and the team wanted to lean into that as much as possible, so whenever the schema changes an updated [GraphiQL explorer](https://developers.buffer.com/explorer.html), [documentation](https://developers.buffer.com/reference.html), and [changelog](https://developers.buffer.com/changelog.html) are automatically published to the [Developer Portal](https://developers.buffer.com), and the schema is published to Hive's registry too. The schema being the contract is what makes all of that automation possible, so the more you can lean on it, the less change management you end up doing by hand. Joe Birch went into more detail on this in a [talk at GraphQL Conf 2025](https://graphql.org/conf/2025/schedule/e6262da79f7c90fd01a2a13570d6b6bc/).

## Protecting an open GraphQL endpoint

Opening up GraphQL introduces a lot of new attack vectors to account for, because queries can be expensive or malicious in ways an internal-only API never really has to worry about, so Buffer wanted to understand that surface and put the right protections in place before letting anyone in.

On the security side, Buffer guards the API through a combination of complexity scoring, depth limiting, alias limiting, token limiting, and pagination limiting, which together keep queries within reasonable bounds and protect against both accidental and malicious load. Alongside this, rate limiting takes a tier-based approach that depends on the account's plan, along with a trusted partner status for integrations the team has verified, which lets them apply limits appropriate for different types of access rather than putting a single blanket limit on everyone.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a super interesting area all by itself!
I'm approving the whole article without it, but if you ever get more time to expand on as much details as you can possibly share here, or even create a new article just on that subject, I'm sure many would benefit from that knowledge sharing!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Urigo definitely open to creating a separate article to focus on this section :)


## Understanding who uses what

With a public API, the hard part is knowing how it's actually being used, and without that, decisions like changing or removing a field come down to guesswork. Buffer needed to see usage down to the field level, not just overall traffic.

Buffer uses Hive for this, all the way down to the field level, which means the team can see exactly which parts of the schema are being used and by which clients. The Schema Explorer is easy to navigate, and being able to see precisely who is using what, and how often, has been genuinely useful. It's changed the kind of decisions the team can make quickly and with less friction:

- While the API was in beta, the team needed to migrate the structure of some input types before the stable release, and were able to do this without affecting clients. Using Hive, Buffer identified which clients were using these fields and then automated an email prompting them to migrate.
- The team has been refactoring internal operations so they're ready for public consumption, and using Hive to know which internal clients are calling these, along with what they're using, is what enabled Buffer to ship extended Ideas support so soon after the initial release of the Public API.
- Buffer identified an internal operation that was silently failing over 90% of the time, because it was being called too early in the app start-up, and was able to spot this in Hive and ship a fix.

## Telling internal and external traffic apart

When Buffer started ingesting internal traffic alongside external, it became difficult to tell what Hive traffic was split between the two. Getting an external-only view meant manually selecting each internal client in the filter, which isn't ideal, and there was no easy way to save those filters for later use.

Buffer shared this feedback with The Guild, and one of their designers hopped on a call, arriving with a prototype that already solved the problem. A few days later it shipped to production, and it now lets the team easily switch between internal and external traffic, as well as filter between internal web and mobile traffic. It's a good example of how the two teams work together, where a specific bit of friction on Buffer's side turned into a shipped feature quickly, and it's worth noting the insights filters improvement came directly out of that conversation.

## Migrating to Yoga

Buffer had been using Apollo Server since it first adopted GraphQL, but had always heard about the benefits of the alternatives. [Yoga stood out on performance alone](https://the-guild.dev/graphql/yoga-server/docs/comparison#graphql-yoga-and-apollo-server), and having access to the improvements available in Hive through the use of Yoga made the switch even more appealing. Having access to OpenTelemetry data and improved insights will allow Buffer to make even more informed decisions for their API, as well as debug issues for users as they arise.

In the near future, Buffer will be exploring GraphQL Subscriptions for internal real-time functionality, which Yoga makes even simpler as it ships with built-in support.

## Where the gaps are

Beyond the Explorer, having API access to Hive has been essential. Buffer is currently building its own deprecation triaging system to stay on top of [deprecations for internal clients](https://github.com/graphql-hive/console/issues/8245), which would have been very difficult without an API.

Buffer is also [missing alerts and proactive monitoring(https://github.com/graphql-hive/console/issues/253), so right now the team has to check Hive manually when they'd rather get notifications when certain events occur, such as error rate or performance changes. Custom policies would help too, as Buffer has specific standards defined for its GraphQL schema with custom ESLint checks written for them, and without a way to express those in Hive, the team is keeping all its checks in one place for now.

Buffer limits requests using complexity scoring, but there's [no way to see the complexity score for requests in Hive](https://github.com/graphql-hive/console/issues/7803), so it would be great to be able to configure complexity values and then view that data in Hive alongside everything else. Right now the team tracks this using their own internal solutions, which puts API observability data in more than one place.

Hive has already improved Buffer's experience when it comes to identifying clients, but the team has run into some limits with how little data there is for each client beyond its name and version. Being able to add extra data or tags to clients, and filter by them, would make the observability even more flexible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can add GH issues links to each of of these suggestions?


There are a couple of oppourtunities for Buffer to utilise Hive features. One is schema checks in Hive, which would mean the team could move away from their own implementation of breaking-change checks in GitHub Actions. The other is building Buffer's own MCP server on top of Hive's API, so that folks inside Buffer can easily get at data about the API through Claude.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@enisdenjo maybe also here we could link to an issue about Hive Console exposing MCP


## Looking Ahead

The longer-term goal for Buffer is to bridge the gap between the in-app experience and the Public API, so that no matter where you interact with Buffer, the same experience and functionality are available, and users can choose where they want to use Buffer. A lot of this is about feature parity, as there's plenty of existing functionality the team hasn't exposed through the API yet, and the aim is to expose new features by default going forward.

As the surface of the Public API expands, Buffer plans to invest further in developer tooling and experience, while continuing to build on functionality based on feedback from the community. If you're interested in building with Buffer on its Public API, [sign up](https://buffer.com/) today!

<LargeCallout
cta={<ContactButton variant="secondary-inverted" />}
heading="Elevate Your GraphQL Journey"
icon={<DocsIcon />}
variant="primary"
>
Open up your GraphQL API with confidence like Buffer. Whether you're exposing
an internal schema to the public, tracking field-level usage across clients,
or evolving your API without breaking consumers, Hive gives you the visibility
and tooling to do it safely.
</LargeCallout>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BufferLogo,
HemnetLogo,
SoundYXZLogo,
ToastLogo,
Expand All @@ -10,6 +11,7 @@ import {
* Take note that these logos may have different dimensions than logos used elsewhere.
*/
export const companyLogos = {
buffer: <BufferLogo height={64} width={212} />,
hemnet: <HemnetLogo height={64} width={212} />,
"sound-xyz": <SoundYXZLogo height={64} width={193} />,
toast: <ToastLogo height={64} width={158} />,
Expand Down
11 changes: 11 additions & 0 deletions packages/documentation/src/components/company-logos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,14 @@ export function TrivagoLogo(props: LogoProps) {
</svg>
);
}

export function BufferLogo(props: LogoProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" {...props} viewBox="0 0 1800 445">
<path
d="M392.956 337.802L195.426 443.67L0 337.802L62.1006 304.233L195.426 376.534L330.153 304.233L392.956 337.802ZM867.577 292.962C867.577 326.355 887.907 343.568 916.852 343.568C948.208 343.568 971.639 325.322 971.639 282.634V150.092H1035.39V395.553H971.294V368.011C956.822 390.044 930.979 402.437 897.556 402.438C844.491 402.438 803.486 366.29 803.486 300.88V150.092H867.577V292.962ZM1506.07 142.844C1579.47 142.844 1631.84 200.336 1631.84 272.632C1631.84 282.271 1630.81 290.534 1629.43 298.796H1441.98C1451.29 331.157 1478.16 344.928 1511.59 344.928C1536.74 344.928 1556.73 334.6 1567.41 320.485L1619.09 349.747C1596.01 383.141 1558.79 402.42 1510.9 402.42C1427.51 402.42 1374.79 345.96 1374.79 272.632C1374.79 199.304 1427.86 142.844 1506.07 142.844ZM1246.95 148.054C1246.95 80.2336 1284.51 40.6423 1363.76 45.4619V106.397C1330.33 103.988 1311.04 114.66 1311.04 148.054V150.463H1363.76V211.398H1311.04V395.924H1246.95V211.398H1173.22V395.924H1109.12V211.398H1073.29V150.463H1109.12V148.054C1109.12 80.2335 1146.68 40.6422 1225.94 45.4619V106.397C1192.86 103.988 1173.22 114.66 1173.22 148.054V150.463H1246.95V148.054ZM1799.99 215.856C1766.57 211.725 1724.53 226.529 1724.53 278.169V395.563H1660.44V150.103H1724.53V192.446C1737.28 159.053 1768.64 145.282 1799.99 145.282V215.856ZM652.215 51.6387C711.137 51.6387 758 90.8847 758 148.377C758 177.984 745.596 201.049 725.266 217.229C753.176 233.066 771.094 260.263 771.783 295.723C771.783 354.592 723.543 395.215 663.242 395.215H515.42V51.6387H652.215ZM583.99 332.215H662.898C686.329 332.215 703.213 315.001 703.213 291.591C703.213 268.525 686.329 251.313 662.898 251.312H583.99V332.215ZM392.956 220.75L195.426 316.912L0 220.75L62.1006 190.152L195.426 255.718L330.153 190.152L392.956 220.75ZM1506.07 200.337C1471.62 200.337 1448.19 218.582 1440.95 249.91H1567.75C1559.83 214.451 1532.95 200.337 1506.07 200.337ZM392.956 101.233L195.426 202.818L0 101.233L195.426 0.00195312L392.956 101.233ZM583.99 191.065H652.561C673.924 191.065 689.774 174.885 689.774 153.196C689.774 131.508 674.613 115.327 652.561 115.327H583.99V191.065Z"
fill="currentColor"
/>
</svg>
);
}
Loading