To sell software from deepgrav.com, we needed a licensing system: key generation, key validity, update delivery, and a customer account where a buyer can see and manage what they own. We surveyed what already exists first. The survey is why this post exists.
The systems we found fell into shapes we could not use. SaaS licensing services phone home on their terms, with their data collection. Systems built on GPL foundations cannot be the base of a sellable product of ours. Commercial systems came with licenses we could not verify to our own standard. And the privacy promise we intend to keep — a check that sends a license key, an anonymous site ID, a product slug, and an installed version, and nothing else — rules out the domain-activation models most systems are built around. No existing system fit the locks. So we built our own, to a spec written before the build started, and the system we built is itself a product we intend to sell: a licensing system for a WooCommerce store, with no “deepgrav” hardcoding in it — products are data, not code.
The mechanism
Every trust decision happens server-side. The licensing server generates keys from a cryptographically secure random source and is the sole authority on validity: status, expiry, activation count, revocation, all computed on the server at check time. Keys are stored as SHA-256 hashes for lookup; the usable key is additionally stored encrypted under a key derived from the site’s WordPress salts, solely so the owning customer can see their own key in My Account — a bare table read does not yield working keys. Claim codes are hash-only and shown exactly once when they are minted.
The buyer’s side lives in WooCommerce. A product is flagged as licensed software, with its slug, term, and activation limit as fields. When an order completes, one key is minted per line quantity and bound to the customer’s account. My Account gains a Licenses endpoint: keys, status, expiry, activations, self-service deactivation for a site, downloads, and a claim-code redeem form for purchases made through other channels, so a buyer from anywhere is serviced at home, through their account here.
The client side, inside each paid product, is deliberately unprivileged. It generates and keeps an anonymous, resettable site ID, sends exactly the four fields, and installs what the server serves. It holds no secrets and makes no decisions; a customer who edits it can only break their own updates. Downloads are short-lived HMAC-signed URLs, and a gated download re-validates the license at download time, so a leaked URL dies with the license. Each package carries a SHA-256 checksum, registered with the package and returned in the update metadata.
There are two update channels on the same infrastructure. The gated channel serves feature updates to valid keys. The public channel serves security releases to everyone — no key, no account, no registration. That channel existing is a lock, not a feature toggle: security patching is the floor. A lapsed subscription keeps the installed version working; only update delivery stops, and the server’s validity answer says so outright, in a field named installed_copy_unaffected.
One codebase runs in three modes — authority, store, or both — set by a setting, not a fork. On our staging environment it runs both, the accepted staging simplification; the modes split across installs in production.
The battery
A build is not evidence. Before the system counted as built, it had to pass a seven-item battery on staging, end to end, over real HTTP:
- A test order mints a key visible in the customer’s My Account.
- That key activates the client plugin.
- A gated update is delivered to the activated site.
- A lapsed or revoked key is refused, and the installed software is unaffected.
- An ungated security release reaches an unregistered install.
- A claim code redeems once, and not twice.
- The whole check transcript, inspected by hand, contains only the four allowed fields.
The battery earned its keep. Three runs, three real bugs, each found by a failing item and fixed with evidence:
Plugin load order. The first run failed at item 1: no key was minted for a completed order, and nothing said why. The WooCommerce connector was loaded only if WooCommerce already existed at plugin-include time — and plugins do not load in an order you can rely on, so on requests where our plugin was included first, the connector file never loaded and none of its order hooks existed. The authority side worked the whole time, which is what made the silence dangerous. The fix loads the connector unconditionally and initialises it on plugins_loaded, when the check is deterministic — and records the connector’s state where an operator can see it, because the real defect was the silence.
Settings registration. The battery configured the system through the WordPress REST settings API, and the configuration silently did nothing: the settings were registered only on admin_init, which never fires on a REST request, so they never appeared in the settings endpoint at all. The fix registers the same settings on init and rest_api_init as well. The battery now asserts a settings round-trip — post the values, read them back, compare — and fails loud at configuration instead of surfacing one stage late.
A WordPress core column-format landmine. The second run failed at items 3 and 4 for a stranger reason: activation reported success and counted the activation, but the update check then refused the same site as never activated. The write and read paths in our code were symmetric, so the divergence was below our code, in $wpdb. WordPress core seeds a column-name format map that types any column named site_id as an integer — in core, that name means a numeric network ID. Our activations table has a site_id column holding a string, and $wpdb->insert() applied the map when we passed no explicit formats, integer-casting the string site ID to 0 on the way in. Every read looked it up as a string and could never match. Reproduced on a local stack running the same WordPress version as staging, with the generated SQL captured as direct proof, the fix is two call sites passing explicit formats — and the same defect had silently disabled deactivation, which also keys off the same lookup. One methodological note, stated plainly: an earlier stub harness passed the buggy code cleanly, because the stub did not model the part of WordPress where the bug lived. The real-stack replay is the check of record for that fix.
After the third fix, the rerun scored all seven items green on live staging. Item 7 was hand-verified on the new run’s event log: every keyed check event carried exactly ["license_key", "site_id", "product", "version"] as the fields received. Item 4’s hardest sub-check — a download URL issued while a license was active refusing after revocation, while the validity answer reports the installed copy unaffected — passed with it.
What happened next, and what has not
The licensing client was then integrated into the DeepGravity API Manager as product integration number one: API Manager 0.0.6 is the first product carrying it, and there is a separate release post for that build.
What has not happened: the licensing system is not in service in production, and it is not on sale. It is built and verified on our staging environment, and it is being prepared for release as a product — no price and no date announced. Its status page is on the Products index, and it will say so there when that changes.
By Dora