<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[   feature flags - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[   feature flags - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 26 Aug 2026 10:16:59 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/feature-flags/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Production-Ready Feature Flag System with Next.js and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ Feature flags are powerful tools that let you control which features are visible to users without deploying new code. They enable gradual rollouts, A/B testing, and instant feature toggles, which are all essential for modern software development. In ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-production-ready-feature-flag-system-with-nextjs-and-supabase/</link>
                <guid isPermaLink="false">69851c4ec8140c13f9fa09c8</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[   feature flags ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Thu, 05 Feb 2026 22:40:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770312675718/c462d3b5-5369-45e0-ad47-c91b441fe96f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Feature flags are powerful tools that let you control which features are visible to users without deploying new code. They enable gradual rollouts, A/B testing, and instant feature toggles, which are all essential for modern software development.</p>
<p>In this article, we’ll build a real, production-ready feature flag system, not just a simple boolean toggle.</p>
<p>Specifically, we’ll implement:</p>
<ul>
<li><p>A global on/off flag to enable or disable features instantly</p>
</li>
<li><p>User-specific flags to grant access to individual users (for beta testing or internal users)</p>
</li>
<li><p>Percentage-based rollouts to gradually expose features to a subset of users</p>
</li>
<li><p>A React-powered admin dashboard to manage flags without redeploying</p>
</li>
<li><p>Client-side and server-side enforcement, so features are gated consistently everywhere</p>
</li>
</ul>
<p>By the end, we’ll finish by wiring a real Todo feature behind a feature flag, showing how entire pages and components can be safely toggled on and off in production.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-feature-flags">What Are Feature Flags?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-database-schema-design">Database Schema Design</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-supabase">Setting Up Supabase</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-core-feature-flag-logic">Building the Core Feature Flag Logic</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-react-query">Setting Up React Query</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-the-react-hook">Creating the React Hook</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-admin-dashboard">Building the Admin Dashboard</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-implementing-a-real-world-example">Implementing a Real-World Example</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-server-side-usage">Server-Side Usage</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-react-query">Why React Query?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>A basic understanding of React and Next.js</p>
</li>
<li><p>Familiarity with TypeScript</p>
</li>
<li><p>A Supabase account (free tier works perfectly)</p>
</li>
<li><p>A code editor like VS Code</p>
</li>
<li><p>Basic understanding of React Query (TanStack Query) for server state management</p>
</li>
</ul>
<h2 id="heading-what-are-feature-flags">What Are Feature Flags?</h2>
<p>Feature flags (also called feature toggles) are configuration mechanisms that let you enable or disable features in your application without changing code. Think of them as light switches for your features.</p>
<p>Here are some common use cases:</p>
<ul>
<li><p><strong>Gradual rollouts</strong>: Release a feature to 10% of users first, then gradually increase</p>
</li>
<li><p><strong>User-specific access</strong>: Enable features for beta testers or VIP users</p>
</li>
<li><p><strong>Emergency kill switches</strong>: Instantly disable a feature if something goes wrong</p>
</li>
<li><p><strong>A/B testing</strong>: Test different versions of features with different user groups</p>
</li>
</ul>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Start by creating a new Next.js project with TypeScript:</p>
<pre><code class="lang-bash">npx create-next-app@latest supabase-feature-flag --typescript --tailwind --app
<span class="hljs-built_in">cd</span> supabase-feature-flag
</code></pre>
<p>Next, install the required dependencies:</p>
<pre><code class="lang-bash">npm install @supabase/ssr @supabase/supabase-js @tanstack/react-query
</code></pre>
<p>The <code>@supabase/ssr</code> package provides server-side rendering support for Supabase, which is essential for Next.js App Router. <code>@tanstack/react-query</code> provides powerful server state management with automatic caching, invalidation, and real-time updates, which is perfect for feature flags that need to reflect changes immediately without page refreshes.</p>
<h2 id="heading-database-schema-design">Database Schema Design</h2>
<p>Before writing any code, you need to design your database schema. A feature flag needs several properties:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769945095969/86610ba1-e0c8-4c0c-a500-8cdc061555ba.webp" alt="Database-schema-design" class="image--center mx-auto" width="2300" height="1246" loading="lazy"></p>
<ul>
<li><p>A unique key to identify the flag</p>
</li>
<li><p>A name and description for human readability</p>
</li>
<li><p>An enabled/disabled state</p>
</li>
<li><p>Support for user-specific access</p>
</li>
<li><p>Support for percentage-based rollouts</p>
</li>
</ul>
<p>Here's the SQL migration that creates the <code>feature_flags</code> table:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create feature_flags table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> feature_flags (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid(),
  <span class="hljs-keyword">key</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  description <span class="hljs-built_in">TEXT</span>,
  enabled <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  enabled_for_users JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'[]'</span>::jsonb,
  enabled_for_percent <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span> <span class="hljs-keyword">CHECK</span> (enabled_for_percent &gt;= <span class="hljs-number">0</span> <span class="hljs-keyword">AND</span> enabled_for_percent &lt;= <span class="hljs-number">100</span>),
  metadata JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  updated_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);
</code></pre>
<p>Let's break down each field:</p>
<ul>
<li><p><code>id</code>: A unique identifier for each flag</p>
</li>
<li><p><code>key</code>: A unique string identifier (like "new-dashboard" or "beta-feature")</p>
</li>
<li><p><code>name</code>: A human-readable name</p>
</li>
<li><p><code>description</code>: Optional description of what the flag controls</p>
</li>
<li><p><code>enabled</code>: Global on/off switch</p>
</li>
<li><p><code>enabled_for_users</code>: JSON array of user IDs who have access</p>
</li>
<li><p><code>enabled_for_percent</code>: Percentage of users who should see the feature (0-100)</p>
</li>
<li><p><code>metadata</code>: Flexible JSON field for additional configuration</p>
</li>
<li><p><code>created_at</code> and <code>updated_at</code>: Timestamps for tracking</p>
</li>
</ul>
<p>The migration also includes indexes for performance:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create indexes for fast lookups</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_key <span class="hljs-keyword">ON</span> feature_flags(<span class="hljs-keyword">key</span>);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_enabled <span class="hljs-keyword">ON</span> feature_flags(enabled);
</code></pre>
<p>Indexes on <code>key</code> and <code>enabled</code> ensure fast queries when checking flag status.</p>
<h2 id="heading-setting-up-supabase">Setting Up Supabase</h2>
<h3 id="heading-step-1-create-a-supabase-project">Step 1: Create a Supabase Project</h3>
<p>To start, go to <a target="_blank" href="http://supabase.com">supabase.com</a> and sign up or log in. Then click on "New Project". Fill in your project details and wait for it to initialize.</p>
<h3 id="heading-step-2-run-the-migration">Step 2: Run the Migration</h3>
<p>In your Supabase dashboard, navigate to the SQL Editor:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769945875099/72595159-3301-422e-a648-49602c4088ec.png" alt="Supabase-row-level-security(RLS)-policies" class="image--center mx-auto" width="3020" height="1650" loading="lazy"></p>
<p>Then click "New Query". Copy and paste the complete migration SQL (including the indexes and RLS policies shown below) and click "Run".</p>
<p>Here's the complete migration with Row Level Security (RLS) policies:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create feature_flags table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> feature_flags (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid(),
  <span class="hljs-keyword">key</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  description <span class="hljs-built_in">TEXT</span>,
  enabled <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  enabled_for_users JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'[]'</span>::jsonb,
  enabled_for_percent <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span> <span class="hljs-keyword">CHECK</span> (enabled_for_percent &gt;= <span class="hljs-number">0</span> <span class="hljs-keyword">AND</span> enabled_for_percent &lt;= <span class="hljs-number">100</span>),
  metadata JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  updated_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-comment">-- Create indexes</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_key <span class="hljs-keyword">ON</span> feature_flags(<span class="hljs-keyword">key</span>);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_enabled <span class="hljs-keyword">ON</span> feature_flags(enabled);

<span class="hljs-comment">-- Auto-update updated_at timestamp</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> update_updated_at_column()
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TRIGGER</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
  NEW.updated_at = <span class="hljs-keyword">NOW</span>();
  RETURN NEW;
<span class="hljs-keyword">END</span>;
$$ language 'plpgsql';

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> update_feature_flags_updated_at
  <span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
  <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> update_updated_at_column();

<span class="hljs-comment">-- Enable Row Level Security</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> feature_flags <span class="hljs-keyword">ENABLE</span> <span class="hljs-keyword">ROW</span> <span class="hljs-keyword">LEVEL</span> <span class="hljs-keyword">SECURITY</span>;

<span class="hljs-comment">-- Policy: Allow public read access</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> <span class="hljs-string">"Allow public read access"</span>
  <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">SELECT</span>
  <span class="hljs-keyword">USING</span> (<span class="hljs-literal">true</span>);

<span class="hljs-comment">-- Policy: Allow public write access (for admin operations)</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> <span class="hljs-string">"Allow public write access"</span>
  <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">ALL</span>
  <span class="hljs-keyword">USING</span> (<span class="hljs-literal">true</span>)
  <span class="hljs-keyword">WITH</span> <span class="hljs-keyword">CHECK</span> (<span class="hljs-literal">true</span>);
</code></pre>
<p>The RLS policies allow:</p>
<ul>
<li><p>Public read access: Anyone can check if a feature flag is enabled</p>
</li>
<li><p>Public write access: Allows the admin dashboard to create/update flags (in production, you'd restrict this further)</p>
</li>
</ul>
<h3 id="heading-step-3-get-your-api-credentials">Step 3: Get Your API Credentials</h3>
<p>Go to Settings and then API in your Supabase project. Copy your Project URL and Publishable Key. THen create a <code>.env.local</code> file in your project root:</p>
<pre><code class="lang-bash">NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=your_publishable_key
</code></pre>
<h2 id="heading-building-the-core-feature-flag-logic">Building the Core Feature Flag Logic</h2>
<p>Now let's build the core logic for checking feature flags. You'll create separate utilities for client-side and server-side usage.</p>
<h3 id="heading-typescript-types">TypeScript Types</h3>
<p>First, define the types you'll use throughout the application:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// types/feature-flag.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> FeatureFlag {
  id: <span class="hljs-built_in">string</span>;
  key: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  description: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>;
  enabled: <span class="hljs-built_in">boolean</span>;
  enabled_for_users: <span class="hljs-built_in">string</span>[];
  enabled_for_percent: <span class="hljs-built_in">number</span>;
  metadata: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
  created_at: <span class="hljs-built_in">string</span>;
  updated_at: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> FeatureFlagCheckResult {
  enabled: <span class="hljs-built_in">boolean</span>;
  reason?: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>The <code>FeatureFlag</code> interface matches your database schema. The <code>FeatureFlagCheckResult</code> includes a <code>reason</code> field that explains why a flag is enabled or disabled, which is useful for debugging.</p>
<h3 id="heading-supabase-client-setup">Supabase Client Setup</h3>
<p>Create the Supabase client for client-side usage:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/supabase/client.ts</span>
<span class="hljs-keyword">import</span> { createBrowserClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;

<span class="hljs-keyword">const</span> supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
<span class="hljs-keyword">const</span> supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createClient = <span class="hljs-function">() =&gt;</span>
  createBrowserClient(supabaseUrl!, supabaseKey!);
</code></pre>
<p>The <code>createBrowserClient</code> function from <code>@supabase/ssr</code> creates a client optimized for browser usage.</p>
<p>For server-side usage:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/supabase/server.ts</span>
<span class="hljs-keyword">import</span> { createServerClient, <span class="hljs-keyword">type</span> CookieOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/headers"</span>;

<span class="hljs-keyword">const</span> supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
<span class="hljs-keyword">const</span> supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createClient = <span class="hljs-function">(<span class="hljs-params">cookieStore: ReturnType&lt;<span class="hljs-keyword">typeof</span> cookies&gt;</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> createServerClient(
    supabaseUrl!,
    supabaseKey!,
    {
      cookies: {
        getAll() {
          <span class="hljs-keyword">return</span> cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          <span class="hljs-keyword">try</span> {
            cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value, options }</span>) =&gt;</span>
              cookieStore.set(name, value, options)
            )
          } <span class="hljs-keyword">catch</span> {
            <span class="hljs-comment">// The `setAll` method was called from a Server Component.</span>
            <span class="hljs-comment">// This can be ignored if you have middleware refreshing</span>
            <span class="hljs-comment">// user sessions.</span>
          }
        },
      },
    },
  );
};
</code></pre>
<p>This server client handles cookies properly for Next.js server components and API routes.</p>
<h3 id="heading-client-side-feature-flag-logic">Client-Side Feature Flag Logic</h3>
<p>Create the client-side utility for checking feature flags:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/feature-flags/client.ts</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/client'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag, FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-comment">// Simple cache with 5 second TTL (React Query handles primary caching)</span>
<span class="hljs-comment">// This cache is just for reducing redundant calls within a very short window</span>
<span class="hljs-keyword">const</span> cache = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, { data: FeatureFlag | <span class="hljs-literal">null</span>; expires: <span class="hljs-built_in">number</span> }&gt;();
<span class="hljs-keyword">const</span> CACHE_TTL = <span class="hljs-number">5000</span>; <span class="hljs-comment">// 5 seconds - short enough to not interfere with React Query invalidation</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCached</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span> | <span class="hljs-title">undefined</span> </span>{
  <span class="hljs-keyword">const</span> cached = cache.get(key);
  <span class="hljs-keyword">if</span> (cached &amp;&amp; cached.expires &gt; <span class="hljs-built_in">Date</span>.now()) {
    <span class="hljs-keyword">return</span> cached.data;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-literal">undefined</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setCached</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, data: FeatureFlag | <span class="hljs-literal">null</span></span>): <span class="hljs-title">void</span> </span>{
  cache.set(key, { data, expires: <span class="hljs-built_in">Date</span>.now() + CACHE_TTL });
}
</code></pre>
<p>The cache reduces database queries by storing flag data in memory for 5 seconds. This is a secondary cache layer – React Query handles the primary caching and automatic invalidation, ensuring changes reflect immediately across all components.</p>
<p>The <code>getFeatureFlag</code> function fetches a flag from the database:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span>&gt; </span>{
  <span class="hljs-keyword">const</span> cached = getCached(key);
  <span class="hljs-keyword">if</span> (cached !== <span class="hljs-literal">undefined</span>) <span class="hljs-keyword">return</span> cached;

  <span class="hljs-keyword">const</span> supabase = createClient();
  <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'feature_flags'</span>)
    .select(<span class="hljs-string">'*'</span>)
    .eq(<span class="hljs-string">'key'</span>, key)
    .single();

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'PGRST116'</span>) {
      setCached(key, <span class="hljs-literal">null</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching feature flag:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  setCached(key, data);
  <span class="hljs-keyword">return</span> data;
}
</code></pre>
<p>The function first checks the cache. If the flag isn't cached, it queries Supabase. The error code <code>PGRST116</code> means "not found." In that case, you cache <code>null</code> to avoid repeated queries for non-existent flags.</p>
<p>The core logic is in <code>isFeatureEnabled</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isFeatureEnabled</span>(<span class="hljs-params">
  key: <span class="hljs-built_in">string</span>,
  userId?: <span class="hljs-built_in">string</span>
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlagCheckResult</span>&gt; </span>{
  <span class="hljs-keyword">const</span> flag = <span class="hljs-keyword">await</span> getFeatureFlag(key);

  <span class="hljs-keyword">if</span> (!flag) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag not found'</span> };
  }

  <span class="hljs-keyword">if</span> (!flag.enabled) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag is globally disabled'</span> };
  }

  <span class="hljs-comment">// Check user-specific access</span>
  <span class="hljs-keyword">if</span> (userId &amp;&amp; flag.enabled_for_users.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">if</span> (flag.enabled_for_users.includes(userId)) {
      <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'User has explicit access'</span> };
    }
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'User not in allowed list'</span> };
  }

  <span class="hljs-comment">// Check percentage rollout</span>
  <span class="hljs-keyword">if</span> (flag.enabled_for_percent &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> hash = simpleHash(userId || key);
    <span class="hljs-keyword">const</span> percentage = hash % <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> enabled = percentage &lt; flag.enabled_for_percent;

    <span class="hljs-keyword">return</span> {
      enabled,
      reason: enabled
        ? <span class="hljs-string">`User falls within <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>
        : <span class="hljs-string">`User falls outside <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>,
    };
  }

  <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'Flag is globally enabled'</span> };
}
</code></pre>
<p>The function follows this logic:</p>
<ol>
<li><p><strong>Flag doesn't exist</strong>: Return disabled</p>
</li>
<li><p><strong>Flag is globally disabled</strong>: Return disabled</p>
</li>
<li><p><strong>User-specific list exists</strong>: Check if the user is in the list</p>
</li>
<li><p><strong>Percentage rollout is set</strong>: Use a hash function to assign users to buckets deterministically</p>
</li>
<li><p><strong>Otherwise</strong>: Flag is globally enabled</p>
</li>
</ol>
<p>The hash function ensures consistent assignment, so that the same user always gets the same result:</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleHash</span>(<span class="hljs-params">str: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">number</span> </span>{
  <span class="hljs-keyword">let</span> hash = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; str.length; i++) {
    <span class="hljs-keyword">const</span> char = str.charCodeAt(i);
    hash = (hash &lt;&lt; <span class="hljs-number">5</span>) - hash + char;
    hash = hash &amp; hash;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.abs(hash);
}
</code></pre>
<p>This creates a deterministic hash, so <code>simpleHash("user-123")</code> always returns the same number, ensuring consistent feature flag decisions.</p>
<h3 id="heading-server-side-feature-flag-logic">Server-Side Feature Flag Logic</h3>
<p>The server-side version is similar but uses the server Supabase client:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/feature-flags/server.ts</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag, FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span>&gt; </span>{
  <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
  <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);

  <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'feature_flags'</span>)
    .select(<span class="hljs-string">'*'</span>)
    .eq(<span class="hljs-string">'key'</span>, key)
    .single();

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'PGRST116'</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching feature flag:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">return</span> data;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isFeatureEnabled</span>(<span class="hljs-params">
  key: <span class="hljs-built_in">string</span>,
  userId?: <span class="hljs-built_in">string</span>
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlagCheckResult</span>&gt; </span>{
  <span class="hljs-keyword">const</span> flag = <span class="hljs-keyword">await</span> getFeatureFlag(key);

  <span class="hljs-keyword">if</span> (!flag) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag not found'</span> };
  }

  <span class="hljs-keyword">if</span> (!flag.enabled) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag is globally disabled'</span> };
  }

  <span class="hljs-comment">// Check user-specific access</span>
  <span class="hljs-keyword">if</span> (userId &amp;&amp; flag.enabled_for_users.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">if</span> (flag.enabled_for_users.includes(userId)) {
      <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'User has explicit access'</span> };
    }
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'User not in allowed list'</span> };
  }

  <span class="hljs-comment">// Check percentage rollout</span>
  <span class="hljs-keyword">if</span> (flag.enabled_for_percent &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> hash = simpleHash(userId || key);
    <span class="hljs-keyword">const</span> percentage = hash % <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> enabled = percentage &lt; flag.enabled_for_percent;

    <span class="hljs-keyword">return</span> {
      enabled,
      reason: enabled
        ? <span class="hljs-string">`User falls within <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>
        : <span class="hljs-string">`User falls outside <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>,
    };
  }

  <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'Flag is globally enabled'</span> };
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleHash</span>(<span class="hljs-params">str: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">number</span> </span>{
  <span class="hljs-keyword">let</span> hash = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; str.length; i++) {
    <span class="hljs-keyword">const</span> char = str.charCodeAt(i);
    hash = (hash &lt;&lt; <span class="hljs-number">5</span>) - hash + char;
    hash = hash &amp; hash;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.abs(hash);
}
</code></pre>
<p>The logic is identical to the client version, but it uses the server Supabase client that handles cookies correctly.</p>
<h2 id="heading-setting-up-react-query">Setting Up React Query</h2>
<p>We’ll rely heavily on React Query throughout this tutorial because feature flags are server-driven values that can change at runtime while users are actively using the application.</p>
<p>React Query provides robust server-state management through caching, background refetching, and cache invalidation. This allows feature flag changes to propagate automatically across the app without forcing page refreshes or manual state synchronization.</p>
<h3 id="heading-create-the-query-provider">Create the Query Provider</h3>
<p>First, create the <code>providers/QueryProvider.tsx</code> file to configure and initialize React Query for the entire application:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { QueryClient, QueryClientProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">QueryProvider</span>(<span class="hljs-params">{ children }: { children: React.ReactNode }</span>) </span>{
  <span class="hljs-keyword">const</span> [queryClient] = useState(
    <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">new</span> QueryClient({
        defaultOptions: {
          queries: {
            <span class="hljs-comment">// With SSR, we usually want to set some default staleTime</span>
            <span class="hljs-comment">// above 0 to avoid refetching immediately on the client</span>
            staleTime: <span class="hljs-number">20</span> * <span class="hljs-number">1000</span>,
            refetchOnWindowFocus: <span class="hljs-literal">true</span>,
            refetchOnReconnect: <span class="hljs-literal">true</span>,
          },
        },
      })
  );

  <span class="hljs-keyword">return</span> (
    &lt;QueryClientProvider client={queryClient}&gt;{children}&lt;/QueryClientProvider&gt;
  );
}
</code></pre>
<p>What’s happening in this code:</p>
<ul>
<li><p>This file runs on the client because React Query relies on React hooks, which only execute in the browser.</p>
</li>
<li><p>A single QueryClient instance is created and stored in state so it persists across renders and isn’t recreated.</p>
</li>
<li><p>The QueryClient defines how server data is cached, when it becomes stale, and when it should be refetched.</p>
</li>
</ul>
<p>This is important because components no longer need to handle fetch logic, loading states, or caching manually. Also, server data is shared and reused across components instead of being refetched repeatedly. And feature flag updates propagate automatically, keeping the app consistent without manual refreshes.</p>
<h3 id="heading-add-provider-to-root-layout">Add Provider to Root Layout</h3>
<p>Next, update the <code>app/layout.tsx</code> file to wrap the application with the React Query provider.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'./globals.css'</span>
<span class="hljs-keyword">import</span> { QueryProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/providers/QueryProvider'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
  title: <span class="hljs-string">'Feature Flag System'</span>,
  description: <span class="hljs-string">'Production-ready feature flag system with Next.js and Supabase'</span>,
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{
  children,
}: {
  children: React.ReactNode
}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;html lang=<span class="hljs-string">"en"</span>&gt;
      &lt;body&gt;
        &lt;QueryProvider&gt;{children}&lt;/QueryProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}
