4.6 KiB
StartOS activate-license template
A drop-in folder that adds Keysat licensing to your existing Start9 (StartOS 0.4.0.x) package. Your buyers get:
- "Buy license" — pay with Bitcoin/Lightning on your BTCPay, never copy/paste the key. Optional discount/referral code field.
- "Redeem free license" — paste a free-license code from the seller; key is issued immediately, no payment.
- "Activate license" — paste a key you got out-of-band.
- "Check license status" — re-verify on demand.
- "Deactivate license" — wipe the key locally.
All actions work offline for the common case (boot-time signature check) and reach out to your Keysat instance for revocation and fingerprint enforcement only when the device is online.
Prerequisites
- You (the seller) are running
Keysaton your own Start9, with BTCPay connected. - You have created a product in your Keysat and have its slug.
- You know your Keysat's public URL (clearnet, .onion, or both).
- You have your Keysat's Ed25519 public key in PEM form. Get it from the "Show admin credentials" action on your Start9, or hit
GET /v1/pubkeyon your service.
Install
Copy these folders into your buyer-side package's startos/ directory:
startos/licensing/ ← config, store shape, runtime gate
startos/actions/ ← five actions (merge with your existing actions/)
Add the SDK to your package's package.json:
npm install @keysat/licensing-client
Wire-up, in 4 edits
1. Fill in startos/licensing/config.ts
There are three constants to set: LICENSING_BASE_URL, PRODUCT_SLUG, and
ISSUER_PUBKEY_PEM. Each is documented inline.
2. Extend your store shape
In the file where you call sdk.setupStore(...) (usually startos/init/index.ts):
import { licensingStoreDefaults, type LicensingStoreFields } from '../licensing/store'
export interface StoreShape extends LicensingStoreFields {
// ...your existing fields...
}
export const initStore: StoreShape = {
// ...your existing defaults...
...licensingStoreDefaults,
}
3. Register the actions
In startos/actions/index.ts:
import { licensingActions } from './licensing-actions' // or wherever you dropped them
export const actions = sdk.Actions.of()
// ...your own actions...
.addAction(licensingActions.activateLicense)
.addAction(licensingActions.buyLicense)
.addAction(licensingActions.finishLicensePurchase)
.addAction(licensingActions.redeemFreeLicense)
.addAction(licensingActions.checkLicense)
.addAction(licensingActions.deactivateLicense)
4. Gate your app at startup
In startos/main.ts, before you launch your daemon:
import { checkLicenseGate } from './licensing/gate'
export const main = sdk.setupMain(async ({ effects, started }) => {
const store = await sdk.store.getOwn(effects, sdk.StorePath).const()
const gate = checkLicenseGate(store)
if (!gate.licensed) {
// Pick your poison:
// (a) hard-fail:
throw new Error(
`No valid license. Run the "Buy license" or "Activate license" action. ` +
`(${gate.reason})`,
)
// (b) OR boot in trial mode by passing an env flag to your daemon,
// e.g. LICENSE_STATE=trial.
}
// ...normal startup...
})
What the buyer sees
- Installs your package on their Start9.
- Sees four new actions in the dashboard under the "License" group.
- Either:
- Clicks Buy license → opens the returned URL → pays → clicks Finish license purchase → done.
- Pastes a key they got from you into Activate license → done.
- Your app boots.
No typing long keys. No copying bearer tokens. Keys live in the Start9 store, which means they are included in the buyer's regular Start9 backups.
Revocation and fingerprint binding
The online path (any time the buyer's Start9 can reach your licensing server) does three things the offline path cannot:
- Revocation: if you revoke a leaked key server-side, that rejection propagates to the buyer on their next online check.
- Fingerprint binding: the first device to check in with a key binds it. The same key replayed from a different machine is rejected.
- Audit trail: your admin dashboard sees the check-in.
The offline path (signature-only) is the fallback. It always succeeds as long as the key was legitimately issued and the signature is intact. This is the right tradeoff: a buyer with a revoked key who never goes online can still run the app, which is an edge case; a buyer whose internet drops should not have their paid software held hostage.
License of this template
MIT.