# Setup

Set up your development environment for CMS integration with Next.js


Connect a Next.js App Router website to erxes using a server-side Client Portal credential. Complete these steps before the [SSR](https://erxes.io/docs/content-plug-cms-next-developers-ssr) and [CSR](https://erxes.io/docs/content-plug-cms-next-developers-csr) examples.

## 1. Prepare the erxes instance

Enable the **Content** plugin and create a Client Portal for the website using an account with the required administrative access. Confirm the website's CMS records belong to that portal. Interface labels vary by release; the backend stores the credential as [`ClientPortal.token`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/db/models/ClientPortal.ts).

Use your deployment's gateway URL. A hosted installation commonly exposes `https://YOUR_TENANT.app.erxes.io/gateway/graphql`; the local setup guide uses `http://localhost:4000/graphql`. A self-hosted proxy may expose another prefix.

Create a published test post with a title and slug. Also create a draft so you can verify that it is excluded from the website.

## 2. Obtain the portal token

Copy the token from the intended Client Portal using your authorized administration interface. Send it as `x-app-token`. This is different from an Apps token sent as `erxes-app-token`.

The [token manager](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/clientportal/services/auth/jwtManager.ts) signs a JWT containing the portal ID. The [gateway](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/src/middlewares/userMiddleware.ts) verifies its signature and loads that portal.

The `clientPortalChangeToken` implementation stores a newly signed token, but gateway verification does not compare incoming JWTs with that stored token. Do not assume changing the stored token invalidates previously issued JWTs. Coordinate credential invalidation with the deployment owner if a token was exposed.

## 3. Configure the website

Add these variables to the website's private local environment file or hosting environment:

```dotenv
ERXES_API_URL=https://YOUR_TENANT.app.erxes.io/gateway/graphql
ERXES_APP_TOKEN=REPLACE_WITH_CLIENT_PORTAL_TOKEN
ERXES_FILE_URL=https://YOUR_TENANT.app.erxes.io/gateway/read-file?key=
```

Here `ERXES_APP_TOKEN` is a website variable name for the **Client Portal token**; it is not the administrative Apps credential. Keep the private file out of version control and put empty placeholders in the project's example environment file.

Do not add these credentials to `next.config`'s `env` object or prefix them with `NEXT_PUBLIC_`. Use `server-only` to detect accidental imports into a client component's dependency graph:

```bash
npm install server-only
```

Use the equivalent command if your website uses another package manager. The examples use native `fetch`, so Apollo Client is optional. Existing Apollo applications can use the same GraphQL documents and credential boundaries.

## 4. Add the server fetch helper

Create this module in the **website**, not in the erxes product repository:

```ts
// lib/erxes/server.ts
import "server-only";

type GraphQLResponse<T> = {
  data?: T | null;
  errors?: Array<{ message: string }>;
};

export async function erxesQuery<T>(
  query: string,
  variables: Record<string, unknown> = {},
): Promise<T> {
  const apiUrl = process.env.ERXES_API_URL;
  const token = process.env.ERXES_APP_TOKEN;

  if (!apiUrl || !token) {
    throw new Error("CMS server configuration is missing");
  }

  const response = await fetch(apiUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-app-token": token,
    },
    body: JSON.stringify({ query, variables }),
    cache: "no-store",
    signal: AbortSignal.timeout(10000),
  });

  if (!response.ok) {
    throw new Error(`CMS HTTP request failed (${response.status})`);
  }

  const result = (await response.json()) as GraphQLResponse<T>;
  if (result.errors?.length || result.data == null) {
    throw new Error("CMS returned a GraphQL error or no data");
  }

  return result.data;
}
```

The helper checks HTTP and GraphQL failures separately. It uses a fixed server-configured URL and credential, and disables caching. The generic type describes the expected response; it is not runtime schema validation.

Do not expose this generic helper through an endpoint that accepts arbitrary client-supplied queries. The [CSR guide](https://erxes.io/docs/content-plug-cms-next-developers-csr) provides a fixed operation with limited inputs.

## 5. Build file URLs

Attachment URLs may already be complete URLs or may be storage keys. A website helper can make this distinction:

```ts
// lib/erxes/file-url.ts
import "server-only";

export function erxesFileUrl(value?: string | null): string {
  if (!value) return "";
  if (/^https?:\/\//i.test(value)) return value;
  if (value.startsWith("/")) return value;

  const base = process.env.ERXES_FILE_URL;
  if (!base) throw new Error("CMS file endpoint is missing");

  const url = new URL(base);
  url.searchParams.set("key", value);
  url.searchParams.set("inline", "true");
  return url.toString();
}
```

Confirm whether relative paths in your stored content belong to the website or the gateway; this helper preserves them. Pass the resulting URLs as props to browser components.

The [file route](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/routes/fileRoutes.ts) accepts a file key and options such as `inline` and `width`. If you use `next/image`, configure its remote image patterns for the actual asset hosts and paths, which can include storage/CDN hosts beyond the gateway.

## 6. Check it works

1. Fetch the published test post with the [SSR example](https://erxes.io/docs/content-plug-cms-next-developers-ssr); it renders, and the draft does not appear.
2. Send an incorrect token; the request errors.
3. In the [CSR example](https://erxes.io/docs/content-plug-cms-next-developers-csr), inspect browser requests: they reach the website's `/api/posts` endpoint and carry no erxes credential.

> [!WARNING]
> **Empty response or browser CORS errors**
>
> An empty successful response usually means the records are not associated to the portal or not published; check that before changing the query. Browser CORS errors mean browser code is calling the gateway directly; the examples route through the website's own endpoint.

For Next.js server/client boundaries, see the [official documentation](https://nextjs.org/docs/app/getting-started/server-and-client-components).

