Skip to main content
Entitlements determine what features a customer can access based on their active plan, purchased add-ons, and subscription status. The Entitlement Engine evaluates these rules in real-time to gate access to your application.

How Entitlements Work

Every feature check follows this flow:
  1. Check entitlement before performing the action
  2. Execute the action if allowed
  3. Report usage for metered features (see Usage Metering)
Always check entitlements before consuming resources. Checking after the fact can lead to abuse and revenue loss.

Checking Entitlements

Use the Node.js SDK for authoritative checks:
import { Revstack } from "@revstackhq/node";

const revstack = new Revstack({ secretKey: process.env.REVSTACK_SECRET_KEY });

// Check a boolean feature
const ssoCheck = await revstack.entitlements.check(
  "usr_abc123", // customerId
  "sso", // featureId
);

if (!ssoCheck.allowed) {
  throw new Error("SSO is not available on your plan");
}

// Check a static feature with current usage
const seatsCheck = await revstack.entitlements.check(
  "usr_abc123",
  "seats",
  { amount: 1 }, // requesting 1 seat
);

if (!seatsCheck.allowed) {
  return res.status(403).json({
    error: "Seat limit reached",
    reason: seatsCheck.reason, // 'limit_reached'
    upgrade_url: "/billing/upgrade",
  });
}

// Check a metered feature before consuming
const tokensCheck = await revstack.entitlements.check(
  "usr_abc123",
  "ai_tokens",
  { amount: 5000 }, // requesting 5K tokens
);

if (tokensCheck.allowed) {
  // Perform the AI operation
  const response = await openai.chat.completions.create({ /* ... */ });

  // Report actual usage
  await revstack.usage.report({
    customerId: "usr_abc123",
    featureId: "ai_tokens",
    amount: response.usage.total_tokens,
  });
}

Client-Side (Non-Authoritative)

Use the React SDK for UI gating:
import { useEntitlements, UpgradePrompt } from "@revstackhq/react";

function SettingsPage() {
  const { check, loading } = useEntitlements();
  const ssoEnabled = check("sso");

  if (loading) return <Skeleton />;

  return (
    <div>
      {ssoEnabled.allowed ? (
        <SSOConfigPanel />
      ) : (
        <UpgradePrompt feature="sso" />
      )}
    </div>
  );
}
Best Practice: Always re-check entitlements on the server even if the client UI hides features. Client-side checks are for UX only.

The CheckResult Object

Every entitlement check returns a CheckResult with the following structure:
interface CheckResult {
  allowed: boolean; // Can the action proceed?
  reason?: "feature_missing" | "limit_reached" | "past_due" | "included" | "overage_allowed";
  remaining?: number; // Units left before hitting limit (Infinity if unlimited)
  cost_estimate?: number; // Estimated overage cost in cents
  granted_by?: string[]; // Which plan/addon slugs granted access
}

Example Responses

{
  "allowed": false,
  "reason": "feature_missing"
}

The Entitlement Engine

Under the hood, Revstack uses the EntitlementEngine class to evaluate access. You can use this directly for offline checks:
import { EntitlementEngine } from "@revstackhq/core";
import type { PlanDef, AddonDef } from "@revstackhq/core";

// Define a plan
const proPlan: PlanDef = {
  slug: "pro",
  name: "Pro",
  is_default: false,
  is_public: true,
  type: "paid",
  status: "active",
  prices: [{ amount: 2900, currency: "USD", billing_interval: "monthly" }],
  features: {
    seats: { value_limit: 5, is_hard_limit: true },
    sso: { value_bool: false },
  },
};

// Define an add-on
const ssoAddon: AddonDef = {
  slug: "sso_module",
  name: "SSO Module",
  type: "recurring",
  amount: 10000,
  currency: "USD",
  billing_interval: "monthly",
  features: {
    sso: { has_access: true },
  },
};

// Create engine instance
const engine = new EntitlementEngine(
  proPlan,
  [ssoAddon], // active addons
  "active", // subscription status
);

// Check entitlements
const ssoResult = engine.check("sso");
console.log(ssoResult);
// { allowed: true, remaining: Infinity, granted_by: ['sso_module'] }

const seatsResult = engine.check("seats", 3);
console.log(seatsResult);
// { allowed: true, reason: 'included', remaining: 2, granted_by: ['pro'] }

Engine Behavior

The engine follows these rules when aggregating entitlements:
1

