> ## Documentation Index
> Fetch the complete documentation index at: https://docs.authforge.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# Offline licensing best practices

> How to store, deliver, refresh and enforce cloud-minted .authforge license files on air-gapped machines, and how to write software that is protected by them.

This page assumes you have read [Offline license files](/features/offline-license-files) and decided that a machine genuinely cannot reach AuthForge. If it can reach the internet even occasionally, the default online activation + [grace period](/concepts#activation-and-the-grace-period) is simpler, cheaper and revocable; use that instead.

## Decide the policy before the first mint

Every `.authforge` file is a promise you cannot take back. Settle these three questions up front:

| Question                     | Recommended default                                            | Why                                                                                                                             |
| ---------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| How long should a file live? | 30 to 90 days                                                  | Re-issuing is your only revocation lever. Shorter files bound the damage of a lost laptop or a churned customer.                |
| Bound or unbound?            | **Bound** to the machine's HWID                                | An unbound (`any`) file is a working license for anyone who copies it. Accept `any` only for appliances you control end to end. |
| Lifetime files?              | Only for perpetual licenses you are prepared to honour forever | A lifetime file has no expiry and cannot be revoked.                                                                            |

Write the answers down as your offline SKU's terms so support, sales and engineering all say the same thing to customers.

## Collecting the HWID

A bound file needs the target machine's HWID before you mint. Every SDK exposes it without any network access:

| SDK     | Call                |
| ------- | ------------------- |
| Python  | `client.get_hwid()` |
| Node.js | `client.getHwid()`  |
| Go      | `client.HWID()`     |
| Rust    | `client.hwid()`     |
| C#      | `client.GetHwid()`  |
| C++     | `client.GetHwid()`  |

Ship a small "Show machine ID" affordance (a menu item, a `--hwid` flag, a first-run screen) that prints the value and lets the user copy it — or, better, writes an [activation request](/guides/activation-requests) (`.authforge-request`) they can email you. The dashboard checksums that file so truncation and line-wrapping cannot silently mint to the wrong HWID. Because HWIDs are [SDK-specific](/features/hwid-locking), collect it from the same SDK build that will later verify the file. If the customer already activated online once, the dashboard prefills the HWIDs bound to the license and you can skip this step.

<Tip>
  For appliances you image yourself, pass `hwidOverride` with a serial number you control (for example `unit:SN-00042`) so the HWID survives disk swaps and NIC replacements.
</Tip>

## Delivering the file

The file is plain text with PEM-style armor, so it survives every channel that carries text: email, a support portal download, a USB stick through a data diode, a QR code for very short payloads, or a printed sheet in a pinch. The parser tolerates CRLF conversion, a UTF-8 BOM, re-wrapped lines and surrounding text (an email preamble), so copy/paste damage is rare.

Keep the extension `.authforge`. Use `.authforge.lic` only where a platform refuses unknown extensions.

Treat the file like a bearer credential for bound files and like cash for unbound files. Send it over the same channel you would use for a password reset link.

## Storing the file on the machine

Store the file in a per-user or per-machine application data directory, not next to the executable:

* Windows: `%ProgramData%\<Vendor>\<App>\license.authforge` (machine-wide) or `%LocalAppData%\...` (per user)
* macOS: `/Library/Application Support/<App>/license.authforge` or `~/Library/Application Support/...`
* Linux: `/etc/<app>/license.authforge` or `$XDG_CONFIG_HOME/<app>/license.authforge`

Recommendations:

* Accept the file through an explicit **Import license file...** action that copies it into place, so users are not editing config folders by hand.
* Do not encrypt or obfuscate the file. It is already signed; encrypting it buys nothing against a user who controls the machine and complicates support.
* Keep the previous file around when you import a new one (`license.authforge.bak`) so a bad import is recoverable.
* The SDKs accept either a filesystem path or the armored text, so you can also embed the file contents in your own settings store if that is easier.

## Refreshing before expiry

Because revocation is impossible, expiry is the lifecycle. Build the refresh into the product rather than leaving it to a support ticket:

1. **Surface the expiry.** After `loginFromFile`, read `expiresAt` from `getOfflineLicense()` and show it in the About/License screen. Start warning at 14 days and again at 3 days.
2. **Make re-issue a routine operator task.** Mint the replacement from the license page (or script it against `POST /v1/licenses/{licenseKey}/offline-files`) a few weeks before the old file lapses. Mint history on the license page tells you what is outstanding.
3. **Track the fleet.** For every offline machine keep the license key, HWID, file `jti`, and `expiresAt` in your CRM or asset inventory. The Developer API `GET /v1/licenses/{licenseKey}/offline-files` returns the same fields if you prefer to sync from AuthForge.
4. **Let a new file replace an old one without downtime.** Import the new file, verify it with `verifyLicenseFile` before overwriting the old one, then swap. A file with a later `expiresAt` for the same license is always safe to install early.

If a customer churns, stop re-issuing and revoke the license online so no new files can be minted. The outstanding file runs out on schedule.

## Writing software protected by an offline file

<Steps>
  <Step title="Verify at startup, once">
    Call `loginFromFile` at launch and gate the app on the result. Do not re-verify on every action; the file does not change while the app runs.
  </Step>

  <Step title="Show the reason before you exit">
    `loginFromFile` never terminates the process on its own. Map the code you receive in `onFailure("offline_login_failed", error)` to a human message:

    | Code                             | Message to show                                                                |
    | -------------------------------- | ------------------------------------------------------------------------------ |
    | `expired`                        | "Your offline license expired on {date}. Ask {vendor} for a new license file." |
    | `hwid_mismatch`                  | "This license file is for a different machine. Your machine ID is {hwid}."     |
    | `bad_signature`, `wrong_app`     | "This file is not a valid license for {product}."                              |
    | `bad_armor`, `malformed_payload` | "The license file is damaged. Re-download it."                                 |
    | `unsupported_version`            | "Please update {product} to use this license file."                            |

    Always print the machine's HWID on the failure screen: it is the one piece of information support needs to fix a mismatch.
  </Step>

  <Step title="Read entitlements from the file">
    `getLicenseVariables()` and `getAppVariables()` return the snapshots frozen at mint time. Use them for tiering and feature flags exactly as you would online. Remember they only change when a new file is imported.
  </Step>

  <Step title="Do not start the online machinery">
    `loginFromFile` never starts the grace-period timer or online check-ins, and you should not start them either. If your product supports both connected and air-gapped customers, branch on the presence of a license file: `loginFromFile` when one is installed, otherwise the normal online `login()`.
  </Step>

  <Step title="Keep the app usable on expiry day">
    Decide what happens when the file lapses while the app is running: most products keep running until restart and refuse to start the next time. Whatever you choose, do not silently corrupt or lock user data.
  </Step>
</Steps>

### Supporting both modes in one binary

```text theme={null}
if license.authforge exists in the app data directory:
    ok = client.loginFromFile(path)        # offline mode, no network
else:
    ok = client.login(licenseKey)          # online activation + grace period
```

Never fall back from a *rejected* offline file to an online login automatically; a rejected file means something is wrong that the user should see.

## Threat model, honestly

* **Clock roll-back.** The expiry check uses the machine's clock. A user can extend a file by winding the clock back. Short expiries and HWID binding are the mitigations; there is no server to consult.
* **Copying a bound file.** Useless on another machine unless the HWID collides, which is why bound is the default.
* **Copying an unbound file.** Works everywhere. Only mint `any` files when you would be comfortable posting them publicly.
* **Tampering.** Any edit to the payload fails `bad_signature`. Headers are not signed and are ignored by the SDKs.
* **Key rotation.** Rotating your app signing key means new files carry a new `kid`. SDKs accept a list of public keys, so ship the old and new key together until every old file has expired.
* **Your own mistakes.** Mint history and the audit log record every file (actor, time, expiry, binding, file id, hash). Review them when a customer reports something unexpected.

## Checklist

* [ ] Offline SKU terms written: expiry length, binding policy, lifetime policy
* [ ] HWID visible in the product without network access
* [ ] Import flow that validates before overwriting the previous file
* [ ] Expiry shown in the UI, warnings at 14 and 3 days
* [ ] Fleet inventory of license key / HWID / `jti` / `expiresAt`
* [ ] Re-issue runbook (dashboard or scripted Developer API call) owned by a named team
* [ ] Failure screen shows the code, a plain-language message, and the machine HWID
* [ ] Air-gapped builds omit the App Secret (public key + app id + file is enough)
* [ ] Online customers still use `login()` + grace period; offline files only where required
