Skip to content

🏁 Feature flags powered by GitHub issues and NextJS. Toggle the features of your app by ticking a checkbox in a GitHub issue. Supports server-side rendering, multiple environments, custom conditions, and can be deployed as a stand-alone feature flag server.

License

Notifications You must be signed in to change notification settings

TimMikeladze/next-flag

Repository files navigation

🏁 next-flag

Feature flags powered by GitHub issues and NextJS. Toggle the features of your app without deploying a new version by ticking a checkbox in the body of a GitHub issue.

✨ Features

  • Enable or disable features by ticking a checkbox in a GitHub issue.
  • Define feature flags across multiple environments or branches.
  • Supports React Server Side and Client Side Components. Powered by the NextJS Cache.
  • Define custom conditions that are evaluated at runtime to enable or disable features.
  • Can be deployed as a stand-alone service to manage feature flags for multiple NextJS apps.

Check-out a fully working NextJS example or jump to Getting started.

πŸ“‘ Install

npm install next-flag

yarn add next-flag

pnpm add next-flag

πŸ‘‹ Hello there! Follow me @linesofcode or visit linesofcode.dev for more cool projects like this one.

πŸ—οΈ Architecture

architecture

πŸŽ₯ Demo

demo.mov

πŸš€ Getting started

πŸ“„ Create a new issue

First, create a new issue in your repository with the following format. It is optional to include a list of environments that the feature should be enabled in.

# 🏁 Feature Flags

## WIP feature

- [x] Enabled

## New feature

- [x] Enabled

### Production

- [ ] Enabled

### Preview

- [ ] Enabled

### Development

- [ ] Enabled

πŸ™ Setup GitHub

Now let's get an auth token from GitHub and create a Webhook.

  1. Create a new personal access token in GitHub with Read access to issues and metadata.
  2. Create a GitHub Webhook by navigating to https://github.com/<OWNER>/<REPO>/settings/hooks/new
    • Set the Payload URL to https://<YOUR_DOMAIN>/api/next-flag. Hint: Use ngrok for local development.
    • Set the Content type to application/json
    • Set the Secret to a random string
    • Select the Issues event.
  3. Add the GitHub token and webhook secret to the .env file of your NextJS app.
NEXT_FLAG_GITHUB_TOKEN=""
NEXT_FLAG_WEBHOOK_SECRET=""

πŸ’» Configure your NextJS app

Finally, let's write some code to use the next-flag package.

// src/app/api/next-flag/index.ts
import { NextFlag } from 'next-flag';
import { revalidateTag, unstable_cache } from 'next/cache';

export const nf = new NextFlag({
  paths: [
    {
      repository: '<OWNER>/<REPO>',
      issue: 123,
    },
  ],
  cache: {
    revalidateTag,
    unstable_cache,
  },
});

Next, create a new API route to handle the incoming Webhook requests.

// src/app/api/next-flag/route.ts
import { NextRequest } from 'next/server';

import { nf } from '.';

export const POST = (req: NextRequest) => nf.POST(req);

export const GET = (req: NextRequest) => nf.GET(req);

You can now use the nf instance to check if a feature is enabled in your app.

This can be done in a React Server Component:

// src/app/page.tsx
'use server';

import { nf } from './api/next-flag';

export default async function Page() {
  const wipFeatureEnabled = await nf.isFeatureEnabled('wip-feature');

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
}

Or in a React Client Component:

// src/app/components/Feature.tsx
'use client';

import { useNextFlag } from 'next-flag/react';

export const Feature = () => {
  const nf = useNextFlag();

  if (nf.loading) {
    return null;
  }

  const wipFeatureEnabled = nf.isFeatureEnabled('wip-feature');

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
};

You can also wrap your client side app with the NextFlagProvider to fetch features once on mount and provide them to child components when using the useNextFlag hook.

// src/app/components/Feature.tsx
'use client';

import { NextFlagProvider, useNextFlag } from 'next-flag/react';

const ContextProvider = () => {
  return (
    <NextFlagProvider>
      <Component />
    </NextFlagProvider>
  );
};

