| name | bq-analytics-flags |
| description | Add feature flags to a project that already has (or is adding) bq-analytics, then operate them — flip on/off, change rollout %, manage allowlists, materialize cohorts from BigQuery, and evaluate flag impact. Backed by Vercel Edge Config; exposures auto-track to events.raw. Use when the user wants flags, says "add flags", or asks how to ramp / kill / experiment with a feature. |
bq-analytics feature flags
Flags live in a single Vercel Edge Config item under the key flags. The
SDK reads at runtime via edgeConfigSource(); you mutate via the
bq-flags CLI shipped in the package. Propagation is sub-second.
{
"new-checkout": { "on": true, "rollout": 0.5 },
"ai-suggestions": { "on": true, "users": ["u_john", "u_beta1"] },
"kill-old-flow": { "on": false }
}
Detect current state
Before doing anything, figure out which path you're on:
| Signal | What it means | Where to start |
|---|
bq-analytics in package.json deps and EDGE_CONFIG= in .env.local | fully wired | jump to Operate |
bq-analytics in deps, no EDGE_CONFIG= | events wired, flags not yet | Step 1 below |
| neither | greenfield | run /bq-analytics-install first; it handles flags as Phase 3 |
grep -E '"bq-analytics"' package.json && echo "sdk present"
grep -E '^EDGE_CONFIG=' .env.local 2>/dev/null && echo "edge config provisioned"
Step 1 — Add the flag dependency
pnpm add bq-analytics @vercel/edge-config
@vercel/edge-config is an optional peer of bq-analytics. Only needed
when using edgeConfigSource() server-side; browser/RN clients use
httpSource() and don't need it.
Step 2 — Provision Edge Config
./node_modules/bq-analytics/scripts/setup-edge-config.sh
What it does (idempotent — safe to re-run):
vercel link if not already linked
- Creates an Edge Config store named
bq-analytics-flags (or reuses one
with that slug)
- Initializes the
flags key as {}
- Mints a read token, pushes
EDGE_CONFIG env var to Vercel Production
vercel env pull .env.local --environment production so local dev
can read it
Note: only Production is set automatically (CLI quirk on preview/dev
envs). For full env propagation, set VERCEL_TOKEN first, or replicate
via the Vercel dashboard.
After this, bq-flags is available via pnpm exec bq-flags (or
./node_modules/.bin/bq-flags).
Step 3 — Wire the SDK
Pick the surfaces relevant to the project. Skip any that don't apply.
Server (Next.js / Hono / Node on Vercel)
Drop a singleton next to your existing analytics one:
import { Flags } from "bq-analytics";
import { edgeConfigSource } from "bq-analytics/edge-config";
import { analytics } from "./analytics";
declare global { var __bqf: Flags | undefined; }
export function flags() {
return globalThis.__bqf ??= new Flags({
source: edgeConfigSource(),
analytics: analytics(),
refreshIntervalMs: 60_000,
});
}
Use anywhere on the server:
import { flags } from "@/lib/flags";
await flags().ready();
if (flags().isOn("new-checkout", userId)) { }
Browser / Expo / RN — never expose the Edge Config token
Drop a /api/flags route on the server first (one line):
import { createFlagsRoute } from "bq-analytics/next/flags";
export const GET = createFlagsRoute({
resolveUser: async (req) => null,
filter: (flags) => Object.fromEntries(
Object.entries(flags).map(([k, v]) => [k, { ...v, users: undefined }]),
),
});
Then on the client:
import { Flags, httpSource } from "bq-analytics";
const flags = new Flags({ source: httpSource({ url: "/api/flags" }) });
await flags.ready();
flags.isOn("new-checkout", userId);
const flags = new Flags({
source: httpSource({
url: `${API_URL}/api/flags`,
headers: { authorization: `Bearer ${deviceToken}` },
}),
});
Verify the install
pnpm exec bq-flags on smoke-test --rollout 100%
pnpm exec bq-flags list
pnpm exec bq-flags delete smoke-test
Then in code, gate something on a flag, redeploy, and confirm exposures
land in BigQuery:
bq query --nouse_legacy_sql --format=pretty \
"SELECT * FROM \`<gcp>.events.raw\` WHERE event_name = '\$flag_called' ORDER BY ts DESC LIMIT 5"
Operate
bq-flags list
bq-flags get new-checkout
bq-flags raw
bq-flags on new-checkout --rollout 25%
bq-flags on new-checkout --rollout 50% --users u_a,u_b
bq-flags rollout new-checkout 100%
bq-flags off new-checkout
bq-flags allow ai-suggestions u_alice u_bob
bq-flags disallow ai-suggestions u_alice
bq-flags delete ai-suggestions
--rollout accepts 0..1, 0..100, or N%. Allowlists are deduped.
All mutations are read-modify-write under the hood; safe to interleave.
Materializing a cohort from BigQuery
When the user asks "turn on ai-suggestions for all Pro users who imported
3+ videos":
-
Translate to BigQuery against events.users / events.raw. Always
return a deduped user_id[]:
SELECT user_id FROM `proj.events.users` u
JOIN (
SELECT user_id, COUNT(*) c FROM `proj.events.raw`
WHERE event_name = 'import.completed'
GROUP BY 1 HAVING c >= 3
) i USING (user_id)
WHERE JSON_VALUE(u.traits, '$.plan') = 'pro'
-
Run via the bq-analytics-query skill or bq query --format=csv,
capture the user IDs, then pipe into bq-flags allow:
USERS=$(bq query --nouse_legacy_sql --format=csv --quiet "<query>" \
| tail -n +2 | tr '\n' ' ')
bq-flags on ai-suggestions --rollout 0
bq-flags allow ai-suggestions $USERS
--rollout 0 means "nobody by default"; allow adds the cohort.
Static lists go stale. Re-run on cadence (cron / /loop / weekly).
Evaluating impact
The SDK auto-emits $flag_called exposure events to events.raw. The CLI
runs the standard analysis:
bq-flags eval new-checkout
bq-flags eval new-checkout --outcome subscription.started
bq-flags eval new-checkout --outcome subscription.started --days 30
This prints (variant, users, exposures) coverage and (with --outcome)
(variant_on, users, conversions, rate_pct) for first-exposure ITT lift.
Compute lift = (on_rate - off_rate) / off_rate. Flag borderline results
to the user before adding sequential / chi-square stats.
If bq-flags eval complains about GCP_PROJECT_ID, pass it:
bq-flags eval new-checkout --project my-gcp-project --dataset events
Where flags evaluate
| Runtime | Pattern | Source |
|---|
| Next.js / Hono / Node server | direct read from Edge Config | edgeConfigSource() |
| Node CLI | direct read | edgeConfigSource({ connectionString: process.env.EDGE_CONFIG }) |
| Browser (Next.js client) | fetch from /api/flags route | httpSource({ url: '/api/flags' }) |
| React Native / Expo | fetch from your API server | httpSource({ url: \${API_URL}/api/flags`, headers: ... })` |
Server code consumes Edge Config directly (no HTTP). Browser/RN MUST go
through your own /api/flags route — never expose the EDGE_CONFIG token
to clients. The route is one line:
export { createFlagsRoute as GET } from "bq-analytics/next/flags";
Pass resolveUser to authenticate, or filter to strip users[]
allowlists before they hit the wire.
Removing the Edge Config (full teardown)
Skip unless the user explicitly asks. Destructive.
EC_ID=$(grep '^EDGE_CONFIG=' .env.local | sed -E 's|.*/(ecfg_[^?]+)\?.*|\1|')
vercel edge-config remove "$EC_ID"
vercel env rm EDGE_CONFIG production
Notes
- 512 KB total store cap, 8 KB per-item — don't put more than ~20k user
IDs in a single allowlist. Past that, materialise in a separate flag
per cohort or move to attribute-based gating in code.
- Exposure event name is
$flag_called unless overridden. Match in SQL.
- Edge Config writes propagate in <1s globally. Reads take 8–15ms warm.