$ guides / subscriptions

Subscription Lifecycle Management

Manage the full lifecycle of subscription-based licenses: creation, renewal, cancellation, and edge cases.

Guide

This guide covers patterns for managing licenses tied to recurring subscriptions, whether you use Stripe, Paddle, or any other payment provider.

Lifecycle overview

stateDiagram-v2
    [*] --> Created: Payment received
    Created --> Active: User authenticates
    Active --> Renewed: Subscription renews
    Renewed --> Active: Expiry extended
    Active --> GracePeriod: Payment fails
    GracePeriod --> Active: Payment retried
    GracePeriod --> Revoked: Grace period ends
    Active --> Cancelled: User cancels
    Cancelled --> Revoked: Period ends
    Revoked --> Active: User re-subscribes
    Revoked --> [*]: License deleted

Creating a subscription license

When a subscription starts, create a license with an expiration matching the billing period:

const response = await fetch("https://api.authforge.cc/v1/licenses", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    appId: process.env.AUTHFORGE_APP_ID,
    quantity: 1,
    expiresAt: periodEndDate.toISOString(),
    maxHwidSlots: 1,
    label: `sub_${subscriptionId}`,
  }),
});

Store the mapping between your subscription ID and the AuthForge license key in your database.

Renewal

When a subscription renews successfully, extend the license expiration:

await fetch(`https://api.authforge.cc/v1/licenses/${licenseKey}`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    action: "extend",
    expiresAt: newPeriodEndDate.toISOString(),
  }),
});

The user’s SDK session continues uninterrupted; the next heartbeat or re-authentication will pick up the new expiry.

Cancellation

When a user cancels, you have two options:

Option A: Immediate revocation

Revoke the license immediately on cancellation:

await fetch(`https://api.authforge.cc/v1/licenses/${licenseKey}`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ action: "revoke" }),
});

Option B: Let it expire

Don’t revoke; let the license expire at the end of the already-paid period. This is the more user-friendly approach. The license expiresAt was set to the billing period end, so it will naturally stop working.

Payment failure

When a payment fails, decide how to handle the grace period:

async function handlePaymentFailed(subscriptionId, attemptCount) {
  const record = await db.getLicenseBySubscription(subscriptionId);
  if (!record) return;

  if (attemptCount >= 3) {
    // Final attempt failed; revoke
    await revokeLicense(record.licenseKey);
    await notifyCustomer(record.email, "subscription_cancelled");
  } else {
    // Early attempt; warn the user but keep the license active
    await notifyCustomer(record.email, "payment_failed");
  }
}

Re-subscription

When a user re-subscribes after cancellation or revocation:

  1. Check if they have an existing (revoked) license key.
  2. If yes, re-activate it and extend the expiry; this preserves their HWID bindings.
  3. If no, create a new license.
async function handleResubscription(customerId, newPeriodEnd) {
  const existing = await db.getLicenseByCustomer(customerId);

  if (existing) {
    // Re-activate the existing license
    await fetch(`https://api.authforge.cc/v1/licenses/${existing.licenseKey}`, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ action: "activate" }),
    });

    // Extend expiry
    await fetch(`https://api.authforge.cc/v1/licenses/${existing.licenseKey}`, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        action: "extend",
        expiresAt: newPeriodEnd.toISOString(),
      }),
    });
  } else {
    // Create a fresh license
    await createNewLicense(customerId, newPeriodEnd);
  }
}

Plan upgrades and downgrades

Use license variables to control features per plan tier. When a user upgrades or downgrades:

async function handlePlanChange(licenseKey, newPlan) {
  const planConfig = {
    basic: { plan: "basic", maxProjects: 3 },
    pro: { plan: "pro", maxProjects: 10 },
    enterprise: { plan: "enterprise", maxProjects: 50 },
  };

  await fetch(
    `https://api.authforge.cc/v1/licenses/${licenseKey}/variables`,
    {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(planConfig[newPlan]),
    }
  );
}

The user picks up the new plan features the next time they authenticate.

Database schema suggestion

Store the mapping between your payment provider and AuthForge:

CREATE TABLE subscription_licenses (
  id SERIAL PRIMARY KEY,
  customer_id TEXT NOT NULL,
  customer_email TEXT NOT NULL,
  subscription_id TEXT UNIQUE NOT NULL,
  license_key TEXT UNIQUE NOT NULL,
  plan TEXT NOT NULL DEFAULT 'basic',
  status TEXT NOT NULL DEFAULT 'active',
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

Next steps

Contact support

Feel free to reach out if you have questions, need help getting set up, or run into something unexpected. We'll get back to you as soon as we can.

Email us at support@authforge.cc