Budibase/budibase · error · HTTPError

Offline license has expired

Error message

Offline license has expired

What it means

After the token signature is verified, verifyOfflineLicenseToken calls verifyExpiry and converts any expiry failure (past expireAt or unparseable date) into HTTPError('Offline license has expired', 400). This is the user-facing expiry error for offline license activation and refresh (it also surfaces when getOfflineLicense refreshes the cached license).

Source

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

  license.quotas = merge(license.quotas, _quotas)

  return license
}

export async function verifyOfflineLicenseToken(
  token: string
): 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> {

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Renew the subscription and activate a fresh offline license token from the account portal.
  2. Check the server clock (NTP) — correct time if drift caused the false expiry.
  3. Replace the stored token: activateOfflineLicenseToken(newToken), which saves and refreshes the cache.
  4. If the license should still be valid, decode the token and compare expireAt with the portal's records; re-export if they disagree.

Example fix

// before
await activateOfflineLicenseToken(savedOldToken)
// after
try {
  await activateOfflineLicenseToken(savedOldToken)
} catch (e) {
  if (e instanceof HTTPError && e.message === "Offline license has expired") {
    const fresh = await promptUserForNewOfflineToken()
    await activateOfflineLicenseToken(fresh)
  } else {
    throw e
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

function isExpired(token: string): boolean {
  const license = /* decode token payload */ as { expireAt: string }
  const expireAt = new Date(license.expireAt).getTime()
  return !Number.isFinite(expireAt) || Date.now() > expireAt
}
if (isExpired(token)) throw new Error("Renew offline license before activation")

Type guard

function isExpiryError(e: unknown): boolean {
  return e instanceof HTTPError && e.message === "Offline license has expired"
}

Try / catch

try {
  await activateOfflineLicenseToken(token)
} catch (e) {
  if (isExpiryError(e)) {
    // begin renewal flow: obtain fresh token from account portal
  } else throw e
}

Prevention

When it happens

Trigger: verifyOfflineLicenseToken(token) where the decoded license's expireAt is before the current time (or invalid), e.g. activating a stale offline token or the periodic cache refresh re-verifying an aged license.

Common situations: Annual offline license not renewed before the expiry date; air-gapped environments that can't auto-renew; server clock drift making a valid license appear expired; backups restored with an old saved offlineLicenseToken.

Related errors


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