join v0.1

for defining supergraphs which join multiple subgraphs

StatusRelease
Version0.1
Schema joining multiple subgraphs
graph LR classDef bg fill:none,color:#22262E; s1(auth.graphql):::bg-->core(composed schema: photos.graphql) s2(images.graphql):::bg-->core s3(albums.graphql):::bg-->core style core fill:none,stroke:fuchsia,color:fuchsia;

This document defines a core schema named join for describing core schemas which join multiple subgraph schemas into a single supergraph schema.

This specification provides machinery to:

1How to read this document

This document uses RFC 2119 guidance regarding normative terms: MUST / MUST NOT / REQUIRED / SHALL / SHALL NOT / SHOULD / SHOULD NOT / RECOMMENDED / MAY / OPTIONAL.

1.1What this document isn't

This document specifies only the structure and semantics of supergraphs. It’s expected that a supergraph will generally be the output of a compilation process which composes subgraphs. The mechanics of that process are not specified normatively here. Conforming implementations may choose any approach they like, so long as the result conforms to the requirements of this document.

2Example: Photo Library

This section is non‐normative.

We’ll refer to this example of a photo library throughout the document:

Example № 1 Photos library composed schema
schema
  @core(feature: "https://specs.apollo.dev/core/v1.0")
  @core(feature: "https://specs.apollo.dev/join/v1.0") {
  query: Query
}

directive @core(feature: String!) repeatable on SCHEMA

directive @join__owner(graph: join__Graph!) on OBJECT

directive @join__type(
  graph: join__Graph!
  key: String!
) repeatable on OBJECT | INTERFACE

directive @join__field(
  graph: join__Graph
  requires: String
  provides: String
) on FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

enum join__Graph {
  AUTH @join__graph(name: "auth", url: "https://auth.api.com")
  ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
  IMAGES @join__graph(name: "images", url: "https://images.api.com")
}

type Query {
  me: User @join__field(graph: AUTH)
  images: [Image] @join__field(graph: IMAGES)
}

type User
    @join__owner(graph: AUTH)
    @join__type(graph: AUTH, key: "id")
    @join__type(graph: ALBUMS, key: "id") {
  id: ID! @join__field(graph: AUTH)
  name: String @join__field(graph: AUTH)
  albums: [Album!] @join__field(graph: ALBUMS)
}

type Album
    @join__owner(graph: ALBUMS)
    @join__type(graph: ALBUMS, key: "id") {
  id: ID!
  user: User
  photos: [Image!]
}

type Image
    @join__owner(graph: IMAGES)
    @join__type(graph: ALBUMS, key: "url")
    @join__type(graph: IMAGES, key: "url") {
  url: Url @join__field(graph: IMAGES)
  type: MimeType @join__field(graph: IMAGES)
  albums: [Album!] @join__field(graph: ALBUMS)
}

scalar Url
scalar MimeType

The meaning of the @join__* directives is explored in the Directives section.

The example represents one way to compose three input schemas, based on federated composition. These schemas are provided for purposes of illustration only. This spec places no normative requirements on composer input. It does not require that subgraphs use federated composition directives, and it does not place any requirements on how the composer builds a supergraph, except to say that the resulting schema must be a valid supergraph document.

The auth subgraph provides the User type and Query.me.

Example № 2 Auth schema
type User @key(fields: "id") {
  id: ID!
  name: String
}

type Query {
  me: User
}

The images subgraph provides the Image type and URL scalar.

Example № 3 Images schema
type Image @key(fields: "url") {
  url: Url
  type: MimeType
}

type Query {
  images: [Image]
}

extend type User {
  favorite: Image
}

scalar Url
scalar MimeType

The albums subgraph provides the Album type and extends User and Image with album information.

Example № 4 Albums schema
type Album @key(fields: "id") {
  id: ID!
  user: User
  photos: [Image!]
}

extend type Image {
  albums: [Album!]
}

extend type User {
  albums: [Album!]
  favorite: Album
}

3Actors

