Entitlements and Feature Flags, Driven by the License
How to gate features and enforce numeric limits by license tier: signed entitlement templates, reading flags and limits offline in your app, enforcing at the capability boundary, and changing what a tier unlocks without shipping a new build.
Most licensing tutorials stop at a boolean: is the license valid or not? That answers whether the customer may run your software at all. It does not answer the question your product actually asks a hundred times a day — what is this particular customer allowed to do? Can they use SSO? Export to PDF? Create their eleventh project? That is the job of entitlements: named capabilities attached to a license, read locally by your app.
The shape of an entitlement
An entitlement is one of two things:
- A feature flag — a boolean capability, named by what it unlocks:
sso,export,api-access,white-label. - A numeric limit — a cap the app enforces:
max-projects,max-seats,api-calls-per-day.
These live on a tier (say Free, Pro, Enterprise) as a template. When you issue a license at a tier, it inherits that tier’s entitlements, and they are baked into the signed license the customer activates. A Pro license might carry { sso: false, export: true, "max-projects": 25 }; Enterprise, { sso: true, export: true, "max-projects": -1 } (using -1 for unlimited).
Reading entitlements in your app
After activation, the SDK caches a signed lease and parses its entitlements. You read them synchronously, with no network call:
var client = KeyrightClient.Initialize(options);
var info = await client.ActivateAsync(licenseKey); // once; the lease is then cached
// Later, anywhere in your app:
if (client.IsEnabled("sso"))
EnableSingleSignOn();
int maxProjects = client.GetLimit("max-projects", fallback: 1);
Two properties make this safe. First, it fails closed: an unknown flag returns false, and a missing limit returns the fallback you supply — so you pass the most conservative value, never an optimistic one. Second, it is offline: the entitlement template is part of the signed lease, verified against the public key you embed at build time. Alter any value and the signature no longer matches, so the check fails rather than silently granting more.
Gate at the capability boundary, not just the UI
Hiding a greyed-out “Export” button is good UX, but it is not enforcement — anyone can call the method behind it. Put the real check at the boundary of the capability itself: the function that does the thing.
public Report ExportToPdf(ExportOptions options)
{
if (!_license.IsEnabled("export"))
throw new FeatureNotLicensedException("export");
return _renderer.Render(options);
}
Numeric limits are enforced at the moment of creation, where you already know the current count:
public Project CreateProject(string name)
{
int limit = _license.GetLimit("max-projects", fallback: 1);
if (limit >= 0 && _store.ProjectCount() >= limit)
throw new LimitReachedException("max-projects", limit);
return _store.Add(name);
}
Note the limit >= 0 guard: a negative limit (-1) means unlimited, so the cap is skipped. Picking one sentinel for “unlimited” and applying it consistently keeps every call site honest.
Change what a tier unlocks without shipping a build
Because entitlements are defined on the tier server-side, repackaging is a configuration change, not a code change. Add api-access to the Pro tier and every Pro license picks it up on its next refresh — no new build, no reissued keys, no customer action. Launch a new add-on by flipping one flag on the tiers that should have it. This is the real payoff of the capability-not-plan model: your packaging and your release cadence become independent.
It also means pricing experiments are cheap. Move export from Enterprise down to Pro for a quarter, measure conversion, move it back — all without touching the application your customers run.
Design rules that age well
- Name flags by capability, not by plan.
export, notpro-feature. When you later split Pro into Pro and Team, nothing in your code has to change. - Make limits numeric and fail to the floor. The fallback you pass to
GetLimitshould be the lowest tier’s value, so a malformed or missing entitlement degrades gracefully instead of unlocking everything. - Never reuse a retired flag name. If you drop
legacy-sync, don’t recycle the key for something else — old cached leases may still carry it. Treat entitlement keys as an append-only vocabulary. - Decide what expiry does per capability. When a license lapses into its grace window, some products keep read-only features on and gate only writes; others hard-stop. Encode that policy where you read the flag, not scattered through the UI.
Entitlements turn a license from a gate into a contract your code can query. Once features and limits flow from a signed template, “upgrade this customer” and “ship this feature to everyone on Pro” stop being engineering tasks and become a toggle — which is exactly where that control belongs.
Try Nebula.NET
Harden your .NET code in minutes — start with the free edition.