Skip to main content
Introducing packages.sweber.dev
Documentation menuLicense server

License server

Turn Polar orders and subscriptions into signed licenses automatically.

lib/licenses.ts

import { createPolarClient } from "@sweberdev/integral/polar";
import { createLicenseServer, sqlStore } from "@weber-development/integral-server";

export const licenses = createLicenseServer({
  privateKey: process.env.INTEGRAL_PRIVATE_KEY!,
  product: "my-app",
  store: sqlStore((sql, params) => pool.query(sql, params).then((r) => r.rows), {
    dialect: "postgres",
  }),
  plans: {
    // Polar product id → plan
    "a1b2…": { plan: "pro" },
    "c3d4…": { plan: "team", seats: 10, limits: { projects: null } },
  },
  polar: createPolarClient({ organizationId: process.env.POLAR_ORG_ID! }),
  onLicense: async (record, event) => {
    if (event === "issued") await mail(record.email, `Your license: ${record.license}`);
  },
});

plans can also be a function ({ id, name }) => PlanMapping | null. Products that map to null are ignored, so one Polar organization can sell several products.

Routes

app/api/integral/[...path]/route.ts

import { createLicenseHandler } from "@weber-development/integral-server";
import { licenses } from "@/lib/licenses";

const handler = createLicenseHandler(licenses, {
  webhookSecret: process.env.POLAR_WEBHOOK_SECRET!,
  basePath: "/api/integral",
  corsOrigin: "https://app.example.ch", // only if a browser app on another origin calls it
});

export { handler as GET, handler as POST, handler as OPTIONS };
RoutePurpose
POST /webhookPolar webhook endpoint. Verifies the signature and age of each delivery
POST /exchange { key }Polar license key → the customer's signed license
POST /refresh { license }Current version of a license, e.g. after a renewal
GET /public-keyPublic key derived from the private key

In Polar, add a webhook (Settings → Webhooks) to https://your-app/api/integral/webhook with the events order.paid, order.refunded and all subscription.* events. For /exchange, attach a License Keys benefit to the products.

Lifecycle

Polar eventLicense
order.paid (one-time)Issued without end, e.g. Lifetime
subscription.activeIssued, updatesUntil = end of period + graceDays (default 3)
subscription.updated (renewal)Re-issued with the same id and a later updatesUntil
subscription.canceledStatus canceled, license unchanged until the period ends
subscription.revoked / endedStatus ended, updatesUntil = end date
order.refundedRevoked: exp set to now

With afterCancel: "expire", licenses also get exp, so they stop working when the subscription ends.

Storage

  • memoryStore() for tests.
  • sqlStore(query, { dialect, table? }) for Postgres ($1), SQLite and MySQL (?). query(sql, params) must return the rows. sqlSchema({ dialect }) returns the CREATE TABLE statement.
  • Or implement LicenseStore (get, findBySource, findByCustomer, save, list) for anything else.

Manual licenses

await licenses.issueManual({ email: "kunde@example.ch", plan: "team", reference: "INV-2026-041", updatesUntil: "2027-10-31" });
await licenses.revoke(licenseId);

Other frameworks

verifyPolarWebhook(rawBody, headers, secret) and licenses.handleEvent(event) work without the fetch handler, e.g. in Express. Always pass the raw request body: re-serialised JSON breaks the signature.