Actors and roles within an example composition pipeline
flowchart TB classDef bg fill:#EBE6FF; subgraph A [subgraph A] schemaA([schema A]):::bg style schemaA color:#000 endpointA([endpoint A]):::bg style endpointA color:#000 end style A fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; subgraph B [subgraph B] schemaB([schema B]):::bg style schemaB color:#000 endpointB([endpoint B]):::bg style endpointB color:#000 end style B fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; subgraph C [subgraph C] schemaC([schema C]):::bg style schemaC color:#000 endpointC([endpoint C]):::bg style endpointC color:#000 end style C fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; subgraph producer["Producer ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"] Composer style Composer color:#000 end style producer fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; supergraph([Supergraph]):::bg style supergraph color:#000 subgraph consumer["Consumer ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"] Router style Router color:#000 end style consumer fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; A-->Composer:::bg B-->Composer:::bg C-->Composer:::bg Composer-->supergraphSchema([Supergraph Schema]):::bg style supergraphSchema color:#000 supergraphSchema-->Router:::bg Router-->published([Published Schema]):::bg style published color:#000 published-->Clients:::bg style Clients color:#000 Clients-->Router:::bg

Producers generate supergraphs. This spec places requirements on supergraph producers.

Consumers consume supergraphs. This spec places requirements on supergraph consumers.

Composers (or compilers) are producers which compose subgraphs into a supergraph. This document places no particular requirements on the composition algorithm, except that it must produce a valid supergraph.

Routers are consumers which serve a composed schema as a GraphQL endpoint. This definition is non‐normative.

Endpoints are running servers which can resolve GraphQL queries against a schema. In this version of the spec, endpoints must be URLs, typically http/https URLs.

Subgraphs are GraphQL schemas which are composed to form a supergraph. Subgraph names and metadata are declared within the special join__Graph enum.

This spec does not place any requirements on subgraph schemas. Generally, they may be of any shape. In particular, subgraph schemas do not need to be supergraphs themselves or to follow this spec in any way; neither is it an error for them to do so. Composers MAY place additional requirements on subgraph schemas to aid in composition; composers SHOULD document any such requirements.

4Overview

This section is non‐normative. It describes the motivation behind the directives defined by this specification.

A supergraph schema describes a GraphQL schema that can be served by a router. The router does not contain logic to resolve any of the schema’s fields; instead, the supergraph schema contains directives starting with @join__ that tell the router which subgraph endpoint can resolve each field, as well as other information needed in order to construct subgraph operations.

The directives described in this specification are designed for a particular query planning algorithm, and so there are some restrictions on how they can be combined that originate from the requirements of this algorithm. For example, this specification describes a concept of type ownership which exists not because we believe it describes the ideal method of structuring your subgraphs, but because this query planning algorithm depends on type ownership. We hope that future versions of this specification can relax some of these restrictions.

Each supergraph schema contains a list of its included subgraphs. The join__Graph enum represents this list with an enum value for each subgraph. Each enum value is annotated with a @join__graph directive telling the router what endpoint can be used to reach the subgraph, and giving the subgraph a human‐readable name that can be used for purposes such as query plan visualization and server logs.

To resolve a field, the router needs to know to which subgraphs it can delegate the field’s resolution. One explicit way to indicate this in a supergraph schema is by annotating the field with a @join__field directive specifying which subgraph should be used to resolve that field. (There are other ways of indicating which subgraphs can resolve a field which will be described later.)

In order for the router to send an operation that resolves a given field on a parent object to a subgraph, the operation needs to first resolve the parent object itself. There are several ways to accomplish this, described below. The examples below include abbreviated versions of the supergraph schemas which do not include the schema definition, directive definitions, or the join__Graph definition. This specification does not require the subgraph operations to be the same as those described in these examples; this is just intended to broadly describe the meanings of the directives.

4.1Root fields

If a field appears at the root of the overall operation (query or mutation), then it can be placed at the root of the subgraph operation.

Example № 5 Root fields
# Supergraph schema
type Query {
  fieldA: String @join__field(graph: A)
  fieldAlsoFromA: String @join__field(graph: A)
  fieldB: String @join__field(graph: B)
}

