Licences, activation and the extension manager
How the .lic files, the activation routes, the six-hourly heartbeat and the per-product status flags actually work, and why a working install stops working after a move or a firewall change.
Licensing is the single most common way a working Bicrypto install stops working. Almost always it is one of three things: the machine changed, the server lost outbound HTTPS, or a product is licensed but its status flag is still off. This page is the model behind all three.
What a licence actually is
Activation writes one file per product: lic/<productId>.lic, in a lic/
directory at the project root.
That file is an AES-256-GCM envelope. The encryption key is derived with scrypt
from a shipped secret plus this host's hardware fingerprint, so the file can
only be decrypted on the machine that wrote it. There is no fallback and no
degraded mode — a .lic presented on different hardware simply fails to
authenticate, and the product reads as unlicensed.
Copying lic/ to a new server, restoring a backup onto different hardware,
cloning a VM, or in some hosting environments a hardware change under a running
VM, all produce files that cannot decrypt. It does not fail over. It fails shut.
Never carry lic/ across a server move. Reactivate on the new box with the
purchase codes instead. lic/ is deliberately excluded from the backup guidance
in Backup and restore for exactly this reason.
lic/ must be writable by the user PM2 runs the backend as. Activation
writes atomically (temp file, then rename) into that directory, so a
non-writable directory fails activation rather than corrupting anything — but
the installer's blanket permission pass sets every file to 644 and chowns the
tree to the app owner, which can leave a licence written earlier as root
unwritable by the app.
ls -ld lic/
ls -l lic/Two independent flags decide whether a feature is on
This is the distinction that costs operators the most time.
| Flag | Where it lives | What it controls |
|---|---|---|
| The licence | lic/<productId>.lic on disk |
Opens the licence gate. Without it, the product's API routes answer 403. |
| The status flag | status on the product's extension, ecosystemBlockchain or exchange row |
Enables the feature: menus, screens, cron jobs, everything the platform reads to decide the product is in use. |
A .lic alone unblocks the gate and does nothing visible. A status flag alone
switches a menu on that then 403s. Both are needed, and they are set from
different places — the licence from the activation screen, the status flag from
the extension manager's toggle.
The headless activation script does both in one pass, which is why it is the easier path when you have shell access.
The activation routes
All four are on the permission create.license.
The screen is /admin/system/license. It has no entry in the admin menu —
you arrive there from an Extension or Exchange detail page, from the System
Updates screen, or by typing the URL. With no query parameters it activates the
core product; ?productId=<id> activates a specific addon, which is how those
detail pages link to it. It also accepts ?return=/some/path to send you back
afterwards and ?needsActivation=true when a gate redirected you there.
GET status is the one to read when something looks wrong. It returns
licenseStatus (active, expired, revoked, invalid, not_found,
not_activated), isValid, and a securityLevel from 0 to 4. If the cached
status says invalid it forces a fresh revalidation against the file on disk
before answering, so it self-heals the common "I activated but it still says
no" case.
verify is deliberately not on the audit trail: it only asks the question.
activate is the audited action.
The activate route catches its own errors and returns
{ success: false, message: "…" } with a 200 status, so the reason is in the
body rather than the status code. Read the message line on screen; do not judge
it by the response code.
Air-gapped activation
If the server cannot make outbound calls at activation time, download the
licence file from Envato, put it in lic/ at the project root, and use the
file-based option on the same screen. The backend reads the purchase code out of
that file and activates with it.
This does not remove the need for outbound HTTPS afterwards — see the heartbeat below.
Headless activation for managed hosting
pnpm activate-product <productId> <purchaseCode> [clientName]scripts/activate-product.mjs does exactly what the panel's activate flow does,
without a browser, and prints a single JSON line. It runs the same server code —
verify with the licence service and write the encrypted machine-bound .lic,
flip the product's status flag on its extension / ecosystemBlockchain /
exchange row, then revalidate so the gate opens without a restart. It prefers
the compiled backend/dist and falls back to the source tree in development.
Why a screen can stay locked immediately after activating
Activation writes the file. It then has to make that file visible to a process that has already cached the pre-activation verdict in four independent places:
- The per-product licence validator cache — memory plus Redis. An invalid result is held for five minutes, and its Redis key embeds the process start time, so in practice only a restart used to clear it.
- SecurityManager's cached security level, which the enforcement gate turns straight into a 403 whenever it is below 2.
- The server's 60-second core-licence decision cache. The activation page redirects after about two seconds, so the first request after activation landed inside that window every time.
- The extension
.licexistence cache inside the request middleware, with a five-minute TTL of its own.
Both activation routes now clear all four, in that order, before returning. It is best-effort by design: the licence is activated by the time that runs, so a failure in any step is logged rather than turned into a reported failure, and the next request self-heals.
If a screen is still locked a minute after a successful activation, restart the backend before you go looking for another fault.
The heartbeat, and the 72-hour grace period
A cron job named licenseHeartbeat runs every 6 hours. It collects every
product on the install that has a purchase code and sends one batched request
to https://updates.mashdiv.com, carrying:
- the host hardware fingerprint,
- the server's public IP address (looked up once a minute and cached; falls
back to
127.0.0.1if the lookup fails), - the domain, taken from the host part of
NEXT_PUBLIC_SITE_URL(orAPP_PUBLIC_URL), - version, Node version, platform, architecture, uptime, memory, load and timezone.
The response can mark a product revoked, expired or not_activated, which
forces an immediate revalidation of that product, and can carry commands to
clear a cache or force a revalidation.
When the licence service is unreachable the install keeps running on a grace
period of 72 hours, and the same 72 hours is the maximum total offline time
allowed per validation cycle. So a firewall change that blocks egress to
updates.mashdiv.com on 443 produces a site that worked perfectly for three
days and then started refusing. Allow that host outbound, permanently — it is a
runtime dependency, not just an install-time one.
curl -sS -o /dev/null -w '%{http_code}\n' https://updates.mashdiv.com
pm2 logs cron --lines 200 | grep -i heartbeatThe heartbeat runs on the scheduler, so on a default deployment it is the cron
process that needs the egress, not just the API.
One product changes a setting when its licence lapses
If the Chart Engine licence (product 61364182) comes back revoked or
expired, the heartbeat rewrites your chart settings to TradingView — including
display.chartType inside the binarySettings row. That is deliberate: the
alternative is a chart area that renders nothing. If your binary chart silently
changed from Chart Engine to TradingView, check that licence before you check
anything else. See
Binary options: switching it on.
Before activation, almost everything answers 403
An unactivated core refuses nearly every API route with 403. A short exempt list keeps the install recoverable — enough to sign in and reach the licence screen, and nothing more:
/api/auth
/api/user/profile
/api/settings
/api/admin/system/license
/api/admin/system/extension
/api/admin/finance/exchange/provider/active
/api/geo
/api/admin/system/geo-restrictionPlus /api/health, /uploads/, and /api/docs. Everything else is gated.
The two geo entries are on that list on purpose: a geo-blocked visitor must be able to load the page explaining why, and you must be able to correct a bad country rule — neither can sit behind licensing without creating a state nobody can get out of.
The System Updates screen behaves the same way: without a verified licence it replaces itself with a purchase-code activation form, so there is no update UI at all until activation succeeds. See Updating.
The extension manager
Admin → System → Extension Manager, at /admin/system/extension. The page is
gated on access.extension.
The listing is the single place that shows both flags side by side. For each product it returns:
| Field | Meaning |
|---|---|
status |
The status flag on the product's own row — whether the feature is enabled. |
licenseVerified |
Whether lic/<productId>.lic exists on disk. |
hasLicenseUpdate |
Whether the licence service reports a newer version than the installed one. |
licenseVersion · licenseReleaseDate · licenseSummary |
What the licence service says about the latest release. |
licenseVerified is an existence check on the file, done with a single
directory read of lic/. It answers "is there a licence file for this product",
not "does it decrypt on this machine". A .lic carried over from another server
therefore shows as verified here and still 403s at the gate — which is the exact
shape of the "the panel says it is licensed but the feature is dead" report.
The id in the status route is the product's productId, not a row id.
The status toggle updates the row and clears the settings cache. It leaves the
.lic in place, so turning the product back on needs no reactivation. Turning it
off is the correct way to retire a feature; deleting its .lic is not.
When activation will not go through
Work down this list; each step rules out one layer.
-
Confirm outbound HTTPS from the server itself, not from your laptop.
curl -sS -o /dev/null -w '%{http_code}\n' https://updates.mashdiv.com -
Confirm
lic/is writable by the app user.ls -ld lic/ sudo chown -R <app-user>:<app-user> lic/ -
Read the message, not the status code. The activate route answers 200 with
success: falseand a reason. -
Read the backend log.
pm2 logs backend --lines 200 | grep -i license -
Confirm the account has
create.license. The System Updates screen is a known trap here: the page is gated onaccess.system.updatebut every button on it calls a route requiringcreate.license, so a role with only the first sees a fully rendered screen and gets permission errors from every action. -
If it activated but the screen is still locked, wait a minute for the caches above, then restart the backend.
-
If it worked and stopped after a move, reactivate on the new hardware. Do not copy the old
.licacross.
For the symptom-first version of this list, see Troubleshooting.