A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Jon Williams
Jan 29, 2026
  24
(0 votes)

Optimizely Graph Best Practices - Security, Access Control and Performance Optimisation

Introduction

Building on Part 1's content modeling and querying practices, this Part 2 focuses on the security and performance considerations essential for running Optimizely Graph in production environments.

We'll explore authentication strategies - from Single Key for public content to HMAC with role-based access control for restricted scenarios - and discuss when a Backend for Frontend (BFF) layer provides crucial security benefits. You'll also learn performance optimisation techniques including cached templates, query fragments, and approaches to eliminate inefficient query patterns.

These operational practices ensure your Graph implementation delivers content securely and efficiently at scale. In the final post of this series, we'll cover API key management, integration strategies, and governance approaches for long-term maintainability.

1. Security & Access Control

Do:

1. Use role-based API keys

If your content is publicly available then you can use the 'Single key' mechanism for authenticating and reading content from Graph.

This key can be accessed from the PaaS Portal:

Note that 'Single key' access only returns content that is accessible to the the Everyone group within the Content Management System. It is good for general website content, as it is read-only, fast and can be called directly from a frontend (headless) application.

2. Respect CMS content permissions

If you need to access restricted content (i.e. content that is not accessible by the Everyone group) then you'll need to do this using 'role-based access control' (RBAC) and preferably through HMAC.

HMAC access uses the 'App key' and 'Secret' (available from the PaaS Portal) and can be combined with `cg-username` and `cg-roles` request headers to only fetch content where one of the roles or the username specified has read access.

However, given that this mechanism requires the 'App key' and 'Secret', which could be used to provide full access to all Graph resources without restriction, it important that these are not used from frontend code. If these are needed then a Backend for Frontend (BFF) layer is recommended to securely proxy requests to Graph while keeping credentials server-side.

3. Avoid exposing draft or non-public content

Using the 'Single key' authentication mechanism only gives access to published, non-expired content that is available to everyone.

However, if using the 'App key' and 'Secret' to authenticate then it is possible to include additional headers (`cg-include-deleted`, `cg-include-hard-deleted` and `cg-include-expired`) to also include deleted and expired content. If you are using these (or user / role based access) for any reason then proceed with care, as there may be out-of-date or potentially sensitive content being exposed.

4. Be aware that GraphiQL is openly available

Be aware that anyone with your Single key can access the GraphiQL interface at https://cg.optimizely.com/app/graphiql?auth={singlekey}, which exposes your full content schema through introspection.

If you choose to expose the Single key in client-side code (as is common for a headless website) then at least be aware that this is a possibility and potentially increases the attack vector on your estate.

If you elect to implement a BFF then this can be used to proxy requests to Optimizely Graph, allowing the Single key to be hidden, as well as adding functionality like caching, rate limiting, transformation and more sophisticated authentication.

 

2. Performance Optimisation

Do:

1. Use cached templates and cached queries

This is one of the biggest performance gains to be made when querying Optimizely Graph!

Rather than 'hardcoding' queries, such as this:

query GetArticlesByCategory {
  ArticlePage(
    where: { TeaserText: { contains: "Technology" } }
    limit: 10
    locale: "en"
  ) {
    items {
      Name
      TeaserText
      RelativePath
    }
  }
}

... Using a query containing GraphQL variables, such as the following, enables Optimizely Graph's cached template feature:

query GetArticlesByCategory {
  ArticlePage(
    where: { TeaserText: { contains: "Technology" } }
    limit: 10
    locale: "en"
  ) {
    items {
      Name
      TeaserText
      RelativePath
    }
  }
}

Including the query `parameter stored=true` and the header `cg-stored-query: template`, along with this restructuring allows the translated query structure to be cached and only variable values are substituted, dramatically improving performance.

More details on this can be found in Jonas Bergqvist's blog post listed in the Related Links below. 

2. Cache responses at edge or client

Another advantage of using a BFF middleware would be to facilitate the caching of content coming from the Optimizely Graph API. 

In this situation, if the content rarely changes then this could be cached within the BFF application or by providing a more fine-grained caching strategy for clients of the BFF (or CDN).

3. Use query fragments for reusable content blocks

Define GraphQL fragments to standardise and optimise how frequently used structures (e.g. banners, nav items) are retrieved.

For example, rather than repeating the sections for `` and ``, the following query has been implemented using fragments:

# Common image fragment
fragment ImageFields on ContentReference {
  Url
  AltText
  Width
  Height
}

# Common link fragment  
fragment LinkFields on ContentReference {
  Url
  Text
  Title
  Target
}

# Common page metadata fragment
fragment PageMetadata on IContent {
  Name
  RelativePath
  _metadata {
    published
    lastModified
    locale
  }
}