# Operation
{ fieldA fieldAlsoFromA fieldB }
# Generated subgraph operations
## On A:
{ fieldA fieldAlsoFromA }
## On B:
{ fieldB }

4.2Fields on the same subgraph as the parent operation

If a field’s parent field will be resolved by an operation on the same subgraph, then it can be resolved as part of the same operation, by putting it in a nested selection set on the parent field’s subgraph operation. Note that this example contains @join__owner and @join__type directives on an object type; these will be described later.

Example № 6 Fields on the same subgraph as the parent operation
# Supergraph schema
type Query {
  fieldA: X @join__field(graph: A)
}

type X @join__owner(graph: A) @join__type(graph: A, key: "nestedFieldA") {
  nestedFieldA: String @join__field(graph: A)
}

# Operation
{ fieldA { nestedFieldA } }
# Generated subgraph operations
## On A:
{ fieldA { nestedFieldA }}

4.3Fields provided by the parent field

Sometimes, a subgraph G may be capable of resolving a field that is ordinarily resolved in a different subgraph if the field’s parent object was resolved in G. Consider an example where the Product.priceCents: Int! field is usually resolved by the Products subgraph, which knows the priceCents for every Product in your system. In the Marketing subgraph, there is a Query.todaysPromotion: Product! field. While the Marketing subgraph cannot determine the priceCents of every product in your system, it does know the priceCents of the promoted products, and so the Marketing subgraph can resolve operations like { todaysPromotion { priceCents } }.

When this is the case, you can include a provides argument in the @join__field listing these “pre‐calculated” fields. The router can now resolve these fields in the “providing” subgraph instead of in the subgraph that would usually be used to resolve those fields.

Example № 7 Provided fields
# Supergraph schema
type Query {
  todaysPromotion: Product! @join__field(graph: MARKETING, provides: "priceCents")
  randomProduct: Product! @join__field(graph: PRODUCTS)
}

type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "id") {
  id: ID! @join__field(graph: PRODUCTS)
  priceCents: Int! @join__field(graph: PRODUCTS)
}

# Operation showing that `priceCents` is typically resolved on PRODUCTS
{ randomProduct { priceCents } }
# Generated subgraph operations
## On PRODUCTS
{ randomProduct { priceCents } }

# Operation showing that `provides` allows `priceCents` to be resolved on MARKETING
{ todaysPromotion { priceCents } }
# Generated subgraph operations
## On MARKETING
{ todaysPromotion { priceCents } }

4.4Fields on value types

Some types have the property that all of their fields can be resolved by any subgraph that can resolve a field returning that type. These types are called value types. (Imagine a type type T { x: Int, y: String } where every resolver for a field of type T actually produces an object like {x: 1, y: "z"}, and the resolvers for the two fields on T just unpack the values already in the object.) In a supergraph schema, a type is a value type if it does not have a @join__owner directive on it.

Example № 8 Value types
# Supergraph schema
type Query {
  fieldA: X @join__field(graph: A)
  fieldB: X @join__field(graph: B)
}

type X {
  anywhere: String
}

# Operation
{ fieldA { anywhere } }
# Generated subgraph operations
## On A
{ fieldA { anywhere } }

# Operation
{ fieldB { anywhere } }
# Generated subgraph operations
## On B
{ fieldB { anywhere } }

4.5Owned fields on owned types

We’ve finally reached the most interesting case: a field that must be resolved by an operation on a different subgraph from the subgraph on which its parent field was resolved. In order to do this, we need a way to tell the subgraph to resolve that parent object. We do this by defining a special root field in the subgraph’s schema: Query._entities(representations: [_Any!]!): [_Entity]!. This field takes a list of “representations” and returns a list of the same length of the corresponding objects resulting from looking up the representations in an application‐dependent way.

What is a representation? A representation is expressed as the scalar type _Any, and can be any JSON object with a top‐level __typename key with a string value. Often, a representation will be something like {__typename: "User", id: "abcdef"}: the type name plus one or more fields that you can use to look up the object in a database.

