Skip to main content

Play+ Feature Flag Helper: playfeature

Introduction

In the Play+ ecosystem, we believe shipping code should be empowering, not risky. This helper is based on the concept of decoupling deployment from release, a modern development practice that allows us to merge and deploy features to production safely, while precisely controlling their visibility.

Feature flags (or toggles) are essential for mitigating risk, enabling A/B testing, and personalizing user experiences at scale. This aligns directly with our Adaptive pillar, allowing the application to change dynamically based on user context or business needs. It also promotes an Intuitive development workflow by simplifying a complex topic and makes the product more Engaging by allowing for controlled experiments with new features.

Package Info

The playfeature helper is provided through the @playplus/features package, which is included by default in the Golden Path.

DescriptionPackage / Path
Golden Path (Recommended)Pre-installed (/system/play.feature.ts)
Uplift Pathnpm install @playplus/features

Folder Reference

The feature flag helper and its configuration follow our standardized folder structure for core system logic.

File / DirectoryPurpose & Guidelines
system/play.feature.tsThe core feature flag service. It abstracts the third-party provider and provides a consistent API.
config/play.feature.config.jsonUser-overridable configuration for the flag provider, client keys, and default values for offline development.

Helper - Pillars Alignment

The playfeature helper is a key enabler of our core design pillars.

PillarHow This Helper Aligns
AdaptivePrimary Pillar: Allows the application's UI and behavior to adapt in real-time based on user, region, or experimental group.
IntuitiveAbstracts the complexity of third-party SDKs into a simple, synchronous API, making it trivial for developers to use flags.
EngagingEmpowers teams to safely A/B test new, delightful features and interactions, gathering data to create more engaging experiences.

Helper Overview

The playfeature helper is your application's runtime control panel. It provides a clean, synchronous, and framework-agnostic interface to your feature flagging provider (e.g., LaunchDarkly, Optimizely, Flagsmith). Its purpose is to abstract the plumbing of flag management, so developers can focus on building features, not on SDK integration.

Behind the scenes, the helper automates the entire process:

  • Initialization: On app startup, it connects to your flag provider using the configured client key.
  • User Identification: It automatically identifies the current user to fetch targeted flags.
  • Real-time Updates: It maintains a live connection, so any change made in your provider's dashboard is reflected in the app in real-time, without a page refresh.
  • Offline Graceful Degradation: If the provider is unreachable, it falls back to default values defined in the configuration.

This means a developer can simply ask playfeature.isEnabled('my-flag') and trust that the system is handling all the complex state management in the background.

Config Options

Global configuration is managed in config/play.feature.config.json.

Config VariableDefault ValueDescriptionRecommended Value
provider"local"The name of your feature flag provider (e.g., launchdarkly, flagsmith, optimizely). local uses bootstrap values.Your provider's name.
clientKeynullThe client-side ID or key for your feature flag project.Your provider's client key.
bootstrapAn object of default flag values to use during initial load or for offline development and testing.Define key flags for testing.

Example play.feature.config.json:

{
"provider": "launchdarkly",
"clientKey": "sdk-12345-abcde",
"bootstrap": {
"feat-new-dashboard-2025-q3": false,
"enable-beta-analytics": true
}
}

Helper Methods

The helper provides a simple, consistent API for accessing flag values.

Method NameWhat It DoesMethod Signature
isEnabledChecks if a boolean feature flag is enabled. Returns defaultValue if the flag doesn't exist.isEnabled(key: string, defaultValue?: boolean): boolean
getVariantGets the string value for a multivariate or experiment flag.getVariant(key: string, defaultVariant?: string): string
onUpdateSubscribes to all flag changes from the provider, allowing the UI to react in real-time.onUpdate(callback: () => void): void
offUpdateUnsubscribes from flag changes to prevent memory leaks.offUpdate(callback: () => void): void

Usage Examples

React: The useFeature Hook

For a seamless experience in React, the starter kit provides a useFeature hook that automatically subscribes to live updates.

// hooks/useFeature.ts
import { useState, useEffect } from 'react';
import { playfeature } from '../system/play.feature';