const Component = () => {
  const nf = useNextFlag();
  const gettingStarted = nf.isFeatureEnabled('getting-started');
  return (
    <>
      {gettingStarted && (
        <p>
          Get started by editing&nbsp;
          <code className={styles.code}>src/app/page.tsx</code>
        </p>
      )}
    </>
  );
};

πŸ’ͺ Advanced Usage

🚦 Conditions

Each feature flag can have a list of conditions that must be met for the feature to be enabled. Conditions are defined as a list of expressions that are evaluated at runtime. If any of the expressions return false, the feature will be disabled.

To get started, add a #### Conditions subheading to the feature issue and list the conditions as a series of checkboxes. If all conditions are met, the feature will be enabled. If a condition checkbox is unchecked, it will be ignored during evaluation. In other words, if a condition checkbox is not checked, it will not affect the feature flag.

# 🏁 Feature Flags

## My feature

- [x] Enabled

#### Conditions

- [ ] Only if admin

Now define how the condition is evaluated during runtime.

  1. Define a requestToContext function that takes a request object and returns a context object. The context object is passed to the condition functions.
  2. For each path, define a condition function that takes the context object and returns a boolean.

The requestToContext is a good place to extract information from the request object that is needed to evaluate the conditions. For example, you can extract cookies or headers from the request object to determine if a user is signed in.

❗ Important: The requestToContext function is only called when communicating with the NextFlag API over HTTP. If you are using the NextFlag directly in a server-side component, you must build the context object yourself and pass it to the isFeatureEnabled method directly.

// src/app/api/next-flag/index.ts
import { NextFlag } from 'next-flag';
import { revalidateTag, unstable_cache } from 'next/cache';

export const nf = new NextFlag({
  paths: [
    {
      repository: 'TimMikeladze/next-flag',
      issue: 3,
      conditions: {
        'only-if-admin': (context) => context.isAdmin,
      },
    },
  ],
  async requestToContext(req) {
    return {
      isAdmin: false,
    };
  },
  cache: {
    revalidateTag,
    unstable_cache,
  },
});
// src/app/page.tsx
'use server';

import { nf } from './api/next-flag';

export default async function Page() {
  const wipFeatureEnabled = await nf.isFeatureEnabled('wip-feature', {
    context: {
      isAdmin: true,
    }
  });

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
}

When using next-flag in a server-side component, you can also pass an and or or async function to the isFeatureEnabled method options to define extra in-line conditions that must be met for the feature to be enabled.

// src/app/page.tsx
'use server';

export default async function Page() {
  const wipFeatureEnabled = await nf.isFeatureEnabled('wip-feature', {
    and: () => true,
  });

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
}

🏝️ Multiple environments or branches

By default next-flag will try to read process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.NEXT_PUBLIC_ENV || process.env.NEXT_PUBLIC_STAGE || process.env.VERCEL_ENV || process.env.ENV || process.env.STAGE || process.env.NODE_ENV to determine the current environment.

You can customize how the current environment is determined during runtime by passing a getEnvironment function to the NextFlag constructor.

To associate a feature with a specific environment, add a subheading to the feature issue with the name of the environment (case-insensitive).

  • When using multiple environments, the top-level feature flag will control whether the feature is enabled or disabled.
  • If the top-level feature flag is disabled, the feature will be disabled in all environments.
  • If the top-level feature flag is enabled, then the environment-specific flags will determine whether the feature is enabled.
# 🏁 Feature Flags

## My feature

- [x] Enabled

### Production

- [ ] Enabled

### Preview

- [ ] Enabled

### Development

- [ ] Enabled

βœ… Getting all features

You can always get all features by calling the getFeatures method. You can also open the /api/next-flag route in your browser to see the enabled features as a JSON array.

import { nf } from './api/next-flag';
import { getFeatures, isFeatureEnabled } from 'next-flag/client';

// server side
const features = await nf.getFeatures();
// or client side with an HTTP request
const features = await getFeatures();
// check if a feature is enabled
const wipFeatureEnabled = await isFeatureEnabled('wip-feature');

πŸ“¦ Deploying a stand-alone next-flag server

