@kasava/feature-gates
v0.1.1
Published
Plan/feature/limit entitlement gating for SaaS apps. Typed config with Zod schema validation.
Maintainers
Readme
@kasava/feature-gates
Plan/feature/limit entitlement gating for SaaS apps. Pure functions against a single typed config — no framework dependency, no network calls.
Extracted from Kasava's own plan-gating logic, which had drifted into five duplicated copies across its backend and frontend before this package existed.
Install
npm install @kasava/feature-gateszod is an optional peer dependency, only required if you use the /zod entry point.
The config shape
Everything is evaluated against a PlanConfig: a map of plan IDs to plan entries, each declaring boolean/string feature flags and numeric limits.
import type { PlanConfig } from '@kasava/feature-gates'
const config: PlanConfig<'sso' | 'apiAccess', 'repositories'> = {
plans: {
free: {
features: { sso: false, apiAccess: false },
limits: { repositories: 1 },
},
pro: {
features: { sso: true, apiAccess: 'full' },
limits: { repositories: -1 }, // -1 = unlimited
stripePriceIds: { monthly: 'price_xxx', annual: 'price_yyy' },
},
},
}- Limits: a number, or
-1for unlimited. Any other non-positive value is rejected by the/zodschema. - Features:
true/false, or one of a fixed set of tiered strings — see below.
Core API
import { canAccessFeature, getPlanLimits, getEffectiveLimit, getPlanIdForStripePrice, getStripePriceId } from '@kasava/feature-gates'
canAccessFeature(config, 'pro', 'sso')
// → { allowed: true }
canAccessFeature(config, 'free', 'sso')
// → { allowed: false, requiredPlan: 'pro' }
getPlanLimits(config, 'pro')
// → { repositories: -1 }
getEffectiveLimit(config, 'free', 'repositories')
// → 1
getPlanIdForStripePrice(config, 'price_xxx')
// → 'pro'
getStripePriceId(config, 'pro', 'monthly')
// → 'price_xxx'All functions are total — an unrecognized planId or priceId returns undefined / { allowed: false } rather than throwing. If your application needs to distinguish "denied by policy" from "unrecognized plan," normalize the plan ID to a known value (e.g. 'free') before calling these functions; this package doesn't do that for you.
requiredPlan is the first plan, in the config's declared key order, that grants the feature — declare plans from lowest to highest tier.
Tiered string values
Alongside true/false, a feature value can be one of three fixed strings that also count as "allowed":
import { TIERED_ALLOW_VALUES } from '@kasava/feature-gates'
// Set(['full', 'priority', 'dedicated'])This set is not configurable in 0.1.x. A feature value using a different tiered string (e.g. 'premium') is silently treated as denied. If your app's vocabulary doesn't fit this set, use true/false for that feature instead.
Runtime validation (/zod)
import { parsePlanConfig } from '@kasava/feature-gates/zod'
const config = parsePlanConfig(JSON.parse(rawJson))
// Throws a ZodError if `plans` is missing, empty, or any plan is missing
// `features`/`limits`, or any limit value isn't -1 or a positive number.Requires zod (^3.23.0 || ^4.0.0) as a peer dependency. Import from the base package if you don't need validation and don't want the peer.
Unknown top-level and per-plan keys are preserved (.passthrough()), so a richer config (pricing, display names, quotas) round-trips through parsePlanConfig unchanged — the schema validates the entitlement-relevant shape, not the whole document.
Notes
- ESM only — no CommonJS build.
require('@kasava/feature-gates')will fail; useimport. - No
/honoor/reactadapters ship in 0.1.x, despite the name suggesting a wider surface. Both were cut before release — this is core logic plus optional validation, nothing else.
License
MIT
