Budibase/budibase · error · HTTPError

Offline license does not match this installation

Error message

Offline license does not match this installation

What it means

verifyOfflineLicenseToken calls verifyInstallation and, when it throws OfflineLicenseMismatchError, rethrows it as HTTPError('Offline license does not match this installation', 400). The license's signed identifier (installId, unique tenantId) doesn't match the current installation, so the token is rejected. Other (non-mismatch) errors from verifyInstallation are rethrown unchanged.

Source

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

): Promise<OfflineLicense> {
  let license: OfflineLicense
  try {
    license = await signing.verifyLicenseToken(token)
  } catch {
    throw new HTTPError("Invalid offline license token", 400)
  }

  try {
    verifyExpiry(license)
  } catch {
    throw new HTTPError("Offline license has expired", 400)
  }

  try {
    await verifyInstallation(license)
  } catch (e) {
    if (e instanceof OfflineLicenseMismatchError) {
      throw new HTTPError(
        "Offline license does not match this installation",
        400
      )
    }
    throw e
  }

  return license
}

export async function getOfflineLicense(): Promise<License | undefined> {
  try {
    const token = await getOfflineLicenseToken()
    if (token) {
      const license = await verifyOfflineLicenseToken(token)
      return enrichLicense(license)
    }
  } catch (e) {

View on GitHub (pinned to a81a902e9a)

Solutions

  1. On the target installation, export its identifier (getIdentifierBase64) and request a new offline token bound to it from the account portal.
  2. If migrating servers, deactivate the license on the old install and issue a token for the new install's identifier.
  3. After a restore that changed installId, contact Budibase support to reissue the license for the new install.
  4. Confirm the activation runs under the same tenant the token was generated for.

Example fix

// before
await activateOfflineLicenseToken(tokenIssuedForOldServer)
// after
const identifierBase64 = await getIdentifierBase64()
// use identifierBase64 when requesting the token in the account portal, then:
await activateOfflineLicenseToken(tokenIssuedForThisServer)
Defensive patterns

Strategy: validation

Validate before calling

const identifierBase64 = await getIdentifierBase64()
// Request the offline token from the portal using identifierBase64 so it is bound to this install
// Then decode the returned token and confirm:
const decoded = /* decode token */ as OfflineLicense
const identifier = await getIdentifier()
if (decoded.identifier.installId !== identifier.installId) {
  throw new Error("Token bound to a different installId")
}

Type guard

function isMismatchError(e: unknown): boolean {
  return e instanceof HTTPError && e.message === "Offline license does not match this installation"
}

Try / catch

try {
  await activateOfflineLicenseToken(token)
} catch (e) {
  if (isMismatchError(e)) {
    // re-export this install's identifier and request a matching token
  } else throw e
}

Prevention

When it happens

Trigger: verifyOfflineLicenseToken(token) (via activateOfflineLicenseToken or getOfflineLicense) where the token's identifier.installId or identifier.tenantId differs from the current install's — the token was issued for a different install or tenant.

Common situations: Moving an offline license from one self-hosted server to another without re-issuing; re-provisioned servers where installation.getInstall() produced a new installId; multi-tenant misuse (token issued for tenant A used under tenant B); backups restored into a fresh install.

Related errors


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