DEV Community

Cover image for Building Brew Haven: A/B Testing My Coffee Shop Dreams with DevCycle
Rajeev R. Sharma
Rajeev R. Sharma Subscriber

Posted on

Building Brew Haven: A/B Testing My Coffee Shop Dreams with DevCycle

This is a submission for the DevCycle Feature Flag Challenge: Feature Flag Funhouse

Do you love coffee? As developers, many of us jokingly claim to be "powered by coffee", and the thought of opening a quaint coffee shop someday often lingers in the back of our minds — perhaps as a post-dev career dream.

When brainstorming ideas for a fun project to showcase feature flags, this coffee shop fantasy kept nudging me: "Pick me! Build me!". So, I decided to indulge that thought and create Brew Haven, a dummy coffee shop app that explores the power of feature flags.

What I Built

As you might have guessed, I built a playground to explore the power of feature flags, packaged as a coffee shop app. The app features customizable menus, seasonal items, and dynamic A/B testing for promotions using the DevCycle SDK. It also leverages the DevCycle Management APIs to power an admin panel, giving shop managers full control over these features in real time.

Demo

You can try out the live app here: Brew Haven

The admin page uses a dummy password auth. Use admin@123 passowrd to view the admin panel and play around with the available feature flags.

Video Demo

The following video gives a short walkthrough of the app.

Showcasing Feature Flags in Brew Haven

1. The Coffee Menu

The shop menu is customizable and evolves with the seasons—thanks to feature flags. The Coffee Menu feature flag consists of 3 variables that control:

  • Showing nutritional info for health-conscious customers.
  • Enabling the seasonal menu for limited-time offerings.
  • Allowing order customization for that perfect latte.

You can choose from 3 different variations of the flag:

  1. Basic: Disables all menu variables.
  2. Standard: Allows order customization and shows nutrition info.
  3. Seasonal: Enables all three variables.

2. Payment and Ordering

This feature flag makes the ordering process adaptable. Depending on the selected Payment and Ordering flag variation, admins can:

  • Allow online payments.
  • Enable loyalty points redemption.
  • Enable live order tracking.

This flag also consists of 3 variables to control the above aspects, and the following three variations:

  1. Basic: Disables all the variables.
  2. Standard: Allows online payments and live order tracking.
  3. Premium: Allows loyalty points apart from other standard features.

3. A/B Testing with Promotions

The app also demonstrates A/B testing through a promotions feature flag, which allows admins to:

  • Offer discounts to a random subset of customers.
  • Customize promotion details, like discount percentages/amount and minimum cart values to avail the promotions.

This feature flag consists of four variables that allows setting:

  • The promotion text (string).
  • Discount value (number).
  • Discount type (enum with values none, amount, percentage).
  • Min cart value (number, 0 for no min cart value).

These feature flags made it easy to bring my coffee shop dreams closer to reality, blending fun experimentation with real-world use cases like A/B testing and customizations.

App Screenshots

Home Page

Home page

Menu Page

Menu page

Checkout Page

Checkout page

Admin Page

Admin page

My Code

The source code of the app is open source, and is available on GitHub.

GitHub logo ra-jeev / brew-haven

Dummy Coffee Shop App with DevCycle Feature Flags

Brew Haven ☕

Project Overview

Brew Haven is a dynamic coffee shop web application that demonstrates the use of feature flags using DevCycle. The app provides a simple customizable menu, checkout experience, and an innovative admin interface for managing feature variations and A/B testing.

Key Features

  • Home page showcasing the coffee shop
  • Interactive menu display
  • Dummy checkout process
  • Feature flag management through DevCycle
  • Admin page for feature flag control

Technologies Used

  • React
  • Vite
  • Shadcn UI
  • DevCycle SDK
  • Netlify Functions

Prerequisites

  • Node.js (recommended version 18+)
  • pnpm package manager
  • DevCycle account
  • Netlify account and CLI (for local development and deployment)

Environment Setup

  1. Clone the repository
  2. Install dependencies:
pnpm install
Enter fullscreen mode Exit fullscreen mode
  1. Rename .env.example to .env and update the following environment variables:
VITE_DEVCYCLE_CLIENT_SDK_KEY=your_client_sdk_key
DEVCYCLE_API_CLIENT_ID=your_client_id
DEVCYCLE_API_CLIENT_SECRET=your_client_secret
DEVCYCLE_PROJECT_ID=your_project_id
Enter fullscreen mode Exit fullscreen mode

