Budibase/budibase · error · OfflineLicenseMismatchError

Invalid offline license

Error message

Invalid offline license

What it means

verifyInstallation compares the license's embedded identifier (installId + unique tenantId) against the current installation's identifier and throws OfflineLicenseMismatchError('Invalid offline license') when they differ. The message is intentionally vague for security reasons. It prevents an offline license issued for one installation/tenant from being used on another.

Source

Thrown at packages/pro/src/sdk/licensing/licenses/offline/offline.ts:85

export function verifyExpiry(license: OfflineLicense) {
  const now = Date.now()
  const expireAt = new Date(license.expireAt).getTime()
  if (!Number.isFinite(expireAt) || now > expireAt) {
    throw new Error(`Offline license has expired. expireAt=${license.expireAt}`)
  }
}

export class OfflineLicenseMismatchError extends Error {}

export async function verifyInstallation(license: OfflineLicense) {
  const identifier = await getIdentifier()
  if (
    license.identifier.installId !== identifier.installId ||
    license.identifier.tenantId !== identifier.tenantId
  ) {
    // be intentionally vague
    throw new OfflineLicenseMismatchError("Invalid offline license")
  }
}

export function enrichLicense(license: OfflineLicense) {
  const planType = license.plan.type
  const hosting = Hosting.SELF // offline only applicable in self host
  const _features = features.getOfflineFeatures(planType)
  const _quotas = quotas.getQuotas(hosting, planType)

  // apply the latest features and quotas to license
  license.features = union(license.features, _features)
  license.quotas = merge(license.quotas, _quotas)

  return license
}

export async function verifyOfflineLicenseToken(
  token: string

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Export the offline license token from the installation you intend to activate it on (matching installId and tenant).
  2. Regenerate the identifier file from the target installation and request a new token from the account portal using that identifier.
  3. If the installId legitimately changed (restore/migration), contact support to rebind the license to the new install.
  4. Ensure the tenant context (tenantId) when activating matches the tenant the token was issued for.

Example fix

// before
await activateOfflineLicenseToken(tokenFromOtherInstall)
// after
const identifier = await getIdentifier()
console.log("Request token for installId:", identifier.installId) // then activate the matching token
await activateOfflineLicenseToken(tokenForThisInstall)
Defensive patterns

Strategy: validation

Validate before calling

const localIdentifier = await getIdentifier()
const tokenLicense = /* decode token payload */
if (tokenLicense.identifier.installId !== localIdentifier.installId ||
    tokenLicense.identifier.tenantId !== localIdentifier.tenantId) {
  throw new Error("Token was not issued for this installation")
}

Type guard

function matchesInstallation(license: OfflineLicense, identifier: OfflineIdentifier): boolean {
  return license.identifier.installId === identifier.installId &&
         license.identifier.tenantId === identifier.tenantId
}

Try / catch

try {
  await activateOfflineLicenseToken(token)
} catch (e) {
  if (e instanceof HTTPError && e.message.includes("does not match this installation")) {
    // re-export identifier and request a token for THIS install
  } else throw e
}

Prevention

When it happens

Trigger: verifyInstallation(license) called via verifyOfflineLicenseToken when the token was generated for a different installId or tenantId than the current environment — e.g. token exported from install A activated on install B.

Common situations: Copying an offline license token between dev/staging/prod environments; restoring a backup where the installId was regenerated; cloned VMs sharing one token; multi-tenant setups using a token issued for another tenant; re-issued install (installId changed) after a rebuild.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/f53643a73b3536d6. Report an issue: GitHub.