Licensing
How license files are produced, distributed, and verified.
Every Crudy deployment runs against a license issued by Crudy. The license is verified cryptographically once at boot, then a cheap expiry guard runs on every request and a timer terminates the process exactly at expiry. There is no fallback path and no online verification. If the file is missing, expired, or its audience does not match the app, the backend exits.
Licenses are issued only by Crudy. To request one for evaluation or production use, visit our pricing page and pick the plan that fits your deployment. We will send you the files described below.
What you receive
For each deployment we deliver two files:
license.licis the signed license token.license-public.pemis the matching verification key.
You point the backend at them with LICENSE_FILE and LICENSE_PUBLIC_KEY_FILE. Both files are safe to keep at the app root and to commit to your private deployment repo. Neither contains any secret material you need to manage.
What the license controls
The license declares, for your deployment:
- The application it is valid for. It must match the
audiencefield in yourapp.config.json. - An expiry timestamp. After that instant the backend refuses to start.
- One or more scope entries describing which modules are enabled and what their limits are.
Each scope entry covers either a single org, all descendants of an org, or the deployment as a whole. The framework picks the most specific scope that applies and uses its limits.
Limit values
The same convention applies to every limit:
| Value | Meaning |
|---|---|
| key absent | 0 (blocked) |
null | unlimited |
| non-negative integer | explicit cap |
Forgetting a key is never silently permissive. If the license does not mention a particular limit, the framework treats it as zero and blocks the corresponding operation.
Behavior at boot
When the backend starts:
- Required env vars (
LICENSE_FILE,LICENSE_PUBLIC_KEY_FILE, and the other secrets) are checked. - The license is verified cryptographically against the public key.
- The audience and expiry are checked against the running app.
- If any check fails, the process exits with a non-zero status. There is no degraded mode.
A successful verification is recorded in the audit log as LICENSE_VERIFIED so you can correlate boots with the deployed license.
Behavior at runtime
The license is verified cryptographically only once at boot. After that, the backend keeps the verified expiry timestamp in memory and enforces it two ways:
- Per-request guard. A Fastify
onRequesthook compares the current time to the expiry on every incoming request. If the license has expired, the request returns503 { error: 'license_expired' }before any auth, routing, or business logic runs. The check is a single in-memory timestamp comparison; there is no re-verification, no disk I/O, and no measurable overhead. - Self-termination. A timer is armed at boot to fire when the license expires. When it fires, the backend logs a fatal message and calls
process.exit(1)so the orchestrator restarts the container. The fresh boot then trips the boot-time fail-closed check.
The /health endpoint is exempt from the per-request guard so orchestrator probes keep working in the brief window between the request-time 503 and the process exit.
In effect: a license that expires at 12:00:00 stops granting access at 12:00:00 even if the backend has been running for days. There is no grace period and no way for a request to slip through past expiry.
Layered with per-org module toggles
A module is enabled for an org if and only if both the license and the row in app.org_modules allow it. Disabling a module per-org in the database can revoke a license-granted module, but the database cannot grant a module the license does not allow.
Endpoints
GET /licensereturns the entitlements visible to the caller, filtered to the orgs they can reach. Other tenants' entitlements are not exposed.GET /license/quotas?orgId=<uuid>returns[{ moduleId, key, limit, used }]for the given org. Useful for rendering quota meters in module pages.
Both require an authenticated request.
Renewing
When your license approaches expiry, request a renewal at /pricing. We send a fresh license.lic. Replace the file and restart the backend. Public keys typically stay the same across renewals; if a rotation is required we will ship the new license-public.pem alongside the new license.
What the framework does not do
- No automatic background renewal.
- No phone-home check.
- No grace period on expiry. Requests are refused with
503from the moment the expiry timestamp passes; the process self-terminates at the same instant. - No silent fallback if the file is unreadable.
These are deliberate. The license model is offline-first and fail-closed; everything is verifiable from two files on your deployment host.