Featurevisor — GitOps Feature Flags

Deep Study: Architecture, GitOps Pattern, A/B Testing & SDK Integration

GitOps Feature Flags A/B Testing No Server Required TypeScript SDK YAML as Source of Truth

1. What is Featurevisor

Featurevisor is an open-source feature management system built around one radical idea: your feature flags live in Git, not in a database. All configuration is defined in YAML files, committed to version control, built into JSON datafiles, and served from a CDN. SDKs read the datafile locally — no API call needed at evaluation time.

🌿
Git as Source of Truth
GitOps-first approach

All features, segments, and rules are YAML files in a Git repo. Every change is a commit with a PR, review, and audit history built in.

Zero-latency Evaluation
Client-side, local SDK

SDKs download one JSON datafile at startup. Flag evaluation is pure in-memory computation — no network round-trips per evaluation.

🏗️
No Server Required
Serve from any CDN

Built datafiles are static JSON. Serve from S3, CloudFront, Nginx, GitHub Pages — no dedicated flag server to run or maintain.

🧪
A/B Testing Built-in
Experimentation native

Supports bucketing, traffic allocation, multi-variant experiments, and sticky assignment — all defined in YAML alongside feature flags.

🔐
Type-safe & Validated
Schema validation on build

All YAML files are validated against JSON Schema at build time. Invalid configs fail CI — broken flags can never reach production.

🔄
Multi-environment
staging / production / custom

One YAML file defines rules per environment. Build produces separate datafiles per environment, deployed independently.

2. The GitOps Pattern — Core Idea

The Fundamental Shift

Traditional feature flag tools: UI → Database → API Server → SDK (4 hops, server dependency, network latency).
Featurevisor: YAML in Git → Build → JSON on CDN → SDK reads locally (zero runtime hops after initial download).

What GitOps means for feature flags

GitOps is the practice of using Git as the single source of truth for declarative infrastructure and application config. Featurevisor applies this to feature management:

1

Declare in YAML

Engineers write features/checkout.yml, segments/premium_users.yml — human-readable, version-controlled config.

2

Open Pull Request

Change goes through normal code review. Teammates can see exactly what changed. Required approvals enforced by GitHub/GitLab branch protection rules.

3

CI Validates & Builds

featurevisor build runs in CI. Validates schemas, resolves dependencies, compiles datafiles per environment. Fails fast on errors.

4

Deploy to CDN

Built JSON files are uploaded to S3/CDN/Nginx. SDKs poll for updates (configurable interval). No server restart needed.

5

SDK Evaluates Locally

SDK has the datafile in memory. sdk.isEnabled("checkout_v2", { userId }) is pure in-memory computation — nanoseconds, always available.

GITOPS FLOW ARCHITECTURE

