MoccoDocs

Feature flags in browsers and apps

Code that runs on your users' devices can't hold your flag rules: rules can name customers, internal accounts or plans you haven't announced. In a browser or an app, Mocco evaluates flags itself and sends back only the answers, such as new-checkout = true. Your code still uses the standard OpenFeature API, and Mocco's provider tells it at once when a flag changes.

This guide assumes you have a project with feature flags and an environment, as in the quickstart.

1. Choose which flags devices may see

Open a flag and tick Available to browsers and apps. Only flags marked this way are answered for a publishable key; any other flag looks like it doesn't exist (FLAG_NOT_FOUND), so an internal flag's name never reaches a device.

A flag's page with "Available to browsers and apps" ticked

2. Allow your site's origin

A publishable key works on a web page only if the page's origin belongs to one of the project's Web apps. On the project's Overview page, add a Web app (or choose Edit origins on one) and enter each origin your pages are served from, such as https://app.acme.com or http://localhost:5173 for local development. An origin is a scheme and host, with no path.

Editing a Web app's origins on the project overview

Apps (React Native) don't send an origin, so this step is only for web pages.

3. Create a publishable key for one environment

On the API keys page, create a Publishable key with flags:read and choose the environment it reads. Like a server key, it reads exactly one environment, so build each environment of your app with its own key. A publishable key is safe to ship: it can only read the answers for the flags you marked in step 1.

Creating a publishable flags:read key for Production

4a. A web page or React app

Install the OpenFeature web SDK and Mocco's web provider:

npm install @openfeature/web-sdk @mocco/openfeature-web

Register the provider once, with the user's context:

import { MoccoWebProvider } from '@mocco/openfeature-web';
import { OpenFeature } from '@openfeature/web-sdk';

await OpenFeature.setContext({ targetingKey: user.id, plan: user.plan });
await OpenFeature.setProviderAndWait(new MoccoWebProvider({ publishableKey: 'mk_pub_…' }));

const showNewCheckout = OpenFeature.getClient().getBooleanValue('new-checkout', false);

Evaluations read the answers the provider already has, so they never wait on the network. The provider fetches all of them in one request and listens to Mocco's change stream: when you change a flag in the console, open pages update within a second or two and OpenFeature emits PROVIDER_CONFIGURATION_CHANGED. If the stream can't connect, it checks every 60 seconds instead (pollInterval). It also checks when the tab becomes visible again, and keeps the last answers in localStorage so a reload shows them before the network answers.

When the user signs in or changes plan, call OpenFeature.setContext(...) again; the provider fetches the answers for the new context.

React

With @openfeature/react-sdk, wrap the app once and read flags with hooks. Components re-render when a flag changes:

import { MoccoWebProvider } from '@mocco/openfeature-web';
import { OpenFeature, OpenFeatureProvider, useBooleanFlagValue } from '@openfeature/react-sdk';

OpenFeature.setProvider(new MoccoWebProvider({ publishableKey: 'mk_pub_…' }));

export function App() {
  return (
    <OpenFeatureProvider>
      <Checkout />
    </OpenFeatureProvider>
  );
}

function Checkout() {
  const isNew = useBooleanFlagValue('new-checkout', false);
  return isNew ? <NewCheckout /> : <OldCheckout />;
}

4b. A React Native app

Install the OpenFeature SDKs, Mocco's React Native provider, and the storage and event-stream libraries it uses:

npm install @openfeature/web-sdk @openfeature/react-sdk @mocco/openfeature-react-native \
  @react-native-async-storage/async-storage react-native-sse

React Native has no localStorage, page visibility or EventSource, so you pass them in:

import AsyncStorage from '@react-native-async-storage/async-storage';
import { MoccoReactNativeProvider } from '@mocco/openfeature-react-native';
import { OpenFeature, OpenFeatureProvider, useBooleanFlagValue } from '@openfeature/react-sdk';
import { AppState } from 'react-native';
import EventSource from 'react-native-sse';

OpenFeature.setProvider(
  new MoccoReactNativeProvider({
    publishableKey: 'mk_pub_…',
    storage: AsyncStorage,
    appState: AppState,
    EventSource,
  }),
);

Then use OpenFeatureProvider and the hooks as in the React example above.

What each piece does:

  • storage keeps the last answers on the device. On the next launch the app starts with them straight away, even offline, and refreshes in the background; while Mocco can't be reached the provider reports PROVIDER_STALE and keeps answering from the stored copy.

  • appState fetches fresh answers whenever the app comes back to the foreground, and pauses checking in the background.

  • EventSource listens to Mocco's change stream while the app is open, so changes arrive within seconds. Without it the provider checks every 60 seconds while the app is active (pollIntervalMs).

Only the very first launch, with nothing stored and no network, starts without answers: every flag then returns your code's default.

Server-side: the Vercel Flags SDK

In a Next.js app that uses the Flags SDK, evaluate on the server with a secret key and Mocco's server provider, through the Flags SDK's OpenFeature adapter:

npm install flags @flags-sdk/openfeature @openfeature/server-sdk @mocco/openfeature-server
import { createOpenFeatureAdapter } from '@flags-sdk/openfeature';
import { MoccoProvider } from '@mocco/openfeature-server';
import { OpenFeature } from '@openfeature/server-sdk';
import { flag } from 'flags/next';

const mocco = createOpenFeatureAdapter(async () => {
  await OpenFeature.setProviderAndWait(new MoccoProvider({ secretKey: process.env.MOCCO_FLAGS_KEY! }));
  return OpenFeature.getClient();
});

export const newCheckout = flag<boolean>({
  key: 'new-checkout',
  defaultValue: false,
  adapter: mocco.booleanValue(),
  identify: async () => ({ targetingKey: (await currentUser()).id }),
});

await newCheckout() in a server component or route then evaluates in memory, as described in the quickstart.