</code></pre>
<p>We wrap the entire application with <code>QueryProvider</code>, so React Query can manage server data globally across the app.</p>
<h4 id="heading-what-queryprovider-actually-does">What <code>QueryProvider</code> actually does</h4>
<p><code>QueryProvider</code> creates and shares a single Query Client that is responsible for:</p>
<ul>
<li><p>Caching data fetched from the server</p>
</li>
<li><p>Tracking loading and error states</p>
</li>
<li><p>Automatically refetching data when needed</p>
</li>
<li><p>Synchronizing data between components</p>
</li>
</ul>
<p>By wrapping the app, every component inside it can use React Query hooks without any extra setup.</p>
<h2 id="heading-creating-the-react-hook">Creating the React Hook</h2>
<p>Create the <code>hooks/useFeatureFlag.ts</code> file to check whether a feature flag is enabled for a user, using React Query to cache and share the data across components:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { isFeatureEnabled } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/feature-flags/client'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, userId?: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> {
    data: result = { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Loading...'</span> },
    isLoading: loading,
    error,
  } = useQuery&lt;FeatureFlagCheckResult&gt;({
    queryKey: [<span class="hljs-string">'featureFlag'</span>, key, userId],
    queryFn: <span class="hljs-function">() =&gt;</span> isFeatureEnabled(key, userId),
    staleTime: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>,
    refetchOnWindowFocus: <span class="hljs-literal">true</span>,
  });

  <span class="hljs-keyword">return</span> { ...result, loading, error };
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>'use client';</code> ensures this file runs in the browser because React Query hooks can only run on the client.</p>
</li>
<li><p><code>useQuery</code> fetches the feature flag status from the server and caches the result.</p>
</li>
<li><p><code>queryKey: ['featureFlag', key, userId]</code> uniquely identifies this query so React Query can cache it separately for each feature and user.</p>
</li>
<li><p><code>queryFn: () =&gt; isFeatureEnabled(key, userId)</code> is the function that actually checks if the feature is enabled.</p>
</li>
<li><p><code>staleTime: 30 * 1000</code> keeps the cached data fresh for 30 seconds before refetching.</p>
</li>
<li><p><code>refetchOnWindowFocus: true</code> automatically refetches the data when the user switches back to the tab.</p>
</li>
<li><p><code>return { ...result, loading, error }</code> makes it easy for components to access the flag status, loading state, and any errors.</p>
</li>
</ul>
<h3 id="heading-admin-hooks-for-managing-flags">Admin Hooks for Managing Flags</h3>
<p>We’re going to create a set of React Query hooks to manage feature flags from the admin dashboard. These hooks allow you to fetch, create, update, and delete feature flags while automatically keeping your UI in sync.</p>
<h4 id="heading-step-1-create-the-hooks-file">Step 1: Create the hooks file</h4>
<p>Start by creating a new file at <code>hooks/useFeatureFlags.ts</code> file. This is where we’ll implement all the hooks for the admin to manage feature flags.</p>
<p>After creating the <code>hooks/useFeatureFlags.ts</code> file, import React Query hooks and the FeatureFlag type so we can fetch, update, create, and delete feature flags with type safety and caching.</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useQuery, useMutation, useQueryClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>useQuery</code> fetches and caches server data automatically.</p>
</li>
<li><p><code>useMutation</code> sends updates, creates, or deletes data on the server.</p>
</li>
<li><p><code>useQueryClient</code> gives access to the query cache so we can invalidate or update queries after mutations.</p>
</li>
<li><p><code>FeatureFlag</code> is the TypeScript type definition for feature flags, ensuring our hooks use correct data structures.</p>
</li>
</ul>
<h4 id="heading-step-2-add-a-hook-to-fetch-all-feature-flags">Step 2: Add a hook to fetch all feature flags</h4>
<p>Next, in the same <code>hooks/useFeatureFlags.ts</code> file, add a hook to fetch all feature flags for the admin page. This hook will allow components to retrieve the list of flags and automatically cache the data using React Query.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFeatureFlags</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> useQuery&lt;FeatureFlag[]&gt;({
    queryKey: [<span class="hljs-string">'featureFlags'</span>],
    queryFn: <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/feature-flags'</span>);
      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data || [];
    },
    staleTime: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>,
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useQuery</code> fetches all feature flags from the server and caches them automatically.</p>
</li>
<li><p><code>queryKey: ['featureFlags']</code> uniquely identifies this query so React Query can manage caching and refetching.</p>
</li>
<li><p><code>queryFn</code> is an async function that calls the <code>/api/feature-flags</code> endpoint and returns the data.</p>
</li>
<li><p><code>staleTime: 30 * 1000</code> keeps the cached data fresh for 30 seconds before refetching.</p>
</li>
</ul>
<p>This matters because admin components always display the latest flags without manual refresh. Also, cached data reduces unnecessary network requests. Finally, any component using this hook will automatically update when the flags change.</p>
<h4 id="heading-step-3-add-a-hook-to-update-a-feature-flag">Step 3: Add a hook to update a feature flag</h4>
<p>Next, in the same <code>hooks/useFeatureFlags.ts</code> file, add a hook to update an existing feature flag. This hook will allow the admin to modify a flag and ensure all components using it get the updated value automatically.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useUpdateFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> ({
      key,
      updates,
    }: {
      key: <span class="hljs-built_in">string</span>;
      updates: Partial&lt;FeatureFlag&gt;;
    }) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, {
        method: <span class="hljs-string">'PATCH'</span>,
        headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify(updates),
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to update feature flag'</span>);
      }

      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data;
    },
    onSuccess: <span class="hljs-function">(<span class="hljs-params">data, variables</span>) =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
      <span class="hljs-comment">// Invalidate the specific feature flag check</span>
      queryClient.invalidateQueries({
        queryKey: [<span class="hljs-string">'featureFlag'</span>, variables.key],
      });
      <span class="hljs-comment">// Invalidate all feature flag checks (in case userId was involved)</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlag'</span>] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to update a feature flag on the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that sends a PATCH request to <code>/api/feature-flags/${key}</code> with the updated data.</p>
