Step 7 of 11 (64% complete)

Integration with Optimizely SDK

Step Code

The code for this specific step can be found on the following branch:

Click on a link to view the code for this step on GitHub.

Optimizely Feature Experimentation SDK + Next.js Integration Guide

Our implementation uses:

Setting Up the Foundation

1. Environment Variables

OPTIMIZELY_FEATURE_EXP_API_KEY=your_sdk_key_here
OPTIMIZELY_WEBHOOK_SECRET=your_webhook_secret_here
  • OPTIMIZELY_FEATURE_EXP_API_KEY → found in Settings → Environments tab OPTIMIZELY_FEATURE_EXP_API_KEY
  • OPTIMIZELY_WEBHOOK_SECRET → generated when creating a webhook OPTIMIZELY_WEBHOOK_SECRET

2. Install Packages

npm install @optimizely/optimizely-sdk
npm install crypto
npm install flags

3. Core Optimizely Integration

We start by creating a reusable Optimizely instance and fetching the datafile:

// lib/optimizely-feature-exp/index.ts
import optimizelySdk from '@optimizely/optimizely-sdk'

export const OPTIMIZELY_DATAFILE_TAG = 'optimizely_datafile'

export async function fetchDatafileFromCDN() {
  const sdkKey = process.env.OPTIMIZELY_FEATURE_EXP_API_KEY

  try {
    const response = await fetch(
      `https://cdn.optimizely.com/datafiles/${sdkKey}.json`,
      {
        next: { tags: [OPTIMIZELY_DATAFILE_TAG] },
      }
    )
    const responseJson = await response.json()
    return responseJson
  } catch (error) {
    console.log(error)
  }
}

export async function getOptimizelyInstance() {
  const datafile = await fetchDatafileFromCDN()

  const optimizelyInstance = optimizelySdk.createInstance({
    datafile: datafile as object,
  })

  return optimizelyInstance
}

What’s happening here and why it’s important:

  • We fetch the datafile once from Optimizely’s CDN. This JSON file contains all feature flag and experiment definitions.

  • We use Next.js cache tags (OPTIMIZELY_DATAFILE_TAG) so the datafile is stored and reused until explicitly refreshed.

  • We create a single Optimizely instance with the latest datafile, which avoids rebuilding it on every request.

  • This setup improves performance, consistency, and scalability while reducing unnecessary API calls - a best practice when integrating Optimizely.


4. Real-Time Datafile Updates

In the Optimizely admin panel (Settings → Webhooks), configure a webhook that triggers only on datafile updates.

OPTIMIZELY_WEBHOOK_SECRET

Then, handle webhook requests in Next.js:

// app/api/revalidate/datafile/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { revalidateTag } from 'next/cache'
import { OPTIMIZELY_DATAFILE_TAG } from '@/lib/optimizely-feature-exp'
import crypto from 'crypto'

export async function POST(req: NextRequest) {
  try {
    // Read the request body once
    const text = await req.text()

    // Verify the webhook request came from Optimizely
    const isVerified = await verifyOptimizelyWebhook(req.headers, text)

    if (!isVerified) {
      return NextResponse.json(
        { success: false, message: 'Invalid webhook request' },
        { status: 401 }
      )
    }

    revalidateTag(OPTIMIZELY_DATAFILE_TAG)
    console.log('Revalidating Optimizely datafile tag')
    return NextResponse.json({ success: true }, { status: 200 })
  } catch (error) {
    console.error('Error processing webhook:', error)
    return NextResponse.json(
      { success: false, message: 'Internal server error' },
      { status: 500 }
    )
  }
}

