@happyvertical/smrt-subscriptions

Tenant subscription plans, feature grants, usage thresholds, and entitlement resolution — including AI-token-quota metering off the framework's built-in AI usage table.

v0.29.34DomainBillingEntitlementsMulti-tenant

Overview

@happyvertical/smrt-subscriptions answers "what is this subscriber allowed to do, and have they used it up?" It pairs subscription plans (feature grants + usage thresholds) with a usage meter and an entitlement resolver, and ships provider-neutral Svelte UI for plan selection and usage display.

Three models back it:

  • SubscriptionPlan — a plan with feature grants and usage thresholds.
  • TenantSubscription — a subscriber's current plan and status.
  • TenantUsageMetric — recorded usage rows aggregated per window.

Installation

bash
pnpm add @happyvertical/smrt-subscriptions

Meter usage

TenantUsageMeter records and summarizes usage rows. The collection layer enforces the subscriber XOR invariant — passing subscriberKind: 'external' without a non-empty subscriberExternalId throws:

typescript
import { TenantUsageMeter } from '@happyvertical/smrt-subscriptions';

const meter = await TenantUsageMeter.create({ db });

// Record one usage row. Defaults to a 'tenant'-kind row keyed off
// tenantId; pass subscriberKind: 'external' + subscriberExternalId for
// per-buyer / per-agent metering.
await meter.record({
  tenantId: 'tenant-123',
  metricKey: 'documents.processed',
  quantity: 1,
  windowStart: monthStart,
  windowEnd: monthEnd,
  source: 'pipeline',
});

// Summarize usage over a window for a subscriber.
const summary = await meter.summarize({
  tenantId: 'tenant-123',
  metricKey: 'documents.processed',
  window: { start: monthStart, end: monthEnd },
});
console.log(summary.quantity);

AI-token-quota metering

This is the headline integration. The SMRT framework's AI layer records every model call into a tenant-scoped _smrt_ai_usage system table. TenantUsageMeter.summarize() recognizes ai.* metric keys and short-circuits straight to that table — so you meter real token consumption without recording a single usage row yourself:

typescript
// metricKey values starting with 'ai.' short-circuit to the tenant's
// _smrt_ai_usage system table (populated by the smrt-core AI layer), so
// you meter token usage without recording rows yourself.
const promptTokens = await meter.summarize({
  tenantId: 'tenant-123',
  metricKey: 'ai.tokens.prompt',
  window: { start: monthStart, end: monthEnd },
});

// Or pull the full AI rollup for the tenant in one call:
const ai = await meter.summarizeAiUsage({
  tenantId: 'tenant-123',
  window: { start: monthStart, end: monthEnd },
});
// ai => { promptTokens, completionTokens, totalTokens,
//         estimatedCost, requestCount, windowStart, windowEnd }
typescript
// Recognized ai.* metric keys (each maps onto the AI usage rollup):
//   ai.tokens.prompt      -> promptTokens
//   ai.tokens.completion  -> completionTokens
//   ai.tokens.total       -> totalTokens
//   ai.cost.estimated     -> estimatedCost
//   ai.requests           -> requestCount
//
// The ai.* short-circuit only fires for 'tenant'-kind subscribers, since
// _smrt_ai_usage is tenant-scoped. External subscribers fall through to
// the normal _smrt_tenant_usage_metrics aggregation.

Resolve entitlements

SubscriptionResolver ties plans, subscriptions, and usage together. Construct it with three readers (the collections above satisfy them) and ask what a subscriber is entitled to. For each of the plan's thresholds it pulls the matching usage summary for the right window and evaluates it, returning an allowed verdict plus per-threshold detail:

typescript
import {
  SubscriptionResolver,
  SubscriptionPlanCollection,
  TenantSubscriptionCollection,
  TenantUsageMeter,
} from '@happyvertical/smrt-subscriptions';

const plans = await SubscriptionPlanCollection.create({ db });
const subscriptions = await TenantSubscriptionCollection.create({ db });
const usage = await TenantUsageMeter.create({ db });

