Skip to main content

Server-Side Usage

The @ops-ai/nextjs-toggly-server package provides feature flag functionality for Server Components, Server Actions, and Route Handlers.

It evaluates flags in your Node process: initServerToggly loads definitions once (process-wide singleton) and evaluates with @ops-ai/toggly-eval. Prefer ambient EvalContext — bind identity, groups, claims, and headers once per request (like .NET AddTogglyWeb), then call isServerFeatureOn('X') / <Feature featureKey> without threading props. Per-call options remain overrides.

Filter catalog and which SDKs evaluate each filter: SDK × filter matrix.

Installation​

npm install @ops-ai/nextjs-toggly-core @ops-ai/nextjs-toggly-server

Initialization​

Initialize Toggly at the top of your Server Component or in a layout:

app/layout.tsx
import { initServerToggly } from '@ops-ai/nextjs-toggly-server'

export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
// Initialize once in the root layout (process-wide; safe to call per request)
await initServerToggly({
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
onError: (message, error) => {
console.warn('Toggly server error:', message, error)
},
})

return (
<html>
<body>{children}</body>
</html>
)
}

Error reporting and fallback behavior​

Pass onError to initServerToggly() to observe fetch, parse, cache, and refresh failures:

app/layout.tsx
await initServerToggly({
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
onError: (message, error) => {
monitoring.captureException(error, {
tags: { source: 'toggly-server' },
extra: { message }
});
},
})

After one successful load, refresh failures preserve the last-known-good flags instead of replacing rendered content with defaults or empty flags. You can inspect toggly.state.error through useServerToggly() for the latest error.

For the shared reliability contract, see Reliability and Error Handling.

Bind request-scoped EvalContext once with withEvalContext or runWithEvalContext (AsyncLocalStorage). Helpers and <Feature> merge that ambient default; you do not pass identity / groups / claims / headers on every call.

lib/toggly-request.ts
import { withEvalContext } from '@ops-ai/nextjs-toggly-server'
import { headers, cookies } from 'next/headers'

/** Bind context for checks executed inside fn on the Node server. */
export async function withRequestEvalContext<T>(
fn: () => T | Promise<T>,
): Promise<T> {
return withEvalContext(async () => {
const h = await headers()
const cookieStore = await cookies()
const userId = cookieStore.get('user-id')?.value
// Resolve groups/claims from your session / JWT as needed
return {
identity: userId,
groups: ['beta'],
claims: { role: 'admin' },
headers: h, // → fromHttpRequest for Browser / Country / Device / OS
}
}, fn)
}
app/dashboard/page.tsx
import { isServerFeatureOn } from '@ops-ai/nextjs-toggly-server'
import { withRequestEvalContext } from '@/lib/toggly-request'

export default async function DashboardPage() {
return withRequestEvalContext(async () => {
const mobile = await isServerFeatureOn('MobileCheckout')
const beta = await isServerFeatureOn('BetaBanner')

return (
<div>
{mobile ? <MobileCheckout /> : <DesktopCheckout />}
{beta ? <Banner /> : <LegacyBanner />}
</div>
)
})
}
Prefer Node server / root helper — not Edge middleware

Ambient EvalContext uses node:async_hooks (AsyncLocalStorage). Prefer a Node.js server runtime and bind in a root layout helper, Server Component, or Route Handler — not Edge middleware. Next.js Edge middleware cannot safely host node:async_hooks.

The binding covers checks executed inside the callback. Returning JSX does not guarantee that React will render nested server components inside that async context; evaluate booleans inside the callback or pass explicit context props to those components.

runWithEvalContext(ctx, fn) binds a concrete options object; withEvalContext(provider, fn) resolves a sync/async provider first. Nested binds replace (do not deep-merge) the ambient store for their duration. The process-global client identity is never mutated.

Feature Components​

Feature Component​

Conditionally render content based on a feature flag (ambient context already bound):

app/dashboard/page.tsx
import { Feature } from '@ops-ai/nextjs-toggly-server'

export default async function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>

<Feature featureKey="analytics-v2">
<AnalyticsV2 />
</Feature>
<Feature featureKey="analytics-v2" negate>
<AnalyticsV1 />
</Feature>
</div>
)
}

Negate​

Render content when a feature is disabled:

import { Feature } from '@ops-ai/nextjs-toggly-server'

export default async function MaintenancePage() {
return (
<Feature featureKey="maintenance-mode" negate>
<MaintenanceBanner />
</Feature>
)
}

FeatureVariant Component​

