Featurevisor

Use cases

User entitlements

Describe what each plan can do in one global variable, and evaluate it with the user's current plan. A plan change then updates every entitlement together, through one reviewed pull request.

This page follows a social media application where anyone can like and comment on posts, while creating posts needs a paid or enterprise plan.

Map entitlements to plans

EntitlementFreePaid and enterprise
Like postsYesYes
Comment on postsYesYes
Create postsNoYes
Upload videosNoIn the US only

This table is usually owned by product or pricing, not engineering. Keeping it in one definition means a packaging change, such as offering video uploads in every market, is one reviewed edit, and the spec below shows reviewers exactly who gains or loses what.

Your authentication layer loads the current user's profile from a trusted profile service:

{
"id": "user-2",
"plan": "paid",
"country": "nl"
}

Declare the attributes

These examples use the default scaffold, without environments or tag selectors. It already declares country. Add the plan attribute from Attributes:

attributes/plan.yml
description: Subscription plan
type: string
enum:
- free
- paid
- enterprise

Add a boolean for a posting grant that your application records for individual users. When it is absent or false, the plan alone decides:

attributes/overrideEntitlements.yml
description: Posting access grant from the User Profile service
type: boolean

Define the entitlements

Create variables/entitlements.yml. It starts from the plan's entitlements, then adds conditional access and individual grants. continue: true lets later matching overrides add to the value, and :appendUnique adds an entitlement only if it is not already there:

variables/entitlements.yml
description: Activities available to the current user
type: array
items:
type: string
defaultValue: []
overrides:
- key: paid
conditions:
- attribute: plan
operator: in
value:
- paid
- enterprise
continue: true
value: [likePosts, commentOnPosts, createPosts]
- key: free
conditions:
- attribute: plan
operator: equals
value: free
continue: true
value: [likePosts, commentOnPosts]
- key: paid-us
conditions:
- attribute: plan
operator: in
value:
- paid
- enterprise
- attribute: country
operator: equals
value: us
continue: true
mutate:
":appendUnique": uploadVideos
- key: posting-access
conditions:
- attribute: overrideEntitlements
operator: equals
value: true
mutate:
":appendUnique": createPosts
description: Activities available to the current user
type: array
items:
type: string
defaultValue: []
overrides:
- key: paid
conditions: plan in ['paid', 'enterprise']
continue: true
value: [likePosts, commentOnPosts, createPosts]
- key: free
conditions: plan == 'free'
continue: true
value: [likePosts, commentOnPosts]
- key: paid-us
conditions: |
plan in ['paid', 'enterprise']
and country == 'us'
continue: true
mutate:
":appendUnique": uploadVideos
- key: posting-access
conditions: overrideEntitlements == true
mutate:
":appendUnique": createPosts
The entitlements of a paid user in the US, built in layers: an empty default, the three of the paid plan, then uploadVideos appended.

An unknown or missing plan gets the empty default, so a user with only a posting grant gets [createPosts]. Keep the plan values before the additive overrides, because a later value would replace the array built so far. See accumulating overrides for how each override builds on the current value.

Read them in your application

Load a published datafile once. The Node.js guide covers refreshing it:

your-app/index.js
import { createFeaturevisor } from '@featurevisor/sdk'
const response = await fetch('https://cdn.yoursite.com/featurevisor-all.json')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const f = createFeaturevisor({ datafile: await response.json() })

In a request handler, build the context from the profile your authentication layer supplies:

function getEntitlements(userProfile) {
const context = {
plan: userProfile.plan,
country: userProfile.country,
overrideEntitlements: userProfile.overrideEntitlements === true,
}
const entitlements = f.getVariable('entitlements', context)
return Array.isArray(entitlements) ? entitlements : []
}

Then read the result as application data:

const entitlements = getEntitlements(userProfile)
const canCreatePosts = entitlements.includes('createPosts')
const canLikePosts = entitlements.includes('likePosts')
const canCommentOnPosts = entitlements.includes('commentOnPosts')

Passing the context to each call keeps requests independent, so a plan lookup needs no child instance or sticky values.

Always verify in the backend

Featurevisor does not authenticate users or authorize actions. For every protected request, derive the plan, country and any grant from trusted server data and enforce your access rules before performing the action. Never accept a plan or grant sent by the client as proof of access. If the profile or the configuration cannot be loaded, deny the action. Evaluations in the browser are for presentation only.

Grant access to one user

For a temporary posting grant, the profile service returns the flag with the profile:

{
"id": "user-9",
"plan": "free",
"country": "de",
"overrideEntitlements": true
}

The posting-access override adds createPosts to whatever the plan gives, without removing anything. The profile service owns the grant's expiry and stops returning true when it ends. The field selects a grant defined in the project, never an arbitrary list of entitlements.

The paid-us override works the same way for paid and enterprise users in the US, adding uploadVideos. Its continue: true lets the posting grant run after it, so a paid US user with a grant keeps all four entitlements, with createPosts listed once.

Use one variable per entitlement

If callers only need individual decisions, use a boolean global variable for each instead of an array:

variables/canCreatePosts.yml
description: Whether the current user can create posts
type: boolean
defaultValue: false
overrides:
- key: posting-enabled
conditions:
- or:
- attribute: overrideEntitlements
operator: equals
value: true
- attribute: plan
operator: in
value:
- paid
- enterprise
value: true
description: Whether the current user can create posts
type: boolean
defaultValue: false
overrides:
- key: posting-enabled
conditions: |
overrideEntitlements == true
or plan in ['paid', 'enterprise']
value: true

Read it like a flag, with the global variable getter:

const context = {
plan: userProfile.plan,
overrideEntitlements: userProfile.overrideEntitlements === true,
}
const canCreatePosts =
f.getVariable('canCreatePosts', context) === true

Choose the array or the booleans to suit your callers, rather than keeping the same policy twice. A feature's conditions can also read a global variable, as Conditions shows.

Test them

This spec covers each plan and how grants overlap with them:

tests/variables/entitlements.spec.yml
variable: entitlements
assertions:
- description: Free plan
context: { plan: free }
expectedValue: [likePosts, commentOnPosts]
- description: Paid plan
context: { plan: paid }
expectedValue: [likePosts, commentOnPosts, createPosts]
- description: Enterprise plan
context: { plan: enterprise }
expectedValue: [likePosts, commentOnPosts, createPosts]
- description: Paid plan in the US
context: { plan: paid, country: us }
expectedValue: [likePosts, commentOnPosts, createPosts, uploadVideos]
- description: Unknown plan
context: { plan: unknown }
expectedValue: []
- description: Missing plan
context: {}
expectedValue: []
- description: Individual posting grant
context: { plan: free, overrideEntitlements: true }
expectedValue: [likePosts, commentOnPosts, createPosts]
expectedEvaluation:
variableOverrideKey: posting-access
- description: Posting grant preserves conditional entitlements
context: { plan: paid, country: us, overrideEntitlements: true }
expectedValue: [likePosts, commentOnPosts, createPosts, uploadVideos]
- description: Posting grant without a plan
context: { overrideEntitlements: true }
expectedValue: [createPosts]
- description: Posting grant with an unknown plan
context: { plan: unknown, overrideEntitlements: true }
expectedValue: [createPosts]
- description: Expired posting grant
context: { plan: free, overrideEntitlements: false }
expectedValue: [likePosts, commentOnPosts]

Run npx featurevisor lint, npx featurevisor test and npx featurevisor build in the project. Its default all target includes global variables without tags.

Previous
Environment variables
Next
RBAC