Layout Implementation: - Fixed import to use DefaultTemplate from '@payloadcms/next/templates' - Added proper template props interface with i18n, locale, payload, etc. - Restructured component to use DefaultTemplate wrapper correctly - Created FeatureFlagsContent as child component for template Template Structure: - Component now receives standard Payload admin view props - DefaultTemplate provides proper admin layout with sidebar navigation - All template props (i18n, locale, params, payload, permissions, etc.) are passed through - Maintains theme integration and responsive design within admin layout The feature flags dashboard now properly integrates with Payload's admin layout system, including the navigation sidebar and standard admin styling, while preserving all spreadsheet functionality and inline editing capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
@xtr-dev/payload-feature-flags
Feature flags plugin for Payload CMS v3. Manage feature toggles, A/B tests, and rollouts from your admin panel.
⚠️ Pre-release Warning: This package is currently in active development (v0.0.x). Breaking changes may occur before v1.0.0. Not recommended for production use.
Features
- 🚀 Easy setup - Add to your Payload config and you're done
- 🎛️ Admin dashboard - Manage flags from your Payload admin panel
- 🔄 Gradual rollouts - Roll out features to a percentage of users
- 🧪 A/B testing - Test different versions of features
- 🛣️ REST API - Check flag status via API endpoints
- 🗃️ Quick demo - Try it instantly with no database setup
Installation
npm install @xtr-dev/payload-feature-flags
# or with pnpm
pnpm add @xtr-dev/payload-feature-flags
# or with yarn
yarn add @xtr-dev/payload-feature-flags
Requirements
- Payload CMS v3.37.0+
- Node.js 18.20.2+ or 20.9.0+
- React 19.1.0+
Quick Start
Basic Setup
Add the plugin to your Payload config:
import { buildConfig } from 'payload'
import { payloadFeatureFlags } from '@xtr-dev/payload-feature-flags'
export default buildConfig({
// ... your existing config
plugins: [
payloadFeatureFlags({
// All options are optional - these are the defaults
defaultValue: false, // New flags start disabled
enableRollouts: true, // Allow percentage rollouts
enableVariants: true, // Allow A/B testing
disabled: false, // Plugin enabled
}),
],
})
Configuration Options
Available plugin options:
export type PayloadFeatureFlagsConfig = {
/**
* Default value for new feature flags
* @default false
*/
defaultValue?: boolean
/**
* Enable percentage-based rollouts
* @default true
*/
enableRollouts?: boolean
/**
* Enable variant/experiment support (A/B testing)
* @default true
*/
enableVariants?: boolean
/**
* Disable the plugin while keeping the database schema intact
* @default false
*/
disabled?: boolean
/**
* Override collection configuration
*/
collectionOverrides?: {
// Override any collection config
slug?: string // @default 'feature-flags'
access?: CollectionConfig['access']
admin?: CollectionConfig['admin']
hooks?: CollectionConfig['hooks']
timestamps?: CollectionConfig['timestamps']
versions?: CollectionConfig['versions']
// ... any other collection config
// Customize fields
fields?: (args: { defaultFields: Field[] }) => Field[]
}
}
Custom Fields and Access
Add custom fields or change permissions using collectionOverrides:
payloadFeatureFlags({
collectionOverrides: {
// Custom collection slug
slug: 'my-feature-flags',
// Custom access control
access: {
read: ({ req: { user } }) => !!user, // authenticated users only
update: ({ req: { user } }) => user?.role === 'admin', // admins only
},
// Add custom fields
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'environment',
type: 'select',
options: ['development', 'staging', 'production'],
required: true,
},
{
name: 'expiresAt',
type: 'date',
admin: {
description: 'Auto-disable this flag after this date',
},
},
],
// Disable versioning (enabled by default)
versions: false,
// Add hooks
hooks: {
beforeChange: [
async ({ data, req }) => {
// Add audit log, validation, etc.
console.log(`Flag ${data.name} changed by ${req.user?.email}`)
return data
},
],
},
},
})
Security Considerations
Collection Access Control: The feature flags collection uses Payload's standard access control system:
// Example: Secure collection access
access: {
// Option 1: Simple authentication check
read: ({ req: { user } }) => !!user, // Only authenticated users
// Option 2: More granular control
read: ({ req: { user } }) => {
if (!user) return false // No anonymous access
if (user.role === 'admin') return true // Admins see all flags
return { environment: { equals: 'public' } } // Others see public flags only
}
}
Production Security Best Practices:
For production environments, consider implementing these additional security measures:
// Example: API key authentication for external services
collectionOverrides: {
access: {
read: ({ req }) => {
// Check for API key in headers for service-to-service calls
const apiKey = req.headers['x-api-key']
if (apiKey && apiKey === process.env.FEATURE_FLAGS_API_KEY) {
return true
}
// Fall back to user authentication
return !!req.user
}
}
}
Rate Limiting: Use Payload's built-in rate limiting or implement middleware:
// Example with express-rate-limit
import rateLimit from 'express-rate-limit'
const featureFlagLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
})
// Apply to your API routes
app.use('/api/feature-flags', featureFlagLimiter)
Important: The plugin uses Payload's native REST API for the collection, which respects all access control rules.
Usage
Managing Feature Flags
Once installed, the plugin automatically:
- Creates a dedicated collection - A
feature-flagscollection (or custom name) for managing all flags - Provides a clean admin interface - Manage flags directly from the Payload admin panel
- Adds a custom dashboard view - Enhanced UI for managing flags at
/admin/feature-flags-overview - Exposes REST API endpoints - Simple endpoints for checking flag states
- Keeps your data clean - No modifications to your existing collections
Admin Interface
The plugin provides a custom admin view with enhanced UI for managing feature flags:
📍 Access: /admin/feature-flags-overview
Features:
- 📊 Dashboard Overview - Visual stats showing total, enabled, and rolling out flags
- 🔍 Search & Filter - Find flags by name/description and filter by status
- 🎛️ Quick Toggle - Enable/disable flags with visual toggle switches
- 🏷️ Smart Labels - Visual indicators for rollout percentages, A/B tests, and environments
- 📱 Responsive Design - Works seamlessly on desktop and mobile devices
The custom view provides a more user-friendly interface compared to the standard collection view, with:
- Real-time status indicators
- One-click flag toggling
- Better visual organization
- Advanced filtering capabilities
Using Feature Flags in React Server Components
The plugin provides server-side hooks for React Server Components:
import {
isFeatureEnabled,
getFeatureFlag,
isUserInRollout,
getUserVariant
} from '@xtr-dev/payload-feature-flags/rsc'
// Simple feature check
export default async function HomePage() {
const showNewDesign = await isFeatureEnabled('new-homepage-design')
return showNewDesign ? <NewHomePage /> : <LegacyHomePage />
}
// Percentage-based rollout
export default async function Dashboard({ userId }: { userId: string }) {
const inRollout = await isUserInRollout('beta-dashboard', userId)
return inRollout ? <BetaDashboard /> : <ClassicDashboard />
}
// A/B testing with variants
export default async function ProductPage({ userId }: { userId: string }) {
const variant = await getUserVariant('product-page-test', userId)
switch(variant) {
case 'layout-a':
return <ProductLayoutA />
case 'layout-b':
return <ProductLayoutB />
default:
return <DefaultProductLayout />
}
}
Using Feature Flags via REST API
The plugin uses Payload's native REST API for the collection. You can access feature flags through the standard Payload REST endpoints:
// Get all feature flags
const response = await fetch('/api/feature-flags')
const result = await response.json()
// result.docs contains the array of feature flags
// Query specific feature flags
const response = await fetch('/api/feature-flags?where[name][equals]=new-dashboard')
const result = await response.json()
if (result.docs.length > 0 && result.docs[0].enabled) {
// Show new dashboard
}
// Get only enabled flags
const response = await fetch('/api/feature-flags?where[enabled][equals]=true')
const result = await response.json()
Important Security Notes:
- API endpoints respect your collection access controls - they don't bypass security
- Configure access permissions using
collectionOverrides.access(see example above) - Anonymous users can only access flags if you explicitly allow it in access controls
API Endpoints
The plugin uses Payload's standard REST API endpoints for the feature-flags collection:
Get All Feature Flags
GET /api/feature-flags
Returns paginated feature flags:
{
"docs": [
{
"id": "...",
"name": "new-dashboard",
"enabled": true,
"rolloutPercentage": 50,
"variants": null,
"metadata": {},
"createdAt": "...",
"updatedAt": "..."
},
{
"id": "...",
"name": "beta-feature",
"enabled": true,
"rolloutPercentage": 100,
"variants": [
{ "name": "control", "weight": 50, "metadata": {} },
{ "name": "variant-a", "weight": 50, "metadata": {} }
],
"metadata": {},
"createdAt": "...",
"updatedAt": "..."
}
],
"totalDocs": 2,
"limit": 10,
"page": 1,
"totalPages": 1,
"pagingCounter": 1,
"hasPrevPage": false,
"hasNextPage": false
}
Query Specific Feature Flag
GET /api/feature-flags?where[name][equals]=new-dashboard
Returns matching feature flags:
{
"docs": [
{
"id": "...",
"name": "new-dashboard",
"enabled": true,
"rolloutPercentage": 50,
"variants": null,
"metadata": {},
"createdAt": "...",
"updatedAt": "..."
}
],
"totalDocs": 1,
"limit": 10,
"page": 1,
"totalPages": 1,
"pagingCounter": 1,
"hasPrevPage": false,
"hasNextPage": false
}
Feature Flag Schema
The plugin creates a collection with the following fields:
name(required, unique) - Unique identifier for the feature flagdescription- Description of what the flag controlsenabled(required) - Toggle the flag on/offrolloutPercentage- Percentage of users (0-100) who see this featurevariants- Array of variants for A/B testingname- Variant identifierweight- Distribution weight (all weights should sum to 100)metadata- Additional variant data
tags- Array of tags for organizationmetadata- JSON field for additional flag data
Advanced Usage
Conditional Feature Rendering
// Example: Check feature flag from your frontend
async function checkFeature(flagName: string): Promise<boolean> {
try {
const response = await fetch(`/api/feature-flags/${flagName}`)
if (!response.ok) return false
const flag = await response.json()
return flag.enabled
} catch {
return false // Default to disabled on error
}
}
// Usage in your application
if (await checkFeature('new-dashboard')) {
// Show new dashboard
} else {
// Show legacy dashboard
}
Implementing Gradual Rollouts
// Example: Hash-based rollout
function isUserInRollout(userId: string, percentage: number): boolean {
// Simple hash function for consistent user bucketing
const hash = userId.split('').reduce((acc, char) => {
return ((acc << 5) - acc) + char.charCodeAt(0)
}, 0)
return (Math.abs(hash) % 100) < percentage
}
// Check if user should see the feature
const flag = await fetch('/api/feature-flags/new-feature').then(r => r.json())
if (flag.enabled && isUserInRollout(userId, flag.rolloutPercentage)) {
// Show feature to this user
}
A/B Testing with Variants
// Example: Select variant based on user
function selectVariant(userId: string, variants: Array<{name: string, weight: number}>) {
const hash = Math.abs(userId.split('').reduce((acc, char) => {
return ((acc << 5) - acc) + char.charCodeAt(0)
}, 0))
const bucket = hash % 100
let cumulative = 0
for (const variant of variants) {
cumulative += variant.weight
if (bucket < cumulative) {
return variant.name
}
}
return variants[0]?.name || 'control'
}
// Usage
const flag = await fetch('/api/feature-flags/homepage-test').then(r => r.json())
if (flag.enabled && flag.variants) {
const variant = selectVariant(userId, flag.variants)
// Render based on variant
}
Performance Considerations
Client-Side Caching
For improved performance, consider implementing client-side caching when fetching feature flags:
// Example: Simple cache with TTL
class FeatureFlagCache {
private cache = new Map<string, { data: any; expiry: number }>()
private ttl = 5 * 60 * 1000 // 5 minutes
async get(key: string, fetcher: () => Promise<any>) {
const cached = this.cache.get(key)
if (cached && cached.expiry > Date.now()) {
return cached.data
}
const data = await fetcher()
this.cache.set(key, { data, expiry: Date.now() + this.ttl })
return data
}
}
const flagCache = new FeatureFlagCache()
// Use with the hooks
const flags = await flagCache.get('all-flags', () =>
fetch('/api/feature-flags?limit=1000').then(r => r.json())
)
Migration
Disabling the Plugin
If you need to temporarily disable the plugin (e.g., during migrations), set disabled: true in the configuration. This keeps the database schema intact while disabling plugin functionality.
payloadFeatureFlags({
disabled: true, // Plugin functionality disabled, schema preserved
})
Development
Try the Demo
Test the plugin with zero setup:
git clone https://github.com/xtr-dev/payload-feature-flags
cd payload-feature-flags
pnpm install
pnpm dev
# Visit http://localhost:3000
What you get:
- 🗃️ No database needed - Uses in-memory MongoDB
- 🎯 Sample data included - Ready-to-test feature flag
- 🔑 Auto-login - Use
dev@payloadcms.com / test - 📱 Working dashboard - See flags in action
Building the Plugin
# Install dependencies
pnpm install
# Build the plugin
pnpm build
# Run tests
pnpm test
# Run linting
pnpm lint
Development Mode
# Start development server with hot reload
pnpm dev
# Generate types
pnpm generate:types
# Generate import map
pnpm generate:importmap
API Reference
Main Plugin Export
import { payloadFeatureFlags } from '@xtr-dev/payload-feature-flags'
payloadFeatureFlags: Main plugin configuration functionPayloadFeatureFlagsConfig: TypeScript type for configuration options
Server Component Hooks (RSC Export)
import {
getFeatureFlag,
isFeatureEnabled,
getAllFeatureFlags,
isUserInRollout,
getUserVariant,
getFeatureFlagsByTag
} from '@xtr-dev/payload-feature-flags/rsc'
Available Functions:
getFeatureFlag(flagName: string)- Get complete flag dataisFeatureEnabled(flagName: string)- Simple boolean checkgetAllFeatureFlags()- Get all active flagsisUserInRollout(flagName: string, userId: string)- Check rollout percentagegetUserVariant(flagName: string, userId: string)- Get A/B test variantgetFeatureFlagsByTag(tag: string)- Get flags by tag
Troubleshooting
Common Issues
Plugin not loading:
- Ensure Payload CMS v3.37.0+ is installed
- Check that the plugin is properly added to the
pluginsarray in your Payload config
Feature flags not appearing:
- Verify that collections are specified in the plugin configuration
- Check that the plugin is not disabled (
disabled: false)
TypeScript errors:
- Ensure all peer dependencies are installed
- Run
pnpm generate:typesto regenerate type definitions
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For issues, questions, or suggestions, please open an issue on GitHub.
Changelog
See CHANGELOG.md for a list of changes.
Authors
- XTR Development Team
Acknowledgments
- Built for Payload CMS
- Inspired by modern feature flag management systems