Cool BeansCool Beans
Sign inStart free

Quickstart

Issue a key, wire three lines into your app, and see the verdict.

Two steps: get a key, then make one call in your app.

This assumes you already have an instance and a product: a cloud account at app.coolbeans.tools or a self-hosted box (see Self-hosting), with a product created in the console or via beans product create. The examples use a product whose slug is clementine; substitute yours.

1. Issue a key

In the console. Sign in, pick the product, and hit Issue key in the header. You get a product slug, a key prefix, and the key itself.

Or from the CLI. The beans CLI talks to the same admin API:

beans key issue --product clementine --email you@example.com

That prints the key. Add --kind subscription, --plan "Pro monthly", --seats 5 or --entitlements '{"export_4k":true}' if you want more than the defaults. See the CLI reference for every command, and note it isn’t published to npm yet, so today you run it from the repo.

Keys look like CLEM-XXXX-XXXX-XXXX-XXXX. The prefix is per-product, and it’s how the server resolves which product a key belongs to.

2. Wire it into your app

This is the whole integration:

const state = await cb.open(licenseKey);   // on launch, and when a key is pasted
if (state.decision === 'deny') lock(state);
await cb.release();                        // on sign-out

With the constructor, in full:

import { CoolBeans } from '@coolbeans/sdk';

const cb = new CoolBeans({
  baseUrl: 'https://licences.example.com',
  product: 'clementine',
  publicKeys: { '1': 'BASE64_PUBLIC_KEY' }, // bundle these; optional, the SDK can fetch them
  storage: myDurableStorage,                // required outside the browser
});

const state = await cb.open(licenseKey, {
  onChange: (next) => setLicensed(next.decision === 'allow'),
});
if (state.decision === 'deny') lockOut(state);

open() activates on first run, validates after that, refreshes when it can, falls back to the cached signed token when it can’t, and holds a floating seat itself. Every inconclusive answer keeps the app unlocked. Only a fetched disabled or a signed expiry denies.

Outside a browser, always pass a durable storage. The SDK refuses to construct without one (it throws storage_required) precisely because a memory fallback would mint a fresh device id on every start and consume another activation seat each time, until the customer is locked out of their own licence. The browser gets localStorage automatically. Tests can opt into the unsafe behaviour explicitly with allowEphemeralStorage: true.

What the verdict looks like

{ decision: 'allow', reason: 'online' | 'cached' | 'grace' | 'clock_rollback',
  license: LicenseObject | null, expiresAt: string | null, entitlements?: {...} }

{ decision: 'deny',  reason: 'revoked' | 'expired' | 'uninitialized',
  license: LicenseObject | null, entitlements?: {...} }

Branch on decision. Nothing else. reason is for what you say to the user: grace means nudge them online, uninitialized means ask for a key, revoked means the licence is gone.

Gate paid features on state.entitlements, never on state.license.plan:

if (state.entitlements?.export_4k) enableExport4k();

Per-host storage

The licensing is identical everywhere. The only thing that changes between hosts is where the device identity lives, because that’s what decides whether a restart burns another seat.

Host Storage to pass Why
Browser none (default) localStorage is picked up automatically
Electron electron-store or a file in app.getPath('userData') survives restarts and updates
Tauri the Stronghold plugin or a file in the app config dir same
Node / CLI a file under ~/.config/<app> a daemon restart must not re-activate

Copyable single-file quickstarts for all four live in examples/ in the repo: browser.ts, electron-main.ts, tauri.ts, node-cli.ts.

Hand it to a coding agent

Every instance serves two markdown documents written for an AI coding agent to read once and wire Cool Beans in with no back-and-forth:

Both are public markdown with no secrets in them. Point your agent at the two URLs on your own instance and it has everything.

Next