</li>
<li><p><code>onSuccess</code> runs after a successful update to <strong>invalidate cached queries</strong> so the latest data is available everywhere:</p>
<ul>
<li><p><code>['featureFlags']</code> updates the full list of flags.</p>
</li>
<li><p><code>['featureFlag', variables.key]</code> updates the specific flag that was changed.</p>
</li>
<li><p><code>['featureFlag']</code> updates any other cached flag checks (for example, per user checks).</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-step-4-add-a-hook-to-delete-a-feature-flag">Step 4: Add a hook to delete a feature flag</h4>
<p>Next, add the <code>useDeleteFeatureFlag</code> hook to delete a feature flag. This hook allows the admin to remove a flag from the system and ensures the UI updates automatically everywhere the flag was used.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useDeleteFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> (key: <span class="hljs-built_in">string</span>) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, {
        method: <span class="hljs-string">'DELETE'</span>,
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to delete feature flag'</span>);
      }
    },
    onSuccess: <span class="hljs-function">(<span class="hljs-params">_, key</span>) =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
      <span class="hljs-comment">// Invalidate the specific feature flag check</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlag'</span>, key] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to delete a feature flag from the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that sends a DELETE request to <code>/api/feature-flags/${key}</code>.</p>
</li>
<li><p><code>onSuccess</code> runs after the flag is successfully deleted to <strong>invalidate the cache</strong> so the UI updates:</p>
<ul>
<li><p><code>['featureFlags']</code> refetches the full list of flags.</p>
</li>
<li><p><code>['featureFlag', key]</code> removes the deleted flag from any cached queries.</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-step-5-add-a-hook-to-create-a-new-feature-flag">Step 5: Add a hook to create a new feature flag</h4>
<p>Finally, add the <code>useCreateFeatureFlag</code> hook to create a new feature flag. This allows the admin to add new flags and ensures the dashboard updates automatically when a new flag is created.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useCreateFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> (flag: {
      key: <span class="hljs-built_in">string</span>;
      name: <span class="hljs-built_in">string</span>;
      description?: <span class="hljs-built_in">string</span>;
      enabled?: <span class="hljs-built_in">boolean</span>;
      enabled_for_users?: <span class="hljs-built_in">string</span>[];
      enabled_for_percent?: <span class="hljs-built_in">number</span>;
      metadata?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
    }) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/feature-flags'</span>, {
        method: <span class="hljs-string">'POST'</span>,
        headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify(flag),
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">const</span> error = <span class="hljs-keyword">await</span> response.json();
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(error.error || <span class="hljs-string">'Failed to create feature flag'</span>);
      }

      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data;
    },
    onSuccess: <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to send a new feature flag to the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that posts the flag data to <code>/api/feature-flags</code>.</p>