Dual-slot helper for on vs off content. Prefer <Feature> / <Feature negate> when you want the same off-path pattern as .NET:

import { FeatureVariant } from '@ops-ai/nextjs-toggly-server'

export default async function PricingPage() {
return (
<FeatureVariant
featureKey="checkout-flow"
enabled={<NewPricing />}
disabled={<LegacyPricing />}
/>
)
}

Programmatic API​

useServerToggly​

Access the raw shared Toggly client in Server Components. It does not automatically merge ambient request context; the example below uses process defaults. Use isServerFeatureOn for request-targeted decisions:

import { useServerToggly } from '@ops-ai/nextjs-toggly-server'

export default async function Page() {
const toggly = useServerToggly()

const isNewFeatureEnabled = await toggly.isFeatureOn('new-feature')
const express = await toggly.isFeatureOn('ExpressCheckout', order, 'Order')

return (
<div>
{isNewFeatureEnabled ? <NewFeature /> : <OldFeature />}
{express ? <ExpressButton order={order} /> : null}
</div>
)
}

getServerToggly​

Get the raw shared Toggly client (may return null if not initialized). getServerToggly() and useServerToggly() do not bind AsyncLocalStorage overrides to the returned client. Prefer isServerFeatureOn for ambient context, or pass the core client's explicit per-call overrides:

import { getServerToggly } from '@ops-ai/nextjs-toggly-server'

export default async function Page() {
const toggly = getServerToggly()

if (!toggly) {
return <div>Toggly not initialized</div>
}

const isEnabled = await toggly.isFeatureOn('my-feature')
// ...
}

Server Actions​

Use feature flags in Server Actions:

app/actions.ts
'use server'

import { checkFeature, checkFeatureGate, withFeature } from '@ops-ai/nextjs-toggly-server'

// Simple check (string = user identity)
export async function submitForm(data: FormData) {
const canUseNewSubmit = await checkFeature('new-submit-flow', userId)

if (canUseNewSubmit) {
// New submission logic
} else {
// Legacy submission logic
}
}

// Entity gate
export async function chargeOrder(order: Order) {
const express = await checkFeature('ExpressCheckout', {
context: order,
contextKind: 'Order',
})
// …
}

// Gate with multiple features
export async function processPayment(data: FormData) {
const canProcess = await checkFeatureGate({
featureKeys: ['payments-enabled', 'stripe-v2'],
requirement: 'all',
})

if (!canProcess.allowed) {
throw new Error('Payment processing is not available')
}

// Process payment
}

// Wrap entire action with feature gate
export const enhancedAction = withFeature(
'enhanced-processing',
async (data: FormData) => {
return { success: true }
},
{
onDisabled: async () => ({ success: false, reason: 'Feature not available' }),
}
)

Caching​

The server package supports caching for improved performance:

import { cachedIsFeatureOn, cachedEvaluateFeatureGate } from '@ops-ai/nextjs-toggly-server'

export default async function Page() {
// Cached for the specified duration (uses Next.js unstable_cache)
const isEnabled = await cachedIsFeatureOn('my-feature', {
revalidate: 60, // Cache for 60 seconds
tags: ['feature-flags'],
})

const canAccess = await cachedEvaluateFeatureGate(['feature-a', 'feature-b'], {
requirement: 'all',
revalidate: 60,
})

const express = await cachedIsFeatureOn('ExpressCheckout', {
context: order,
contextKind: 'Order',
revalidate: 60,
})

return <div>{isEnabled ? 'Enabled' : 'Disabled'}</div>
}

Route Handlers​

Use in API routes:

app/api/data/route.ts
import { initServerToggly, getServerToggly } from '@ops-ai/nextjs-toggly-server'
import { NextResponse } from 'next/server'

export async function GET() {
await initServerToggly({
appKey: process.env.TOGGLY_APP_KEY!,
})

const toggly = getServerToggly()!
const useNewApi = await toggly.isFeatureOn('api-v2')

if (useNewApi) {
return NextResponse.json({ version: 2, data: 'new' })
}

return NextResponse.json({ version: 1, data: 'legacy' })
}

Overrides (per-call EvalContext)​

Prefer ambient binding above. When a single check needs different identity, groups, claims, or headers, pass FeatureCheckOptions (string still means identity-only). Explicit per-call fields win field-by-field over ambient.

