Skip to content

Licensing

Licensing is an abstraction with real providers, not hardcoded DRM.

Honest positioning

Desktop and client software can never have perfect license protection. This system is honest-user licensing, feature entitlement and commercial-readiness hooks — not "unbreakable" protection. Treat it as such.

The contract

class LicenseProvider(Protocol):
    def current_plan(self) -> str: ...
    def is_entitled(self, required_plan: str | None) -> bool: ...
    def has_feature(self, feature: str) -> bool: ...
    def info(self) -> LicenseInfo: ...   # plan, licensee, expiry, features, source

Plans are ranked (free < pro < enterprise); features are explicit named grants in the license. Every adapter — API, CLI, desktop — resolves the same provider from the same settings, so an install is licensed once, everywhere.

The providers

Provider Source of truth Use case
LocalDevLicenseProvider Local config (APP_LICENSE_DEV_PLAN) Development, open-source "feature flags" mode
SignedLicenseProvider An Ed25519-signed offline token (env var or file) Selling licenses with no server at all
HttpLicenseProvider A vendor-run validation endpoint Central control, revocation, seat counting

Resolution order: APP_LICENSE_URLAPP_LICENSE_TOKENAPP_LICENSE_FILE (default license.key) → the dev stub. Invalid or expired licenses degrade to the free plan with a human-readable message — they never crash the app.

Selling licenses in three commands (no server)

You are the vendor. Once:

opk license keygen        # writes license-signing.private / .public
# bake the public key into the app: APP_LICENSE_PUBLIC_KEY=<printed value>

Per customer:

opk license issue --licensee "Acme Corp" --plan pro --days 365 --feature export
# -> LIC1.eyJ...  (send this token to the customer)

The customer activates it:

opk license install LIC1.eyJ...   # writes license.key
opk license status                # plan: pro, licensee: Acme Corp, ...

The token is a signed JSON payload (licensee, plan, features, expires_at). The app verifies it offline with only your public key; tampering or the wrong key degrades it to free. Expiry is checked on every read, so it takes effect without a restart — as does replacing the license file.

The HTTP contract

HttpLicenseProvider POSTs {"token": "..."} to your endpoint and expects:

{"valid": true, "plan": "pro", "licensee": "Acme Corp",
 "features": ["export"], "expires_at": null, "message": "ok"}

Responses are cached (default 5 min). If your server is unreachable, the last good response keeps working (offline grace) instead of locking paying customers out; a fresh install that has never validated stays free. Implement the endpoint in anything — including a few lines of FastAPI in your own license-server project, or a webhook layer over Stripe / Lemon Squeezy / Paddle. Payments has complete copy-paste recipes for all three.

Gating things

Plugins declare required_plan in their manifest; unlicensed plugin routes are never mounted and their CLI commands never attach.

Backend routes use FastAPI dependencies (the idiomatic decorator form):

from .licensing import require_feature, require_plan

@router.get("/export", dependencies=[Depends(require_plan("pro"))])
def export(): ...

@router.get("/report", dependencies=[Depends(require_feature("reports"))])
def report(): ...

A failed gate returns 403 with a structured detail ({"error": "license_required", "required_plan": "pro", "current_plan": "free"}).

Frontend components use the entitlement hook and lock card:

const { entitled } = useEntitlement("pro");
return entitled
  ? <ExportButton />
  : <LockedFeatureCard title="Export vault" requiredPlan="pro" />;

Plan ranks come from the /api/license response, so the client never hardcodes tiers. The demo app gates its Export vault feature exactly this way — run with APP_LICENSE_DEV_PLAN=free to see the locked state.

Where the pieces live

  • packages/licensing — providers, token signing/verification, LicenseInfo
  • apps/backend/src/…/licensing.py — resolved provider + require_plan / require_feature
  • GET /api/license — status for the UI (License tab in the admin)
  • opk license … — status/install (customer) and keygen/issue/verify (vendor)