next-flag
v1.3.0
Published
π 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, and can be deployed as a stand-alone feature flag server.
Downloads
13
Maintainers
Readme
π 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
- [x] Enable or disable features by ticking a checkbox in a GitHub issue.
- [x] Define feature flags across multiple environments or branches.
- [x] Supports React Server Side and Client Side Components. Powered by the NextJS Cache.
- [x] Define custom conditions that are evaluated at runtime to enable or disable features.
- [x] 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
π₯ Demo
https://github.com/TimMikeladze/next-flag/assets/702718/d1707ae0-f9cf-4f80-a20f-6a6f9f715dc2
π 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.
- Create a new personal access token in GitHub with Read access to issues and metadata.
- 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.
- Set the Payload URL to
- 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
<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.
- Define a
requestToContext
function that takes a request object and returns a context object. The context object is passed to the condition functions. - 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 theNextFlag
API over HTTP. If you are using theNextFlag
directly in a server-side component, you must build the context object yourself and pass it to theisFeatureEnabled
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.
- Follow the steps above to setup GitHub and create a new NextJS app.
- When initializing the
NextFlag
instance, pass multiple projects to thepaths
option and setstandalone
totrue
. - Deploy this NextJS app somewhere...
- In a different NextJS app:
- Configure the
.env
file with aNEXT_PUBLIC_NEXT_FLAG_PROJECT
andNEXT_PUBLIC_NEXT_FLAG_ENDPOINT
. - Use
isFeatureEnabled
from thenext-flag/client
package to check if a feature is enabled in a React Server Component. - Use the
useNextFlag
hook from thenext-flag/react
package to check if a feature is enabled in a React Client Component.
- Configure the
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
:toolbox: Functions
:gear: getFeatures
| Function | Type |
| ------------- | --------------------------------------------------- |
| getFeatures
| (props?: GetFeaturesArgs) => Promise<GetFeatures>
|
:gear: isFeatureEnabled
| Function | Type |
| ------------------ | -------------------------------------------------------------------------------------- |
| isFeatureEnabled
| (feature: string or string[], options?: IsFeatureEnabledOptions) => Promise<boolean>
|
:gear: useNextFlag
| Function | Type |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| useNextFlag
| (props?: UseNextFlagHookProps) => { loading: boolean; features: GetFeatures; error: Error or undefined; isFeatureEnabled: (feature: string or string[]) => boolean; }
|
:gear: NextFlagProvider
| Function | Type |
| ------------------ | --------------------------------------------------- |
| NextFlagProvider
| (props: NextFlagProviderProps) => Element or null
|
:wrench: Constants
:gear: NextFlagContext
| Constant | Type |
| ----------------- | ----------------------------------- |
| NextFlagContext
| Context<GetFeatures or undefined>
|
:factory: NextFlag
Methods
:gear: GET
| Method | Type |
| ------ | ---------------------------------------------------------- |
| GET
| (req: NextRequest) => Promise<NextResponse<GetFeatures>>
|
:gear: 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<...>
|
:gear: getFeatures
| Method | Type |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| getFeatures
| (options?: { context?: Context or undefined; environment?: string or undefined; project?: string or undefined; }) => Promise<GetFeatures>
|
:gear: POST
| Method | Type |
| ------ | -------------------------------------------------------------------------------------------------------- |
| POST
| (req: NextRequest) => Promise<NextResponse<{ error: string; }> or NextResponse<{ success: boolean; }>>
|