Docs
Licensing
How keys, seats, trials and the offline grace window work — and an honest account of what the gate does not stop.
There is a licence gate — subscriptions, 14-day trials, 3
machines per key. Whether it enforces is derived from two values in the app’s
license-config.js: the licence server’s address and the public key answers are verified
against. Empty, and the gate returns before it does anything; filled in, and every launch
asks.
They are filled in. A current build enforces, and a 14-day trial key is self-serve from the activation screen it shows you. Charging is a separate switch and is not on yet — see Pricing.
This page describes how it behaves.
The shape of it
Dispatch (installed) licence server
───────────────────── ──────────────
launch
└─ POST /v1/validate ──────────────▶ key active? machine known?
{ key, fingerprint } seats free?
└─ Ed25519-signed token ─┐
◀───────────────────────────────────────────────────────────────┘
verify the signature against the
public key baked into the build
└─ opens the dashboard
Every launch asks. The security of the whole thing rests on one property: the server signs, the app only verifies.
Pointing an installed copy at a different server — by DNS, proxy or hosts file — gets it tokens that fail the signature check. The API base is not a trusted value and does not need to be.
Keys
Format: {LICENCE.keyFormat}.
One key covers 3 machines, entered once per machine in the activation window.
Seats
Seats bind to a hardware id — Windows MachineGuid, and the equivalent on macOS and Linux.
Deliberately not MAC addresses: Hyper-V, WSL, Docker and every USB dock add and remove adapters, and a fingerprint that moved when a VPN came up would spend a seat every time.
A fourth machine gets a refusal that includes the list of machines using the seats, and the activation screen turns it into a Release button. This is the support ticket you would otherwise answer by hand every time somebody buys a laptop.
Trials
Self-serve, email-gated, 14 days, issued once per address and once per machine.
Each lock alone is trivial to walk around — plus-addressing beats one, a fresh VM beats the other — and together they cost more effort than they are worth.
The key is emailed and returned to the app, so a trial starts where the person is sitting rather than in a webmail tab.
Someone who trials and then subscribes keeps the same key: the webhook upgrades the trial row rather than minting a second one, so the key already typed into their app just keeps working.
When the server can’t be reached
An answer that cannot be reached falls back to the last signed token for 72 hours. After that the app stops opening.
An answer that says no — revoked, expired, cancelled — takes effect immediately, and no cached token overrides it.
Three details make that survivable rather than fragile:
- A 5xx is treated as unreachable, not as a refusal.
- The check races a six-second timeout instead of blocking indefinitely.
- The grace window is 72 hours, not one.
A hard gate with strict launch checks means our outage is your outage. That is the honest cost of this design, and it is why the grace window exists at all rather than being a nice-to-have.
What this stops, and what it doesn’t
It stops: sharing one key across a team, using it after cancelling, using it after a refund, running past a trial, and casual “I’ll just download it” copying.
It does not stop a determined person with a hex editor. Dispatch is JavaScript in an asar; the check can be patched out by someone who wants to badly enough. What is in place raises that cost:
| Measure | Effect |
|---|---|
Ed25519-signed entitlements |
Tokens cannot be forged or edited, and one lifted from another machine fails the fingerprint check. This is what makes key sharing pointless rather than merely against the rules. |
onlyLoadAppFromAsar |
An unpacked app/ directory can no longer shadow the packaged one — the easy way to swap a file. |
| Embedded asar integrity validation | The asar is hashed and checked against a hash embedded in the binary. Editing the licence code inside the package stops the app starting. |
NODE_OPTIONS and inspect args disabled |
No --require patch.js, and no attaching a debugger to the main process. |
One fuse is deliberately not set: runAsNode. The MCP installer launches Dispatch.exe with
ELECTRON_RUN_AS_NODE=1 for machines with no Node on PATH, so disabling it would harden the app
by silently breaking the MCP server for exactly the people who need it most.
Still open, and stated rather than hidden
- Code signing. An Authenticode certificate is not in place yet. Without it, a tampered build is indistinguishable from a real one, and every install shows a SmartScreen warning anyway — see Download.
- The update token. Every build carries a read-only token for the release feed, and it is extractable from the package. With the gate on, a leaked token gets someone installers they cannot run, which is an acceptable thing to hand out that way.
- Plugins. The plugin host loads JavaScript from the user-data folder, and a plugin is not sandboxed from the app.