# GraphQL

Source: https://docs.qiick.io/externa/graphql

Externa exposes a Lighthouse GraphQL endpoint at **`/api/graphql`** for the same headless surface as REST `/api/v1`. Auth, Collection access, field ACL, and `item_filter` match the Public CMS API.

> **Related**
> REST setup and matrices: [Public CMS API](https://docs.qiick.io/externa/public-cms-api). **1.x compatibility:** [API & GraphQL compatibility](https://docs.qiick.io/externa/api-compatibility). Filter dialect: [Collections API — Advanced list filters](https://docs.qiick.io/externa/collections-api#advanced-list-filters). Ready-to-run collection: **[externa-bruno](https://github.com/qiick-io/externa-bruno)** (`PublicApi/GraphQL/` — open folder in Bruno, env **Local**, or duplicate for staging). Ops cache: [Operations](https://docs.qiick.io/externa/operations) · [Environment variables](https://docs.qiick.io/externa/environment-variables).

## Endpoint

| Item       | Detail                                                                                                                                                                                               |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| URI        | **`/api/graphql` only** (route name `graphql`)                                                                                                                                                       |
| Schema     | `graphql/schema.graphql`                                                                                                                                                                             |
| Config     | `config/lighthouse.php`                                                                                                                                                                              |
| Middleware | `ResolveApiAccess` + `throttle:api` + `ApiRequestCostMiddleware` (optional cost / body caps); env `GRAPHQL_MAX_DEPTH` / `GRAPHQL_MAX_COMPLEXITY` — [API abuse controls](https://docs.qiick.io/externa/api-abuse-controls) |
| CSRF       | Excepted for `api/graphql` (and a bare `graphql` path that is **not** registered as a route)                                                                                                         |

There is **no** `/graphql` application route — clients must post to `/api/graphql`.

```bash
curl -sS http://externa-core.test/api/graphql \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ek_YOUR_SECRET" \
  -d '{"query":"{ collections { slug name } }"}'
```

Omit `Authorization` for anonymous access via the **`public`** role matrix.

## Auth

Same as REST:

| Style     | How                                        |
| --------- | ------------------------------------------ |
| Anonymous | Role **`public`** Collection access matrix |
| API key   | `Authorization: Bearer ek_…` → key’s role  |

No session cookies. Super-admin is not available via API keys.

## Queries

| Field         | Args                                                                        | Returns          |
| ------------- | --------------------------------------------------------------------------- | ---------------- |
| `collections` | —                                                                           | `[Collection!]!` |
| `collection`  | `slug: String!`                                                             | `Collection`     |
| `items`       | `collection: String!`, `filter: JSON`, `page: Int = 1`, `perPage: Int = 15` | `ItemPage!`      |
| `item`        | `collection: String!`, `id: ID!`                                            | `Item`           |

`Collection` fields: `id`, `name`, `slug`, `is_singleton`.

`Item` fields: `id`, `collection_id`, `data`, `created_at`, `updated_at`, `user_created_id`, `user_updated_id`.

`ItemPage`: `data`, `meta { total, current_page, … }`.

Note: GraphQL uses camelCase `perPage` (REST uses `per_page`). Locale flattening for translatable fields follows the same resolver rules as REST when applicable — prefer REST `?locale=` when you need explicit locale control.

### Query collections

```json
{
  "query": "{ collections { id slug name is_singleton } }"
}
```

```json
{
  "data": {
    "collections": [
      { "id": "1", "slug": "posts", "name": "Posts", "is_singleton": false }
    ]
  }
}
```

### Query items (+ collection)

```graphql
query ($slug: String!, $filter: JSON) {
  collection(slug: $slug) {
    id
    name
    is_singleton
  }
  items(collection: $slug, filter: $filter, perPage: 15) {
    data {
      id
      data
      user_created_id
    }
    meta {
      total
      current_page
    }
  }
}
```

```json
{
  "query": "query ($slug: String!, $filter: JSON) { items(collection: $slug, filter: $filter) { data { id data } meta { total current_page } } }",
  "variables": {
    "slug": "posts",
    "filter": { "status": { "_eq": "published" } }
  }
}
```

```json
{
  "data": {
    "items": {
      "data": [
        { "id": "1", "data": { "title": "Hello", "status": "published" } }
      ],
      "meta": { "total": 1, "current_page": 1 }
    }
  }
}
```

From externa-core **1.4.0**, `items` also takes `sort: [String!]` (max 3 keys, `-` = desc, same keys as REST `?sort=`), and `filter` accepts the system keys `id` / `created_at` / `updated_at` / `published_at` and depth-1 `relation.field` paths. Invalid values raise a validation error. Details: [Public CMS API — Advanced list filters](https://docs.qiick.io/externa/public-cms-api#advanced-list-filters).

```json
{
  "query": "query ($filter: JSON, $sort: [String!]) { items(collection: \"posts\", filter: $filter, sort: $sort) { data { id data } } }",
  "variables": {
    "filter": {
      "published_at": { "_gte": "2026-01-01" },
      "author.name": { "_eq": "Ada" }
    },
    "sort": ["-published_at", "title"]
  }
}
```

### Query item

```json
{
  "query": "query ($slug: String!, $id: ID!) { item(collection: $slug, id: $id) { id collection_id data created_at updated_at } }",
  "variables": { "slug": "posts", "id": "1" }
}
```

## Mutations

| Field        | Args                       | Notes                                             |
| ------------ | -------------------------- | ------------------------------------------------- |
| `createItem` | `collection`, `data`       | Same create semantics as REST                     |
| `updateItem` | `collection`, `id`, `data` | Partial update (same merge as REST `PATCH`)       |
| `deleteItem` | `collection`, `id`         | Soft-delete (boolean); no force-delete on GraphQL |

### createItem

```json
{
  "query": "mutation ($slug: String!, $data: JSON!) { createItem(collection: $slug, data: $data) { id data } }",
  "variables": {
    "slug": "posts",
    "data": { "title": "GraphQL create", "status": "draft" }
  }
}
```

```json
{
  "data": {
    "createItem": {
      "id": "42",
      "data": { "title": "GraphQL create", "status": "draft" }
    }
  }
}
```

Requires `create` on the collection for the actor’s role.

### updateItem

```json
{
  "query": "mutation ($slug: String!, $id: ID!, $data: JSON!) { updateItem(collection: $slug, id: $id, data: $data) { id data } }",
  "variables": {
    "slug": "posts",
    "id": "42",
    "data": { "status": "published" }
  }
}
```

Requires `update`. Field rules + `item_filter` apply like REST `PATCH`. From externa-core **1.3.0** ([PR #293](https://github.com/qiick-io/externa-core/pull/293)), `updateItem` merges `data` over current values — omitted fields and locales are kept; send `null` to clear. Value sync is atomic (same as REST).

### deleteItem

```json
{
  "query": "mutation ($slug: String!, $id: ID!) { deleteItem(collection: $slug, id: $id) }",
  "variables": { "slug": "posts", "id": "42" }
}
```

```json
{
  "data": { "deleteItem": true }
}
```

Requires `delete`. Soft-delete semantics match REST.

## Ops notes

- Query cache defaults to **opcache** (`LIGHTHOUSE_QUERY_CACHE_MODE`) so parsed ASTs are not stored in the database cache driver.
- Bruno: [externa-bruno](https://github.com/qiick-io/externa-bruno) `PublicApi/GraphQL/`. Pest: `tests/Feature/Api/GraphqlCollectionApiTest.php`.

## Source map

| Concern         | Location                                                                       |
| --------------- | ------------------------------------------------------------------------------ |
| Schema          | `graphql/schema.graphql`                                                       |
| Resolvers       | `app/GraphQL/Queries/*`, `app/GraphQL/Mutations/*`                             |
| Config          | `config/lighthouse.php`                                                        |
| Auth middleware | `ResolveApiAccess` + `EnforcePublicApiOrigin` (same as `/api/v1`)              |
| Bruno           | [github.com/qiick-io/externa-bruno](https://github.com/qiick-io/externa-bruno) |
