License keys (self-hosted, week 21)
A self-hosted EntryWick is the same image and Compose file as the cloud, told apart by one setting and licensed by one key. PRICING_ENTITLEMENTS §5, ADR-019, ADR-020.
Who opens it: the person who installs and runs EntryWick on their organization's own server, when they install it, renew, or change plan; and an EntryWick staff member who issues the key.
What they came to do: put the key EntryWick sent them into the install, and see that it is accepted.
What must be true when they leave: the instance knows which organization it is licensed to, for how many attendees, until when, and on which update channel — without ever having to reach EntryWick to know it. Nothing they do with the key, right or wrong, can stop the app from running or lock anybody out of an event.
On the instance
In .env:
EW_HOSTING=self_hosted
EW_LICENSE_KEY=ew_lic_…
EW_HOSTING is cloud (the default) everywhere EntryWick runs the app itself; on the cloud none of this applies.
After changing the key, restart the app containers and run php artisan ew:license:check, which verifies the key,
stores the result and prints it — validity: active, the cap, the dates and the channel. It never prints the key.
- Offline. The key is checked against public keys built into the app. No connection is needed, at boot or ever.
- When: as the app boots, and every night at 03:08 (before the entitlements sync at 03:10).
- What it reads as:
activethrough the last day of the key (UTC);gracefor the 30 days after;expiredafter that;not_yet_validbefore its first day;missingwith no key;invalidfor anything that is not a genuine key (mistyped, cut short, changed, or signed by someone else). Each of these is a state the app runs in. Whatgrace,expired,missingandinvalidchange — a banner, updates, publishing new events — is in license-states.md; check-in never stops. - Online, once a day: the heartbeat reports counts only and hears whether the license was revoked (heartbeat.md). It never overrides the key's dates, and the app runs without it.
- What the key grants. The organization the key names gets the Enterprise plan with the key's attendee cap and the
features listed on it; other organizations on the same instance get nothing from it. A key past its dates keeps
granting the same. A key that disappears or stops verifying leaves the organization on what the last good key
granted — a typo in
.envnever drops an install to Free in the middle of an event.
Issuing a key (EntryWick staff, on the platform)
php artisan ew:license:issue bigorg --cap=5000 --valid-until=2028-02-15 [--valid-from=2027-02-15] [--channel=beta] \
[--feature=sso --feature=white_label_full …] [--instance=…]
- The organization must be on Enterprise (the
self_hostedentitlement), or the command refuses. --capis required: a number, orunlimited.--valid-untilis required; dates are calendar days in UTC.- Without
--feature, every Enterprise feature is on. Naming features switches on only those — listattendee_overagetoo, or the cap blocks registrations instead of counting overage. - The key is printed once. EntryWick keeps its hash and a short prefix (
ew_lic_and five characters), never the key; the issue is in the organization's audit log aslicense.created, fromcli. - Refused on a self-hosted instance, and on any server without
EW_LICENSE_SIGNING_KEYexcept a developer's machine, which signs with the development key that onlylocalandtestingaccept.
The format
ew_lic_ + base64url(payload JSON) + . + base64url(Ed25519 signature), without padding. The signature covers every
byte before the dot, ew_lic_ included. The payload:
| Field | Meaning |
|---|---|
v |
format version, 1 |
kid |
which public key verifies it |
license |
the license's ULID on the platform |
tenant_ulid |
the organization it licenses |
attendee_cap |
a whole number, or null for unlimited |
valid_from, valid_until |
YYYY-MM-DD, UTC; active through the whole valid_until day |
update_channel |
stable or beta |
features |
the boolean entitlement keys switched on (PRICING §7) |
issued_at |
ISO 8601, UTC |
Signing keys
php artisan ew:license:keygen prod-2027 prints a new pair and stores nothing. The public half is added to
App\Domain\Billing\Support\LicenseKeys::PUBLIC_KEYS and committed; the secret half goes into the platform server's
.env as EW_LICENSE_SIGNING_KEY with EW_LICENSE_SIGNING_KID, and into the password manager — never into the
repository or a self-hosted install. Rotation is a new kid alongside the old one; the old one is removed only when no
key signed with it is still in use. dev-2026 is the development key: its seed is published, so it is accepted only
where APP_ENV is local or testing.