🌿 Git Repo attributes/*.yml segments/*.yml features/*.yml push / merge ⚙️ CI Pipeline featurevisor build schema validation → datafile.json upload ☁️ CDN / S3 production.json staging.json static, cacheable HTTP GET on startup 📱 Browser SDK in-memory evaluation 🖥️ Node.js SDK in-memory evaluation isEnabled() → true getVariation() → "v2" ~0ms latency isEnabled() → false getVariable() → 42 ~0ms latency polls STEP 1 STEP 2-3 STEP 4 STEP 5 — RUNTIME
Why this is powerful: The CDN and SDK are the only runtime components. Even if your Git server, CI system, and build pipeline all go down, your SDKs keep serving flags correctly because the datafile is cached in memory.

3. Core Concepts

Featurevisor has four building blocks. They compose together: Features reference Segments, Segments use Attributes, Datafiles bundle everything.

🏷️
Attributes
Properties about a user or context

Atomic properties you know about the current user or environment. Used as inputs to segment conditions.

  • userId, email, country, plan, deviceType
  • Typed: string, boolean, integer, double, date
  • Capture flag = include in analytics events
👥
Segments
Named groups of users

Boolean conditions on attributes that define a user group. Reusable across many features.

  • Combine attributes with AND / OR / NOT
  • Operators: equals, contains, startsWith, gt, lt…
  • Example: premium_eu = plan=premium AND country=EU
🚩
Features
The actual flags

Declare ON/OFF flags, multi-variant experiments, or config values. Rules determine which segment sees which value.

  • Variables: typed configuration per variation
  • Traffic allocation: percentage-based bucketing
  • Environment overrides per env block
📦
Datafiles
The compiled output

Build-time output: one JSON file per environment, per tag group. This is what SDKs download and cache.

  • Contains only features/segments relevant to tag
  • Includes revision number for cache busting
  • Immutable once built — safe for aggressive CDN caching

4. YAML File Structure — Full Examples

Define every property you can know about a user or request context.

# attributes/userId.yml type: string description: "Unique user identifier" capture: true # include in analytics events --- # attributes/plan.yml type: string description: "Subscription plan: free | starter | pro | enterprise" capture: false --- # attributes/country.yml type: string description: "ISO 3166-1 alpha-2 country code" --- # attributes/deviceType.yml type: string # "mobile" | "desktop" | "tablet" --- # attributes/accountAge.yml type: integer # days since registration

Reusable user groups composed from attribute conditions.

# segments/premium_users.yml description: "Users on paid plans" conditions: - attribute: plan operator: in value: ["starter", "pro", "enterprise"] --- # segments/mobile_users.yml conditions: - attribute: deviceType operator: equals value: "mobile" --- # segments/eu_users.yml conditions: - attribute: country operator: in value: ["DE", "FR", "NL", "SE", "PL"] --- # Compound condition: AND logic (default when conditions is array) # segments/premium_mobile_eu.yml conditions: operator: and conditions: - attribute: plan operator: notIn value: ["free"] - attribute: deviceType operator: equals value: "mobile" - attribute: country operator: in value: ["DE", "FR", "NL"]

Supported Operators

OperatorTypeExample
equals / notEqualsAnyplan equals "pro"
in / notInArraycountry in ["DE","FR"]
contains / notContainsStringemail contains "@company.com"
startsWith / endsWithStringuserId startsWith "beta_"
greaterThan / lessThanNumber/DateaccountAge greaterThan 30
semverGreaterThanString (semver)appVersion semverGreaterThan "2.0.0"

The most important file — defines the flag, its variations, variables, and rollout rules per environment.

# features/new_checkout.yml description: "New checkout flow with saved cards" tags: ["checkout", "payments"] # used for datafile grouping # Variables = typed config values per variation variablesSchema: - key: buttonColor type: string defaultValue: "blue" - key: maxSavedCards type: integer defaultValue: 3 - key: showExpressCheckout type: boolean defaultValue: false # Variations (for A/B testing) variations: - value: "control" # old checkout weight: 50 # 50% of traffic - value: "treatment" # new checkout weight: 50 variables: # override defaults for this variation - key: buttonColor value: "green" - key: maxSavedCards value: 5 - key: showExpressCheckout value: true # Bucketing key: what determines which bucket a user falls into bucketBy: userId # sticky per user # Rules per environment environments: staging: rules: - key: "all_staging" segments: "*" # everyone in staging percentage: 100 production: rules: - key: "premium_early_access" segments: premium_users # segment name percentage: 100 - key: "gradual_rollout" segments: "*" percentage: 20 # 20% of all other users
// featurevisor.config.js — project configuration module.exports = { projectRootPath: __dirname, // Where YAML files live attributesDirectoryPath: "./attributes", segmentsDirectoryPath: "./segments", featuresDirectoryPath: "./features", // Output directory for built datafiles outputDirectoryPath: "./dist", // Environments — one datafile per environment environments: ["staging", "production"], // Tags — group features into separate datafiles for different apps tags: ["checkout", "onboarding", "admin", "mobile"], // Revision format in built datafiles revision: "git", // uses git short SHA }; // Produces: dist/staging/datafile-tag-checkout.json // dist/staging/datafile-tag-onboarding.json // dist/production/datafile-tag-checkout.json // dist/production/datafile-tag-onboarding.json

5. Build Pipeline & CI/CD

📝

YAML Edit

Engineer writes feature YAML

🔀

Pull Request

Review + approval required

CI Validate

featurevisor lint

⚙️

CI Build

featurevisor build

📤

Deploy Staging

Upload to staging CDN

🚀

Deploy Prod

Upload to prod CDN

GitHub Actions Example

# .github/workflows/featurevisor.yml name: Featurevisor CI/CD on: push: branches: [main] pull_request: branches: [main] jobs: validate-and-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci # Lint: schema validation, broken references, etc. - run: npx featurevisor lint # Build: compile YAML → JSON datafiles for all environments - run: npx featurevisor build # Only deploy on push to main (not on PRs) - name: Deploy staging datafile if: github.event_name == 'push' run: | aws s3 sync ./dist/staging/ s3://my-flags-bucket/staging/ \ --cache-control "max-age=30" aws cloudfront create-invalidation --distribution-id $CF_ID --paths "/staging/*" env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} CF_ID: ${{ secrets.CF_DISTRIBUTION_ID }} - name: Deploy production datafile if: github.event_name == 'push' run: | aws s3 sync ./dist/production/ s3://my-flags-bucket/production/ \ --cache-control "max-age=60"
PR Review as the approval gate: You can require featurevisor lint as a required CI check. This means no broken feature flag config can merge to main — the same workflow engineers already use for code review.

6. SDK Integration

The SDK downloads the datafile and evaluates flags entirely in-process. There is no per-evaluation network call.

import { createInstance } from "@featurevisor/sdk"; // Create SDK instance — downloads datafile once const sdk = createInstance({ // URL of compiled datafile for this environment + tag group datafileUrl: "https://cdn.example.com/production/datafile-tag-checkout.json", // How often to refresh (milliseconds). 0 = no polling. refreshInterval: 30_000, // 30 seconds // Called when a new datafile version is fetched onRefresh: () => console.log("flags refreshed"), // Optional: initial datafile (for SSR / edge hydration) datafile: window.__FEATUREVISOR_DATAFILE__, // Sticky bucketing: remember which bucket user was in stickyFeatures: { "new_checkout": { variation: "treatment" } }, }); // Await initial datafile load await sdk.onReady();
// Context = attributes for this evaluation const context = { userId: "user_abc123", plan: "pro", country: "DE", deviceType: "mobile", }; // 1. Simple boolean flag const isEnabled = sdk.isEnabled("new_checkout", context); // → true / false // 2. Get variation (for A/B testing) const variation = sdk.getVariation("new_checkout", context); // → "control" | "treatment" | null // 3. Get a typed variable const buttonColor = sdk.getVariable("new_checkout", "buttonColor", context); // → "green" (for treatment) | "blue" (for control) | "blue" (default) const maxCards = sdk.getVariableInteger("new_checkout", "maxSavedCards", context); // → 5 (treatment) | 3 (control/default) const showExpress = sdk.getVariableBoolean("new_checkout", "showExpressCheckout", context); // → true (treatment) | false (control/default) // 4. Get all variables at once const allVars = sdk.getAllVariables("new_checkout", context); // → { buttonColor: "green", maxSavedCards: 5, showExpressCheckout: true } // 5. Typed helper methods sdk.getVariableString("feat", "key", ctx); sdk.getVariableDouble("feat", "key", ctx); sdk.getVariableArray("feat", "key", ctx); sdk.getVariableObject("feat", "key", ctx);
// App.tsx — provide the SDK instance import { FeaturevisorProvider, useFeature, useVariation, useVariable } from "@featurevisor/react"; function App() { return ( <FeaturevisorProvider sdk={sdk} context={{ userId, plan, country }}> <CheckoutPage /> </FeaturevisorProvider> ); } // CheckoutPage.tsx — consume flags with hooks function CheckoutPage() { // Boolean flag const isNewCheckout = useFeature("new_checkout"); // Which variant (for analytics / rendering) const variation = useVariation("new_checkout"); // Individual typed variables const buttonColor = useVariable("new_checkout", "buttonColor"); const showExpress = useVariable("new_checkout", "showExpressCheckout"); if (!isNewCheckout) return <OldCheckout />; return ( <div> {showExpress && <ExpressCheckoutButton />} <Button color={buttonColor}>Pay now</Button> {/* track variation for A/B analysis */} <Analytics event="checkout_viewed" variation={variation} /> </div> ); }
// server.ts — Node.js / Express example import { createInstance } from "@featurevisor/sdk"; // Create ONE instance at startup (module-level singleton) const featurevisor = createInstance({ datafileUrl: `https://cdn.example.com/production/datafile-tag-api.json`, refreshInterval: 60_000, // refresh every 60s }); await featurevisor.onReady(); // Use in request handlers — evaluation is synchronous app.get("/api/checkout", (req, res) => { const context = { userId: req.user.id, plan: req.user.subscription.plan, country: req.geoip?.country ?? "US", }; const useNewCheckout = featurevisor.isEnabled("new_checkout", context); const maxCards = featurevisor.getVariableInteger("new_checkout", "maxSavedCards", context); if (useNewCheckout) { return res.json(newCheckoutFlow(maxCards)); } return res.json(oldCheckoutFlow()); }); // Graceful shutdown process.on("SIGTERM", () => featurevisor.stopRefreshing());

