# GraphQL API (Carbon 2.0)

> Every public item in carbon_core::graphql — the Juniper schema and Axum router helpers, and the GraphQL scalars for Solana types.

`carbon_core::graphql` serves indexed data over GraphQL with [Juniper](https://docs.rs/juniper/0.16) and [Axum](https://docs.rs/axum/0.8). It has two parts: [`server`](#server), which builds a schema and an Axum router with `/graphql` and `/graphiql`, and [`primitives`](#primitives), scalars that carry Solana pubkeys and wide integers without losing precision. For a walkthrough, see [Serve data over GraphQL](https://carbon.sevenlabs.org/guides/graphql.md).

The module is behind the `graphql` feature of `carbon-core`:

`Cargo.toml`:

```toml
[dependencies]
carbon-core = { version = "2.0.0", features = ["graphql"] }
juniper = "0.16.2"
axum = "0.8.7"
tokio = { version = "1.48.0", features = ["full"] }
```

> **Note: Not on docs.rs.** docs.rs builds `carbon-core` 2.0.0 with default features only, so the `graphql` module has no docs.rs page. The links below point to the source at `v2.0.0`. Run `cargo doc -p carbon-core --features graphql --open` for local API docs.

## Server

`carbon_core::graphql::server`. Source: [`server.rs`](https://github.com/sevenlabs-hq/carbon/blob/v2.0.0/crates/core/src/graphql/server.rs).

### `build_schema`

Wraps a query root in a Juniper schema with no mutations and no subscriptions.

```rust
pub fn build_schema<Q, C>(
    query_root: Q,
) -> Arc<RootNode<'static, Q, DefaultMutation<C>, DefaultSubscription<C>>>
where
    Q: juniper::GraphQLType<Context = C, TypeInfo = ()> + Send + Sync + 'static,
    C: juniper::Context + Send + Sync + 'static,
```

**Details.**

| Parameter    | Type | Description                                                                                                                  |
| ------------ | ---- | ---------------------------------------------------------------------------------------------------------------------------- |
| `query_root` | `Q`  | Your query type, usually a unit struct with a `#[graphql_object(context = C)]` impl. Generated decoders provide `QueryRoot`. |

Returns the schema in an `Arc`, ready for [`graphql_router`](#graphql_router). The schema is read-only: you cannot add mutations or subscriptions through this helper. To have them, build a `juniper::RootNode` yourself and mount it with `juniper_axum` directly.

**Example.**

```rust
let schema = build_schema(Query);
```

**See also.** [`graphql_router`](#graphql_router), [`DefaultMutation`](#defaultmutation-and-defaultsubscription).

### `graphql_router`

Builds an Axum `Router` that serves the schema.

```rust
pub fn graphql_router<Q, C>(
    schema: Arc<RootNode<'static, Q, DefaultMutation<C>, DefaultSubscription<C>>>,
    context: C,
) -> axum::Router
where
    Q: juniper::GraphQLTypeAsync<Context = C, TypeInfo = ()>
        + juniper::GraphQLValueAsync<Context = C, TypeInfo = ()>
        + Send + Sync + 'static,
    C: juniper::Context + Clone + Send + Sync + 'static,
```

**Details.**

| Parameter | Type               | Description                                                                       |
| --------- | ------------------ | --------------------------------------------------------------------------------- |
| `schema`  | `Arc<RootNode<…>>` | The schema from [`build_schema`](#build_schema).                                  |
| `context` | `C`                | Shared context, cloned into each request. Typically holds an `Arc<sqlx::PgPool>`. |

Routes:

| Path        | Methods       | Serves                                                                                                           |
| ----------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
| `/graphql`  | `GET`, `POST` | Executes a GraphQL request. `GET` reads `?query=…`; `POST` reads a JSON body `{"query": "…", "variables": {…}}`. |
| `/graphiql` | `GET`         | The GraphiQL playground, pointed at `/graphql`.                                                                  |

There is no subscriptions endpoint, no CORS layer and no authentication. Merge the router into your own app with `Router::merge` or `Router::nest` to add them. The router does not bind a port; pass it to `axum::serve`.

**Example.**

```rust
let app = graphql_router(schema, Context { slot: 380_000_000 });
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
```

**See also.** [Example: a minimal server](#example-a-minimal-server).

### `DefaultMutation` and `DefaultSubscription`

Type aliases for the empty mutation and subscription roots `build_schema` uses.

```rust
pub type DefaultMutation<C> = juniper::EmptyMutation<C>;
pub type DefaultSubscription<C> = juniper::EmptySubscription<C>;
```

**Details.** Use them to name the schema type, for example in a function signature or a struct field.

**Example.**

```rust
use carbon_core::graphql::server::{DefaultMutation, DefaultSubscription};

type Schema = juniper::RootNode<'static, Query, DefaultMutation<Context>, DefaultSubscription<Context>>;
```

**See also.** [`build_schema`](#build_schema).

## Primitives

`carbon_core::graphql::primitives`. Source: [`primitives.rs`](https://github.com/sevenlabs-hq/carbon/blob/v2.0.0/crates/core/src/graphql/primitives.rs).

GraphQL's built-in `Int` is 32-bit and JavaScript numbers lose precision above 2⁵³, so Carbon's scalars send wide integers as **strings**. Each is a newtype with one public field, derives `Clone, Debug, PartialEq, Eq, Serialize, Deserialize` and `juniper::GraphQLScalar`, and appears in the schema under its Rust name.

| Scalar                              | Wraps                   | Output                               | Accepted input           |
| ----------------------------------- | ----------------------- | ------------------------------------ | ------------------------ |
| [`Pubkey`](#pubkey)                 | `solana_pubkey::Pubkey` | base58 string                        | base58 string            |
| [`I64`](#i64-i128-u8-u32-u64-u128)  | `i64`                   | decimal string                       | decimal string           |
| [`I128`](#i64-i128-u8-u32-u64-u128) | `i128`                  | decimal string                       | decimal string           |
| [`U8`](#i64-i128-u8-u32-u64-u128)   | `u8`                    | decimal string                       | decimal string           |
| [`U32`](#i64-i128-u8-u32-u64-u128)  | `u32`                   | decimal string                       | decimal string           |
| [`U64`](#i64-i128-u8-u32-u64-u128)  | `u64`                   | decimal string                       | decimal string           |
| [`U128`](#i64-i128-u8-u32-u64-u128) | `u128`                  | decimal string                       | decimal string           |
| [`Json`](#json)                     | `serde_json::Value`     | GraphQL value (object, list, scalar) | a string containing JSON |

There is no `U16` or `I8`/`I16`/`I32` scalar; use GraphQL `Int` (`i32`) for those.

### `Pubkey`

A Solana public key as a base58 string.

```rust
#[derive(Clone, Debug, Deserialize, Eq, GraphQLScalar, PartialEq, Serialize)]
#[graphql(parse_token(String), description = "A Solana's public key")]
pub struct Pubkey(pub solana_pubkey::Pubkey);

impl Pubkey {
    pub fn new<S: Into<String>>(value: S) -> Result<Self, String>
}
impl Deref for Pubkey { type Target = solana_pubkey::Pubkey; }
impl Display for Pubkey
```

**Details.** `new` parses a base58 string and returns the parse error as a `String`. As an input value the scalar accepts only a base58 string; a number literal fails with `There needs to be an Int type`. It is a different type from `carbon_core::postgres::primitives::Pubkey`; convert with `graphql::primitives::Pubkey(*pg_pubkey)`.

**Example.**

```rust
let program = Pubkey::new("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo").unwrap();
println!("{program}");
```

**See also.** [Postgres `Pubkey`](https://carbon.sevenlabs.org/reference/postgres.md#pubkey).

### `I64`, `I128`, `U8`, `U32`, `U64`, `U128`

Integers sent as decimal strings.

```rust
pub struct I64(pub i64);
pub struct I128(pub i128);
pub struct U8(pub u8);
pub struct U32(pub u32);
pub struct U64(pub u64);
pub struct U128(pub u128);
```

**Details.** Output is always a string, for example `"slot": "380000000"`. Input must be a string literal or variable. For a field `echo(v: U64!)`, `echo(v: "18446744073709551615")` works, `echo(v: 5)` fails with `There needs to be an Int type`, and a string that does not parse fails with `Invalid value "\"abc\"" for type "U64"`. These types have no `From` or `Deref` implementations: construct them with `U64(value)` and read with `.0`.

**Example.**

```rust
#[derive(juniper::GraphQLObject)]
struct Status {
    slot: U64,
    program: Pubkey,
}
```

**See also.** [Example: a minimal server](#example-a-minimal-server).

### `Json`

An arbitrary JSON value.

```rust
#[derive(Clone, Debug, Serialize, Deserialize, Eq, GraphQLScalar, PartialEq)]
#[graphql(parse_token(String), description = "Arbitrary JSON value")]
pub struct Json(pub serde_json::Value);
```

**Details.** Output converts the JSON value to a GraphQL value of the same shape. Numbers that fit in `i32` stay numbers; larger integers and every `u64` above `i64::MAX` become strings, and floats stay floats. As input, the scalar accepts only a string that contains JSON (`j(v: "{\"a\":1}")`); a GraphQL object literal (`j(v: {a: 1})`) is a parse error. Generated decoders use it for the `accounts` field of instructions and for nested types.

**Example.**

```rust
let accounts = Json(serde_json::json!({ "lb_pair": "…" }));
```

**See also.** [Generated decoder types](#generated-decoder-types).

## Generated decoder types

A decoder built with its `graphql` feature adds a schema that reads the decoder's [typed Postgres tables](https://carbon.sevenlabs.org/reference/postgres.md#generated-decoder-types).

> **Warning: Enable postgres too.** The generated GraphQL code queries the typed tables, but a decoder's `graphql` feature does not enable its `postgres` feature. With `features = ["graphql"]` alone, `carbon-meteora-dlmm-decoder` 2.0.0 fails to compile (`cannot find postgres in accounts`). Use `features = ["postgres", "graphql"]`.

| Item                   | Path                                          | Description                                                                                                                                                            |
| ---------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `QueryRoot`            | `graphql` (re-exported from `graphql::query`) | Query root with two fields per account type and two per instruction type.                                                                                              |
| `GraphQLContext`       | `graphql::context`                            | `#[derive(Clone)] pub struct GraphQLContext { pub pool: Arc<sqlx::PgPool> }`.                                                                                          |
| `<Account>GraphQL`     | `accounts::graphql`                           | GraphQL object for one account type, with an `account_metadata: AccountMetadataGraphQL { pubkey, slot }` field (`accountMetadata` in the schema).                      |
| `<Instruction>GraphQL` | `instructions::graphql`                       | GraphQL object for one instruction, with `instruction_metadata: InstructionMetadataGraphQL { signature, instruction_index, stack_height, slot }` and `accounts: Json`. |

`QueryRoot` fields (Juniper exposes them in camelCase):

| Field                                        | Arguments                                      | Returns                                          | Query                                                             |
| -------------------------------------------- | ---------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------- |
| `<account>` (for example `lbPair`)           | `pubkey: String!`                              | the account or `null`                            | `Lookup` by pubkey                                                |
| `list<Account>` (for example `listLbPair`)   | `limit: Int!`, `offset: Int!`                  | list                                             | `ORDER BY __slot DESC`                                            |
| `<instruction>` (for example `swap`)         | `signature: String!`, `instructionIndex: Int!` | list of every row for that top-level instruction | `ORDER BY __stack_height ASC`                                     |
| `list<Instruction>` (for example `listSwap`) | `limit: Int!`, `offset: Int!`                  | list                                             | `ORDER BY __slot DESC, __signature DESC, __instruction_index ASC` |

Rows that fail to convert are skipped silently.

**Example.** Serve a generated decoder's schema over the tables written by [`PostgresInstructionProcessor`](https://carbon.sevenlabs.org/reference/postgres.md#postgresinstructionprocessor):

`src/main.rs`:

```rust
use {
    carbon_core::graphql::server::{build_schema, graphql_router},
    carbon_meteora_dlmm_decoder::graphql::{context::GraphQLContext, QueryRoot},
    std::sync::Arc,
};

#[tokio::main]
async fn main() {
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let pool = sqlx::PgPool::connect(&database_url).await.unwrap();

    let schema = build_schema(QueryRoot);
    let app = graphql_router(schema, GraphQLContext { pool: Arc::new(pool) });

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
```

```graphql
{
  listSwap(limit: 5, offset: 0) {
    instructionMetadata { signature slot }
    amountIn
    minAmountOut
  }
}
```

Each generated `QueryRoot` is a distinct type, so one router serves one decoder. To serve several programs, wrap their query roots in a query type of your own that delegates to each.

## Example: a minimal server

A complete server with a custom query, no database required. Copy it to check your setup before wiring Postgres.

`Cargo.toml`:

```toml
[dependencies]
carbon-core = { version = "2.0.0", features = ["graphql"] }
carbon-meteora-dlmm-decoder = "2.0.0"
juniper = "0.16.2"
axum = "0.8.7"
tokio = { version = "1.48.0", features = ["full"] }
```

`src/main.rs`:

```rust
use {
    carbon_core::graphql::{
        primitives::{Pubkey, U64},
        server::{build_schema, graphql_router},
    },
    juniper::{graphql_object, GraphQLObject},
};

#[derive(Clone)]
struct Context {
    slot: u64,
}

impl juniper::Context for Context {}

#[derive(GraphQLObject)]
struct Status {
    slot: U64,
    program: Pubkey,
}

struct Query;

#[graphql_object(context = Context)]
impl Query {
    fn status(context: &Context) -> Status {
        Status {
            slot: U64(context.slot),
            program: Pubkey(carbon_meteora_dlmm_decoder::PROGRAM_ID),
        }
    }
}

#[tokio::main]
async fn main() {
    let schema = build_schema(Query);
    let app = graphql_router(schema, Context { slot: 380_000_000 });

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
    println!("GraphiQL: http://127.0.0.1:8080/graphiql");
    axum::serve(listener, app).await.unwrap();
}
```

```sh
cargo run
```

Output:

```text
GraphiQL: http://127.0.0.1:8080/graphiql
```

In another terminal:

```sh
curl -s -X POST http://127.0.0.1:8080/graphql \
  -H 'content-type: application/json' \
  -d '{"query":"{ status { slot program } }"}'
```

Output:

```json
{"data":{"status":{"slot":"380000000","program":"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo"}}}
```

The `slot` field is a string because `U64` is a string scalar.

---

Carbon 2.0.0 documentation · Web: https://carbon.sevenlabs.org/reference/graphql · Last updated 2026-11-09 · Index of all pages: https://carbon.sevenlabs.org/llms.txt