// Wire the three readers the resolver needs.
const resolver = new SubscriptionResolver({ plans, subscriptions, usage });

// Resolve everything a subscriber is entitled to right now.
const ent = await resolver.resolveTenantEntitlements('tenant-123');
// ent => { planKey, status, featureKeys, thresholds,
//          thresholdEvaluations, allowed, ... }

// Feature gate.
if (await resolver.isFeatureEnabled('tenant-123', 'bulk-export')) {
  // ...
}

// Throw if any 'block' threshold is exceeded (e.g. before an AI call).
await resolver.assertWithinThresholds('tenant-123');

resolveEntitlements(subscriber) is the polymorphic surface (works for tenant and external subscribers); resolveTenantEntitlements(tenantId) is the thin single-tenant wrapper. assertWithinThresholds(...) throws when any threshold's evaluation is not allowed — a natural pre-flight check before an expensive operation.

Thresholds & enforcement

A PlanThreshold caps a metricKey at a limit over a window with an enforcement mode of observe, warn, or block. evaluateThreshold(threshold, usage) (exported standalone) computes the verdict:

typescript
import { evaluateThreshold } from '@happyvertical/smrt-subscriptions';

// A plan threshold caps an AI-token budget per month and blocks once hit.
const evaluation = evaluateThreshold(
  {
    metricKey: 'ai.tokens.total',
    limit: 1_000_000,
    window: 'month',
    enforcement: 'block',   // 'observe' | 'warn' | 'block'
    warningRatio: 0.8,       // warn state at 80% (default)
  },
  aiTokenUsageSummary,        // a UsageSummary for this metric/window
);
// evaluation => { ratio, state: 'ok' | 'warn' | 'blocked',
//                 allowed, remaining, threshold, usage }
EnforcementBehavior
observeTracks usage; never blocks. Always allowed.
warnSurfaces a warn state past warningRatio (default 0.8); still allowed.
blockFlips to blocked / allowed: false once usage reaches the limit.

Each ThresholdEvaluation carries ratio, state, allowed, and remaining so a UI can render a progress bar and an upgrade nudge. Windows are day, week, month, year, or rolling.

Svelte UI

The /svelte subpath ships three provider-neutral components. They render state and emit actions back to the host app — they do not call any billing provider themselves:

svelte
<script lang="ts">
  import {
    PlanPicker,
    SubscriptionSummary,
    UsageThresholds,
  } from '@happyvertical/smrt-subscriptions/svelte';

  let { plans, resolution } = $props();
</script>

<PlanPicker {plans} selectedPlanKey={resolution.planKey}
  onSelect={(plan) => choose(plan)} />

<SubscriptionSummary {resolution} />

<UsageThresholds evaluations={resolution.thresholdEvaluations} />
ComponentProps
PlanPickerplans: SubscriptionPlan[], selectedPlanKey?: string | null, onSelect?: (plan) => void
SubscriptionSummaryresolution?: EntitlementResolution | null
UsageThresholdsevaluations?: ThresholdEvaluation[]

API surface

ExportDescription
SubscriptionPlan / TenantSubscription / TenantUsageMetricModels, each with a matching *Collection.
TenantUsageMeterrecord, summarize (with ai.* short-circuit), summarizeAiUsage.
SubscriptionResolverresolveEntitlements, resolveTenantEntitlements, isFeatureEnabled, assertWithinThresholds.
evaluateThreshold / evaluateThresholdsStandalone threshold evaluation helpers.
PlanPicker, SubscriptionSummary, UsageThresholdsFrom @happyvertical/smrt-subscriptions/svelte.
Subscriber, PlanThreshold, ThresholdEvaluation, EntitlementResolution, AiUsageSummary, UsageSummary, ThresholdEnforcement, SubscriptionStatusKey exported types (plus utilities like normalizeSubscriber, getWindowForThreshold).

Related Modules