Skip to main content

Subscriptions Module

The Subscriptions Module (@ever-works/agent/subscriptions) manages subscription plans, usage-based billing, and the billing provider abstraction. It handles plan seeding, user plan assignment, cadence allowance resolution, and usage ledger recording.

Module Structure

packages/agent/src/subscriptions/
├── index.ts # Barrel exports
├── subscriptions.module.ts # NestJS module definition
├── subscription.service.ts # SubscriptionService (plan management)
├── usage-ledger.service.ts # UsageLedgerService (usage recording)
└── billing/
└── billing.provider.ts # BillingProvider abstract class + ManualBillingProvider

Architecture

Key Components

SubscriptionService

The @Injectable() service implementing OnModuleInit for automatic plan seeding at startup.

Plan Seeding

On module initialization, the service seeds the database with the three default plans if they do not already exist:

PlanCodeDescription
FreefreeBasic access with limited features
StandardstandardStandard features with more generous limits
PremiumpremiumFull access to all features
private readonly PLAN_SEED_DATA: Array<{
code: SubscriptionPlanCode;
name: string;
description: string;
monthlyPriceCents: number;
yearlyPriceCents: number;
allowedCadences: WorkScheduleCadence[];
}>;

Each plan defines which schedule cadences it allows. For example, the Free plan might only allow monthly updates, while Premium allows daily.

Key Methods

MethodSignatureDescription
seedPlans() => Promise<void>Creates default plans if they do not exist (called on module init)
resolvePlanForUser(userId: string) => Promise<SubscriptionPlan>Resolves the active plan for a user. Falls back to the free plan if no active subscription is found.
getCadenceAllowances(userId: string) => Promise<CadenceAllowances>Returns which schedule cadences the user's plan permits, used by the scheduling UI.
assignPlanToUser(userId, planCode, options?) => Promise<UserSubscription>Creates or updates a user's subscription to a specific plan.
requiresUsageBilling(userId: string) => Promise<boolean>Checks if the user's plan requires pay-per-use billing for scheduled updates.

Cadence Allowances

The getCadenceAllowances method returns an object mapping each cadence to whether it is allowed under the user's current plan:

interface CadenceAllowances {
daily: boolean;
weekly: boolean;
biweekly: boolean;
monthly: boolean;
}

This is used by the frontend to enable/disable cadence options in the schedule configuration UI.

UsageLedgerService

Records usage-based billing events when subscriptions are configured for pay-per-use.

type RecordUsageOptions = {
userId: string;
workId: string;
schedule?: WorkSchedule | null;
triggerType: UsageLedgerTriggerType;
billingMode: WorkScheduleBillingMode;
generationHistoryId?: string;
};

Key behavior:

  1. Guard check: If subscriptions are disabled globally (config.subscriptions.isEnabled() === false) or the billing mode is not usage, the method returns null (no-op).
  2. Record creation: Creates a UsageLedgerEntry with the configured price per use (config.subscriptions.getPayPerUsePriceCents()).
  3. Billing provider call: Delegates to billingProvider.recordUsageCharge(entry) for external payment processing.
const entry = await usageLedger.recordUsage({
userId,
workId,
schedule,
triggerType: UsageLedgerTriggerType.SCHEDULED,
billingMode: WorkScheduleBillingMode.USAGE,
generationHistoryId: history.id
});

BillingProvider

An abstract class defining the billing provider contract:

abstract class BillingProvider {
abstract getDefaultCurrency(): string;

// Optional hook for forwarding charges to an external gateway
async recordUsageCharge(_entry: UsageLedgerEntry): Promise<void> {
return; // No-op by default
}
}

ManualBillingProvider

The default implementation that reads currency from configuration without connecting to any external payment system:

@Injectable()
class ManualBillingProvider extends BillingProvider {
getDefaultCurrency(): string {
return config.billing.getDefaultCurrency(); // e.g., 'usd'
}
}

To integrate with an external payment gateway (e.g., Stripe), create a new class extending BillingProvider that implements recordUsageCharge() and register it in the module.

SubscriptionsModule

@Module({
imports: [DatabaseModule],
providers: [
SubscriptionService,
UsageLedgerService,
{
provide: BillingProvider,
useClass: ManualBillingProvider
}
],
exports: [SubscriptionService, UsageLedgerService]
})
export class SubscriptionsModule {}

The BillingProvider token is bound to ManualBillingProvider by default. Override this binding to plug in a real payment provider.

SubscriptionPlan Entity

ColumnTypeDescription
iduuid (PK)Auto-generated
codevarchar (unique)Plan identifier (free, standard, premium)
namevarcharDisplay name
descriptionvarcharPlan description
monthlyPriceCentsintMonthly price in cents
yearlyPriceCentsintYearly price in cents
allowedCadencesjsonArray of allowed WorkScheduleCadence values
isActivebooleanWhether the plan is available for new subscriptions

UserSubscription Entity

ColumnTypeDescription
iduuid (PK)Auto-generated
userIduuid (FK)Subscriber
planIduuid (FK)References SubscriptionPlan
statusenumactive, cancelled, expired, past_due
billingProviderenummanual, stripe, paddle
currentPeriodStarttimestampBilling period start
currentPeriodEndtimestampBilling period end
externalSubscriptionIdvarchar (nullable)External provider reference

UsageLedgerEntry Entity

ColumnTypeDescription
iduuid (PK)Auto-generated
userIdvarcharUser reference
workIdvarcharWork reference
scheduleIdvarchar (nullable)Schedule reference
triggerTypeenummanual, scheduled, api
billingModeenumincluded, usage
unitsintNumber of units consumed
amountCentsintCharge amount in cents
currencyvarcharCurrency code (e.g., usd)
statusenumpending, charged, failed, refunded
generationHistoryIdvarchar (nullable)Links to generation run
metadatajson (nullable)Additional data (e.g., cadence)

Usage

Checking Plan Allowances

import { SubscriptionService } from '@ever-works/agent/subscriptions';

@Injectable()
export class ScheduleService {
constructor(private readonly subscriptions: SubscriptionService) {}

async canUseCadence(userId: string, cadence: WorkScheduleCadence) {
const allowances = await this.subscriptions.getCadenceAllowances(userId);
return allowances[cadence] === true;
}
}

Recording Usage

import { UsageLedgerService } from '@ever-works/agent/subscriptions';

@Injectable()
export class GenerationService {
constructor(private readonly usageLedger: UsageLedgerService) {}

async afterGeneration(options: { userId: string; workId: string; schedule: WorkSchedule; historyId: string }) {
await this.usageLedger.recordUsage({
userId: options.userId,
workId: options.workId,
schedule: options.schedule,
triggerType: UsageLedgerTriggerType.SCHEDULED,
billingMode: options.schedule.billingMode,
generationHistoryId: options.historyId
});
}
}

Extending the Billing Provider

import { BillingProvider } from '@ever-works/agent/subscriptions';

@Injectable()
export class StripeBillingProvider extends BillingProvider {
constructor(private readonly stripe: StripeClient) {
super();
}

getDefaultCurrency(): string {
return 'usd';
}

async recordUsageCharge(entry: UsageLedgerEntry): Promise<void> {
await this.stripe.invoiceItems.create({
customer: entry.userId, // Resolve to Stripe customer ID
amount: entry.amountCents,
currency: entry.currency,
description: `Work update: ${entry.workId}`,
});
}
}

// In your module:
{
provide: BillingProvider,
useClass: StripeBillingProvider,
}