</li>
<li><p><code>onSuccess</code> runs after a successful creation to <strong>invalidate the cached list of flags</strong>, so the admin dashboard shows the new flag immediately.</p>
</li>
</ul>
<h3 id="heading-feature-flag-gate-component">Feature Flag Gate Component</h3>
<p>Now we’ll create a wrapper component to conditionally render UI based on feature flags. This helps you show or hide parts of your app depending on whether a flag is enabled for a user.</p>
<p>Create the <code>components/FeatureFlagGate.tsx</code> file and add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { ReactNode } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { useFeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlag'</span>;

<span class="hljs-keyword">interface</span> FeatureFlagGateProps {
  flagKey: <span class="hljs-built_in">string</span>;
  userId?: <span class="hljs-built_in">string</span>;
  children: ReactNode;
  fallback?: ReactNode;
  showLoading?: ReactNode;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FeatureFlagGate</span>(<span class="hljs-params">{
  flagKey,
  userId,
  children,
  fallback = <span class="hljs-literal">null</span>,
  showLoading = <span class="hljs-literal">null</span>,
}: FeatureFlagGateProps</span>) </span>{
  <span class="hljs-keyword">const</span> { enabled, loading } = useFeatureFlag(flagKey, userId);

  <span class="hljs-keyword">if</span> (loading &amp;&amp; showLoading !== <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">return</span> &lt;&gt;{showLoading}&lt;/&gt;;
  }

  <span class="hljs-keyword">if</span> (!enabled) {
    <span class="hljs-keyword">return</span> &lt;&gt;{fallback}&lt;/&gt;;
  }

  <span class="hljs-keyword">return</span> &lt;&gt;{children}&lt;/&gt;;
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useFeatureFlag(flagKey, userId)</code> checks whether the feature is enabled for a specific user and tracks loading state.</p>
</li>
<li><p><code>loading &amp;&amp; showLoading !== null</code>: if the data is still loading, render the optional <code>showLoading</code> UI.</p>
</li>
<li><p><code>!enabled</code>: if the feature is disabled, render the optional <code>fallback</code> UI.</p>
</li>
<li><p><code>children</code> renders the actual content only if the flag is enabled and not loading.</p>
</li>
</ul>
<p>This makes it easy to conditionally render features without scattering logic throughout your components. It also supports custom loading and fallback UI for better user experience. And it works with React Query caching automatically, so flag changes propagate immediately.</p>
<h2 id="heading-building-the-admin-dashboard">Building the Admin Dashboard</h2>
<p>Admins need a way to manage feature flags in your app. To do this, we’ll create API routes that support CRUD operations (Create, Read, Update, Delete). These routes will interact with Supabase to store and modify flag data.</p>
<h3 id="heading-step-1-create-the-main-feature-flags-api-route">Step 1: Create the main feature flags API route</h3>
<p>Start by creating the <code>app/api/feature-flags/route.ts</code> file. This file will handle fetching all feature flags (GET) and creating new ones (POST).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .select(<span class="hljs-string">'*'</span>)
      .order(<span class="hljs-string">'created_at'</span>, { ascending: <span class="hljs-literal">false</span> });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

    <span class="hljs-keyword">if</span> (!body.key || !body.name) {
      <span class="hljs-keyword">return</span> NextResponse.json(
        { error: <span class="hljs-string">'key and name are required'</span> },
        { status: <span class="hljs-number">400</span> }
      );
    }

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .insert({
        key: body.key,
        name: body.name,
        description: body.description || <span class="hljs-literal">null</span>,
        enabled: body.enabled || <span class="hljs-literal">false</span>,
        enabled_for_users: body.enabled_for_users || [],
        enabled_for_percent: body.enabled_for_percent || <span class="hljs-number">0</span>,
        metadata: body.metadata || {},
      })
      .select()
      .single();

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data }, { status: <span class="hljs-number">201</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>GET</code> fetches all feature flags from Supabase, ordered by creation date.</p>
</li>
<li><p><code>POST</code> creates a new feature flag in Supabase. It checks that <code>key</code> and <code>name</code> exist, and sets defaults for optional fields.</p>
</li>
<li><p><code>createClient(cookieStore)</code> authenticates requests with <code>Supabase</code> using cookies from the client.</p>
</li>
<li><p><code>NextResponse.json()</code> sends JSON responses with data or error messages.</p>
</li>
</ul>
<h3 id="heading-step-2-create-the-dynamic-route-for-individual-flags">Step 2: Create the dynamic route for individual flags</h3>
<p>Next, Create the file <code>app/api/feature-flags/[key]/route.ts</code>. This route handles updating (PATCH) and deleting (DELETE) individual flags based on their <code>key</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PATCH</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: { key: <span class="hljs-built_in">string</span> } }
</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .update({
        ...(body.name !== <span class="hljs-literal">undefined</span> &amp;&amp; { name: body.name }),
        ...(body.description !== <span class="hljs-literal">undefined</span> &amp;&amp; { description: body.description }),
        ...(body.enabled !== <span class="hljs-literal">undefined</span> &amp;&amp; { enabled: body.enabled }),
        ...(body.enabled_for_users !== <span class="hljs-literal">undefined</span> &amp;&amp; {
          enabled_for_users: body.enabled_for_users,
        }),
        ...(body.enabled_for_percent !== <span class="hljs-literal">undefined</span> &amp;&amp; {
          enabled_for_percent: body.enabled_for_percent,
        }),
      })
      .eq(<span class="hljs-string">'key'</span>, params.key)
      .select()
      .single();

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DELETE</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: { key: <span class="hljs-built_in">string</span> } }
</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);

    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .delete()
      .eq(<span class="hljs-string">'key'</span>, params.key);

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ success: <span class="hljs-literal">true</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>PATCH</code> updates a feature flag with only the fields provided in the request body.</p>
<ul>
<li>Uses the spread operator <code>(...)</code> to include only fields that exist.</li>
</ul>
</li>
<li><p><code>DELETE</code> removes the feature flag identified by <code>key</code>.</p>
</li>
<li><p><code>eq('key', params.key)</code> ensures the operation targets the correct flag.</p>
</li>
</ul>
<h3 id="heading-step-3-create-the-admin-dashboard-ui">Step 3: Create the Admin Dashboard UI</h3>
<p>Now, create the file <code>app/admin/page.tsx</code>. This is the main admin dashboard where you can view, create, and manage feature flags.</p>
<p>With React Query, the admin dashboard becomes much cleaner and automatically updates when flags change:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagList } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/admin/FeatureFlagList'</span>;
<span class="hljs-keyword">import</span> { CreateFeatureFlagModal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/admin/CreateFeatureFlagModal'</span>;
<span class="hljs-keyword">import</span> { useFeatureFlags } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlags'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AdminPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [showCreateModal, setShowCreateModal] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> { data: flags = [], isLoading: loading } = useFeatureFlags();

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">'min-h-screen bg-gray-50 py-8'</span>&gt;
      &lt;div className=<span class="hljs-string">'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'</span>&gt;
        &lt;div className=<span class="hljs-string">'mb-8 flex justify-between items-center'</span>&gt;
          &lt;div&gt;
            &lt;h1 className=<span class="hljs-string">'text-3xl font-bold text-gray-900'</span>&gt;
              Feature Flags Admin
            &lt;/h1&gt;
            &lt;p className=<span class="hljs-string">'mt-2 text-sm text-gray-600'</span>&gt;
              Manage your feature flags and rollouts
            &lt;/p&gt;
          &lt;/div&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span> setShowCreateModal(<span class="hljs-literal">true</span>)}
            className=<span class="hljs-string">'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'</span>
          &gt;
            Create Feature Flag
          &lt;/button&gt;
        &lt;/div&gt;

        {loading ? (
          &lt;div className=<span class="hljs-string">'text-center py-12'</span>&gt;
            &lt;div className=<span class="hljs-string">'inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600'</span>&gt;&lt;/div&gt;
            &lt;p className=<span class="hljs-string">'mt-4 text-gray-600'</span>&gt;Loading feature flags...&lt;/p&gt;
          &lt;/div&gt;
        ) : (
          &lt;FeatureFlagList flags={flags} /&gt;
        )}

        {showCreateModal &amp;&amp; (
          &lt;CreateFeatureFlagModal
            onClose={<span class="hljs-function">() =&gt;</span> setShowCreateModal(<span class="hljs-literal">false</span>)}
            onSuccess={<span class="hljs-function">() =&gt;</span> {
              setShowCreateModal(<span class="hljs-literal">false</span>);
            }}
          /&gt;
        )}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>'use client';</code> marks this page as a client component so hooks like <code>useState</code> and React Query can run.</p>
</li>
<li><p><code>useState</code> manages whether the “Create Feature Flag” modal is open or closed.</p>
</li>
<li><p><code>useFeatureFlags()</code> fetches all feature flags from the API and keeps the list in sync automatically.</p>
</li>
<li><p><code>const { data: flags = [], isLoading: loading }</code> :</p>
<ul>
<li><p><code>flags</code> contains the feature flag list</p>
</li>
<li><p><code>loading</code> tracks whether the data is still being fetched</p>
</li>
</ul>
</li>
<li><p><code>loading ? ... : &lt;FeatureFlagList /&gt;</code> :</p>
<ul>
<li><p>Shows a loading spinner while flags are being fetched</p>
</li>
<li><p>Renders the feature flag table once data is available</p>
</li>
</ul>
</li>
<li><p><code>FeatureFlagList</code> displays all feature flags and their current states.</p>
</li>
</ul>
<h3 id="heading-using-feature-flags-mutations-in-components">Using Feature Flags Mutations in Components</h3>
<p>Earlier, we created mutation hooks for creating, updating, and deleting feature flags. Now let’s see how those hooks are actually used inside UI components to trigger updates and keep the interface in sync.</p>
<p>Here's how to use the mutation hooks in your components:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { FeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>
<span class="hljs-keyword">import</span> { EditFeatureFlagModal } <span class="hljs-keyword">from</span> <span class="hljs-string">'./EditFeatureFlagModal'</span>
<span class="hljs-keyword">import</span> {
  useUpdateFeatureFlag,
  useDeleteFeatureFlag,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlags'</span>

<span class="hljs-keyword">interface</span> FeatureFlagCardProps {
  flag: FeatureFlag
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FeatureFlagCard</span>(<span class="hljs-params">{ flag }: FeatureFlagCardProps</span>) </span>{
  <span class="hljs-keyword">const</span> [isEditing, setIsEditing] = useState(<span class="hljs-literal">false</span>)
  <span class="hljs-keyword">const</span> updateFlag = useUpdateFeatureFlag()
  <span class="hljs-keyword">const</span> deleteFlag = useDeleteFeatureFlag()

  <span class="hljs-keyword">const</span> handleToggle = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> updateFlag.mutateAsync({
        key: flag.key,
        updates: { enabled: !flag.enabled },
      })
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error toggling flag:'</span>, error)
      alert(<span class="hljs-string">'Failed to toggle feature flag'</span>)
    }
  }

  <span class="hljs-keyword">const</span> handleDelete = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!confirm(<span class="hljs-string">`Are you sure you want to delete "<span class="hljs-subst">${flag.name}</span>"?`</span>)) {
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> deleteFlag.mutateAsync(flag.key)
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error deleting flag:'</span>, error)
      alert(<span class="hljs-string">'Failed to delete feature flag'</span>)
    }
  }

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;div className=<span class="hljs-string">"bg-white rounded-lg shadow p-6"</span>&gt;
        &lt;div className=<span class="hljs-string">"flex items-start justify-between"</span>&gt;
          &lt;div className=<span class="hljs-string">"flex-1"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-3"</span>&gt;
              &lt;h3 className=<span class="hljs-string">"text-lg font-semibold text-gray-900"</span>&gt;
                {flag.name}
              &lt;/h3&gt;
              &lt;span
                className={<span class="hljs-string">`px-2 py-1 text-xs font-medium rounded-full <span class="hljs-subst">${
                  flag.enabled
                    ? <span class="hljs-string">'bg-green-100 text-green-800'</span>
                    : <span class="hljs-string">'bg-gray-100 text-gray-800'</span>
                }</span>`</span>}
              &gt;
                {flag.enabled ? <span class="hljs-string">'Enabled'</span> : <span class="hljs-string">'Disabled'</span>}
              &lt;/span&gt;
            &lt;/div&gt;
            &lt;p className=<span class="hljs-string">"mt-1 text-sm text-gray-600 font-mono"</span>&gt;{flag.key}&lt;/p&gt;
            {flag.description &amp;&amp; (
              &lt;p className=<span class="hljs-string">"mt-2 text-sm text-gray-500"</span>&gt;{flag.description}&lt;/p&gt;
            )}

            &lt;div className=<span class="hljs-string">"mt-4 flex flex-wrap gap-4 text-sm text-gray-600"</span>&gt;
              {flag.enabled_for_users.length &gt; <span class="hljs-number">0</span> &amp;&amp; (
                &lt;div&gt;
                  &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Users:&lt;/span&gt;{<span class="hljs-string">' '</span>}
                  {flag.enabled_for_users.length} user(s)
                &lt;/div&gt;
              )}
              {flag.enabled_for_percent &gt; <span class="hljs-number">0</span> &amp;&amp; (
                &lt;div&gt;
                  &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Rollout:&lt;/span&gt;{<span class="hljs-string">' '</span>}
                  {flag.enabled_for_percent}%
                &lt;/div&gt;
              )}
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div className=<span class="hljs-string">"flex items-center gap-2 ml-4"</span>&gt;
            &lt;button
              onClick={handleToggle}
              disabled={updateFlag.isPending}
              className={<span class="hljs-string">`px-3 py-1.5 text-sm font-medium rounded transition-colors <span class="hljs-subst">${
                flag.enabled
                  ? <span class="hljs-string">'bg-red-100 text-red-700 hover:bg-red-200'</span>
                  : <span class="hljs-string">'bg-green-100 text-green-700 hover:bg-green-200'</span>
              }</span> disabled:opacity-50`</span>}
            &gt;
              {updateFlag.isPending
                ? <span class="hljs-string">'...'</span>
                : flag.enabled
                ? <span class="hljs-string">'Disable'</span>
                : <span class="hljs-string">'Enable'</span>}
            &lt;/button&gt;
            &lt;button
              onClick={<span class="hljs-function">() =&gt;</span> setIsEditing(<span class="hljs-literal">true</span>)}
              className=<span class="hljs-string">"px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-100 rounded hover:bg-blue-200 transition-colors"</span>
            &gt;
              Edit
            &lt;/button&gt;
            &lt;button
              onClick={handleDelete}
              className=<span class="hljs-string">"px-3 py-1.5 text-sm font-medium text-red-700 bg-red-100 rounded hover:bg-red-200 transition-colors"</span>
            &gt;
              Delete
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      {isEditing &amp;&amp; (
        &lt;EditFeatureFlagModal
          flag={flag}
          onClose={<span class="hljs-function">() =&gt;</span> setIsEditing(<span class="hljs-literal">false</span>)}
          onSuccess={<span class="hljs-function">() =&gt;</span> {
            setIsEditing(<span class="hljs-literal">false</span>)
          }}
        /&gt;
      )}
    &lt;/&gt;
  )
}
</code></pre>
<p>The component above works without any manual refetching or state synchronization logic. The moment a mutation succeeds, React Query automatically updates or invalidates the relevant cached data. That behavior is what enables the smooth, real-time updates you see in the UI.</p>
<p>Because all feature flag data is managed by React Query:</p>
<ul>
<li><p>When a flag is toggled, React Query automatically invalidates the relevant queries</p>
</li>
<li><p>Any component using that flag immediately receives the updated value</p>
</li>
<li><p>The admin dashboard stays in sync with the rest of the application without extra code</p>
</li>
<li><p>Loading and error states are handled consistently across all mutations.</p>
</li>
</ul>
<h2 id="heading-implementing-a-real-world-example">Implementing a Real-World Example</h2>
<p>To make all of this concrete, let’s walk through a real-world example.</p>
<p>In this section, we’ll build a simple Todo feature that is <strong>entirely controlled by a feature flag</strong>. When the flag is disabled, users see a message explaining that the feature isn’t available. When it’s enabled, the full Todo interface appears instantly without redeploying or refreshing the page.</p>
<p>This demonstrates how feature flags can safely gate entire pages or features in a live application.</p>
<p>Below is what the experience looks like as the feature flag is toggled on and off from the admin dashboard:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769946162379/33b4a9e3-5284-477e-b8c5-ecc0e137e7c1.png" alt="feature-flag-disabled-page" class="image--center mx-auto" width="2996" height="1402" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769946216049/edea8722-c298-4b2b-a4b5-bcc8306fb15f.png" alt="feature-flag-enabled-page" class="image--center mx-auto" width="2852" height="1536" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770299915271/54c124ac-24d5-4d3f-825c-0507675053ee.png" alt="Todo-app-with-feature-flag" class="image--center mx-auto" width="1900" height="904" loading="lazy"></p>
<h3 id="heading-step-1-create-the-todo-page-file">Step 1: Create the Todo Page File</h3>
<p>Create a new file <code>app/todos/page.tsx</code>. This page shows how to use a feature flag to conditionally render a full component. Let's build a todo app that's controlled by a feature flag. This demonstrates real-world usage.</p>
<p>At the top of the file, import the hooks we need and define the Todo interface.</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagGate } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/FeatureFlagGate'</span>;