7. A/B Testing Deep Dive

How Bucketing Works

Featurevisor uses deterministic hashing — no randomness, no server state, no database.

1

Compute bucket key

Concatenate: featureName + "/" + bucketByValue
e.g. "new_checkout/user_abc123"

2

Hash to 0–100

MurmurHash3 of bucket key → integer 0–99999 → divide by 1000 → 0.0–99.999

3

Match to variation

Bucket 0–50 → "control". Bucket 50–100 → "treatment". Fully deterministic.

Sticky by design: Same userId always hashes to the same bucket. No session storage, no cookie needed. The same user sees the same variation across devices.

Multi-variant Experiment YAML

# features/homepage_hero.yml bucketBy: userId variablesSchema: - key: heroTitle type: string defaultValue: "Build faster" - key: ctaText type: string defaultValue: "Get started" variations: - value: "control" weight: 33.33 # uses defaultValue - value: "v1" weight: 33.33 variables: - key: heroTitle value: "Ship without fear" - key: ctaText value: "Start for free" - value: "v2" weight: 33.34 variables: - key: heroTitle value: "Flag it. Ship it." - key: ctaText value: "Try free" environments: production: rules: - key: "experiment" segments: "*" percentage: 100

Tracking Variation for Analytics

// In your analytics / event tracking code const variation = sdk.getVariation("homepage_hero", context); const heroTitle = sdk.getVariableString("homepage_hero", "heroTitle", context); // Send to analytics (Mixpanel, Amplitude, Segment, etc.) analytics.track("experiment_viewed", { experiment: "homepage_hero", variation: variation, // "control" | "v1" | "v2" userId: context.userId, }); // Set as user property for cohort analysis analytics.identify(context.userId, { `experiment_homepage_hero`: variation, }); // Later: query your analytics tool for // conversion rate by experiment_homepage_hero property
Note: Featurevisor does NOT have a built-in analytics dashboard. It's responsible for flag evaluation only. You integrate with your own analytics tool (Mixpanel, Amplitude, PostHog, Segment) to track and analyze experiment results.

