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#
| Entitlement | Free | Paid and enterprise |
|---|---|---|
| Like posts | Yes | Yes |
| Comment on posts | Yes | Yes |
| Create posts | No | Yes |
| Upload videos | No | In 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:
description: Subscription plantype: stringenum: - free - paid - enterpriseAdd a boolean for a posting grant that your application records for individual users. When it is absent or false, the plan alone decides:
description: Posting access grant from the User Profile servicetype: booleanDefine 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:
description: Activities available to the current usertype: arrayitems: type: stringdefaultValue: []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": createPostsdescription: Activities available to the current usertype: arrayitems: type: stringdefaultValue: []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
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:
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:
description: Whether the current user can create poststype: booleandefaultValue: falseoverrides: - key: posting-enabled conditions: - or: - attribute: overrideEntitlements operator: equals value: true - attribute: plan operator: in value: - paid - enterprise value: truedescription: Whether the current user can create poststype: booleandefaultValue: falseoverrides: - key: posting-enabled conditions: | overrideEntitlements == true or plan in ['paid', 'enterprise'] value: trueRead it like a flag, with the global variable getter:
const context = { plan: userProfile.plan, overrideEntitlements: userProfile.overrideEntitlements === true,}const canCreatePosts = f.getVariable('canCreatePosts', context) === trueChoose 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:
variable: entitlementsassertions: - 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.
Related#
- RBAC, for features that depend on a user's roles
- Remote configuration
- Accumulating overrides