Check subscription status

If the subscription is past_due or canceled, all checks return { allowed: false, reason: "past_due" }.
2

Aggregate limits from plan and add-ons

  • Add-ons with type: "increment" add to the base plan limit
  • Add-ons with type: "set" override the base plan limit completely
  • Add-ons are processed deterministically: "set" overrides before "increment" additions
3

Evaluate hard vs soft limits

  • If any source (plan or add-on) sets is_hard_limit: false, the feature becomes soft-limited
  • Soft limits allow overage with reason: "overage_allowed"
  • Hard limits block access with reason: "limit_reached"
4

Return result

The granted_by array lists all plans/add-ons that contributed to the access decision.

Feature Types in Detail

Boolean Features

Simple on/off toggles (e.g., SSO, Custom Domain, Priority Support).
// In plan config
features: {
  sso: { value_bool: true }, // Enabled
  custom_domain: { value_bool: false }, // Disabled
}

// Check at runtime
const result = engine.check("sso");
// { allowed: true, remaining: Infinity }
For boolean features, remaining is always Infinity when allowed.

Static Features

Fixed limits included in the plan (e.g., Seats, Projects, Workspaces).
// In plan config
features: {
  seats: {
    value_limit: 5,
    is_hard_limit: true, // Block when limit reached
  },
}

// Check with current usage
const result = engine.check("seats", 3); // 3 seats in use
// { allowed: true, reason: 'included', remaining: 2 }

Metered Features

Usage-based features that reset periodically (e.g., API Calls, AI Tokens, Storage).
// In plan config
features: {
  ai_tokens: {
    value_limit: 100000,
    is_hard_limit: false, // Allow overage
    reset_period: "monthly",
  },
}

// Check with requested amount
const result = engine.check("ai_tokens", 95000); // 95K already used
// { allowed: true, reason: 'overage_allowed', remaining: 0 }
See Usage Metering for tracking consumption of metered features.

Hard vs Soft Limits

Blocks access when the limit is reached. Best for finite resources.
features: {
  seats: { value_limit: 5, is_hard_limit: true },
}
Use cases: Team seats, concurrent jobs, storage quotas

Batch Checking

Check multiple features in a single operation:
const results = engine.checkBatch({
  seats: 3,
  api_calls: 5000,
  ai_tokens: 10000,
});

console.log(results);
// {
//   seats: { allowed: true, remaining: 2, ... },
//   api_calls: { allowed: true, remaining: 5000, ... },
//   ai_tokens: { allowed: false, reason: 'limit_reached', ... },
// }

Common Patterns

Optimistic Entitlement Checks

For low-latency requirements, check entitlements before making external API calls:
// 1. Check entitlement (fast)
const check = await revstack.entitlements.check(userId, "ai_tokens", { amount: 5000 });
if (!check.allowed) throw new Error("Token limit exceeded");

// 2. Report usage immediately (non-blocking)
await revstack.usage.report({ customerId: userId, featureId: "ai_tokens", amount: 5000 });

// 3. Call external API (slow)
try {
  const result = await openai.chat.completions.create({ /* ... */ });
} catch (error) {
  // 4. Revert usage if operation fails
  await revstack.usage.revert({
    customerId: userId,
    featureId: "ai_tokens",
    amount: 5000,
    reason: "operation_failed",
  });
  throw error;
}

Showing Remaining Usage

Display limits to users:
import { useEntitlements } from "@revstackhq/react";

function UsageBar() {
  const { check } = useEntitlements();
  const result = check("api_calls");

  if (!result) return null;

  const used = result.remaining === Infinity ? 0 : 10000 - result.remaining;
  const total = result.remaining === Infinity ? "Unlimited" : 10000;

  return (
    <div>
      <ProgressBar value={used} max={total} />
      <p>{used} / {total} API calls this month</p>
    </div>
  );
}

Graceful Degradation

Handle missing features gracefully:
const prioritySupportCheck = await revstack.entitlements.check(userId, "priority_support");

if (prioritySupportCheck.allowed) {
  // Route to priority queue
  await supportQueue.add(ticket, { priority: "high" });
} else {
  // Route to standard queue
  await supportQueue.add(ticket, { priority: "normal" });
}

Next Steps

Usage Metering

Track consumption for metered features

Subscriptions

Manage customer subscriptions and billing

Build docs developers (and LLMs) love