There are several ways that the router can calculate a representation to pass to a subgraph. In this specification, all non‐value types have a specific subgraph referred to as its “owner”, specified via a @join__owner directive on the type. Object types that are not value types are referred to as “entities”; the type _Entity referenced above is a union defined in each subgraph’s schema consisting of the entity types defined by that subgraph. (Only subgraphs which define entities need to define the Query._entities field.) Entity types must also have at least one @join__type directive specifying the owning subgraph along with a key. For each additional subgraph which can resolve fields returning that type, there should be exactly one @join__type directive specifying that subgraph along with a key, which should be identical to one of the keys specified with the owning subgraph.

A key is a set of fields on the type (potentially including sub‐selections and inline fragments), specified as a string. If a type T is annotated with @join__type(subgraph: G, key: "a b { c }"), then it must be possible to resolve the full field set provided as a key on subgraph G. Additionally, if you take an object with the structure returned by resolving that field set and add a field __typename: "T", then you should be able to pass the resulting value as a representation to the Query._entities field on subgraph G.

In order to resolve a field on an entity on the subgraph that owns its parent type, where that subgraph is different from the subgraph that resolved its parent object, the router first resolves a key for that object on the previous subgraph, and then uses that representation on the owning subgraph.

For convenience, you may omit @join__field(graph: A) directives on fields whose parent type is owned by A.

Example № 9 Owned fields on owned types
# Supergraph schema
type Query {
  fieldB: X @join__field(graph: B)
}

type X
  @join__owner(graph: A)
  # As the owner, A is allowed to have more than one key.
  @join__type(graph: A, key: "x")
  @join__type(graph: A, key: "y z")
  # As non-owners, B and C can only have one key each and
  # they must match a key from A.
  @join__type(graph: B, key: "x")
  @join__type(graph: C, key: "y z")
{
  # Because A owns X, we can omit @join__field(graph: A)
  # from these three fields.
  x: String
  y: String
  z: String
}

# Operation
{ fieldB { y } }
# Generated subgraph operations
## On B. `y` is not available, so we need to fetch B's key for X.
{ fieldB { x } }
## On A
## $r = [{__typename: "X", x: "some-x-value"}]
query ($r: [_Any!]!) { _entities(representations: $r]) { y } }

4.6Extension fields on owned types

The previous section described how to jump from one subgraph to another in order to resolve a field on the subgraph that owns the field’s parent type. The situation is a bit more complicated when you want to resolve a field on a subgraph that doesn’t own the field’s parent type — what we call an extension field. That’s because we no longer have the guarantee that the subgraph you’re coming from and the subgraph you’re going to share a key in common. In this case, we may need to pass through the owning type.

Example № 10 Extension fields on owned types
# Supergraph schema
type Query {
  fieldB: X @join__field(graph: B)
}

type X
  @join__owner(graph: A)
  # As the owner, A is allowed to have more than one key.
  @join__type(graph: A, key: "x")
  @join__type(graph: A, key: "y z")
  # As non-owners, B and C can only have one key each and
  # they must match a key from A.
  @join__type(graph: B, key: "x")
  @join__type(graph: C, key: "y z")
{
  x: String
  y: String
  z: String
  c: String @join__field(graph: C)
}

# Operation
{ fieldB { c } }
# Generated subgraph operations
## On B. `c` is not available on B, so we need to eventually get over to C.
## In order to do that, we need `y` and `z`... which aren't available on B
## either! So we need to take two steps. First we use B's key.
{ fieldB { x } }
## On A. We use B's key to resolve our `X`, and we extract C's key.
## $r = [{__typename: "X", x: "some-x-value"}]
query ($r: [_Any!]!) { _entities(representations: $r]) { y z } }
## On C. We can finally look up the field we need.
## $r = [{__typename: "X", y: "some-y-value", z: "some-z-value"}]
query ($r: [_Any!]!) { _entities(representations: $r]) { c } }

We only need to do this two‐jump process because the fields needed for C’s key are not available in B; otherwise a single jump would have worked, like in the owned‐field case.

Sometimes a particular extension field needs its parent object’s representation to contain more information than its parent type’s key requests. In this case, you can include a requires argument in the field’s @join__field listing those required fields (potentially including sub‐selections). All required fields must be resolvable in the owning subgraph (this restriction is why requires is only allowed on extension fields).

