Installation guide
The getting started page is the ten-minute tour. This page is the complete installation: every layer an existing Evolve project has to touch before an editor can sign in and push a page. Authoring spans four places in your stack, and each one gets a section below:
- The CMS service — gains the authoring GraphQL surface
- The authoring service — a new service hosting chat, library, and push routes
- The gateway — learns to forward the editor’s CMS token
- The storefront — mounts the editor chrome
Before you start
Section titled “Before you start”You will need:
- A Contentful OAuth application (Developers → OAuth applications). Its redirect URL points at your authoring service’s callback route.
- An AI provider key (Anthropic, OpenAI, or Google) for chat.
- Local HTTPS with Portless (guide).
This is not optional: the editor session cookie is shared between the
storefront, the authoring service, and the gateway on a common parent
domain (
*.evolve.localhostin dev), which requires real hostnames and HTTPS.
Shared secrets
Section titled “Shared secrets”Generate these once and configure them consistently — most installation problems trace back to a mismatch here:
| Secret | Used by | Purpose |
|---|---|---|
SESSION_ENCRYPT_KEY (32 bytes) |
authoring service, storefront, gateway | Encrypts the editor session cookie |
SESSION_SIGN_KEY (32 bytes) |
authoring service, storefront, gateway | Signs the session data cookie |
JWT_ISSUER / JWT_AUDIENCE |
authoring service, storefront, gateway | Claims validated on the session token |
AUTHORING_INTERNAL_SECRET |
authoring service, CMS service | Protects the internal OAuth exchange endpoints |
CONTENTFUL_OAUTH_CLIENT_ID / _SECRET |
CMS service | The OAuth app credentials |
The three SESSION_*/JWT consumers all read the same cookie — if any
of them uses different keys, the session silently fails verification.
1. The CMS service
Section titled “1. The CMS service”The authoring GraphQL surface (library search, page push, publish flows)
ships as ContentfulAuthoringModule in
@evolve-framework/contentful/authoring. Compose it next to the delivery
module in your CMS service:
import { ContentfulModule } from "@evolve-framework/contentful";import { ContentfulAuthoringModule } from "@evolve-framework/contentful/authoring";import { CompositeModule } from "@evolve-framework/core";import { config } from "./config.ts";
export const createModule = (): CompositeModule => { return new CompositeModule([ new ContentfulModule({ config: { spaceId: config.CONTENTFUL_SPACE_ID, environment: config.CONTENTFUL_ENVIRONMENT, accessToken: config.CONTENTFUL_ACCESS_TOKEN, previewToken: config.CONTENTFUL_PREVIEW_TOKEN, managementToken: config.CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN, }, }), new ContentfulAuthoringModule({ config: { spaceId: config.CONTENTFUL_SPACE_ID, environment: config.CONTENTFUL_ENVIRONMENT, oauthClientId: config.CONTENTFUL_OAUTH_CLIENT_ID, oauthClientSecret: config.CONTENTFUL_OAUTH_CLIENT_SECRET, internalSecret: config.AUTHORING_INTERNAL_SECRET, }, }), ]);};Add the three new fields to the service’s config class as optional
env fields — the service still boots without them, and the OAuth routes
return 503 until they are configured.
What this module brings:
- The
authoring*queries and mutations on the CMS subgraph. Every resolver requires the editor’s CMS token (forwarded by the gateway asX-Authoring-Cms-Token) and rejects withREQUIRES_AUTHORING_ACCESSotherwise — public storefront traffic is unaffected. - The
/internal/oauth/contentful/*REST routes that exchange and refresh editor OAuth tokens on behalf of the authoring service, guarded byAUTHORING_INTERNAL_SECRET.
Whether authoring runs embedded in the CMS service (shown above) or as its own subgraph is a deploy-time choice — drop the module from the composite and mount it in a separate service to split it out.
Finally, recompose the supergraph so the gateway knows the new fields:
task -d backend/services/graphql-gateway supergraph2. The authoring service
Section titled “2. The authoring service”Create a new service (the reference uses port 4007) following the
standard service anatomy. Its module mounts the editor routes, the AI
tools, and optionally the MCP endpoint and workflows — the
getting started page shows
the module code in full.
Configuration highlights beyond the shared secrets:
| Variable | Example (dev) | Purpose |
|---|---|---|
GATEWAY_URL |
http://localhost:4000/graphql |
Where authoring tools execute GraphQL |
ALLOWED_ORIGINS |
https://storefront.evolve.localhost |
CORS allowlist for the chrome |
AUTHORING_SERVER_URL |
https://authoring.evolve.localhost |
Public URL of this service |
STOREFRONT_BASE_URL |
https://storefront.evolve.localhost |
Where login redirects back to |
AUTHORING_COOKIE_DOMAIN |
evolve.localhost |
Parent domain the session cookie is set on |
CMS_CONTENTFUL_INTERNAL_URL |
http://localhost:4004 |
The CMS service’s internal OAuth routes |
CHAT_PROVIDER / CHAT_MODEL + API key |
anthropic / model id |
The chat model |
Two integration points people forget:
- Body limit: chat requests carry inline image uploads; raise the
HTTP
bodyLimit(the reference uses 25 MB) increateDomainService. - Local development: register the service in the monolith
(
backend/services/monolith/src/index.ts, import +moduleNames) and add a Portless alias (portless alias authoring.evolve 4007via the service’ssetup:portlessscript).
3. The gateway
Section titled “3. The gateway”The gateway resolves the editor’s session into a CMS token and forwards
it to subgraphs as X-Authoring-Cms-Token. This is the authoring_token
router plugin, configured in the router config:
plugins: authoring_token: enabled: true config: issuer: "${JWT_ISSUER}" audience: "${JWT_AUDIENCE}" encrypt_keys: - id: "1" secret: "${AUTHORING_SESSION_ENCRYPT_KEY}" sign_keys: - id: "1" secret: "${AUTHORING_SESSION_SIGN_KEY}"The AUTHORING_SESSION_* values must equal the authoring service’s
SESSION_* keys. Two delivery paths flow through this plugin:
- Header path: the authoring service proxies chat/library/push calls
and sets
X-Authoring-Cms-Tokenitself (it decrypted the cookie server-side). The plugin passes it through. - Cookie path: the chrome calls the gateway directly from the
storefront origin with the
__authoring_sessioncookie; the plugin decrypts it and extracts the CMS token.
An invalid or expired session never fails the request — the plugin
simply forwards no token, and the CMS resolvers answer
REQUIRES_AUTHORING_ACCESS. The chrome reacts by refreshing the session
and retrying.
Also on the gateway:
- CORS: add the storefront origin to the router’s CORS policy — the chrome makes credentialed cross-origin calls.
- Production only: the router enforces persisted documents
(
require_id: true), so the chrome’s GraphQL operations must be registered as Hive App Deployment document IDs alongside your storefront’s. - The router only reads its config at boot — restart it after changes.
4. The storefront
Section titled “4. The storefront”Install @evolve-framework/authoring-core and
@evolve-framework/authoring-chrome, then:
- Mount the chrome in the layout that wraps your main pages, via a
wrapper around
AuthoringServerMount— see getting started. The mount reads the session cookie server-side and renders nothing for anonymous shoppers. - Host bindings: implement the bindings provider that bridges the
chrome to your router, block renderer, and locale logic. The platform
reference is
frontend/site/src/authoring/host-bindings.tsx. - Proxy handling: the storefront’s
src/proxy.tsmust let/authoringpaths through before i18n routing and expose the requested path to the chrome (anx-pathnameheader in the reference). - Schema registry codegen: add the
@evolve-framework/authoring-core/codegenplugin to your codegen config (getting started). - Dev origins: with Portless, add the aliases to
allowedDevOriginsinnext.config.ts— Next.js otherwise blocks cross-origin dev resources, which silently prevents all client hydration (the chrome then never appears):
const nextConfig: NextConfig = { allowedDevOrigins: ["storefront.evolve.localhost", "*.evolve.localhost"], // ...};Environment: the storefront needs AUTHORING_SERVER_URL, the gateway
URL, and the same SESSION_* / JWT values as the authoring service (it
verifies the session cookie during server rendering).
5. Verify the installation
Section titled “5. Verify the installation”Work through these in order — each step isolates a layer:
-
Boot:
pnpm dev— the monolith log should show the authoring service starting alongside the others. -
Guard: query the gateway without a session:
Terminal window curl -s localhost:4000/graphql -H 'content-type: application/json' \-d '{"query":"{ authoringListPages { id } }"}'Expect a
REQUIRES_AUTHORING_ACCESSerror — that proves the CMS module is mounted and enforcing. -
Login: visit the authoring service’s login route, complete the Contentful OAuth round-trip, and confirm the
__authoring_sessioncookie is set on the parent domain (Domain=evolve.localhost). -
Chrome: open the storefront — profile badge, prompt bar, and drawer should appear.
-
End to end: open the drawer (library search exercises the cookie→gateway→CMS path), then generate and Push a page.
Common failure modes
Section titled “Common failure modes”| Symptom | Likely cause |
|---|---|
| Cookie set, but no chrome | Storefront can’t verify the session: SESSION_*/JWT mismatch with the authoring service, or blocked dev origins (check allowedDevOrigins) |
| Chrome visible, gateway calls fail with CORS errors | Storefront origin missing from the router CORS policy, or the router wasn’t restarted after the config change |
REQUIRES_AUTHORING_ACCESS while logged in |
Gateway plugin keys differ from the authoring service’s SESSION_* keys, so the cookie decrypts to nothing |
OAuth exchange returns 503 |
OAuth credentials or AUTHORING_INTERNAL_SECRET not configured on the CMS service |
| Chat works but pushes fail | The editor’s Contentful user lacks management permissions on the space |

