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:
Optimizely Feature Experimentation SDK + Next.js Integration Guide
Our implementation uses:
- Optimizely SDK: For feature flag management and experimentation - https://www.npmjs.com/package/@optimizely/optimizely-sdk
- Vercel Flags: For seamless integration with Next.js and deployment - https://www.npmjs.com/package/flags
- Webhook Integration: For real-time datafile updates
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_WEBHOOK_SECRET → generated when creating a webhook

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.

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.jsonjust 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
flagspackage 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!