8. Rollout Strategies

🎯
Segment-first Rollout

Enable only for specific segment first, expand later. Classic beta / early access pattern.

rules: - key: "beta" segments: beta_users percentage: 100 - key: "rest" segments: "*" percentage: 0
📈
Gradual Percentage Rollout

Increase traffic % over time via PRs. Each bump is a commit with a reviewer sign-off.

rules: - key: "rollout" segments: "*" percentage: 10 # PR1: 10% → merge # PR2: change to 25% → merge # PR3: change to 100% → merge
🌍
Geo-based Rollout

Launch in one country/region first, validate, then expand. Rules are evaluated top-to-bottom.

rules: - key: "us_first" segments: us_users percentage: 100 - key: "eu_next" segments: eu_users percentage: 0
🔧
Kill Switch

Set percentage to 0 in a PR. After merge + CDN deploy (~30s), feature is off globally. No server restart needed.

rules: - key: "kill_switch" segments: "*" percentage: 0 # everyone off
🏢
Team / Internal Only

Use a segment targeting internal email domains or a feature flag for employees, useful for dogfooding.

# segments/internal.yml conditions: - attribute: email operator: endsWith value: "@company.com"
🔗
Multi-segment Compound

First rule wins. Priority-ordered rules let you create complex targeting without code.