Local Development

To run the application in development mode:

# For standard development
pnpm dev

# For testing admin page with Netlify Functions
netlify dev
Enter fullscreen mode Exit fullscreen mode

Deployment

The project is…

Technologies Used

To bring Brew Haven to life, I used the following:

  • React with Vite for a fast and modular frontend.
  • Shadcn UI for styling and components.
  • Netlify Functions for secure serverless DevCycle Management API calls.
  • DevCycle SDK for feature flag integration on the client side.

Client Side Usage of DevCycle SDK

After adding the DevCycle React SDK dependency, we first initialize the DevCycleProvider in the App.tsx file as shown below:

// src/App.tsx

import { withDevCycleProvider } from "@devcycle/react-client-sdk";
// ...

function App() {
  return (
    <ThemeProvider defaultTheme="system" storageKey="coffee-shop-ui-theme">
      <Router>
        <Layout>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/menu" element={<Menu />} />
            <Route path="/checkout" element={<Checkout />} />
            <Route path="/orders" element={<Orders />} />
            <Route
              path="/admin"
              element={
                <ProtectedRoute>
                  <Admin />
                </ProtectedRoute>
              }
            />
          </Routes>
        </Layout>
      </Router>
      <Toaster />
    </ThemeProvider>
  );
}

export default withDevCycleProvider({
  sdkKey: import.meta.env.VITE_DEVCYCLE_CLIENT_SDK_KEY,
  options: {
    logLevel: "debug",
  },
})(App);
Enter fullscreen mode Exit fullscreen mode

And then we create a useFeatureFlags hook to get the various feature flags variables associated with the app as shown below:

// src/hooks/use-feature-flags.ts

import { useVariableValue } from "@devcycle/react-client-sdk";
import { featureKeys } from "@/lib/consts";

export function useFeatureFlags() {
  const showNutritionInfo = useVariableValue(
    featureKeys.SHOW_NUTRITION_INFO,
    false,
  );
  const enableOnlinePayment = useVariableValue(
    featureKeys.ENABLE_ONLINE_PAYMENT,
    false,
  );
  const showPromotionalBanner = useVariableValue(
    featureKeys.SHOW_PROMOTIONAL_BANNER,
    "",
  );

  // etc.

  return {
    showNutritionInfo,
    enableOnlinePayment,
    showPromotionalBanner,
    // ...
  };
}
Enter fullscreen mode Exit fullscreen mode

Interacting with DevCycle Management APIs

For securely calling the Management APIs, we use Netlify serverless functions so that the DevCycle API's client_id and client_secret are not exposed to the client.

Before we can interact with the DevCycle APIs, we need to generate an auth token using the DevCycle APIs client id and secret. The below code shows how to generate the auth token:

// netlify/functions/feature-flags.mts

interface AuthToken {
  access_token: string;
  expires_in: number;
  token_type: string;
}

let tokenCache: { token: string; expiresAt: number } | null = null;

