Budibase/budibase · error

Offline license has expired. expireAt=${license.expireAt}

Error message

Offline license has expired. expireAt=${license.expireAt}

What it means

verifyExpiry checks an OfflineLicense's expireAt against the current time and throws this plain Error if the timestamp is unparseable (NaN) or in the past. It is the internal expiry guard for offline license tokens; verifyOfflineLicenseToken catches it and rethrows an HTTPError('Offline license has expired', 400).

Source

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

export async function getIdentifierBase64(): Promise<string> {
  const identifier = await getIdentifier()
  return encoding.objectToBase64(identifier)
}

export function getIdentifierFromBase64(
  identifierBase64: string
): OfflineIdentifier {
  return encoding.base64ToObject(identifierBase64)
}

// LICENSE

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

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Renew the subscription and generate/download a fresh offline license token from the account portal.
  2. Verify the server clock is correct (NTP-synced) — a wrong clock can wrongly expire or extend licenses.
  3. Re-activate with a new token: activateOfflineLicenseToken(newToken), which refreshes the license cache.
  4. Decode the token to inspect expireAt if you suspect the token itself is malformed.

Example fix

// before
await activateOfflineLicenseToken(staleToken)
// after
const license = signing.decodeToken(staleToken)
if (new Date(license.expireAt).getTime() <= Date.now()) {
  throw new Error("Token expired — download a fresh offline license from the account portal")
}
await activateOfflineLicenseToken(staleToken)
Defensive patterns

Strategy: validation

Validate before calling

function isLicenseCurrent(license: { expireAt: string }): boolean {
  const expireAt = new Date(license.expireAt).getTime()
  return Number.isFinite(expireAt) && Date.now() <= expireAt
}
if (!isLicenseCurrent(license)) throw new Error("Offline license expired — renew before use")

Type guard

function isValidDate(v: unknown): v is string {
  return typeof v === "string" && Number.isFinite(new Date(v).getTime())
}

Try / catch

try {
  const license = await getOfflineLicense()
} catch (e) {
  if (e instanceof Error && e.message.includes("has expired")) {
    // trigger renewal flow / alert ops to refresh the token
  } else throw e
}

Prevention

When it happens

Trigger: verifyExpiry(license) called (directly or via verifyOfflineLicenseToken → getOfflineLicense → cache.refresh) when license.expireAt is a past date or not a valid date string.

Common situations: Offline license naturally aged past its renewal date (typically annual); system clock wrong (far future makes tokens 'valid' past true expiry, far past expires fresh tokens); truncated/corrupted token payload losing the expireAt field; token copied from an old expired export.

Related errors


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