rules: - key: "vip" # enterprise users segments: enterprise percentage: 100 - key: "premium" # pro users segments: pro_users percentage: 50 - key: "rest" segments: "*" percentage: 0

9. Environments — One YAML, Multiple Contexts

Each feature YAML has an environments block. The build step produces a separate datafile per environment.

# features/dark_mode.yml description: "Dark mode toggle" tags: ["ui"] bucketBy: userId environments: # Development: everyone gets it development: rules: - key: "all" segments: "*" percentage: 100 # Staging: all testers staging: rules: - key: "testers" segments: "*" percentage: 100 # Production: gradual rollout production: rules: - key: "beta" segments: beta_users percentage: 100 - key: "general" segments: "*" percentage: 10

Build output structure

dist/ ├── development/ │ ├── datafile-tag-ui.json │ └── datafile-tag-checkout.json ├── staging/ │ ├── datafile-tag-ui.json │ └── datafile-tag-checkout.json └── production/ ├── datafile-tag-ui.json └── datafile-tag-checkout.json Each JSON file contains: { "schemaVersion": "1", "revision": "abc1234", // git SHA "attributes": [...], "segments": [...], "features": [...] // only features with matching tag }
Tags = code splitting for flags: A mobile app only downloads the datafile-tag-mobile.json, not the 200 checkout flags it doesn't need. Keeps the payload small.

10. Evaluation Engine — How Decisions Are Made

Understanding the evaluation order is critical for debugging unexpected flag behavior.

EVALUATION DECISION TREE

sdk.isEnabled("feat", ctx) Does feature exist in datafile? no → false (default) Is feature enabled in this environment? Sticky feature override set? yes → use sticky value Walk rules top-to-bottom. Segment match? Hash bucketBy value → within percentage? return sticky variation → return true + variation → return false (no match) no match match + in bucket

Rule evaluation is first-match-wins

// If you have these rules: rules: - key: "enterprise" segments: enterprise_users percentage: 100 ← checked FIRST - key: "general" segments: "*" percentage: 10 ← only checked if enterprise rule didn't match // Enterprise user → matched rule 1 → always enabled (100%) // Free user → skipped rule 1 → checked rule 2 → 10% chance enabled

11. Featurevisor vs Unleash vs LaunchDarkly

Aspect Featurevisor Unleash LaunchDarkly
Source of truth Git (YAML) Database (Postgres) Proprietary cloud DB
Change audit trail Git history — automatic Change log in DB Audit log in UI
Review workflow PR-based code review UI with role-based access UI with approval workflows ($)
Runtime infrastructure CDN only Server + DB + Unleash Edge LaunchDarkly SaaS
Evaluation location 100% client-side (in SDK) Server-side or client-side Server-side or client-side
Evaluation latency ~0ms (in-memory) <1ms local, ~5ms remote <1ms local, ~20ms remote
Real-time updates Polling interval (30–60s) WebSocket / SSE push Streaming (SSE) push
Non-technical users Hard (YAML + PR required) Good (UI available) Excellent (UI + workflows)
SDK ecosystem JS / TS / React / Node (limited) 30+ languages 30+ languages
Analytics integration BYO analytics Impression data API Built-in experiments UI ($)
Cost Free / self-hosted Free OSS / paid cloud Paid SaaS ($$$)
Best for Engineering-led teams, GitOps shops Teams wanting UI + code balance Enterprise, product/non-tech teams

12. Trade-offs — Honest Assessment

Strengths

Audit trail is free

Git blame, PR history, diff view — you know exactly who changed what flag, when, and why (PR description).

No server to run

Zero operational burden at evaluation time. CDN uptime SLA is typically 99.99%+. No flag server to page you at 3am.

Works offline

Once the datafile is in SDK memory, flags keep working even if your CDN is down.

Fits existing workflows

PRs, reviews, CI checks — same tools engineers already use. No new tool to learn.

Validated config

Schema validation means you can never deploy a broken flag silently. CI catches it.

Weaknesses

No instant toggle

Turning off a flag requires a Git commit → CI → CDN deploy. Minimum ~2–5 minutes even with optimized CI.

Engineers only

Product managers and non-engineers cannot change flags without a developer or learning Git + YAML.

No push updates

SDK polling means up to 30–60 seconds before a flag change propagates. Not suitable for true real-time emergency shutoffs.

JS-ecosystem focused

Official SDKs are TypeScript/React/Node. Other languages need community SDKs or manual datafile parsing.

No built-in analytics

You must implement your own experiment tracking and analysis. More flexibility, but more work.

When NOT to use Featurevisor: If you need non-engineers to toggle flags independently without developer involvement, or if you need sub-second flag changes in production emergencies, a traditional flag server (Unleash, LaunchDarkly) is a better fit.

13. Best Practices

Repository Structure

flags-repo/ ├── attributes/ │ ├── userId.yml │ ├── plan.yml │ └── country.yml ├── segments/ │ ├── premium.yml │ ├── internal.yml │ └── mobile.yml ├── features/ │ ├── checkout/ │ │ ├── new_checkout.yml │ │ └── express_pay.yml │ └── onboarding/ │ └── welcome_flow.yml ├── featurevisor.config.js ├── package.json └── .github/ └── workflows/ └── featurevisor.yml

YAML Naming Conventions

1
Use snake_case for feature keys: new_checkout_flow
2
Prefix features with domain: checkout_, onboarding_
3
Always add description field — it becomes documentation
4
Tag features by team or product area for separate datafiles
5
Use meaningful rule keys: "beta_users" not "rule_1"
6
Keep bucketBy: userId as default — ensures sticky user experience

Operational Tips

Set CDN Cache-Control: max-age=30 — short TTL for fast propagation
Include revision in analytics events to correlate experiments with deployments
Use featurevisor test to write unit tests for feature rules before merging
Archive old features instead of deleting — preserves experiment history in git
Monitor datafile size — if it grows >200KB, split tags more aggressively
Use onRefresh callback to emit a metric when SDK downloads a new datafile

Testing Feature Rules with the CLI

# features/new_checkout.tests.yml — co-located test file feature: new_checkout assertions: # Premium user in production should be in treatment - description: "Premium user gets new checkout" environment: production at: 40 # bucket position (0-100) context: userId: "user_abc" plan: "pro" expectedToBeEnabled: true expectedVariation: "treatment" # Free user not in premium segment - description: "Free user in 20% rollout at bucket 15" environment: production at: 15 context: userId: "user_xyz" plan: "free" expectedToBeEnabled: true # bucket 15 < 20% rule # Run: npx featurevisor test # ✓ Premium user gets new checkout # ✓ Free user in 20% rollout at bucket 15

The GitOps Feature Flag Mental Model

Think of Featurevisor as infrastructure-as-code for your feature flags. Just as Terraform stores cloud infrastructure in Git and builds plans before applying, Featurevisor stores feature configuration in Git and builds datafiles before deploying. The same benefits apply: version control, code review, automated validation, and a complete audit trail — with zero additional tooling for teams already using Git-based workflows.