<span class="hljs-keyword">interface</span> Todo {
  id: <span class="hljs-built_in">string</span>;
  text: <span class="hljs-built_in">string</span>;
  completed: <span class="hljs-built_in">boolean</span>;
}
</code></pre>
<h3 id="heading-step-2-initialize-state-in-the-component">Step 2: Initialize state in the component</h3>
<p>Create the component and define state for todos and the input:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TodosPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [todos, setTodos] = useState&lt;Todo[]&gt;([]);
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">''</span>);
</code></pre>
<p>Here’s what’s going on<strong>:</strong></p>
<ul>
<li><p><code>todos</code> stores all todo items.</p>
</li>
<li><p><code>inputValue</code> tracks what the user types in the input field.</p>
</li>
</ul>
<h3 id="heading-step-3-load-and-save-todos-from-localstorage">Step 3: Load and save todos from localStorage</h3>
<pre><code class="lang-typescript">  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> saved = <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'todos'</span>);
    <span class="hljs-keyword">if</span> (saved) setTodos(<span class="hljs-built_in">JSON</span>.parse(saved));
  }, []);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'todos'</span>, <span class="hljs-built_in">JSON</span>.stringify(todos));
  }, [todos]);
</code></pre>
<p>In this code, the first <code>useEffect</code> loads saved todos from the browser’s <code>localStorage</code> when the component mounts. The second <code>useEffect</code> saves todos whenever the list changes. This ensures your todos persist across page reloads.</p>
<h3 id="heading-step-4-add-helper-functions">Step 4: Add helper functions</h3>
<pre><code class="lang-typescript">  <span class="hljs-keyword">const</span> addTodo = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (inputValue.trim()) {
      setTodos([...todos, { 
        id: <span class="hljs-built_in">Date</span>.now().toString(), 
        text: inputValue.trim(), 
        completed: <span class="hljs-literal">false</span> 
      }]);
      setInputValue(<span class="hljs-string">''</span>);
    }
  };

  <span class="hljs-keyword">const</span> toggleTodo = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setTodos(todos.map(<span class="hljs-function"><span class="hljs-params">todo</span> =&gt;</span> 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  <span class="hljs-keyword">const</span> deleteTodo = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setTodos(todos.filter(<span class="hljs-function"><span class="hljs-params">todo</span> =&gt;</span> todo.id !== id));
  };
</code></pre>
<p>In this code,</p>
<ul>
<li><p><code>addTodo</code> adds a new todo item with a unique <code>id</code> and resets the input.</p>
</li>
<li><p><code>toggleTodo</code> marks a todo as completed or incomplete.</p>
</li>
<li><p><code>deleteTodo</code> removes a todo from the list.</p>
</li>
</ul>
<h3 id="heading-step-5-render-the-todo-app-inside-the-featureflaggate">Step 5: Render the Todo App inside the FeatureFlagGate</h3>
<pre><code class="lang-xml">  return (
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-2xl mx-auto p-8"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-4xl font-bold mb-6"</span>&gt;</span>Todo App<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">FeatureFlagGate</span>
        <span class="hljs-attr">flagKey</span>=<span class="hljs-string">"test"</span>
        <span class="hljs-attr">fallback</span>=<span class="hljs-string">{</span>
          &lt;<span class="hljs-attr">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-yellow-50 border-2 border-yellow-200 rounded-lg p-6"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-semibold mb-2"</span>&gt;</span>Feature Not Available<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Enable the "test" feature flag in the admin dashboard.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        }
      &gt;
</code></pre>
<p>What’s going on here:</p>
<ul>
<li><p>We wrap the Todo app in <code>FeatureFlagGate</code>.</p>
</li>
<li><p><code>flagKey="test"</code> only shows the Todo app if this feature flag is enabled.</p>
</li>
<li><p><code>fallback</code> displays a message when the feature is disabled.</p>
</li>
</ul>
<h3 id="heading-step-6-add-the-input-field-and-add-button">Step 6: Add the input field and Add button</h3>
<pre><code class="lang-xml">        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mb-6"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-2"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
              <span class="hljs-attr">value</span>=<span class="hljs-string">{inputValue}</span>
              <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setInputValue(e.target.value)}
              onKeyPress={(e) =&gt; e.key === 'Enter' &amp;&amp; addTodo()}
              placeholder="What needs to be done?"
              className="flex-1 px-4 py-3 border rounded-lg"
            /&gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
              <span class="hljs-attr">onClick</span>=<span class="hljs-string">{addTodo}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"px-6 py-3 bg-purple-600 text-white rounded-lg"</span>
            &gt;</span>
              Add
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>In this code,</p>
<ul>
<li><p>Input field captures new todo text.</p>
</li>
<li><p>Pressing Enter or clicking Add triggers <code>addTodo</code>.</p>
</li>
<li><p>It’s styled with Tailwind CSS for spacing and rounded borders.</p>
</li>
</ul>
<h3 id="heading-step-8-display-the-list-of-todos">Step 8: Display the list of todos</h3>
<pre><code class="lang-xml">
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"space-y-2"</span>&gt;</span>
          {todos.map((todo) =&gt; (
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
              <span class="hljs-attr">key</span>=<span class="hljs-string">{todo.id}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center gap-3 p-4 bg-gray-50 rounded-lg"</span>
            &gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> toggleTodo(todo.id)}
                className={`w-6 h-6 rounded-full border-2 ${
                  todo.completed
                    ? 'bg-green-500 border-green-500'
                    : 'border-gray-300'
                }`}
              &gt;
                {todo.completed &amp;&amp; '✓'}
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{todo.completed</span> ? '<span class="hljs-attr">line-through</span>' <span class="hljs-attr">:</span> ''}&gt;</span>
                {todo.text}
              <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> deleteTodo(todo.id)}
                className="text-red-500"
              &gt;
                Delete
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          ))}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">FeatureFlagGate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  );
}
</code></pre>
<p>This code,</p>
<ul>
<li><p>Loops over <code>todos</code> to display each item.</p>
</li>
<li><p>Shows a toggle button to mark as complete, a delete button, and the todo text.</p>
</li>
<li><p>Completed todos get a <strong>line-through</strong> style.</p>
</li>
<li><p>All interaction happens <strong>inside the FeatureFlagGate</strong>, so users only see this when the flag is enabled.<br>  The entire todo app is wrapped in <code>FeatureFlagGate</code>. When the "test" flag is disabled, users see a message instead of the app. When enabled, they see the full todo interface.</p>
</li>
</ul>
<h3 id="heading-todo-app-overview">Todo App Overview</h3>
<p>This Todo app demonstrates how feature flags work in a live application. It shows how admins can enable or disable the "test" feature flag dynamically from the dashboard.</p>
<p><code>FeatureFlagGate</code> ensures the interface updates immediately when the flag changes, and entire components or pages can be toggled on or off safely using feature flags.</p>
<h2 id="heading-server-side-usage">Server-Side Usage</h2>
<p>Feature flags shouldn’t only live on the client. In many cases, you’ll want to enforce them on the server as well, especially for APIs, background jobs, or sensitive business logic.</p>
<p>In this example, we’ll protect an API route by checking a feature flag on the server before returning any data.</p>
<p>First, create the API route file <code>app/api/some-feature/route.ts</code>. This demonstrates how to check a feature flag on the server before returning data.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { isFeatureEnabled } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/feature-flags/server'</span>;
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">request: Request</span>) </span>{
  <span class="hljs-keyword">const</span> userId = request.headers.get(<span class="hljs-string">'user-id'</span>) || <span class="hljs-literal">undefined</span>;
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> isFeatureEnabled(<span class="hljs-string">'new-feature'</span>, userId);

  <span class="hljs-keyword">if</span> (!result.enabled) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Feature not available'</span> },
      { status: <span class="hljs-number">403</span> }
    );
  }

  <span class="hljs-comment">// Feature is enabled, proceed with logic</span>
  <span class="hljs-keyword">return</span> NextResponse.json({ data: <span class="hljs-string">'Feature content'</span> });
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p>File creation: <code>app/api/some-feature/route.ts</code> defines a new API route.</p>
</li>
<li><p><code>isFeatureEnabled</code>: Checks whether the <code>'new-feature'</code> flag is active for the user.</p>
</li>
<li><p>Conditional response: Returns a <code>403</code> error if the feature is disabled. Otherwise, proceeds normally.</p>
</li>
<li><p>Server-side gating: Lets you protect entire endpoints, so users only access functionality when the feature is enabled.</p>
</li>
</ul>
<h2 id="heading-why-react-query">Why React Query?</h2>
<p>Feature flags introduce a unique challenge because they must remain consistent across the entire UI even as they change dynamically. Without a dedicated server-state solution, you’d need to manually refetch data, coordinate updates between components, and handle edge cases where parts of the UI fall out of sync.</p>
<p>React Query treats feature flags as shared server state. Once fetched, flags are cached and reused across components. When an admin updates a flag, the cache is invalidated and refetched in the background, triggering immediate UI updates everywhere the flag is used. This makes React Query a natural fit for feature flags, where correctness, consistency, and real-time updates are critical.</p>
<h3 id="heading-real-world-impact">Real-World Impact</h3>
<p>Before React Query:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Had to manually refetch after every update</span>
<span class="hljs-keyword">const</span> handleToggle = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, { method: <span class="hljs-string">'PATCH'</span>, ... });
  fetchFlags(); <span class="hljs-comment">// Manual refetch</span>
  <span class="hljs-comment">// Other components still show old data until page refresh</span>
};
</code></pre>
<p>After React Query:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Automatic cache invalidation - everything updates instantly</span>
<span class="hljs-keyword">const</span> updateFlag = useUpdateFeatureFlag();
<span class="hljs-keyword">await</span> updateFlag.mutateAsync({ key, updates });
<span class="hljs-comment">// All components using this flag automatically update!</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You’ve now built a complete, production-ready feature flag system using Next.js and Supabase. The system supports global toggles, user-specific access, and percentage-based rollouts, all backed by a flexible database schema.</p>
<p>Feature flags can be checked on both the client and the server, ensuring consistent behavior across UI components and API routes. With React Query handling caching and invalidation, changes made in the admin dashboard propagate instantly throughout the application without deploying or refreshing the page.</p>
<p>Feature flags are a foundational tool for modern development. They let you deploy code safely, test new ideas with real users, and react quickly when something goes wrong. With this setup in place, you can confidently extend the system with audit logs, analytics, scheduled rollouts, or deeper CI/CD integrations as your product grows.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Flexible API with Feature Flags Using Open Source Tools ]]>
                </title>
                <description>
                    <![CDATA[ Feature flagging has changed the paradigm of how backend developers can test and modify the things they build. With feature flags, we can enable and disable a feature or change the functionality of something on the fly with a single click (no need to... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-flexible-api-with-feature-flags-using-open-source-tools/</link>
                <guid isPermaLink="false">673d179a27c7af0d174dc2fc</guid>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Go Language ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Redis ]]>
                    </category>
                
                    <category>
                        <![CDATA[   feature flags ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend developments ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Pradumna Saraf ]]>
                </dc:creator>
                <pubDate>Tue, 19 Nov 2024 22:56:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732044691446/abd5596c-3523-4278-957c-109388690bcc.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Feature flagging has changed the paradigm of how backend developers can test and modify the things they build. With feature flags, we can enable and disable a feature or change the functionality of something on the fly with a single click (no need to redeploy).</p>
<p>In this tutorial, we will see how feature flags help us to enable and disable a feature/a part of code whenever we want from the UI, without the need to redeploy the whole code.</p>
<p>To understand things more deeply, we will build an app from scratch, look at feature flagging capabilities, and use a tool called Flagsmith to manage our created feature flags from a single dashboard.</p>
<h2 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-a-feature-flag">What is a Feature Flag?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-feature-flags-for-backend-development">Feature Flags for Backend Development</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-use-open-source-tools">Why Use Open Source Tools?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lets-code">Let’s Code!</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-initializing-the-tools">Initializing the tools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-endpoints-for-the-api">Creating endpoints for the API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-add-feature-flagging">How to Add Feature Flagging</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-feature-flag-code-logic">Understanding the Feature Flag code logic</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-feature-flags-in-the-flasgsmith-dashboard">How to Create Feature Flags in the Flasgsmith Dashboard</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-rate-limiting-feature-flag">Rate Limiting Feature Flag</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-beta-feature-flag">Beta Feature Flag</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-the-access-key">Getting the Access Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-running-the-api">Running the API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-the-ratelimit-flag">Updating the rate_limit Flag</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-feature-flags-with-the-github-app">How to Integrate Feature Flags with the GitHub App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-the-flagsmith-github-app">Testing the Flagsmith GitHub App</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><a target="_blank" href="https://go.dev/">Golang</a> installed and a medium-level understanding of it.</p>
</li>
<li><p>A running <a target="_blank" href="https://redis.io">Redis</a> instance (Remote or local instance)</p>
</li>
<li><p><a target="_blank" href="https://www.flagsmith.com/">Flagsmith</a> Account (It’s Free. We will cover this later in the article.)</p>
</li>
</ul>
<h2 id="heading-what-is-a-feature-flag">What is a Feature Flag?</h2>
<p>Feature Flag is a technique in development that allows teams to turn features on or off without modifying the source code or redeploying.</p>
<p>To make it a bit simpler, think of them as functioning sort of like conditional statements (for example, if-else statements): based on when something’s true or false, it determines the code path that will be executed.</p>
<h2 id="heading-feature-flags-for-backend-development">Feature Flags for Backend Development</h2>
<p>You may have seen feature flags used in frontends and websites, but there is much more to them. You can use them on the server side to modify the functionality of an API, doing things like modifying/setting the rate limit, changing the API endpoint's functionality or completely turning it off. As backend developers, we can level up our testing with feature flags.</p>
<p>To demonstrate this, we will go through building a demo app. The demo app is curated to show feature flagging capabilities from modifying the functionality (rate limit) on the fly to adding a new endpoint to the API for beta testing or initial rolling purposes. We’ll use entirely open-source tools along the way!</p>
<h2 id="heading-why-use-open-source-tools">Why Use Open Source Tools?</h2>
<p>We will be using open source tools to build this app (Golang, <a target="_blank" href="https://redis.io/">Redis</a>, and <a target="_blank" href="https://www.flagsmith.com/?utm_source=thirdparty&amp;utm_medium=freecodecamp&amp;utm_campaign=pradumna">Flagsmith</a>). Open source brings more transparency and trust and encourages collaboration with the global community of backend developers.</p>
<p>By integrating open source tools, we get full visibility as we build and test. For example, we will integrate feature flags with GitHub, which lets us track the lifecycle of a feature by linking a Flagsmith feature flag with a GitHub Pull Request or Issue. This lets us stay updated with the changes to our features without having to manually track each modification. We can easily track the status of our features across different environments.</p>
<h2 id="heading-lets-code">Let’s Code!</h2>
<p>In this tutorial, you’ll see how the functionality of an app changes before and after testing with feature flagging mechanisms. The tools and frameworks we’ll use are Golang, Docker, Redis, Flagsmith, and GitHub. As discussed, all are open source and free to create an account to test.</p>
<p>To get started, open your favourite IDE, initialize a Golang project, and then copy the below code in the <code>main.go</code> file. Then run <code>go mod tidy</code> to install all the dependencies it needs.</p>
<p>Let’s understand what’s going on in the below code snippet:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"errors"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"strconv"</span>

    <span class="hljs-string">"github.com/gin-gonic/gin"</span>
    <span class="hljs-string">"github.com/go-redis/redis_rate/v10"</span>
    <span class="hljs-string">"github.com/joho/godotenv"</span>
    <span class="hljs-string">"github.com/redis/go-redis/v9"</span>
)

