Middleware & Edge
The @ops-ai/nextjs-toggly-edge package provides feature flag functionality for Next.js Middleware and Edge Runtime environments.
Installation
npm install @ops-ai/nextjs-toggly-core @ops-ai/nextjs-toggly-edge
Basic Middleware
Simple Feature Gate
Gate access to routes based on a feature flag:
import { createFeatureMiddleware } from '@ops-ai/nextjs-toggly-edge'
import type { NextRequest } from 'next/server'
const featureMiddleware = createFeatureMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
onError: (message, error) => {
// Report edge fetch/cache failures to your monitoring provider
console.warn('Toggly edge error:', message, error)
},
})
export async function middleware(request: NextRequest) {
return featureMiddleware(request, {
featureKey: 'beta-access',
redirectTo: '/waitlist',
})
}
export const config = {
matcher: '/beta/:path*',
}
Error reporting and fallback behavior
Pass onError to the edge config to observe fetch, parse, and cache failures:
const featureMiddleware = createFeatureMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
onError: (message, error) => {
monitoring.captureException(error, {
tags: { source: 'toggly-edge' },
extra: { message }
});
},
})
After one successful load, transient fetch failures preserve the last-known-good flags instead of overwriting the edge cache with defaults or empty flags. This keeps a temporary network problem from unexpectedly changing route gating.
For the shared reliability contract, see Reliability and Error Handling.
Multiple Options
Configure how to handle disabled features:
import { createFeatureMiddleware } from '@ops-ai/nextjs-toggly-edge'
import { NextResponse } from 'next/server'
const featureMiddleware = createFeatureMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
})
export async function middleware(request: NextRequest) {
return featureMiddleware(request, {
featureKey: 'premium-feature',
// Option 1: Redirect
redirectTo: '/upgrade',
redirectStatus: 307, // Temporary redirect
// Option 2: Rewrite (URL stays the same)
rewriteTo: '/feature-unavailable',
// Option 3: Custom handler
onDisabled: (req) => {
return new NextResponse('Feature not available', { status: 403 })
},
})
}
Path-Based Middleware
Gate multiple paths with different features:
import { createPathFeatureMiddleware } from '@ops-ai/nextjs-toggly-edge'
export const middleware = createPathFeatureMiddleware({
config: {
appKey: process.env.TOGGLY_APP_KEY!,
},
routes: [
{
path: '/beta/*',
feature: {
featureKey: 'beta-access',
redirectTo: '/waitlist',
},
},
{
path: '/admin/*',
feature: {
featureKey: 'admin-access',
redirectTo: '/unauthorized',
},
},
{
path: '/api/v2/*',
feature: {
featureKey: 'api-v2',
onDisabled: () => new Response('API v2 not available', { status: 404 }),
},
},
],
})
export const config = {
matcher: ['/beta/:path*', '/admin/:path*', '/api/v2/:path*'],
}
Regex Path Matching
Use regular expressions for complex path patterns:
export const middleware = createPathFeatureMiddleware({
config: { appKey: process.env.TOGGLY_APP_KEY! },
routes: [
{
path: /^\/users\/[^/]+\/settings/, // Match /users/:id/settings
feature: { featureKey: 'user-settings-v2', redirectTo: '/settings' },
},
{
path: /^\/api\/internal\/.*/,
feature: { featureKey: 'internal-api', onDisabled: () => new Response('Not found', { status: 404 }) },
},
],
})
Wrapping Existing Middleware
Add feature gates to your existing middleware:
import { withFeatureGate } from '@ops-ai/nextjs-toggly-edge'
import { NextResponse, type NextRequest } from 'next/server'
// Your existing middleware
async function myMiddleware(request: NextRequest) {
// Authentication, logging, etc.
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
// Wrap with feature gate
export const middleware = withFeatureGate(myMiddleware, {
config: {
appKey: process.env.TOGGLY_APP_KEY!,
},
featureKey: 'new-middleware',
onDisabled: (request) => {
// Fall back to basic response when feature is disabled
return NextResponse.next()
},
})
Feature Handler
Create middleware with full feature context:
import { createFeatureHandler } from '@ops-ai/nextjs-toggly-edge'
import { NextResponse } from 'next/server'
export const middleware = createFeatureHandler({
config: {
appKey: process.env.TOGGLY_APP_KEY!,
},
featureKey: ['premium-tier', 'beta-access'],
requirement: 'any',
handler: async (request, context) => {
const { isEnabled, featureKeys, features, identity } = context
if (!isEnabled) {
return NextResponse.redirect(new URL('/upgrade', request.url))
}
// Add feature info to headers
const response = NextResponse.next()
response.headers.set('x-feature-enabled', String(isEnabled))
response.headers.set('x-user-identity', identity ?? 'anonymous')
return response
},
})
Utility Functions
Check Feature for Request
import { isFeatureEnabledForRequest } from '@ops-ai/nextjs-toggly-edge'
export async function middleware(request: NextRequest) {
const isBetaEnabled = await isFeatureEnabledForRequest(
request,
'beta-feature',
{ appKey: process.env.TOGGLY_APP_KEY! }
)
if (isBetaEnabled) {
// Handle beta users
}
return NextResponse.next()
}
Get All Features
import { getFeaturesForRequest } from '@ops-ai/nextjs-toggly-edge'
export async function middleware(request: NextRequest) {
const features = await getFeaturesForRequest(
request,
{ appKey: process.env.TOGGLY_APP_KEY! }
)
console.log('Available features:', features)
const response = NextResponse.next()
response.headers.set('x-features', JSON.stringify(features))
return response
}
Entity context
The edge client fetches identity-agnostic definitions-signed, caches definitions, and evaluates filters locally for each call. Pass user targeting as the second argument and the entity as the third argument; neither becomes shared request state.
import { createEdgeClient } from '@ops-ai/nextjs-toggly-edge'
const client = createEdgeClient({ appKey: process.env.TOGGLY_APP_KEY! })
const on = await client.isFeatureOn('OrderBadge',
{ identity: 'user-123', groups: ['beta'], claims: { plan: 'pro' } },
{ kind: 'Order', key: String(order.id), attributes: { Status: order.status } },
)
A canonical { kind, key, attributes } entity needs no mapper. Missing entity
context fails closed for Context Property filters. Keep user identity separate
from entity attributes. See Entity & page context.
Identity Handling
Middleware resolves identity and request fields into per-call evaluation overrides. The shared client caches definitions, not a personalized evaluated response. Keep request-specific identity, groups and claims in overrides rather than assigning them to the client's default configuration.
Identity extraction order is:
x-toggly-identityheadertoggly-identitycookie- Configuration fallback
import { createFeatureMiddleware } from '@ops-ai/nextjs-toggly-edge'
const featureMiddleware = createFeatureMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
identity: 'default-identity', // Fallback identity
})
export async function middleware(request: NextRequest) {
// Identity is resolved for this request and passed as an evaluation override:
// 1. request.headers.get('x-toggly-identity')
// 2. request.cookies.get('toggly-identity')
// 3. config.identity
return featureMiddleware(request, {
featureKey: 'personalized-feature',
redirectTo: '/generic',
})
}
Configuration Options
const middleware = createFeatureMiddleware({
// Required
appKey: string,
// Optional
environment: string, // Default: 'Production'
baseUri: string, // Custom API endpoint
identity: string, // Default user identity
featureDefaults: Record<string, boolean>,
// Edge-specific
cache: boolean, // Enable edge caching (default: true)
cacheTtl: number, // Cache TTL in seconds (default: 60)
// Usage + metrics (HTTPS only — no Node gRPC)
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; edge default 0 (no timer)
metricsFlushInterval: number, // ms; edge default 0 (no timer)
})
Edge telemetry posts JSON to api/usage/stats and api/metrics. Call flushTelemetry() or scheduleFlush(waitUntil) so batches leave the isolate — see Usage & metrics. TOGGLY_DISABLE_TELEMETRY=1 forces telemetry off.
Feature Options
const options: MiddlewareFeatureOptions = {
// Required
featureKey: string | string[],
// Optional
requirement: 'all' | 'any', // For multiple features (default: 'all')
negate: boolean, // Invert the check (default: false)
// Handlers (pick one)
redirectTo: string, // Redirect URL
redirectStatus: number, // HTTP status (default: 307)
rewriteTo: string, // Rewrite URL (keeps original URL in browser)
onDisabled: (request) => Response, // Custom handler
}
Caching
Edge caching improves performance by reducing API calls:
const featureMiddleware = createFeatureMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
cache: true, // Enable caching
cacheTtl: 300, // Cache for 5 minutes
})
Cloudflare Workers
The edge client integrates with Cloudflare's cache API:
// Features are automatically cached using Cloudflare's edge cache
// The `cf` property is set on fetch requests with cache settings
Best Practices
- Keep Middleware Fast: Middleware runs on every matching request
- Use Caching: Enable caching of definitions to reduce latency
- Specific Matchers: Use specific path matchers to avoid unnecessary checks
- Handle Errors: Always provide fallback behavior for API failures
- Personalized decisions: Pass per-call targeting overrides; do not mutate shared client defaults
- Feature Defaults: Provide sensible defaults for when API is unreachable
- Entity gates: Pass a canonical entity as the third evaluation argument
- Flush telemetry: After checks,
flushTelemetry()orscheduleFlush(waitUntil)— edge flush intervals default to0
Example: Complete Middleware
import {
createPathFeatureMiddleware,
isFeatureEnabledForRequest,
} from '@ops-ai/nextjs-toggly-edge'
import { NextResponse, type NextRequest } from 'next/server'
// Path-based feature gates
const pathMiddleware = createPathFeatureMiddleware({
config: {
appKey: process.env.TOGGLY_APP_KEY!,
cache: true,
cacheTtl: 60,
},
routes: [
{ path: '/beta/*', feature: { featureKey: 'beta-access', redirectTo: '/waitlist' } },
{ path: '/admin/*', feature: { featureKey: 'admin-access', redirectTo: '/' } },
],
})
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Check path-based features first
if (pathname.startsWith('/beta') || pathname.startsWith('/admin')) {
return pathMiddleware(request, undefined as any)
}
// Custom logic for other paths
if (pathname.startsWith('/api')) {
const hasApiAccess = await isFeatureEnabledForRequest(
request,
'api-access',
{ appKey: process.env.TOGGLY_APP_KEY! }
)
if (!hasApiAccess) {
return new NextResponse('API access denied', { status: 403 })
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/beta/:path*', '/admin/:path*', '/api/:path*'],
}