You can deploy the next-flag server as a separate NextJS app and use it as a feature flagging service for multiple NextJS apps.

  1. Follow the steps above to setup GitHub and create a new NextJS app.
  2. When initializing the NextFlag instance, pass multiple projects to the paths option and set standalone to true.
  3. Deploy this NextJS app somewhere...
  4. In a different NextJS app:
    1. Configure the .env file with a NEXT_PUBLIC_NEXT_FLAG_PROJECT and NEXT_PUBLIC_NEXT_FLAG_ENDPOINT.
    2. Use isFeatureEnabled from the next-flag/client package to check if a feature is enabled in a React Server Component.
    3. Use the useNextFlag hook from the next-flag/react package to check if a feature is enabled in a React Client Component.
NEXT_PUBLIC_NEXT_FLAG_PROJECT="project-1"
NEXT_PUBLIC_NEXT_FLAG_ENDPOINT="https://<YOUR_DOMAIN>/api/next-flag"
// src/app/api/next-flag/index.ts
import { NextFlag } from 'next-flag';
import { revalidateTag, unstable_cache } from 'next/cache';

export const nf = new NextFlag({
  standalone: true,
  paths: [
    {
      project: 'project-1',
      repository: '<OWNER>/<REPO>',
      issue: 123,
    },
    {
      project: 'project-2',
      repository: '<OWNER>/<REPO>',
      issue: 124,
    },
  ],
  cache: {
    revalidateTag,
    unstable_cache,
  },
});

When running in stand-alone mode with multiple projects, you can pass the project option to the isFeatureEnabled method to check if a feature is enabled in a specific project.

You can pass an environment option to the isFeatureEnabled method to check if a feature is enabled in a specific environment.

These options will override the default values pulled from the environment variables.

import { isFeatureEnabled, getFeatures } from 'next-flag/client';

await isFeatureEnabled('wip-feature', {
  project: 'project-1',
  environment: 'development',
});

⛔️ Usage without a Webhook

Using a Github Webhook is optional, but highly recommended. The webhook is responsible for invalidating the NextJS Cache. Without this mechanism, caching of feature flags will be disabled and the feature flags will be fetched on every request.

If you don't want to use a Webhook simply omit the NEXT_FLAG_WEBHOOK_SECRET from the .env file.

πŸ“š TSDoc

🧰 Functions

βš™οΈ getFeatures

Function Type
getFeatures (props?: GetFeaturesArgs) => Promise<GetFeatures>

βš™οΈ isFeatureEnabled

Function Type
isFeatureEnabled (feature: string or string[], options?: IsFeatureEnabledOptions) => Promise<boolean>

βš™οΈ useNextFlag

Function Type
useNextFlag (props?: UseNextFlagHookProps) => { loading: boolean; features: GetFeatures; error: Error or undefined; isFeatureEnabled: (feature: string or string[]) => boolean; }

βš™οΈ NextFlagProvider

Function Type
NextFlagProvider (props: NextFlagProviderProps) => Element or null

πŸ”§ Constants

βš™οΈ NextFlagContext

Constant Type
NextFlagContext Context<GetFeatures or undefined>

🏭 NextFlag

Methods

βš™οΈ GET

Method Type
GET (req: NextRequest) => Promise<NextResponse<GetFeatures>>

βš™οΈ isFeatureEnabled

Method Type
isFeatureEnabled (feature: string or string[], options?: { and?: (() => boolean or Promise<boolean>) or undefined; context?: Context or undefined; environment?: string or undefined; or?: (() => boolean or Promise<...>) or undefined; project?: string or undefined; }) => Promise<...>

βš™οΈ getFeatures

Method Type
getFeatures (options?: { context?: Context or undefined; environment?: string or undefined; project?: string or undefined; }) => Promise<GetFeatures>

βš™οΈ POST

Method Type
POST (req: NextRequest) => Promise<NextResponse<{ error: string; }> or NextResponse<{ success: boolean; }>>

About

🏁 Feature flags powered by GitHub issues and NextJS. Toggle the features of your app by ticking a checkbox in a GitHub issue. Supports server-side rendering, multiple environments, custom conditions, and can be deployed as a stand-alone feature flag server.

Topics

Resources

License

Stars

Watchers

Forks