Example № 11 Required fields
# Supergraph schema
type Query {
  fieldA: X @join__field(graph: A)
}

type X
  @join__owner(graph: A)
  @join__type(graph: A, key: "x")
  @join__type(graph: B, key: "x")
{
  x: String
  y: String
  z: String @join__field(graph: B, requires: "y")
}

# Operation
{ fieldA { z } }
# Generated subgraph operations
## On A. `x` is included because it is B's key for `X`; `y`
## is included because of the `requires`.
{ fieldA { x y } }
## On B..
## $r = [{__typename: "X", x: "some-x-value", y: "some-y-value"}]
query ($r: [_Any!]!) { _entities(representations: $r]) { z } }

5Basic Requirements

Schemas using the join core feature MUST be valid core schema documents with @core directives referencing the core specification and this specification.

Example № 12 @core directives for supergraphs
schema
  @core(feature: "https://specs.apollo.dev/core/v1.0")
  @core(feature: "https://specs.apollo.dev/join/v1.0") {
  query: Query
}

As described in the core schema specification, your schema may use a prefix other than join for all of the directive and enum names defined by this specification by including an as argument to the @core directive which references this specification. All references to directive and enum names in this specification MUST be interpreted as referring to names with the appropriate prefix chosen within your schema.

In order to use the directives described by this specification, GraphQL requires you to include their definitions in your schema.

Processors MUST validate that you have defined the directives with the same arguments, locations, and repeatable flag as given below.

enum Graph

directive @owner(graph: Graph!) on OBJECT

directive @type(
  graph: Graph!,
  key: String!,
) repeatable on OBJECT | INTERFACE

directive @field(
  graph: Graph,
  requires: String,
  provides: String,
) on FIELD_DEFINITION

directive @graph(name: String!, url: String!) on ENUM_VALUE

Processors MUST validate that the schema contains an enum named join__Graph; see its section below for other required properties of this enum.

As described in the core specification, all of the directives and enums defined by this schema should be removed from the supergraph’s API schema. For example, the join__Graph enum should not be visible via introspection.

6Enums

6.1join__Graph

Enumerate subgraphs.

enum join__Graph

Documents MUST define a join__Graph enum. Each enum value describes a subgraph. Each enum value MUST have a @join__graph directive applied to it.

Example № 13 Using join__Graph to define subgraphs and their endpoints
enum join__Graph {
  AUTH @join__graph(name: "auth", url: "https://auth.api.com")
  ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
  IMAGES @join__graph(name: "images", url: "https://images.api.com")
}

The join__Graph enum is used as input to the @join__owner, @join__field, and @join__type directives.

7Directives

7.1@join__graph

Declare subgraph metadata on join__Graph enum values.

directive @join__graph(name: String!, url: String!) on ENUM_VALUE
Example № 14 Using @join__graph to declare subgraph metadata on the join__Graph enum values.
enum join__Graph {
  AUTH @join__graph(name: "auth", url: "https://auth.api.com")
  ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
  IMAGES @join__graph(name: "images", url: "https://images.api.com")
}

The @join__graph directive MUST be applied to each enum value on join__Graph, and nowhere else. Each application of @join__graph MUST have a distinct value for the name argument; this name is an arbitrary non‐empty string that can be used as a human‐readable identifier which may be used for purposes such as query plan visualization and server logs. The url argument is an endpoint that can resolve GraphQL queries for the subgraph.

7.2@join__type

Declares an entity key for a type on a subgraph.

directive @join__type(
  graph: join__Graph!
  key: String!
) repeatable on OBJECT | INTERFACE

When this directive is placed on a type T, it means that subgraph graph MUST be able to:

  • Resolve selections on objects of the given type that contain the field set in key
  • Use Query._entities to resolve representations of objects containing __typename: "T" and the fields from the field set in key
Example № 15 Using @join__type to specify subgraph keys
type Image
    @join__owner(graph: IMAGES)
    @join__type(graph: ALBUMS, key: "url")
    @join__type(graph: IMAGES, key: "url") {
  url: Url @join__field(graph: IMAGES)
  type: MimeType @join__field(graph: IMAGES)
  albums: [Album!] @join__field(graph: ALBUMS)
}

