GraphQL API

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

For credentials, see Authentication & Permissions.

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), 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: ".*").

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

HeaderPrincipal
Authorization: Bearer <jwt> / auth-token cookieTeam member
erxes-app-tokenMachine 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 for the full check order.

Pagination

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

  • Cursor (GQL_CURSOR_PARAM_DEFS, 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:

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) 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).

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.

Was this helpful?