Frontend Feature Flags
URL-based feature flags for internal and B2B SPAs, plus multi-deploy previews by git SHA.
Frontend feature flags can be as simple as query params.
For internal tools and B2B SaaS, this is often enough:
- No backend flag service
- No rollout control plane
- Easy local debugging with a URL
For public consumer websites, this approach is usually not appropriate.
Security model
This pattern is security through obscurity.
- Good fit: internal products and B2B apps where exposed flags are acceptable.
- Bad fit: public apps where hidden features must stay secret.
If a user can view source and URL params, they can discover flags. Use backend authorization and server-side checks for anything security-sensitive.
URL flag helper (extracted pattern)
The snippet below shows a minimal URL-flag helper pattern used in production SPAs.
type FlagValue = string | number | boolean;
const searchParams = new URLSearchParams(window.location.search);
const flagsRegistry: Record<string, FlagValue> = {};
const parseBoolean = (value: string): boolean | undefined => {
if (value === "true" || value === "1") return true;
if (value === "false" || value === "0") return false;
return undefined;
};
const urlFlag = <T extends FlagValue>(flag: string, defaultValue: T): T => {
const raw = searchParams.get(flag);
if (raw === null) return defaultValue;
if (typeof defaultValue === "boolean") {
const parsed = parseBoolean(raw);
if (parsed === undefined) return defaultValue;
flagsRegistry[flag] = parsed;
return parsed as T;
}
if (typeof defaultValue === "number") {
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) return defaultValue;
flagsRegistry[flag] = parsed;
return parsed as T;
}
flagsRegistry[flag] = raw;
return raw as T;
};
export const urlWithFlags = (url: string) => {
const out = new URL(url, window.location.origin);
for (const [key, value] of Object.entries(flagsRegistry)) {
out.searchParams.set(key, String(value));
}
// Return a relative URL for internal app navigation.
return `${out.pathname}${out.search}${out.hash}`;
};
urlFlag("commit_sha", "");
// supported flags
export const flag1 = urlFlag("flag", Boolean(false));Usage:
if (flag1) {
// render feature-gated route or nav item
}
const nextUrl = urlWithFlags("/acme/users");
// keeps active flags while navigatingWhy this pairs well with multi-deploy
When you keep many static builds available (for example on S3), you can switch
both build version and feature toggles directly in the URL: ?commit_sha=<sha> selects a deployed build.
Example:
https://app.example.com/?commit_sha=13afff2This is very useful for QA and product review because the URL is fully shareable and reproducible.
PR preview links in CI
You can expose preview URLs directly from CI by setting a commit status with
target_url that includes the commit SHA as a query parameter.
- name: Set preview status
uses: actions/github-script@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.repos.createCommitStatus({
...context.repo,
sha: context.sha,
state: "success",
target_url: `https://my.example.com/?commit_sha=${context.sha}`,
description: "Preview is ready!",
});With this, every PR gets a clickable environment URL that matches the exact build. Combined with URL flags, reviewers can verify hidden UI states without new backend deployments.
Practical guidelines
- Keep the flag list small and explicit.
- Use booleans for simple gates, strings for IDs.
- Always define a safe default.
- Propagate active flags in internal links (like
urlWithFlags). - Never trust frontend flags for authorization.