Every type with a @join__type MUST also have a @join__owner directive. Any type with a @join__owner directive MUST have at least one @join__type directive with the same graph as the @join__owner directive (the “owning graph”), and MUST have at most one @join__type directive for each graph value other than the owning graph. Any value that appears as a key in a @join__type directive with a graph value other than the owning graph must also appear as a key in a @join__type directive with graph equal to the owning graph.

7.3@join__field

Specify the graph that can resolve the field.

directive @join__field(
  graph: join__Graph
  requires: String
  provides: String
) on FIELD_DEFINITION

The field’s parent type MUST be annotated with a @join__type with the same value of graph as this directive, unless the parent type is a root operation type.

If a field is not annotated with @join__field (or if the graph argument is not provided or null) and its parent type is annotated with @join__owner(graph: G), then a processor MUST treat the field as if it is annotated with @join__field(graph: G). If a field is not annotated with @join__field (or if the graph argument is not provided or null) and its parent type is not annotated with @join__owner (ie, the parent type is a value type) then it MUST be resolvable in any subgraph that can resolve values of its parent type.

Example № 16 Using @join__field to join fields to subgraphs
type User
    @join__owner(graph: AUTH)
    @join__type(graph: AUTH, key: "id")
    @join__type(graph: ALBUMS, key: "id") {
  id: ID! @join__field(graph: AUTH)
  name: String @join__field(graph: AUTH)
  albums: [Album!] @join__field(graph: ALBUMS)
}

type Album
    @join__owner(graph: ALBUMS)
    @join__type(graph: ALBUMS, key: "id") {
  id: ID!
  user: User
  photos: [Image!]
}

type Image
    @join__owner(graph: IMAGES)
    @join__type(graph: ALBUMS, key: "url")
    @join__type(graph: IMAGES, key: "url") {
  url: Url @join__field(graph: IMAGES)
  type: MimeType @join__field(graph: IMAGES)
  albums: [Album!] @join__field(graph: ALBUMS)
}

Every field on a root operation type MUST be annotated with @join__field.

Example № 17 @join__field on root fields
type Query {
  me: User @join__field(graph: AUTH)
  images: [Image] @join__field(graph: IMAGES)
}

The requires argument MUST only be specified on fields whose parent type has a @join__owner directive specifying a different graph than this @join__field directive does. All fields (including nested fields) mentioned in this field set must be resolvable in the parent type’s owning subgraph. When constructing a representation for a parent object of this field, a router will include the fields selected in this requires argument in addition to the appropriate key for the parent type.

The provides argument specifies fields that can be resolved in operations run on subgraph graph as a nested selection under this field, even if they ordinarily can only be resolved on other subgraphs.

7.4@join__owner

Specify the graph which owns the object type.

directive @join__owner(graph: join__Graph!) on OBJECT

The descriptions of @join__type and @join__field describes requirements on how @join__owner relates to @join__type and the requires argument to @join__field.

Note Type ownership is currently slated for removal in a future version of this spec. It is RECOMMENDED that router implementations consider approaches which function in the absence of these restrictions. The overview explains how the current router’s query planning algorithm depends on concept of type ownership.

§Index

  1. @join__field
  2. @join__graph
  3. @join__owner
  4. @join__type
  5. join__Graph
  1. 1How to read this document
    1. 1.1What this document isn't
  2. 2Example: Photo Library
  3. 3Actors
  4. 4Overview
    1. 4.1Root fields
    2. 4.2Fields on the same subgraph as the parent operation
    3. 4.3Fields provided by the parent field
    4. 4.4Fields on value types
    5. 4.5Owned fields on owned types
    6. 4.6Extension fields on owned types
  5. 5Basic Requirements
  6. 6Enums
    1. 6.1join__Graph
  7. 7Directives
    1. 7.1@join__graph
    2. 7.2@join__type
    3. 7.3@join__field
    4. 7.4@join__owner
  8. §Index