<span class="hljs-keyword">var</span> (
    redisClient *redis.Client
    limiter     *redis_rate.Limiter
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initClients</span><span class="hljs-params">()</span></span> {
    redisClient = redis.NewClient(&amp;redis.Options{
        Addr: os.Getenv(<span class="hljs-string">"REDIS_URL"</span>),
    })
    limiter = redis_rate.NewLimiter(redisClient)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    err := godotenv.Load()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Printf(<span class="hljs-string">"Loading environment variable from the host system"</span>)
    } <span class="hljs-keyword">else</span> {
        log.Printf(<span class="hljs-string">"Loading environment from .env file"</span>)
    }

    initClients()
    <span class="hljs-keyword">defer</span> redisClient.Close()

    r := gin.Default()
    r.GET(<span class="hljs-string">"/ping"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        err, remainingLimit := rateLimitCall(c.ClientIP())
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            c.JSON(
                http.StatusTooManyRequests,
                gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"Rate Limit Hit"</span>})
        } <span class="hljs-keyword">else</span> {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"Your left over API request is"</span>: remainingLimit})
        }
    })
    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        c.JSON(
            http.StatusOK,
            gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
    })
    r.Run(<span class="hljs-string">":"</span> + os.Getenv(<span class="hljs-string">"PORT"</span>))
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">rateLimitCall</span><span class="hljs-params">(ClientIP <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(error, <span class="hljs-keyword">int</span>)</span></span> {
    ctx := context.Background()

    rateLimitString := os.Getenv(<span class="hljs-string">"RATE_LIMIT"</span>)
    RATE_LIMIT, _ := strconv.Atoi(rateLimitString)

    res, err := limiter.Allow(ctx, ClientIP, redis_rate.PerHour(RATE_LIMIT))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    <span class="hljs-keyword">if</span> res.Remaining == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"You have hit the Rate Limit for the API. Try again later"</span>), <span class="hljs-number">0</span>
    }

    fmt.Println(<span class="hljs-string">"remaining request for"</span>, ClientIP, <span class="hljs-string">"is"</span>, res.Remaining)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, res.Remaining
}
</code></pre>
<h3 id="heading-initializing-the-tools">Initializing the Tools</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initClients</span><span class="hljs-params">()</span></span> {
    redisClient = redis.NewClient(&amp;redis.Options{
        Addr: os.Getenv(<span class="hljs-string">"REDIS_URL"</span>),
    })
    limiter = redis_rate.NewLimiter(redisClient)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    err := godotenv.Load()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Printf(<span class="hljs-string">"Loading environment variable from the host system"</span>)
    } <span class="hljs-keyword">else</span> {
        log.Printf(<span class="hljs-string">"Loading environment from .env file"</span>)
    }

    initClients()
    <span class="hljs-keyword">defer</span> redisClient.Close()

    r := gin.Default()
    ...
    })