export function useFeature(key: string, defaultValue: boolean): boolean {
const [value, setValue] = useState(() => playfeature.isEnabled(key, defaultValue));

useEffect(() => {
// A function to update state when flags change.
const handleUpdate = () => {
setValue(playfeature.isEnabled(key, defaultValue));
};

// Set initial value and subscribe to live updates.
handleUpdate();
playfeature.onUpdate(handleUpdate);

// Clean up the subscription on component unmount.
return () => {
playfeature.offUpdate(handleUpdate);
};
}, [key, defaultValue]);

return value;
}
// In a component:
// MainDashboard.tsx
import { useFeature } from '../hooks/useFeature';

function MainDashboard() {
const showNewAnalyticsCard = useFeature('feat-new-analytics-card', false);

return (
<div>
<UserInfoCard />
{showNewAnalyticsCard ? <NewAnalyticsCard /> : <OldAnalyticsCard />}
</div>
);
}

Angular: The PlayFeatureGuard for Routes

For Angular, a CanActivate guard is the perfect way to toggle access to entire feature routes.

// guards/play-feature.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router';
import { playfeature } from '@playplus/core'; // Assuming helper is available

@Injectable({ providedIn: 'root' })
export class PlayFeatureGuard implements CanActivate {
constructor(private router: Router) {}

canActivate(route: ActivatedRouteSnapshot): boolean {
const flagName = route.data['featureFlag'];
if (flagName && playfeature.isEnabled(flagName)) {
return true; // Allow access to the route
}

// If flag is off or doesn't exist, redirect away
this.router.navigate(['/dashboard']);
return false;
}
}
// app-routing.module.ts
import { PlayFeatureGuard } from './guards/play-feature.guard';

const routes: Routes = [
{
path: 'next-gen-dashboard',
loadChildren: () => import('./next-gen/dashboard.module').then(m => m.DashboardModule),
canActivate: [PlayFeatureGuard],
data: { featureFlag: 'feat-next-gen-dashboard-enabled' }
}
];

Additional Info

Why We Created This Helper

Without a centralized helper, each developer would need to:

  1. Integrate and learn a complex, third-party feature flag SDK.
  2. Manually handle user identification for targeted rollouts.
  3. Build their own logic for real-time updates and subscriptions.
  4. Write boilerplate code to handle cases where the flag provider is offline.

This is inefficient and error-prone. The playfeature helper abstracts all of this into a single, reliable system. It provides a consistent, tested interface so developers can add or check a feature flag in a single line of code, trusting that the underlying complexity is managed for them.

Best Practices

  • Keep Flags Short-Lived: Flags are temporary. Create a process for cleaning them up after a feature is fully rolled out to avoid technical debt.
  • Use Descriptive Names: A good name like feat-newDashboard-rollout-2025-q3 is self-documenting. It tells you the feature, its purpose, and its expected lifespan.
  • Abstract Flag Checks: Don't sprinkle playfeature.isEnabled() calls directly in JSX. Encapsulate the logic in a variable, hook (useFeature), or function for better readability and maintenance.
  • Always Provide a Default Value: This ensures your application behaves predictably if the flagging service is down or a flag is deleted.

Developer Checklist

  • Does my new flag have a descriptive name and a cleanup plan?
  • Have I provided a safe defaultValue for my isEnabled check?
  • Is the flag check abstracted (e.g., in a variable) rather than being inline in the UI logic?
  • Have I added the flag to the bootstrap config for offline testing?
  • If the flag is long-term, have I discussed its purpose with the team?

Governance & Hygiene

As flag usage grows, discipline is key. Consider establishing formal governance rules:

  • Flag Types: Categorize flags to clarify their purpose (e.g., release-toggle, experiment, ops-switch, kill-switch).
  • Environments: Ensure your provider supports toggling flags independently across different environments (dev, staging, prod).
  • Auditing: Your provider should have an audit log to track who created, toggled, or removed a flag.
  • Lifecycle Metadata: Use tags or descriptions in your provider's UI to add context like owner: team-core-ui, jira: PROJ-123, introduced: 2025-Q3.

Treat feature flags as first-class citizens in your architecture. Without good hygiene, they become technical debt.