A Data-Fetching Layer that Keeps Up with Live Products

Real-time collaboration only feels effortless when the data shown to each user stays current without manual intervention. When a colleague adds a file to a shared project, everyone viewing that project should see the update immediately. Figma's infrastructure team needed a general way to give product engineers that capability without forcing them to manage the mechanics of pushing data around.

The result is LiveGraph, a data-fetching layer on top of Postgres that exposes GraphQL-style real-time data subscriptions to frontend code. LiveGraph queries the database directly and delivers live updates in milliseconds by tailing the database replication stream.

Why Hand-Crafted Events Stopped Scaling

Figma's frontend has been built on React since 2016, originally fetching data from Ruby HTTP endpoints. Early on, each page load pulled all required data in one large request and stored it in Redux global state. Real-time updates came from manually crafted event messages in backend code whenever something was written to the database. Frontend clients subscribed to those messages over WebSocket and applied changes to global state.

That model worked while usage was modest, but two pressures broke it down. First, as users accumulated more data, page loads grew too large, forcing incremental data fetching. That made it unclear which product area was responsible for loading what and when data was guaranteed to be in memory. Second, new product features made ad hoc notifications harder to reason about. A permission change on one resource could cascade into visibility changes across many others, and event delivery order relative to database writes was not guaranteed. The result was a class of consistency bugs where client state no longer matched the server state it was supposed to represent.

Building Rather Than Buying

The team wanted a declarative system where product developers could define data subscriptions explicitly. GraphQL was the natural interface because it lets the system fetch data and keep it live-updated automatically. They chose to build it in-house as LiveGraph rather than adopt an off-the-shelf solution.

Existing options didn't fit the constraints. Figma already runs on Postgres at scale, so LiveGraph had to be a query engine over that infrastructure rather than a new persistence layer—ruling out real-time databases like Firebase or RethinkDB. Within the GraphQL ecosystem, most subscription tooling (such as Hasura, Prisma, and PostGraphile) treats subscriptions as event streams, closer to the old hand-crafted events than the live queries LiveGraph needed. Where they do support subscriptions, scaling them to large concurrent volumes is not their primary design goal.

Polling was considered and rejected because it multiplies database load and forces per-query decisions about poll frequency. Instead, LiveGraph subscribes to the database write-ahead log, distributed across servers via Kafka. This tailing approach yields faster update latency than polling and provides a path to scale by distributing updates from multiple database shards across machines.

Figma's multiplayer service was also not an option for this problem. It handles writes and conflict resolution for individual files, but LiveGraph needed to track data across the broader server-side object graph. Because real-time data is core to Figma's collaborative products, the investment in an in-house system made sense despite alternatives existing.

The Frontend Contract

From a product developer's perspective, LiveGraph accepts GraphQL-like queries and returns results as a JSON tree. A schema describes server-side entities and their relations, and views allow querying a subset of that object graph. Client code typically makes queries through a custom useSubscription React hook.

The client-side library sends the request to the server and reconstructs the result from a series of JSON update messages. Results are statically typed because LiveGraph generates TypeScript bindings for the GraphQL API, and the resulting object gets passed to React components as props.

For the component developer, that's the whole workflow. The component always renders the latest server data without any feature-specific event handling. Under the hood, the library applies incremental updates to build a new tree of in-memory references, and the hook triggers a re-render when needed.

LiveGraph also gives clients ordering guarantees: updates arrive in order and exactly once. If a connection drops, the library reconnects and refetches the view automatically. Access control can be defined on both objects and relations, so the server filters out any data instance that fails permission checks. Since those permission checks can depend on data from subscribed subviews, permissions themselves can update in real time.

The client library also supports optimistic updates. A user edit updates the frontend state before the server write completes, masking latency. LiveGraph detects when the subscription reflects the write result and removes the redundant optimistic update automatically.

The Backend Routing Problem

The backend's job is efficient routing: taking changes from the database and sending them only to the clients whose views are affected. Availability was the primary design constraint—introducing LiveGraph should not cause drastic load increases on the database.

To avoid polling's load penalty, LiveGraph decomposes each view subscription into a tree of granular subqueries. A subquery fetches a single type of object and translates to a simple SELECT columns FROM table WHERE condition with no joins. Those conditions encode relations from the schema. LiveGraph keeps an in-memory representation of the view—the live view tree—where each node is one subquery.

Each subquery subscribes independently. An index of all active subqueries for each object type, keyed by filter condition, lets LiveGraph route each replication stream entry to the right cached results. This decomposition also enables deduplication: many clients viewing the same project sidebar, for instance, all share the same subquery for fetching team names. Once a query result is in memory, subsequent identical subscriptions reuse it rather than hitting the database again.

LiveGraph's implementation mixes NodeJS and Go. The JavaScript portion shares code with the client—both sides use the same live view tree bookkeeping, and updates are sent as targeted patches rather than full JSON serializations of the view. Lower-level, performance-sensitive backend areas use Go for its multithreading and speed.

Scaling LiveGraph for production loads

LiveGraph’s path from prototype to dependable production service requires tackling the same scaling problems that Figma itself faces. Today, a single LiveGraph instance can process the entire database replication stream—roughly 10,000 writes per second—to deliver updates. That design will not hold up as Figma shards its databases. Multiple replication streams will arrive concurrently, changing the consistency guarantees LiveGraph can offer. Eventually, write volume will outpace any single instance, forcing LiveGraph itself to be sharded so that each instance only consumes a subset of the streams.

Horizontal sharding also spreads the CPU and memory costs of maintaining subscriptions across a fleet. Even with that distribution, though, load spikes demand attention. Thundering herds are a particular production hazard: because clients hold persistent WebSocket connections, a mass reconnect after a deployment re-subscribes a large volume of views all at once. Steady-state metrics alone are insufficient; event-driven spikes such as deploys can degrade service if not explicitly engineered for.

Beyond infrastructure: an adoption workstream

LiveGraph is as much a shift in how Figma fetches data as it is a piece of infrastructure. Rolling it out is an engineering-wide effort, not just an infrastructure project. The product-side work includes:

  • Expanding LiveGraph across the codebase without introducing regressions, building on its existing use in mobile apps, comments, and the prototyping and editor views.
  • Partnering with product teams to refactor use cases that don’t translate cleanly into GraphQL.
  • Building tooling and workflows to make schema evolution and versioning straightforward.
  • Defining React architecture patterns and best practices for consuming view subscriptions.
  • Implementing pagination for unbounded views, a particularly difficult problem when data is changing in real time.

These challenges echo those of any REST-to-GraphQL migration, but subscriptions add a distinct layer of complexity to each. Solving them is what enables the real-time product features that define Figma’s collaborative experience.

As a fast-growing company, Figma hits new scaling constraints weekly, and product requirements keep evolving with usage and feature expansion. There’s no fixed milestone at which LiveGraph will be “done”—that’s the natural outcome of building a system around real user problems. It also means the work stays engaging precisely because the target keeps moving.

The Figma team thanks Asana for consulting on LunaDB during LiveGraph’s development.