</code></pre>
<p>At the top, we declare variables to store Redis and Rate limiter clients to reuse and initialise them once. Then we initialise them in the <code>initClients()</code>.</p>
<p>In <code>main()</code>, first, we load the environment variables from the system or the .env file. Then we call <code>initClients()</code>. This will create clients and store them in the variables we created.</p>
<p>Next, we create a <strong>Gin</strong> router that handles all our incoming requests. These are the environment variables we need in our <code>.env</code> file. For this demo, we need a Redis instance running to store all the data for rate-limiting functionality. We can use Docker or any remote machine – just remember to update <code>REDIS_URL</code> accordingly. I am going to use Docker.</p>
<p>We could also go a mile ahead and get all the environment variables from the feature flags, but we won’t do this here.</p>
<pre><code class="lang-bash">REDIS_URL=localhost:6379
PORT=8080
RATE_LIMIT=10
</code></pre>
<h3 id="heading-creating-endpoints-for-the-api">Creating Endpoints for the API</h3>
<pre><code class="lang-go">r.GET(<span class="hljs-string">"/ping"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        err, remainingLimit := rateLimitCall(c.ClientIP())
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            c.JSON(
                http.StatusTooManyRequests,
                gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"Rate Limit Hit"</span>})
        } <span class="hljs-keyword">else</span> {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"Your left over API request is"</span>: remainingLimit})
        }
    })
    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        c.JSON(
            http.StatusOK,
            gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
    })
    r.Run(<span class="hljs-string">":"</span> + os.Getenv(<span class="hljs-string">"PORT"</span>))
</code></pre>
<p>Then we create two <strong>GET</strong> endpoints, <code>/ping</code> and <code>/beta</code>. Every time someone hits the <code>/ping</code> endpoint we call the <code>rateLimitCall()</code> function. It checks and sets the rate limit of incoming requests from an <strong>IP address</strong>. All this is stored in the Redis instance we created.</p>
<p>So, now if the user has interacted with the <code>/ping</code> API endpoint for the first time, will create an entry with a limit of <strong>10 per hour</strong>. The limit number <strong>10</strong> comes from the <code>RATE_LIMIT</code> we set, and the hourly refresh form comes from the <code>redis_rate.PerHour(RATE_LIMIT)</code> function.</p>
<p>Next, we check if the user has a remaining limit. If yes, we will return a message with the number of requests they have remaining. Otherwise, if they hit the limit cap, we return a message letting them know this.</p>
<p>Apart from the <code>/ping</code> endpoint, we have another endpoint <code>/beta</code>. It returns a simple message, but later we’ll see how (using feature flags) we can completely turn on and off the functionality of this endpoint.</p>
<h3 id="heading-how-to-add-feature-flagging">How to Add Feature Flagging</h3>
<p>Now it’s time to add feature flagging capabilities to our app. We are going to use <a target="_blank" href="https://flagsmith.com/">Flagsmith</a>. Flagsmith is an open source software that lets us easily create and manage feature flags across web, mobile, and server-side applications.</p>
<p>Using Flagsmith, we can wrap features in a flag and then toggle them on or off for different environments, users, or user segments. And then you’ll be able to manage all of them from the Flagsmith dashboard without needing to redeploy.</p>
<p>So, let’s install the Flagsmith package by running the below command:</p>
<pre><code class="lang-bash">go get github.com/Flagsmith/flagsmith-go-client/v3
</code></pre>
<p>Then we import the package by giving it an alias <strong>flagsmith</strong>. Below is the updated functionality after we apply feature flagging to our existing code.</p>
<p>Let’s understand the changes we’ve made here (I’ll explain below the code snippet):</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"errors"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"os"</span>

    flagsmith <span class="hljs-string">"github.com/Flagsmith/flagsmith-go-client/v3"</span>
    <span class="hljs-string">"github.com/gin-gonic/gin"</span>
    <span class="hljs-string">"github.com/go-redis/redis_rate/v10"</span>
    <span class="hljs-string">"github.com/joho/godotenv"</span>
    <span class="hljs-string">"github.com/redis/go-redis/v9"</span>
)

<span class="hljs-keyword">var</span> (
    redisClient     *redis.Client
    limiter         *redis_rate.Limiter
    flagsmithClient *flagsmith.Client
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initClients</span><span class="hljs-params">()</span></span> {
    redisClient = redis.NewClient(&amp;redis.Options{
        Addr: os.Getenv(<span class="hljs-string">"REDIS_URL"</span>),
    })
    limiter = redis_rate.NewLimiter(redisClient)
    flagsmithClient = flagsmith.NewClient(os.Getenv(<span class="hljs-string">"FLAGSMITH_ENVIRONMENT_KEY"</span>))
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    err := godotenv.Load()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Printf(<span class="hljs-string">"Loading environment variable from the host system"</span>)
    } <span class="hljs-keyword">else</span> {
        log.Printf(<span class="hljs-string">"Loading environment from .env file"</span>)
    }

    initClients()
    <span class="hljs-keyword">defer</span> redisClient.Close()

    r := gin.Default()
    r.GET(<span class="hljs-string">"/ping"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        err, remainingLimit := rateLimitCall(c.ClientIP())
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            c.JSON(
                http.StatusTooManyRequests,
                gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"Rate Limit Hit"</span>})
        } <span class="hljs-keyword">else</span> {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"Your left over API request is"</span>: remainingLimit})
        }
    })
    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        flags := getFeatureFlags()
        isEnabled, _ := flags.IsFeatureEnabled(<span class="hljs-string">"beta"</span>)
        <span class="hljs-keyword">if</span> isEnabled {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
        } <span class="hljs-keyword">else</span> {
            c.String(http.StatusNotFound, <span class="hljs-string">"404 page not found"</span>)
        }
    })

    r.Run(<span class="hljs-string">":"</span> + os.Getenv(<span class="hljs-string">"PORT"</span>))
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">rateLimitCall</span><span class="hljs-params">(ClientIP <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(error, <span class="hljs-keyword">int</span>)</span></span> {

    ctx := context.Background()

    flags := getFeatureFlags()
    rateLimitInterface, _ := flags.GetFeatureValue(<span class="hljs-string">"rate_limit"</span>)
    RATE_LIMIT := <span class="hljs-keyword">int</span>(rateLimitInterface.(<span class="hljs-keyword">float64</span>))
    fmt.Println(<span class="hljs-string">"Current Rate Limit is"</span>, RATE_LIMIT)

    res, err := limiter.Allow(ctx, ClientIP, redis_rate.PerHour(RATE_LIMIT))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    <span class="hljs-keyword">if</span> res.Remaining == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"You have hit the Rate Limit for the API. Try again later"</span>), <span class="hljs-number">0</span>
    }

    fmt.Println(<span class="hljs-string">"remaining request for"</span>, ClientIP, <span class="hljs-string">"is"</span>, res.Remaining)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, res.Remaining
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getFeatureFlags</span><span class="hljs-params">()</span> <span class="hljs-title">flagsmith</span>.<span class="hljs-title">Flags</span></span> {
    ctx := context.Background()
    flags, _ := flagsmithClient.GetEnvironmentFlags(ctx)
    <span class="hljs-keyword">return</span> flags
}
</code></pre>
<h3 id="heading-understanding-the-feature-flag-code-logic">Understanding the Feature Flag Code Logic</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getFeatureFlags</span><span class="hljs-params">()</span> <span class="hljs-title">flagsmith</span>.<span class="hljs-title">Flags</span></span> {
    ctx := context.Background()
    flags, _ := flagsmithClient.GetEnvironmentFlags(ctx)
    <span class="hljs-keyword">return</span> flags
}
</code></pre>
<p>First, let’s directly jump to the new <code>getFeatureFlags()</code> function we created at the bottom. This function will return all the flags we created on the Flagsmith dashboard, by calling the <code>GetEnvironmentFlags()</code> method on <code>flagsmithClient</code>.</p>
<p>We initiated the <code>flagsmithClient</code> inside the <code>initClients()</code> function. The Flagsmith Client needs the access key (the <code>NewClient()</code> function) that we can get from the Flagsmith dashboard. As we did for the Redis and Limter clients, we will store the client in a global variable for reusability. You’ll understand the dashboard, creating flags, and retrieving the key in later steps.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">rateLimitCall</span><span class="hljs-params">(ClientIP <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(error, <span class="hljs-keyword">int</span>)</span></span> {

    ctx := context.Background()

    flags := getFeatureFlags()
    rateLimitInterface, _ := flags.GetFeatureValue(<span class="hljs-string">"rate_limit"</span>)
    RATE_LIMIT := <span class="hljs-keyword">int</span>(rateLimitInterface.(<span class="hljs-keyword">float64</span>))
    fmt.Println(<span class="hljs-string">"Current Rate Limit is"</span>, RATE_LIMIT)

    res, err := limiter.Allow(ctx, ClientIP, redis_rate.PerHour(RATE_LIMIT))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    <span class="hljs-keyword">if</span> res.Remaining == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"You have hit the Rate Limit for the API. Try again later"</span>), <span class="hljs-number">0</span>
    }

    fmt.Println(<span class="hljs-string">"remaining request for"</span>, ClientIP, <span class="hljs-string">"is"</span>, res.Remaining)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, res.Remaining
}
</code></pre>
<p>Now coming to the <code>rateLimitCall()</code> function, instead of getting <code>RATE_LIMIT</code> from the environment, we get the value from the <code>rate_limit</code> flag (that we will create later). We call <code>getFeatureFlags()</code> and get the flag <code>rate_limit</code> value out from all the flags.</p>
<p>By setting these as feature flags, we can dynamically change the limit anytime from the dashboard. We don’t need to change the code’s functionality or do it the traditional way by changing the <code>RATE_LIMIT</code> value and re-running the server so that it catches new updated values.</p>
<pre><code class="lang-go">    r.GET(<span class="hljs-string">"/beta"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        flags := getFeatureFlags()
        isEnabled, _ := flags.IsFeatureEnabled(<span class="hljs-string">"beta"</span>)
        <span class="hljs-keyword">if</span> isEnabled {
            c.JSON(
                http.StatusOK,
                gin.H{<span class="hljs-string">"message"</span>: <span class="hljs-string">"This is beta endpoint"</span>})
        } <span class="hljs-keyword">else</span> {
            c.String(http.StatusNotFound, <span class="hljs-string">"404 page not found"</span>)
        }
    })
