Client Portal

Build a customer-facing site on top of an erxes Client Portal using the standalone Next.js template.

See Authentication & Permissions for the credential mechanics behind these headers. For a production portal implementation, compare Help Center, which applies the same pattern.

A Client Portal is a configuration record (ClientPortals collection) in the clientportal module in Core API. It carries a portal token plus per-portal settings: authentication options (OTP, verification, token lifetimes, delivery method), styles, and plugin-specific config such as the CMS clientPortalId that scopes public content.

Plugins expose cp* GraphQL operations (cpPosts, cpGetTickets, cpForms, clientPortalCurrentUser, …) that look up the portal from the request context instead of from a logged-in team member. That makes the portal token the only credential a public website needs.

Two credentials, not one

CredentialWhat it isTransportBecomes
Portal tokenJWT { clientPortalId } signed with JWT_TOKEN_SECRET, issued by clientPortalAdd / clientPortalChangeTokenx-app-token headerreq.clientPortal at the gateway, forwarded as the base64 clientportal header
Portal-user sessionJWT { userId, clientPortalId } issued on login/verify/reset; separate refresh token (type: "refresh")client-auth-token cookie (httpOnly) or header when the portal's deliveryMethod is headerreq.cpUser, forwarded as the base64 cpuser header

The gateway only identifies a portal user after the x-app-token has identified a valid portal, and the user must belong to that portal. Default lifetimes are 1 day (access) and 7 days (refresh), overridable per portal via auth.authConfig. clientPortalUserRefreshToken exchanges a refresh token for a new access token.

Portal-user auth flows (register, verify, credentials or OTP login, forgot/reset password, change email/phone) are implemented in backend/core-api/src/modules/clientportal/services/ and documented in docs/auth-flows.md inside that module. The mutations are the clientPortalUser* family plus clientPortalCurrentUser.

The template app

apps/client-portal-template is a standalone Next.js 16.0.3 (React 19) starter, not an Nx project, with its own package-lock.json. Apollo Client 4 plus @apollo/client-integration-nextjs provide both an RSC client and a browser client.

cd apps/client-portal-template
npm install
npm run dev   # next dev --port 3800

Two Apollo entry points exist:

  • modules/apollo/apolloClient.ts: server components. Sends x-app-token: NEXT_PUBLIC_ERXES_CP_TOKEN to ${ERXES_API_URL}/graphql with credentials: 'include' (needed for the client-auth-token cookie round trip).
  • modules/apollo/ApolloWrapper.tsx: client components ("use client"). Same token header against ${NEXT_PUBLIC_ERXES_API_URL}/graphql.

modules/cpposts/ is a worked example: getExamplePosts.ts calls the getCPExamplePosts query exposed by the clientportal module, and CPPosts.tsx renders it.

Environment variables

VariableUsed byPurpose
ERXES_API_URLserver Apollo clientGateway base URL, no trailing slash (e.g. http://localhost:4000)
NEXT_PUBLIC_ERXES_API_URLbrowser Apollo clientSame gateway, reachable from the browser
NEXT_PUBLIC_ERXES_CP_TOKENboth clientsPortal token sent as x-app-token

The template leaks the portal token to the browser

NEXT_PUBLIC_* variables end up in the client bundle, so the template sends x-app-token from the browser. The gateway CORS list must allow the portal's origin. For a hardened deployment move the token to a non-public variable read only by the RSC client (apolloClient.ts), or proxy all GraphQL through your own route handler. Never use an Apps token (erxes-app-token) in a public app.

There is no .env.example or Dockerfile in the template; copy apps/help-center's docker-entrypoint.sh pattern if you want one image to serve multiple gateways.

Walkthrough

  1. Create the portal and copy its token

    In erxes, create a Client Portal (Settings → Client portal, or via clientPortalAdd). The creation call signs and stores the portal token; rotate it later with clientPortalChangeToken. If you use the CMS or Frontline plugins, link them to this portal in their own settings.

  2. Point the template at your gateway

    ERXES_API_URL=http://localhost:4000
    NEXT_PUBLIC_ERXES_API_URL=http://localhost:4000
    NEXT_PUBLIC_ERXES_CP_TOKEN=<portal token>
    

    Restart npm run dev; loadEnvConfig in apolloClient.ts picks up .env.local.

  3. Exercise a query and a session

    getCPExamplePosts renders on the home page. Then exercise a session: call clientPortalUserRegister + clientPortalUserVerify (or clientPortalUserLoginWithCredentials) and confirm the client-auth-token cookie is set, then clientPortalCurrentUser returns the visitor.

Check it works

  1. With only x-app-token set, a cp* query works but clientPortalCurrentUser returns null; the portal is known, the visitor is anonymous.
  2. After login, the same client-auth-token cookie works from the browser and from curl (-H "client-auth-token: <jwt>").
  3. A token from a different portal sees only its own data; the gateway binds cpUser to clientPortalId.
Was this helpful?