async function verifyOptimizelyWebhook(
  headers: Headers,
  body: string
): Promise<boolean> {
  try {
    const WEBHOOK_SECRET = process.env.OPTIMIZELY_FEATURE_EXP_WEBHOOK_SECRET
    if (!WEBHOOK_SECRET) {
      throw new Error(
        'Missing OPTIMIZELY_FEATURE_EXP_WEBHOOK_SECRET environment variable'
      )
    }

    const signature = headers.get('X-Hub-Signature')
    if (!signature) {
      throw new Error('Missing X-Hub-Signature header')
    }

    const [algorithm, hash] = signature.split('=')
    if (algorithm !== 'sha1' || !hash) {
      throw new Error('Invalid signature format')
    }

    const hmac = crypto.createHmac('sha1', WEBHOOK_SECRET)
    const digest = hmac.update(body).digest('hex')

    return crypto.timingSafeEqual(
      Buffer.from(hash, 'hex'),
      Buffer.from(digest, 'hex')
    )
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  } catch (error: any) {
    console.error('Error verifying webhook:', error?.message)
    return false
  }
}

What this does and why:

  • The webhook is called only when the datafile changes.

  • When triggered, it revalidates our cache (revalidateTag) so the next request fetches the newest datafile from the CDN.

  • This means we download datafile.json just once and only update it when Optimizely sends a webhook.

  • As a result, we get fewer API calls, faster responses, and always up-to-date experiments.


5. Using Flags in Next.js

Now we connect Optimizely with the flags package to control how our features behave:

import { reportValue } from 'flags'
import { flag } from 'flags/next'
import { COOKIE_NAME_USER_ID } from '@/lib/optimizely-feature-exp/cookies'
import { getOptimizelyInstance } from './lib/optimizely-feature-exp'

export const getCmsSaasContentVariation = flag<string>({
  key: 'cms-saas-content-variation',
  defaultValue: 'original',
  description: 'Get CMS SaaS Content Variation Key',
  options: [
    { value: 'Original', label: 'Original' },
    { value: 'Variation1', label: 'Variation1' },
    { value: 'Variation2', label: 'Variation2' },
  ],
  async decide({ cookies }) {
    const optimizely = await getOptimizelyInstance()
    let flag = 'original'

    try {
      if (!optimizely) {
        throw new Error('Failed to create client')
      }

      await optimizely.onReady({ timeout: 500 })
      const userdIdValue = cookies.get(COOKIE_NAME_USER_ID)?.value
      const userId = userdIdValue
        ? userdIdValue
        : Math.random().toString(36).substring(2)
      const context = optimizely!.createUserContext(userId)

      if (!context) {
        throw new Error('Failed to create user context')
      }

      const decision = context.decide('opticon-portfolio-demo')
      flag =
        (decision?.variables?.['cms-saas-content-variation'] as string) ??
        'original'
    } catch (error) {
      console.error('Optimizely error:', error)
    } finally {
      reportValue('cms-saas-content-variation', flag)
      return flag
    }
  },
})

export const precomputeFlags = [] as const

Explanation:

  • We use the flags package to define the structure of a flag, including default values and options.

  • In our demo app, we don’t have authentication - so the user ID is randomly generated and saved in cookies. This makes sure users get a consistent variation across sessions.

  • The Optimizely SDK decides which variation to serve, and we return the variable value from Optimizely Graph.

This setup ensures experiments are consistent per user.


Best Practices

1. Error Handling

  • Always provide fallback values
  • Log errors for debugging
  • Don't let experiments break core functionality

2. Performance

  • Use appropriate timeouts
  • Cache datafiles efficiently
  • Revalidate only on updates (via webhook)

3. User Experience

  • Ensure consistent experiences within sessions
  • Avoid flickering between variations
  • Test with real user scenarios

Conclusion

Optimizely Feature Experimentation with Next.js is a powerful setup for data-driven development.

By combining:

  • server-side rendering
  • real-time datafile updates via webhooks
  • structured flags integration

…you get a system that is fast, scalable, and reliable.

This approach helps you:

  • Reduce deployment risks
  • Improve user experiences
  • Make informed product decisions
  • Optimize business metrics

Have questions? I'm here to help!

Contact Me