This guide covers patterns for managing licenses tied to recurring subscriptions, whether you use Stripe, Paddle, or any other payment provider.
Lifecycle overview
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:
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.
Option B is the standard practice. Users have paid for the current period and should retain access until it ends.
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:
- Check if they have an existing (revoked) license key.
- If yes, re-activate it and extend the expiry — this preserves their HWID bindings.
- 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