Server-Side Usage
Use feature flags in Nitro server routes, API handlers, and middleware.
Prefer ambient EvalContext — configure identity, groups, claims, and request
headers once (like .NET AddTogglyWeb), then call isEventFeatureOn(event, 'X')
or useEventToggly(event).isFeatureOn('X') without per-call options. HTTP segment
fields are filled from the H3 event via fromHttpRequest.
Ambient EvalContext (recommended)
configureEventEvalContext
Register providers once (e.g. in a Nitro plugin). Event helpers resolve them per request and cache the result on the H3 event.
import { configureEventEvalContext } from '@ops-ai/nuxt-toggly-server'
export default defineNitroPlugin(() => {
configureEventEvalContext({
getIdentity: (event) => getCookie(event, 'userId'),
getGroups: async (event) => {
const user = await getUserFromSession(event)
return user?.roles
},
getClaims: async (event) => {
const user = await getUserFromSession(event)
return user ? { role: user.role, plan: user.plan } : undefined
},
// Or getContext: (event) => ({ identity, groups, claims, request })
})
})
defineTogglyContextMiddleware
Alternatively, resolve and cache ambient context in middleware:
import { defineTogglyContextMiddleware } from '@ops-ai/nuxt-toggly-server'
export default defineTogglyContextMiddleware({
getIdentity: (event) => getCookie(event, 'userId'),
getGroups: () => ['beta'],
getClaims: () => ({ role: 'admin' }),
})
Missing request keys are still filled from H3 headers even when you supply
getContext.
Server Utilities
useEventToggly
Get a Toggly client scoped to the current request. Ambient EvalContext (from providers / middleware) is bound automatically — no identity props needed:
export default defineEventHandler(async (event) => {
const toggly = useEventToggly(event)
const features = {
newApi: await toggly.isFeatureOn('new-api'),
betaMode: await toggly.isFeatureOn('beta-mode'),
mobileCheckout: await toggly.isFeatureOn('MobileCheckout'),
}
if (features.newApi) {
return getNewApiData()
}
return getLegacyApiData()
})
isEventFeatureOn
Quick check for a single feature in an event handler (uses ambient context):
export default defineEventHandler(async (event) => {
if (await isEventFeatureOn(event, 'new-checkout')) {
return processNewCheckout(event)
}
return processLegacyCheckout(event)
})
evaluateEventFeatureGate
Check multiple features with logical operators.
export default defineEventHandler(async (event) => {
// All features must be enabled
const hasPremiumAccess = await evaluateEventFeatureGate(
event,
['premium-tier', 'api-access'],
'all'
)
if (!hasPremiumAccess) {
throw createError({
statusCode: 403,
message: 'Premium access required',
})
}
return getPremiumData()
})
Protected Handlers
defineFeatureHandler
Wrap entire API routes with feature gates.
export default defineFeatureHandler('beta-api', async (event) => {
// This handler only executes if 'beta-api' feature is enabled
return {
data: 'beta content',
timestamp: Date.now(),
}
})
With custom error response:
export default defineFeatureHandler(
'premium-api',
async (event) => {
return getPremiumData()
},
{
statusCode: 403,
message: 'This feature requires a premium subscription',
}
)
Multiple features required:
export default defineFeatureHandler(
['admin-panel', 'api-v2'],
async (event) => {
return getAdminData()
},
{
requirement: 'all', // Both features must be enabled
statusCode: 404,
message: 'Not found',
}
)
Middleware
defineFeatureMiddleware
Create middleware that gates routes by feature flags.
export default defineFeatureMiddleware({
featureKey: 'beta-access',
statusCode: 404,
message: 'Not found',
})
Multiple features:
export default defineFeatureMiddleware({
featureKey: ['admin-panel', 'internal-tools'],
requirement: 'all',
statusCode: 403,
message: 'Access denied',
})
Route-Specific Middleware
Apply middleware to specific routes:
export default defineEventHandler(async (event) => {
const path = getRequestURL(event).pathname
// Only apply to /beta/* routes
if (path.startsWith('/api/beta/')) {
if (!(await isEventFeatureOn(event, 'beta-access'))) {
throw createError({
statusCode: 404,
message: 'Not found',
})
}
}
})
Server Caching
Features are cached server-side for performance. Long-lived Node processes also
keep definitions fresh via WebSocket live updates by default
(enableLiveUpdates: true, refreshInterval: 0). Set
enableLiveUpdates: false if you need to opt out.
export default defineNuxtConfig({
toggly: {
serverCache: true, // Enable caching (default)
serverCacheTtl: 60000, // Cache TTL: 1 minute (default)
// enableLiveUpdates: false, // opt out of server WebSocket sync
},
})
Cache Behavior
- First request fetches from Toggly API
- Subsequent requests use cached values
- Cache expires after TTL
- WebSocket push (or next TTL miss) refreshes definitions
Disable Caching
For real-time feature updates without the in-memory HTTP cache helper:
export default defineNuxtConfig({
toggly: {
serverCache: false, // Do not seed from/write server storage cache
},
})
Live WebSocket updates still apply unless enableLiveUpdates is set to
false.
Overrides (per-call EvalContext)
Prefer ambient providers above. When one check needs different identity,
groups, claims, or headers, use isServerFeatureOn (or
client.isFeatureOn(key, entity, kind, overrides)). Explicit fields win
field-by-field over ambient.
import { getRequestHeaders } from 'h3'
import { isServerFeatureOn } from '@ops-ai/nuxt-toggly-server'
export default defineEventHandler(async (event) => {
const user = await getUserFromSession(event)
const mobile = await isServerFeatureOn('MobileCheckout', {
identity: user?.id,
groups: user?.roles,
claims: { role: user?.role ?? '' },
headers: getRequestHeaders(event),
})
return { mobile }
})
| Field | Purpose |
|---|---|
identity | Percentage / Targeting |
groups | Targeting groups |
claims | UserClaims |
request | { userAgent, acceptLanguage, country } |
headers | Mapped with fromHttpRequest (explicit request wins) |
Init-time groups / claims on the module config are process-wide defaults on the shared client. Prefer ambient providers (or per-call overrides) under concurrency.
See SDK × filter matrix and Feature filters.
Entity context
Nitro handlers evaluate full definitions with Context Property filters per request entity. See Entity & page context.
Pass the domain object on each isFeatureOn call. Ambient context covers the
user (identity / groups / claims / request); entity args stay per-call.
export default defineEventHandler(async (event) => {
const order = await loadOrder(event)
const toggly = useEventToggly(event)
const express = await toggly.isFeatureOn('ExpressCheckout', order, 'Order')
return { express }
})
Register entity kinds at server startup (PUT sdk/{appKey}/contexts) unless you opt out. Browsers never perform that PUT. Unknown kind fails closed.
User Identity on Server
Configure identity once via ambient providers (see above). Handlers then check
flags without setIdentity or per-call options:
export default defineEventHandler(async (event) => {
const toggly = useEventToggly(event)
const showNewDashboard = await toggly.isFeatureOn('new-dashboard')
return { showNewDashboard }
})
Avoid mutating process-wide defaults with setIdentity on long-lived Node processes.
Error Handling
export default defineEventHandler(async (event) => {
try {
const toggly = useEventToggly(event)
const isEnabled = await toggly.isFeatureOn('my-feature')
return { isEnabled }
} catch (error) {
// Log error, return safe default
console.error('Feature check failed:', error)
return { isEnabled: false }
}
})
Environment Detection
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
return {
environment: config.public.toggly.environment,
hasAppKey: !!config.public.toggly.appKey,
}
})
Combining with Auth
export default defineEventHandler(async (event) => {
// 1. Check authentication
const user = await requireAuth(event)
// 2. Check feature flag
if (!(await isEventFeatureOn(event, 'protected-feature'))) {
throw createError({
statusCode: 403,
message: 'Feature not available',
})
}
// 3. Check user permissions
if (!user.hasPermission('read:data')) {
throw createError({
statusCode: 403,
message: 'Insufficient permissions',
})
}
return getProtectedData(user)
})
Usage & metrics
Usage checks and business metrics: Usage & metrics. Install @grpc/grpc-js and @grpc/proto-loader to send telemetry on Node/Nitro. Nitro edge uses HTTPS (telemetryTransport: 'https'). Set TOGGLY_DISABLE_TELEMETRY=1 to force telemetry off.
TypeScript
Import server types as needed:
import type {
FeatureMiddlewareOptions,
TogglyServerConfig,
} from '@ops-ai/nuxt-toggly-server'