</code></pre>
<p>Now coming to the <code>/beta</code> endpoint, based on whether the beta flag is enabled or disabled, this endpoint will serve the query. Otherwise, it will act as a non-reachable endpoint and return a 404 error message.</p>
<p>In our example, I have added a basic placeholder message to show how it will work, but this opens new possibilities in testing and initial releases (beta). If the API has a new endpoint, we can wrap the functionality in the feature flag and make it available and unavailable with a single click of a button. Also, we can do a lot more like scheduling and canary releases.</p>
<p>Also, our <code>.env</code> file will look like this. We have removed <code>RATE_LIMIT</code> and added <code>FLAGSMITH_ENVIRONMENT_KEY</code>.</p>
<pre><code class="lang-bash">REDIS_URL=localhost:6379
PORT=8080
FLAGSMITH_ENVIRONMENT_KEY=ser.ZRd***********469
</code></pre>
<h3 id="heading-how-to-create-feature-flags-in-the-flasgsmith-dashboard">How to Create Feature Flags in the Flasgsmith Dashboard</h3>
<p>Let’s head to the Flagsmith dashboard to create the flags we used above and get the access key. If you don’t have a Flagsmith account you can sign up for free <a target="_blank" href="https://app.flagsmith.com/signup">here</a>.</p>
<p>After you sign up you will be prompted to create an organisation and a project. Project separation is good, as it helps us isolate logic for different projects. Once you are done, you will see a dashboard, just like the screenshot below.</p>
<p>We have loads of functionalities from integrations to scheduling the flags to compare the changes. Apart from Go, Flagsmith provides many <a target="_blank" href="https://docs.flagsmith.com/clients/">SDKs</a>. You can click on where the language name is written and it will give you some boilerplate code for that language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544211942/57f3651f-b62a-4b8f-beb7-4320ef0e0a8e.png" alt="Screenshot of a web interface labeled &quot;Features&quot; for managing feature flags and remote config. It includes examples of Go code for installing the SDK and initializing a project, with options to test API values. There are buttons and tabs for navigation and settings." class="image--center mx-auto" width="2985" height="1887" loading="lazy"></p>
<h3 id="heading-rate-limiting-feature-flag">Rate Limiting Feature Flag</h3>
<p>Now, let's create our first feature flag for the rate limit. Click on the <strong>Create Feature</strong> button in the top right corner. A sidebar window will open up. Set the name, then to make the flag turn on the right way while creating, we can select <strong>Enabled by default.</strong></p>
<p>In the value section, we need to set the flag value. It can take formats like Txt, JSON, XML, and so on. As our feature value is simple text like 20, 30, and so on, we will choose Txt (the default one) and set a random limit – we’ll go with <strong>20</strong>.</p>
<p>You can also give tags and descriptions. Tags can be helpful when filtering out the Feature Flags. For example, we can create a tag <code>backend</code> to filter out all the feature flags related to Backend. The description is a concise explanation of what this particular future flag does when it is enabled (and will help with future understanding).</p>
<p>The screenshot below shows how it will look after filling in the details. Then, click on the <strong>Create Feature</strong> button to create the flag.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544238847/4e5cf3ab-1fb6-4783-afcc-39adcebae48e.png" alt="A screenshot of a web application interface showing the creation of a new feature. On the left, there is a menu with options like Features and SDK Keys. On the right, fields for adding a new feature are visible, including an ID/Name, a toggle for enabling by default, a value set to 20, and options for tags and descriptions. There is a note indicating feature creation for all environments, with a &quot;Create Feature&quot; button at the bottom." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<h3 id="heading-beta-feature-flag">Beta Feature Flag</h3>
<p>Let’s now create a second, <code>beta</code> feature flag. It will be the same process as the first one, but in this one, we don’t need to set any flag value and leave that column empty. Once we create both flags, our dashboard will look like this. It shows the flag name, value, current state (view), and so on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544256561/3735429f-8dd0-4f0f-a01e-b4a4a7b5aa75.png" alt="A software interface showcasing a &quot;Features&quot; section with toggles for &quot;beta&quot; and &quot;rate_limit&quot; features. The page includes navigation options on the left and buttons for creating features and running tests." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<h3 id="heading-getting-the-access-key">Getting the Access Key</h3>
<p>To get the Access Key, click on the <strong>SDK Keys</strong> from the sidebar, and click the <strong>Create Server-side Environment Key</strong> button to generate a key. As our app is server-side, it’s good to use that one only. Then copy and paste that key into the value placed in <code>.env</code> for the <code>FLAGSMITH_ENVIRONMENT_KEY</code> key.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544280780/fc37cb29-3069-4e2f-b35b-eea7632c47cd.png" alt="Screenshot of a software interface showing &quot;Client-side Environment Key&quot; and &quot;Server-side Environment Keys&quot; sections. A button labeled &quot;Create Server-side Environment Key&quot; is displayed prominently. The sidebar menu includes options like &quot;SDK Keys&quot; and &quot;Environment Settings.&quot;" class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<h3 id="heading-running-the-api">Running the API</h3>
<p>Now everything is set, so let’s head over back to IDE and run the server by executing the <code>go run main.go</code> command in the terminal. We will see this message In the terminal. In case you encounter any errors, just check that the packages are correctly installed, the variables are correctly set, and the app accesses the Redis instance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544780659/95fbbb17-43c3-4cd1-b84f-020c08ec38d3.png" alt="Screenshot of a VS Code window showing a Go project with the file &quot;main.go&quot; open. The code includes functions for rate limiting API calls and retrieving feature flags. The terminal at the bottom displays the output of running the application, with warnings and status messages related to a web server." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>Now if we visit <a target="_blank" href="http://localhost:8080/ping"><strong>localhost:8080/ping</strong></a>, we will get a message <code>{"Your left over API request is":19}</code>. The limit was 20, we did one request now, and the remaining is 19.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544374092/bc97064c-285e-44fa-990d-51a52a671d26.png" alt="A browser window displaying a webpage at &quot;localhost:8080/ping&quot; showing the JSON message: {&quot;Your left over API request is&quot;: 19}." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<h3 id="heading-updating-the-ratelimit-flag">Updating the <code>rate_limit</code> Flag</h3>
<p>Let’s update the <code>rate_limit</code> flag value to 10 and see what happens. To do so, again visit the Flagsmith dashboard and click on the flag name. A side menu bar will open. Update the value to 10, and click on the <strong>Update Feature Value</strong> button.</p>
<p>We can also schedule the update. For example, this can be useful when we expect a spike in traffic at a certain timeframe and reduce the limit per user to reduce server load.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730545050685/f253ea86-de3d-4a6a-b5fd-35f489da86cf.png" alt="Screenshot of a software dashboard showing a feature management interface. The &quot;rate_limit&quot; feature is enabled with a value of 10. Options include editing value, segment overrides, and scheduling updates." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>If you now visit <a target="_blank" href="http://localhost:8080/ping"><strong>localhost:8080/ping</strong></a>, you will get a message <code>{"Your left over API request is":8}</code> – because the total limit is 10 and we have already requested two times.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544415108/97e6fd9c-b5a1-4143-877a-6724cf871a6b.png" alt="Browser window displaying a JSON response with the text: &quot;Your left over API request is: 8&quot;." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<p>Let's now test the <code>/beta</code> endpoint. Visit <a target="_blank" href="http://localhost:8080/beta">localhost:8080/beta</a>, and we will see a message <code>{"message":"This is beta endpoint"}</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544479869/623d8b72-3648-46ae-b79f-409da44c1d38.png" alt="Screenshot of a web browser displaying JSON data at the URL &quot;localhost:8080/beta&quot; with the message: &quot;This is beta endpoint&quot;." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<p>Now go back to the Flagsmith dashboard and toggle the switch to disable this flag. Now visit the the URL. You will get a 404 message like this endpoint never existed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544488522/518c58b2-f767-4396-9020-99e8cd01586a.png" alt="Screenshot of a browser window displaying a &quot;404 page not found&quot; error message." class="image--center mx-auto" width="2324" height="780" loading="lazy"></p>
<p>Now that we’ve set up the functionality and demoed the feature flagging capabilities, let’s see how we can integrate the Flasgsmith GitHub App.</p>
<h3 id="heading-how-to-integrate-feature-flags-with-the-github-app">How to Integrate Feature Flags with the GitHub App</h3>
<p>First, make sure you have pushed your app to GitHub. After that, install the GitHub Flasgsmith App on your repo from the <a target="_blank" href="https://github.com/apps/flagsmith">GitHub Marketplace</a>.</p>
<p>By integrating GitHub and Falagsmith, we can view updates on your feature flags/features as comments in GitHub Issues and Pull Requests. This allows us to easily track features, from creating an issue to merging a PR and deploying the changes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544845464/dcce9af3-a34f-420a-b9c4-2968d47fda70.png" alt="Screenshot of the Flagsmith GitHub app integration page, detailing its features and benefits, with an option to install the app." class="image--center mx-auto" width="2997" height="1885" loading="lazy"></p>
<p>Then select your organisation and the repositories where you want to install the app. You can install it on all of your repos or select a particular one.</p>
<p>As you install it, you will be auto-redirected to the Flagmsith dashboard to configure and complete the integration. Most of the data will be pre-populated, so you just need to select and add a project, and then save the configuration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730544640407/8ac36f96-d61b-47f7-ad3a-f914f0f01824.png" alt="Screenshot of a webpage for configuring GitHub integration with Flagsmith. It includes fields for selecting the organization, project, and repository, with options set for &quot;Pradumna,&quot; &quot;go-api,&quot; and &quot;go-redis-flagsmith.&quot; There is an &quot;Add Project&quot; button and a &quot;Save Configuration&quot; button at the bottom." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>Once you hit the Save <strong>Configuration Button</strong>, it will redirect you back to the main Flagsmith dashboard where we were previously working.</p>
<p>Now let’s link one of the existing flags with the GitHub issue/pull request (raise a dummy PR/issue to test it), or you can create a new flag to test. Let’s proceed with the beta flag which we already created for the <code>beta</code> endpoint.</p>
<p>To link the flag with an existing issue or a pull request, click on the flag name, and a side menu will pop up from the right. Then, choose the 'Link' tab. Then select the Pull Request option, and choose the Pull Request you want to link. All of your Issues and Pull Requests linked to this flag are visible below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730546404697/0fb1c515-ab42-494d-ac84-87e076d30607.png" alt="A screenshot of a development environment interface showing the &quot;Features&quot; section, with a sidebar menu on the left. The &quot;Edit Feature: beta&quot; panel is open on the right, displaying options to link an issue or pull request and a listed pull request titled &quot;feat: Update the beta endpoint feature (#2)&quot; with its status marked as open." class="image--center mx-auto" width="3024" height="1890" loading="lazy"></p>
<p>To verify that the flag is successfully linked, click the hyperlink with the arrow icon below the <strong>Name</strong> column heading. It will navigate you to that particular Issue/Pull Request on GitHub. You can see that the Flagsmith GitHub App has commented below with all the details, such as environment, enabled value, and so on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730545132237/4601a08e-62d9-4ebc-890e-89430bf6624e.png" alt="GitHub pull request page showing a request titled &quot;feat: Update the beta endpoint feature #2&quot; to merge a commit from the &quot;beta&quot; branch into &quot;main&quot;. It includes a user comment about the update and a Flagsmith bot comment showing feature status for production and development environments. The pull request is open, with no reviews yet." class="image--center mx-auto" width="2990" height="1614" loading="lazy"></p>
<h3 id="heading-testing-the-flagsmith-github-app">Testing the Flagsmith GitHub App</h3>
<p>After this, when you make any changes to the flag settings, such as turning on/off the flag or changing the value, the bot will comment with all the updated details.</p>
<p>Let’s test by turning the flag off. As soon as you turn off the flash from the Dashboard, the bot should comment that the flag has now been disabled:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730545146615/b76f2f21-369a-4617-b55b-abc3201a1c52.png" alt="Image showing a GitHub pull request interface. The pull request is titled &quot;feat: Update the beta endpoint feature #2&quot; and shows an update from the flagsmith bot indicating that the &quot;beta&quot; feature for the &quot;Development&quot; environment is currently disabled." class="image--center mx-auto" width="2434" height="688" loading="lazy"></p>
<p>That’s it. That is how it’s simple to integrate Flagsmith with GitHub.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>To sum it up, you now know how you can leverage feature flags as a backend developer to change the functionality of your app on the fly.</p>
<p>To take things to the next level, we integrated our demo app with the Flagsmith GitHub app so it could stay updated with the changes to our feature flags’ status on Pull Requests/Issues without having to manually update them.</p>
<p>Check out the Flagsmith <a target="_blank" href="https://github.com/Flagsmith/flagsmith">repo here</a> and don't forget to give each of these projects a star to show your support. You can also join their amazing <a target="_blank" href="https://discord.com/invite/hFhxNtXzgm">community</a> to get technical support.</p>
<p>You can connect with me - Pradumna Saraf, on socials <a target="_blank" href="https://links.pradumnasaraf.dev/">here</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