# Full query using all fragments
query GetProductPage($id: String!) {
  ProductPage(where: { _metadata: { key: { eq: $id } } }) {
    items {
      ...PageMetadata
      
      ProductName
      ProductDescription
      
      ProductImage {
        ...ImageFields
      }
      
      RelatedProducts {
        ...on ProductPage {
          ProductName
          ThumbnailImage {
            ...ImageFields
          }
          ProductLink {
            ...LinkFields
          }
        }
      }
      
      CallToAction {
        ...LinkFields
      }
    }
  }
}

Whilst there will be a small performance gain from the (slightly) shorter query content, this has the added benefits of: 

  • DRY Principle: Define field selections once, reuse everywhere
  • Consistency: All queries return the same fields for the same content types
  • Maintainability: Update fields in one place, all queries automatically updated
  • Readability: Smaller queries are easier to read, understand and debug
  • Optimisation: Graph can better optimise repeated fragment usage

4. Avoid N+1 issues in nested queries

The N+1 problem occurs when you fetch a list of content items, then make separate queries to retrieve related content for each item (e.g., fetching 10 events pages, then making 10 additional queries to get each event's location). This results in poor performance.

Solution: Use GraphQL's nested query capabilities to fetch related content in a single request. Here is an example of this from the scenario above:

query GetEventsWithLocations {
  EventPage(limit: 10) {
    items {
      Name
      EventDate
      Description
      LocationReference {
        ...on LocationPage {  # Expand the reference inline
          Name
          Address
          City
          PostalCode
          Country
          Latitude
          Longitude
        }
      }
    }
  }
}

 

When querying content with ContentReference or ContentArea fields, expand those references inline using fragments rather than fetching them separately. 

If multiple queries are unavoidable, implement caching to prevent redundant requests for the same content, or use bulk queries with the in operator to fetch multiple related items at once.

Don’t:

1. Use Graph for high-frequency real-time queries (e.g. stock price tickers, rapidly changing datasets)

Optimizely Graph is optimised for content delivery, not real-time data streams. Whilst it can be used to serve data from external (non-Optimizely) systems, it excels with content that changes at human editorial pace (pages, blog articles, products, etc.) rather than data that updates multiple times per second.

If your application requires high-frequency updates like stock price tickers, live sports scores, real-time chat messages, IoT sensor readings, or rapidly changing inventory levels, these should be served from a dedicated real-time backend (Redis, WebSockets, SignalR) or a specialised time-series database. Use Graph for what it excels at - flexible querying and delivery of editorial content.

 

Summary

Running Optimizely Graph securely and efficiently in production requires balancing accessibility with control. Use Single Key authentication for public content, implement HMAC with role-based access for restricted content, and consider a Backend for Frontend (BFF) layer when exposing credentials would create security risks. Maximise performance through cached templates, query fragments, and nested queries that eliminate redundant requests.

Combined with Part 1's content modeling and querying best practices, these operational guidelines ensure your Graph implementation remains secure, performant, and maintainable as your content delivery needs evolve and scale. In the final post of this series I'll be giving our best practices for managing and maintaining the implementation, including: managing API keys, integration strategies and governance.

 

Related Links

  1. https://docs.developers.optimizely.com/platform-optimizely/docs/authentication
  2. https://docs.developers.optimizely.com/platform-optimizely/docs/api-single-key-auth
  3. https://docs.developers.optimizely.com/platform-optimizely/docs/hmac-auth
  4. https://world.optimizely.com/forum/developer-forum/cms-12/thread-container/2025/5/graph-cms-index-jobs---ignoring-restricted-content/
  5. https://docs.developers.optimizely.com/platform-optimizely/docs/cached-templates
  6. https://world.optimizely.com/blogs/Jonas-Bergqvist/Dates/2025/2/boosting-graph-query-performance-with-stored-templates/
  7. https://docs.developers.optimizely.com/platform-optimizely/docs/graphql-best-practices
Jan 29, 2026

Comments

Please login to comment.
Latest blogs
ScheduledJob for getting overview of site content usage

In one of my current project which we are going to upgrade from Optimizely 11 I needed to get an overview of the content and which content types we...

Per Nergård (MVP) | Jan 27, 2026

A day in the life of an Optimizely OMVP: Migrating an Optimizely CMS Extension from CMS 12 to CMS 13: A Developer's Guide

With Optimizely CMS 13 now available in preview, extension developers need to understand what changes are required to make their packages compatibl...

Graham Carr | Jan 26, 2026

An “empty” Optimizely CMS 13 (preview) site on .NET 10

Optimizely CMS 13 is currently available as a preview. If you want a clean sandbox on .NET 10, the fastest path today is to scaffold a CMS 12 “empt...

Pär Wissmark | Jan 26, 2026 |