Skip to main content

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:

middleware.ts
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:

middleware.ts
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:

middleware.ts
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:

middleware.ts
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:

middleware.ts
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:

middleware.ts
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:

  1. x-toggly-identity header
  2. toggly-identity cookie
  3. Configuration fallback
middleware.ts
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​

  1. Keep Middleware Fast: Middleware runs on every matching request
  2. Use Caching: Enable caching of definitions to reduce latency
  3. Specific Matchers: Use specific path matchers to avoid unnecessary checks
  4. Handle Errors: Always provide fallback behavior for API failures
  5. Personalized decisions: Pass per-call targeting overrides; do not mutate shared client defaults
  6. Feature Defaults: Provide sensible defaults for when API is unreachable
  7. Entity gates: Pass a canonical entity as the third evaluation argument
  8. Flush telemetry: After checks, flushTelemetry() or scheduleFlush(waitUntil) — edge flush intervals default to 0

Example: Complete Middleware​

middleware.ts
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*'],
}