Skip to main content

Subscription & Billing System

The subscription system manages plan tiers, cadence allowances, and usage-based billing for the Ever Works platform. It is implemented in packages/agent/src/subscriptions/.

Architecture

ServicePurpose
SubscriptionServicePlan resolution, cadence allowances, plan assignment
UsageLedgerServiceRecords per-run usage charges
BillingProviderAbstract billing gateway (extensible for Stripe, etc.)

Subscription Plans

Three plans are seeded on application startup via OnModuleInit:

PlanCodeMax WorksAllowed CadencesMonthly Price
Freefree1All (currently)$0
Standardstandard5Monthly, Weekly, Daily$29
Premiumpremium15Monthly, Weekly, Daily, Hourly$99

Each plan defines:

  • maxWorks -- maximum number of works a user can create.
  • allowedCadences -- which schedule cadences are included in the plan.
  • monthlyPrice -- base monthly subscription cost.
  • overagePricePerRun -- cost per generation run when using a cadence not included in the plan.
note

Currently, the Free plan allows all cadences for development purposes. In production, cadence restrictions will be enforced based on plan tier.

Plan Seed Data

Plans are upserted (created or updated) on every application start:

async onModuleInit() {
await this.seedPlans();
}

This ensures plan definitions stay in sync with the codebase across deployments.

Plan Resolution

The resolvePlanForUser(user) method determines the active plan using this priority:

  1. Active subscription -- if the user has a paid subscription via UserSubscriptionRepository, use that plan.
  2. Default plan -- if the user has a defaultPlan assigned (e.g., through an admin override), use that.
  3. System default -- fall back to the plan specified by config.subscriptions.getDefaultPlanCode(), which defaults to free.

If subscriptions are disabled globally (config.subscriptions.isEnabled() returns false), the system returns the default plan and allows all cadences without restrictions.

Cadence Allowances

The getCadenceAllowances(user) method returns a list of all cadences with their availability:

interface WorkScheduleAllowedCadence {
cadence: WorkScheduleCadence;
allowed: boolean;
payPerUse: boolean;
reason?: string;
}

For each cadence (Monthly, Weekly, Daily, Hourly):

  • allowed: true -- cadence is included in the user's plan at no extra cost.
  • payPerUse: true -- cadence is available but requires per-run usage billing.
  • reason -- upgrade recommendation (e.g., "Upgrade to Premium for this cadence").

Usage Ledger

The UsageLedgerService records individual usage charges when a generation run occurs outside the user's plan allowance.

Recording Usage

async recordUsage(options: RecordUsageOptions): Promise<UsageLedgerEntry | null>

Usage is recorded only when:

  1. Subscriptions are globally enabled.
  2. The schedule's billingMode is USAGE (pay-per-use).

Each ledger entry captures:

FieldDescription
userIdUser who triggered the run
workIdWork being generated
scheduleIdAssociated schedule (if applicable)
triggerTypeHow the run was triggered (manual, scheduled)
billingModeUSAGE for pay-per-use runs
unitsNumber of units (always 1 per run)
amountCentsCharge amount in cents
currencyCurrency code (from billing provider)
generationHistoryIdLink to the generation history record
metadataAdditional context (cadence, etc.)

After recording the ledger entry, the charge is forwarded to the billing provider:

await this.billingProvider.recordUsageCharge(entry);

Billing Provider

The BillingProvider is an abstract class that defines the billing gateway interface:

abstract class BillingProvider {
abstract getDefaultCurrency(): string;
async recordUsageCharge(_entry: UsageLedgerEntry): Promise<void> {
return; // No-op by default
}
}

ManualBillingProvider

The default implementation (ManualBillingProvider) performs no external billing operations. It returns the configured default currency and treats recordUsageCharge as a no-op. This is suitable for self-hosted deployments or during development.

Extending for Stripe

To integrate with Stripe (or another payment gateway), create a custom provider:

@Injectable()
export class StripeBillingProvider extends BillingProvider {
getDefaultCurrency(): string {
return 'usd';
}

async recordUsageCharge(entry: UsageLedgerEntry): Promise<void> {
// Forward the charge to Stripe's usage-based billing API
await stripe.usageRecords.create(subscriptionItemId, {
quantity: entry.units,
timestamp: Math.floor(Date.now() / 1000)
});
}
}

Feature Flags

Config KeyPurpose
config.subscriptions.isEnabled()Master toggle for the subscription system
config.subscriptions.getDefaultPlanCode()Default plan when no subscription exists
config.subscriptions.getPayPerUsePriceCents()Per-run overage charge in cents
config.billing.getDefaultCurrency()Default currency code

When subscriptions are disabled, all users get full access to all features with no billing.

Plan Assignment

Administrators can assign a plan directly to a user:

await subscriptionService.assignPlanToUser(user, SubscriptionPlanCode.STANDARD);

This updates the user's defaultPlanId field, which takes effect on the next plan resolution.

Plan Summary

The summarizePlan(user) method returns a complete overview for display in the dashboard:

const summary = await subscriptionService.summarizePlan(user);
// {
// plan: { code: 'free', displayName: 'Free', ... },
// allowances: [
// { cadence: 'monthly', allowed: true, payPerUse: false },
// { cadence: 'hourly', allowed: false, payPerUse: true, reason: '...' },
// ],
// enabled: true,
// }