Cool BeansCool Beans
Sign inStart free

What Cool Beans is

A small open-source service that issues software licence keys, activates them on devices, and tells your app whether a key is still good.

Cool Beans is a small MIT-licensed service that issues and validates software licence keys and turns Stripe and PayPal payment events into licence state. A customer buys, gets a key, activates it on their machines, and Cool Beans is the source of truth for whether that key is still good.

Issue a key, activate it, check it’s still good. That’s the whole product.

The mental model

key  ->  activate (takes a seat)  ->  validate (returns a signed offline token)
                                          |
                                          v
                              your app branches on state.decision

The one rule that matters

Access is refused only on a definitive answer. The SDK already sorts the definitive from the inconclusive, so in your code the rule is simply: branch on state.decision, and nothing else.

What that resolves for you:

Don’t add your own checks on top. Don’t compare dates, inspect license.status, or decide what a failed request meant. That’s the whole job of open(), and every lockout we’ve seen came from an app second-guessing it.

The verdict

{ decision: 'allow', reason: 'online' | 'cached' | 'grace' | 'clock_rollback',
  license: LicenseObject | null, expiresAt: string | null,
  entitlements?: Record<string, boolean | number | string> }

{ decision: 'deny',  reason: 'revoked' | 'expired' | 'uninitialized',
  license: LicenseObject | null,
  entitlements?: Record<string, boolean | number | string> }

It’s a union rather than a boolean on purpose. “We have never established an entitlement” and “you were revoked” both mean locked, but they’re different screens, and a boolean loses that.

reason is only for what you say to the user:

Reason What it means What to show
online The server just confirmed it Nothing
cached No fresh answer, cached token still inside its lifetime Nothing
grace Past the token lifetime, licence still good Nothing loud. Optionally nudge them online
clock_rollback Unlocked, but this machine’s clock went backwards Treat like cached
uninitialized No entitlement has ever been established here The licence-key form
expired The licence ended Point at renewal
revoked The licence was taken away Point at support

Gating features

license.plan is a label the vendor types and license.kind is our lifecycle bookkeeping. Both are display only. The only thing to gate a feature on is state.entitlements:

if (state.entitlements?.export_4k) enableExport4k();
const batchLimit = Number(state.entitlements?.batch_limit ?? 1);

Entitlements are authored on the server and signed into the token, which is what makes them safe in client code. if (plan === 'Pro') breaks the day somebody renames a tier.

Design principles

Two homes

It’s also a drop-in for the Lemon Squeezy License API, so existing clients migrate with a base-URL change. See Migrating from LemonSqueezy.

Next