FieldPurpose
identityPercentage / Targeting user key
groupsTargeting group rules
claimsUserClaims (Claim + Value)
requestExplicit { userAgent, acceptLanguage, country } for HTTP segment filters
headersMapped via fromHttpRequest into request (explicit request fields win)
context / contextKindEntity / page object for Context Property (not HTTP state)
import {
Feature,
isServerFeatureOn,
fromHttpRequest,
} from '@ops-ai/nextjs-toggly-server'
import { headers } from 'next/headers'

export default async function Page({ user }: { user: SessionUser }) {
const h = await headers()

// Override ambient for this check only
const mobileCheckout = await isServerFeatureOn('MobileCheckout', {
identity: user.id,
groups: user.roles,
claims: { role: user.role },
headers: h,
})

return (
<Feature
featureKey="BetaBanner"
identity={user.id}
groups={['beta']}
claims={{ plan: user.plan }}
headers={h}
>
<Banner />
</Feature>
)
}

cachedIsFeatureOn / cachedEvaluateFeatureGate accept the same fields; cache keys hash groups, claims, request, and entity context (identity-only keys keep the historical shape).

Segment filter names and parameters: Feature filters. Matrix: SDK × filter matrix.

Entity context​

Server Components evaluate full definitions locally and apply Context Property filters per entity. See Entity & page context.

Pass the domain object on each helper or component (entity context is still per-call). Keep process-wide identity / groups / claims on initServerToggly only as defaults — use ambient EvalContext for request-scoped user targeting.

import { Feature, isServerFeatureOn } from '@ops-ai/nextjs-toggly-server'

export default async function OrderRow({ order }: { order: Order }) {
const express = await isServerFeatureOn('ExpressCheckout', {
context: order,
contextKind: 'Order',
})

return (
<Feature
featureKey="OrderBadge"
context={order}
contextKind="Order"
>
<Badge />
</Feature>
)
}

useServerToggly().isFeatureOn(key, entity, kind, overrides) accepts the same overrides as a string identity or { identity, groups, claims, request }.

The Next.js server package does not call PUT sdk/{appKey}/contexts. Register entity kinds from a Node server SDK (registerContextsOnStartup) or in the dashboard. Unknown kind or missing entity fails closed.

Identity and process-wide defaults​

Bind identity on ambient EvalContext (or pass a per-call override) so concurrent requests on a warm instance cannot overwrite each other. Do not put a per-request user id on initServerToggly({ identity }) — that sets process-wide defaults on the shared client.

import { isServerFeatureOn, runWithEvalContext } from '@ops-ai/nextjs-toggly-server'
import { cookies } from 'next/headers'

export default async function Page() {
const cookieStore = await cookies()
const userId = cookieStore.get('user-id')?.value

return runWithEvalContext({ identity: userId }, async () => {
const inRollout = await isServerFeatureOn('checkout-v2')
return inRollout ? <NewCheckout /> : <OldCheckout />
})
}

Configuration Options​

await initServerToggly({
// Required
appKey: string,

// Optional
environment: string, // Default: 'Production'
baseUri: string, // Custom API endpoint
identity: string, // Process-wide default identity
groups: string[], // Process-wide default groups
claims: Record<string, string>, // Process-wide default UserClaims
featureDefaults: Record<string, boolean>, // Default values

// Server-specific
cache: boolean, // Enable caching (default: true)
cacheTtl: number, // Cache TTL in seconds (default: 60)

// Usage + metrics (optional gRPC — see Usage & metrics)
enableUsageTracking: boolean, // Default: true when appKey is set
enableMetrics: boolean, // Default: true when appKey is set
metricsBaseUrl: string, // Default: https://app.toggly.io/
usageFlushInterval: number, // ms; default 60000
metricsFlushInterval: number, // ms; default 60000
})

Usage checks and business metrics: Usage & metrics. Install @grpc/grpc-js and @grpc/proto-loader to send telemetry. Set TOGGLY_DISABLE_TELEMETRY=1 to force telemetry off.

Best Practices​

  1. Initialize in the root layout: initServerToggly is a process-wide singleton; calling it per request is fine.
  2. Ambient EvalContext: bind identity / groups / claims / headers once with withEvalContext / runWithEvalContext on the Node server; use per-call options only as overrides. Never mutate the shared client for per-request identity.
  3. Avoid Edge middleware for ALS: bind ambient context in Node RSC / helpers, not Edge middleware (node:async_hooks).
  4. Use Caching: Enable caching for production to reduce API calls
  5. Handle Errors: Feature checks gracefully fall back to defaults on errors
  6. Revalidate Cache: Use Next.js revalidation when feature flags change

See also​