@ivneto/entitle
v1.0.1
Published
Self-hosted subscription and entitlement engine SDK for Node.js
Maintainers
Readme
Entitle Node.js SDK
Node.js/TypeScript client for the Entitle subscription and entitlement engine. Gate features, meter usage, and read back plan limits with a few lines of code.
Install
npm install @ivneto/entitleQuick start
import { Entitle } from "@ivneto/entitle";
const entitle = new Entitle({
apiKey: process.env.ENTITLE_API_KEY!,
});
const allowed = await entitle.access.check("usr_123", "prd_myapp", "export_data");
if (allowed) {
// ...
}API keys are prefixed sk_live_... or sk_test_.... The SDK rejects any other format, and rejects a live key used with environment: "dev" (or a test key with environment: "prod").
Configuration
const entitle = new Entitle({
apiKey: process.env.ENTITLE_API_KEY!,
options: {
environment: "prod", // "dev" | "prod", default "prod"
version: "v1", // API version path segment, default "v1"
timeout: 5000, // per-request timeout in ms, default 60000
},
cache: {
maxEntries: 1000, // enables an opt-in in-memory LRU cache
ttlMs: 60_000,
},
});The API endpoint is fixed — there's no baseUrl option to configure; the SDK always talks to Entitle's own API.
Every resource method also accepts a trailing { signal } to abort a single call independently of the client's default timeout.
API
access.check / access.check.detailed
The hot path for gating a feature. check resolves to a boolean; check.detailed resolves to the full decision.
const allowed = await entitle.access.check(userId, productId, feature);
const decision = await entitle.access.check.detailed(userId, productId, feature);
// { allowed, reason, source, value, used, remaining }entitlements.get
Full entitlement snapshot for a user on a product — useful right after login.
const entitlements = await entitle.entitlements.get(productId, userId);
// { state, plan, features: { [key]: { allowed, reason, limit?, used?, remaining? } } }limits.forUser
Current quotas (consumed + remaining) for a user, optionally scoped to a product.
const limits = await entitle.limits.forUser(userId, productId);
// { [featureKey]: { limit, used, remaining } }events.ingest
Records subscription lifecycle events (created, canceled, plan changed, ...) so the backend keeps entitlements and limits in sync with the user's plan. There is no dedicated "subscriptions" resource — this is how lifecycle changes are reported.
import { Subscription } from "@ivneto/entitle";
const event = await entitle.events.ingest(
Subscription.CREATED,
productId,
userId,
planKey,
{ seats: 5, trialDays: 14 }, // payload, free-form
idempotencyKey, // optional — generated automatically if omitted
new Date(),
);
// { id, status }usage.record
Reports metered consumption against a feature (API calls, exports, seats, ...).
const usage = await entitle.usage.record(productId, featureKey, userId, amount, idempotencyKey);health.ping
const status = await entitle.health.ping();
// { status: "ok" | "degraded", message }Error handling
The SDK never throws a bare Error for API failures — it maps responses to typed classes so you can branch on instanceof instead of parsing status codes or messages:
EntitleError // base class: message, code, status, requestId?
├── EntitleAuthError // 401/403 — invalid/expired key, or key/environment mismatch
├── EntitleNotFoundError // 404
├── EntitleConflictError // 409
├── EntitleValidationError // 400/422 — adds .fields: Record<string, string[]>
├── EntitleRateLimitError // 429 — adds .retryAfter: number (ms)
└── EntitleServerError // 5xx — adds .attempts: number
EntitleNetworkError // fetch/transport failure (DNS, TLS, timeout) — code, messagetry {
await entitle.access.check(userId, productId, feature);
} catch (error) {
if (error instanceof EntitleRateLimitError) {
console.error(`Retry after ${error.retryAfter}ms`);
} else if (error instanceof EntitleValidationError) {
console.error(error.fields);
} else if (error instanceof EntitleError) {
console.error(`[${error.code}]`, error.message);
} else {
throw error;
}
}Retries and timeouts
5xx and 429 responses are retried automatically with exponential backoff (up to 3 attempts by default), honoring a Retry-After header when present. Network errors are retried the same way. 4xx responses other than 429 are not retried. You don't need to implement your own retry loop — only handle the typed error once retries are exhausted.
Caching
Caching is opt-in and off by default. Passing cache: { maxEntries, ttlMs } to the constructor turns on a client-side LRU for access.check, access.check.detailed, entitlements.get, and limits.forUser. There is no automatic invalidation — keep ttlMs short (≤60s) for data that can go stale after a plan change.
Examples
Runnable examples live under example/, one per npm script:
npm run example:quickstart:initialize
npm run example:quickstart:check-access
npm run example:subscriptions
npm run example:flags
npm run example:metering
npm run example:webhooks
npm run example:errors
npm run example:cachingEach one expects ENTITLE_API_KEY in a .env file at the repo root.
Development
npm run build # bundle to dist/ (ESM + CJS + .d.ts)
npm test # run the test suite (vitest)
npm run test:watch # watch mode