async function getAuthToken() {
  if (tokenCache && tokenCache.expiresAt > Date.now()) {
    return tokenCache.token;
  }

  try {
    const response = await fetch("https://auth.devcycle.com/oauth/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({
        grant_type: "client_credentials",
        audience: "https://api.devcycle.com/",
        client_id: process.env.DEVCYCLE_API_CLIENT_ID!,
        client_secret: process.env.DEVCYCLE_API_CLIENT_SECRET!,
      }),
    });

    if (!response.ok) {
      throw new Error(`Auth failed: ${response.status}`);
    }

    const data: AuthToken = await response.json();

    tokenCache = {
      token: data.access_token,
      expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
    };

    return data.access_token;
  } catch (error) {
    console.error("Failed to get auth token:", error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we can fetch the available feature flags, and their config (to get the currently active variation) using the below code:

// netlify/functions/feature-flags.mts

interface Distribution {
  _variation: string;
  percentage: number;
}

interface FeatureConfig {
  _feature: string;
  _environment: string;
  status: string;
  targets: {
    _id: string;
    name: string;
    distribution: Distribution[];
    audience: {
      name: string;
      filters: {
        operator: "and" | "or";
        filters: { type: string }[];
      };
    };
  }[];
}

async function getFeatures(
  featuresUrl: string,
  headers: Record<string, string>,
) {
  const featuresResponse = await fetch(featuresUrl, { headers });

  if (!featuresResponse.ok) {
    throw new Error(`Failed to fetch flags: ${featuresResponse.status}`);
  }

  const featuresData = await featuresResponse.json();

  // Fetch the config for each of the feature flags
  // We're only fetching the configs for the development env
  const configPromises = featuresData.map(async (feature: any) => {
    const configResponse = await fetch(
      `${featuresUrl}/${feature._id}/configurations?environment=development`,
      { headers },
    );

    if (!configResponse.ok) {
      console.error(`Failed to fetch config for feature ${feature._id}`);
      return null;
    }

    const configs: FeatureConfig[] = await configResponse.json();

    return {
      ...feature,
      targets: configs[0].targets,
      status: configs[0].status,
    };
  });

  const featuresWithConfigs = await Promise.all(configPromises);
  return featuresWithConfigs.filter((f) => f !== null);
}
Enter fullscreen mode Exit fullscreen mode

To update the feature flags config (changing the variation, or toggle the flag altogether), we can use the below function:

async function updateFeature(
  featuresUrl: string,
  featureId: string,
  headers: Record<string, string>,
  update: any,
) {
  const response = await fetch(
    `${featuresUrl}/${featureId}/configurations?environment=development`,
    {
      method: "PATCH",
      headers,
      body: JSON.stringify(update),
    },
  );

  if (!response.ok) {
    throw new Error(`Failed to update feature: ${response.status}`);
  }

  return await response.json();
}
Enter fullscreen mode Exit fullscreen mode

Finally, here is the Netlify function that uses the above functions to serve the client requests:

export default async (req: Request) => {
  try {
    const { method } = req;
    const token = await getAuthToken();
    const featuresBaseUrl = `https://api.devcycle.com/v1/projects/${process.env.DEVCYCLE_PROJECT_ID}/features`;
    const headers = {
      Authorization: `Bearer ${token}`,
    };

    if (method === "GET") {
      const features = await getFeatures(featuresBaseUrl, headers);

      return Response.json({ features });
    }

    if (method === "PATCH") {
      const body = await req.json();
      const { featureId, update } = body;

      const data = await updateFeature(
        featuresBaseUrl,
        featureId,
        {
          ...headers,
          "Content-Type": "application/json",
        },
        update,
      );

      return Response.json({ data });
    }

    return new Response(JSON.stringify({ error: "Method not allowed" }), {
      status: 405,
      headers: {
        "Content-Type": "application/json",
      },
    });
  } catch (error) {
    console.error("Error:", error);
    return new Response(
      JSON.stringify({
        error: error instanceof Error ? error.message : "Internal server error",
      }),
      {
        status: 500,
        headers: {
          "Content-Type": "application/json",
        },
      },
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

This Netlify function is called by the client in the following way:

// src/lib/api.ts

export async function getFeatureFlags() {
  try {
    const response = await fetch("/.netlify/functions/feature-flags");
    if (!response.ok) {
      throw new Error("Failed to fetch flags");
    }

    const data = await response.json();
    return { success: true, data: data.features as Feature[] };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : "Unknown error",
    };
  }
}

// and, so on...
Enter fullscreen mode Exit fullscreen mode

The above code snippets capture how the DevCycle SDK and its Management APIs are used within the app. You can go through the shared source code to view the complete implementation in more detail.

My DevCycle Experience

This was a fun project to build, thanks to the challenge prompt that said something about "fun". After the initial small learning curve of using the DevCycle dashboard — to create feature flags, adding variables and understanding how variations work — things were quite smooth.

The only issue I faced was with the onboarding tutorial app. The DevCycle web console got stuck in some state like "Waiting for the application launch" with the next button disabled. I had to ultimately login from an incognito browser window to get to the dashboard. But, doing this skipped the tutorial, and I had to figure out the tutorial code working (the feature flags creation etc) on my own as the project readme doesn't provide much info on this.

Additional Prize Categories

API All-Star

Wrapping Up

Brew Haven isn’t just a coffee shop app — it’s a showcase of how feature flags can make your projects more dynamic, responsive, and fun.

Feature flags allow you to:

  • Do gradual rollouts.
  • Reduce deployment risks.
  • Enable rapid experimentation.
  • Personalize user experiences.

Next time you’re sipping coffee and dreaming big, think about how feature flags can brew innovation into your projects. ☕

Until next time.


Keep adding the bits, and soon you'll have a lot of bytes to share with the world.

Top comments (0)