# GraphQL API

Federated endpoint and pagination


Call the federated GraphQL endpoint, authenticate, paginate, and subscribe.

For credentials, see [Authentication & Permissions](https://erxes.io/docs/api-authentication).

## Endpoint

Every backend service (Core API on 3300, each plugin on 3305+) serves an Apollo Server v4 **subgraph** at its own `/graphql` using `@apollo/subgraph`. The gateway composes them into a supergraph with Rover ([supergraph-compose.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/apollo-router/supergraph-compose.ts)), spawns Apollo Router bound to `127.0.0.1:50000` (`APOLLO_ROUTER_PORT`), and proxies the public `/graphql` to it on port **4000** (`PORT`).

Always call the gateway, never a subgraph port: the gateway's `userMiddleware` is what turns your credential into the `user`/`clientportal`/`cpuser` context headers subgraphs trust. The router config propagates **all** request headers to subgraphs (`headers.all.request.propagate.matching: ".*"`).

```bash
curl --fail http://localhost:4000/graphql \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer <team-token>" \
  --data '{"query":"query Health { __typename }"}'
```

A `200` with an `errors` array is still a failed operation: the router sets `include_subgraph_errors: all`, so resolver errors appear there. Check `errors` independently of HTTP status. Router and subgraph timeouts are 300s (`traffic_shaping`).

## Authentication headers

| Header | Principal |
| --- | --- |
| `Authorization: Bearer <jwt>` / `auth-token` cookie | Team member |
| `erxes-app-token` | Machine app (`app:<id>`) |
| `x-app-token` (+ optional `client-auth-token`) | Client Portal (+ portal user) |

Widget operations (`widgets*`, `cp*`, `helpCenterGetConfigByDomain`) skip the team-member wrapper and need only their own context; see [Authentication](https://erxes.io/docs/api-authentication) for the full check order.

## Pagination

`erxes-api-shared` defines two parameter styles; each query declares one:

- **Cursor** (`GQL_CURSOR_PARAM_DEFS`, [constants.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/constants.ts)): `limit`, `cursor`, `cursorMode` (`inclusive`/`exclusive`), `direction` (`forward`/`backward`), `orderBy` (JSON), `sortMode`, `aggregationPipeline` (`[JSON]`). Backed by `cursorPaginate`: `limit` defaults to 20 and must be 1–100; a cursor is base64 JSON of the item's sort fields plus `_id`.
- **Offset** (`GQL_OFFSET_PARAM_DEFS`): `page`, `perPage`, `sortField`, `sortDirection`; used by a minority of legacy queries.

Cursor-style lists return an envelope such as `DealsListResponse { list, totalCount, pageInfo }`; `PageInfo` is `{ hasNextPage, hasPreviousPage, startCursor, endCursor }` (from `commonTypeDefs.ts`). Feed `endCursor` back as `cursor`:

```graphql
query Deals($cursor: String) {
  deals(
    pipelineId: "PIPELINE_ID"
    limit: 20
    cursor: $cursor
    cursorMode: exclusive
    direction: forward
    orderBy: { createdAt: -1 }
  ) {
    list {
      _id
      name
      createdAt
    }
    totalCount
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
  }
}
```

First page: omit `cursor`. Next page: pass the previous `pageInfo.endCursor`. `backward` walks earlier pages; `hasPreviousPage`/`hasNextPage` flip accordingly.

## Subscriptions

The gateway mounts a `graphql-ws` server (`graphql-transport-ws` protocol, [subscription/index.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/subscription/index.ts)) on the **same** HTTP server at path `/graphql`, so `ws://<gateway>:4000/graphql`. Connection auth reads the `auth-token` cookie from the upgrade request; each plugin registers its subscription resolvers over Redis pub/sub (`hasSubscriptions` in `startPlugin`).

```ts
import { createClient } from 'graphql-ws';

const client = createClient({ url: 'wss://gateway.example.com/graphql' });
```

Proxies in front of the gateway must allow WebSocket upgrade on `/graphql`.

## Introspection and limits

- **Introspection**: enabled when `NODE_ENV=development` or `INTROSPECTION=true` in the gateway environment. In production it is off unless you opt in, and the gateway logs a warning if you do.
- **Rate limiting**: a global limiter caps each IP at 5000 requests per 15 minutes (`/health` and `/bullmq-board` exempt). Core's `/initial-setup` route is separately capped at 100 per 15 minutes.
- **Optional query armor**: setting `GRAPHQL_LIMITER` parses and validates every request body against the supergraph and enforces character limit 10,000, zero aliases, and depth ≤ 50; invalid documents get a `400` before reaching the router.

## Naming conventions

Operation names are prefixed by their module (`cms*`, `sales*`, `cp*` portal-scoped, `clientPortal*`, `widgets*` public widget) and must be unique repo-wide. The `cp`/`widgets` prefixes mean "portal/widget context", not "read-only": enforce status filters in your queries.

