Edge Config & Flags
Settings and feature flags your app reads in milliseconds, changed without a redeploy.
What it is
Global Config (until recently Edge Config) is a small key-value store that Vercel copies to every region, so reads are fast enough for every request. It's built for data that's read constantly but changed rarely: switches, settings, allow-lists, redirects. Writing happens outside the app (dashboard, CLI or API), and a change reaches every region within seconds, with no redeploy. The Flags SDK (flags) adds a typed feature-flag layer on top, whatever the flag values come from.
When to use it
- Kill switches and maintenance banners
- Gradual rollouts and experiments
- Config that must change faster than a deploy: allow-lists, redirects, prices
1. The store
The page reads the feature-lab store with getAll() from @vercel/global-config. The SDK finds it through the GLOBAL_CONFIG connection string.
Reading Global Config…
2. A switch: the banner
Reading…
Turn it on, then reload within a few seconds: vercel global-config update feature-lab --patch '[{"operation":"upsert","key":"banner","value":{"enabled":true,"message":"Maintenance tonight at 22:00","tone":"warning"}}]'
3. A setting: the featured page
Reading…
Change it: vercel global-config update feature-lab --patch '[{"operation":"upsert","key":"featured","value":"proxy"}]'
4. A feature flag with a gradual rollout
The new-hero flag is declared with the Flags SDK in src/flags.ts. Proxy gives every visitor a random id in a cookie, the flag hashes it to a bucket from 0 to 99, and the flag is on when the bucket is below the percentage in Global Config.
Evaluating the flag…
Roll out to everyone: vercel global-config update feature-lab --patch '[{"operation":"upsert","key":"rollout","value":{"new-hero":100}}]'
Show code · src/app/edge-config/page.tsx
import { cookies } from "next/headers";
import Link from "next/link";
import { Suspense } from "react";
import { CodePeek } from "@/components/CodePeek";
import { Demo, FeaturePage, JsonView } from "@/components/FeaturePage";
import { newHeroFlag } from "@/flags";
import { getFeature } from "@/lib/features";
import { type ConfigRead, bucketOf, readLabConfig, rolloutPercent } from "@/lib/global-config";
import { VISITOR_COOKIE } from "@/lib/proxy-rules";
type Read = { read: Promise<ConfigRead> };
const Unavailable = ({ reason }: { reason: string }) => <p className="error">{reason}</p>;
async function StoreContents({ read }: Read) {
const result = await read;
if (!result.ok) return <Unavailable reason={result.reason} />;
return (
<>
<p>Read every item in <strong className="stamp">{result.ms.toFixed(1)} ms</strong>, on this request:</p>
<JsonView value={result.config} />
</>
);
}
async function Banner({ read }: Read) {
const result = await read;
if (!result.ok) return <Unavailable reason={result.reason} />;
const banner = result.config.banner;
if (!banner?.enabled || !banner.message) {
return <p className="muted">The banner is <strong>off</strong> (<code>banner.enabled</code> is false), so nothing is shown.</p>;
}
return <p role="status" className={banner.tone === "warning" ? "error" : undefined}><strong>📣 {banner.message}</strong></p>;
}
async function Featured({ read }: Read) {
const result = await read;
if (!result.ok) return <Unavailable reason={result.reason} />;
const feature = result.config.featured ? getFeature(result.config.featured) : undefined;
if (!feature) return <p className="muted">No featured page set (or <code>featured</code> names a page that doesn't exist).</p>;
return <p>Featured today: <Link href={`/${feature.slug}`}><strong>{feature.name}</strong></Link>. {feature.summary}</p>;
}
async function Rollout({ read }: Read) {
const [result, showNewHero, visitorId] = await Promise.all([
read,
newHeroFlag(),
cookies().then((c) => c.get(VISITOR_COOKIE)?.value),
]);
const percent = result.ok ? rolloutPercent(result.config, "new-hero") : 0;
const bucket = visitorId ? bucketOf(visitorId, "new-hero") : null;
return (
<>
<div className="cells">
<div className="cell"><span className="small muted">Rollout</span><strong>{percent}% of visitors</strong></div>
<div className="cell"><span className="small muted">Your bucket</span><strong>{bucket ?? "none"}</strong></div>
<div className="cell"><span className="small muted">new-hero</span><strong>{showNewHero ? "on" : "off"}</strong></div>
</div>
{showNewHero ? (
<div className="cell" style={{ borderColor: "var(--accent)" }}>
<strong>✨ The new hero</strong>
<span className="small">Your bucket ({bucket}) is under {percent}, so you're in the rollout.</span>
</div>
) : (
<div className="cell">
<strong>The current hero</strong>
<span className="small muted">You'll get the new one once the rollout passes your bucket{bucket != null ? ` (${bucket})` : ""}.</span>
</div>
)}
</>
);
}
const patch = (items: object) => `vercel global-config update feature-lab --patch '${JSON.stringify(items)}'`;
export default function EdgeConfigPage() {
// One read for the whole page, shared by every section that needs it.
const read = readLabConfig();
return (
<FeaturePage
title="Edge Config & Flags"
tagline="Settings and feature flags your app reads in milliseconds, changed without a redeploy."
docs="https://vercel.com/docs/global-config"
whatItIs={<p>
<strong>Global Config</strong> (until recently <strong>Edge Config</strong>) is a small key-value store that Vercel copies to
every region, so reads are fast enough for every request. It's built for data that's read constantly but changed
rarely: switches, settings, allow-lists, redirects. Writing happens outside the app (dashboard, CLI or API), and a change
reaches every region within seconds, with no redeploy. The <strong>Flags SDK</strong> (<code>flags</code>) adds a typed
feature-flag layer on top, whatever the flag values come from.
</p>}
whenToUse={[
"Kill switches and maintenance banners",
"Gradual rollouts and experiments",
"Config that must change faster than a deploy: allow-lists, redirects, prices",
]}
>
<Demo title="1. The store">
<p>The page reads the <code>feature-lab</code> store with <code>getAll()</code> from <code>@vercel/global-config</code>. The SDK finds it through the <code>GLOBAL_CONFIG</code> connection string.</p>
<Suspense fallback={<p className="muted">Reading Global Config…</p>}>
<StoreContents read={read} />
</Suspense>
</Demo>
<Demo title="2. A switch: the banner">
<Suspense fallback={<p className="muted">Reading…</p>}>
<Banner read={read} />
</Suspense>
<p className="small muted">
Turn it on, then reload within a few seconds:{" "}
<code>{patch([{ operation: "upsert", key: "banner", value: { enabled: true, message: "Maintenance tonight at 22:00", tone: "warning" } }])}</code>
</p>
</Demo>
<Demo title="3. A setting: the featured page">
<Suspense fallback={<p className="muted">Reading…</p>}>
<Featured read={read} />
</Suspense>
<p className="small muted">Change it: <code>{patch([{ operation: "upsert", key: "featured", value: "proxy" }])}</code></p>
</Demo>
<Demo title="4. A feature flag with a gradual rollout">
<p>
The <code>new-hero</code> flag is declared with the Flags SDK in <code>src/flags.ts</code>. Proxy gives every visitor a
random id in a cookie, the flag hashes it to a bucket from 0 to 99, and the flag is on when the bucket is below the
percentage in Global Config.
</p>
<Suspense fallback={<p className="muted">Evaluating the flag…</p>}>
<Rollout read={read} />
</Suspense>
<p className="small muted">Roll out to everyone: <code>{patch([{ operation: "upsert", key: "rollout", value: { "new-hero": 100 } }])}</code></p>
</Demo>
<CodePeek file="app/edge-config/page.tsx" />
<CodePeek file="lib/global-config.ts" />
<CodePeek file="flags.ts" />
</FeaturePage>
);
}
Show code · src/lib/global-config.ts
import { getAll } from "@vercel/global-config";
import { connection } from "next/server";
// The items in the lab's Global Config store (formerly Edge Config), named
// feature-lab. The SDK finds the store through the GLOBAL_CONFIG connection
// string. Values are edited in the dashboard or with the CLI, never by the app.
export type LabConfig = {
banner?: { enabled?: boolean; message?: string; tone?: "info" | "warning" };
featured?: string;
rollout?: Record<string, number>;
};
export type ConfigRead = { ok: true; config: LabConfig; ms: number } | { ok: false; reason: string };
// Reads every item at request time (Global Config reads are always fresh,
// so they make the component dynamic). Never throws: a missing or broken
// store must not take the page down.
export async function readLabConfig(): Promise<ConfigRead> {
await connection();
if (!process.env.GLOBAL_CONFIG && !process.env.EDGE_CONFIG) {
return { ok: false, reason: "GLOBAL_CONFIG isn't set for this environment." };
}
const started = performance.now();
try {
const config = await getAll<LabConfig>();
return { ok: true, config: config ?? {}, ms: performance.now() - started };
} catch (error) {
// Logged, not shown: SDK errors can include the connection string's URL.
console.error("Global Config read failed", error);
return { ok: false, reason: "Couldn't read Global Config." };
}
}
// Rollout percentage for a flag, clamped to 0–100. Missing means 0 (off).
export function rolloutPercent(config: LabConfig, key: string) {
const value = config.rollout?.[key];
return typeof value === "number" && Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0;
}
// Stable bucket 0–99 for a visitor and flag (FNV-1a hash). Salting with the
// flag key means one visitor isn't always first in line for every rollout.
export function bucketOf(visitorId: string, flagKey: string) {
let hash = 0x811c9dc5;
for (const char of `${flagKey}:${visitorId}`) {
hash ^= char.codePointAt(0)!;
hash = Math.imul(hash, 0x01000193) >>> 0;
}
return hash % 100;
}
Show code · src/flags.ts
import { flag } from "flags/next";
import { bucketOf, readLabConfig, rolloutPercent } from "@/lib/global-config";
import { VISITOR_COOKIE } from "@/lib/proxy-rules";
type Entities = { visitorId: string };
// Feature flags with the Flags SDK. The SDK doesn't care where a decision
// comes from: here the rollout percentage lives in Global Config, so raising
// it takes effect within seconds, with no redeploy.
export const newHeroFlag = flag<boolean, Entities>({
key: "new-hero",
description: "Gradual rollout of the new hero; percentage in Global Config at rollout.new-hero",
defaultValue: false,
// Who is asking: the visitor id that Proxy puts in a cookie.
identify({ cookies }) {
const visitorId = cookies.get(VISITOR_COOKIE)?.value;
return visitorId ? { visitorId } : undefined;
},
// On for the first N% of buckets. A visitor's bucket never changes, so they
// keep their answer as the percentage grows.
async decide({ entities }) {
if (!entities) return false;
const read = await readLabConfig();
if (!read.ok) return false;
return bucketOf(entities.visitorId, "new-hero") < rolloutPercent(read.config, "new-hero");
},
});