Skip to main content
userTourKit
Licensing

Licensing

Set up userTourKit Pro licensing with Polar.sh — installation, configuration, and CI/CD

domidex01Published

Overview

userTourKit uses a Free + Pro licensing model:

  • Free packages (@tour-kit/core, @tour-kit/react, @tour-kit/hints) are MIT-licensed and never require a license key.
  • Pro packages (@tour-kit/adoption, @tour-kit/analytics, @tour-kit/announcements, @tour-kit/checklists, @tour-kit/media, @tour-kit/scheduling, @tour-kit/ai) require a valid license key for production use.

Free packages never require a license key. You can use @tour-kit/core, @tour-kit/react, and @tour-kit/hints without any licensing setup.

Without a valid license, pro packages still render and function normally but display an "UNLICENSED" watermark overlay and a console warning. There is never a crash or blank screen.

Getting a License Key

  1. Visit usertourkit.com/pricing and choose the Pro plan.
  2. Complete the checkout via Polar.sh.
  3. Your license key arrives via email with a TOURKIT- prefix (e.g., TOURKIT-abc123-def456).

Installation

1. Set up npm authentication

Pro packages are published with restricted access on npm. Add the following to your .npmrc at the repo root:

//registry.npmjs.org/:_authToken=${NPM_TOKEN}

This file is safe to commit — the token value comes from your environment, not the file.

2. Install the license package

pnpm add @tour-kit/license

3. Set the environment variable

Add to .env.local:

NEXT_PUBLIC_TOUR_KIT_LICENSE_KEY=TOURKIT-your-key-here

The NEXT_PUBLIC_ prefix is required because <LicenseProvider> runs client-side.

Add to .env:

VITE_TOUR_KIT_LICENSE_KEY=TOURKIT-your-key-here

The VITE_ prefix is required for Vite to expose the variable to client-side code.

Add to .env:

REACT_APP_TOUR_KIT_LICENSE_KEY=TOURKIT-your-key-here

Configuration

Wrap your app (or the section using pro features) with <LicenseProvider>:

import { LicenseProvider } from '@tour-kit/license'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LicenseProvider
      organizationId="your-polar-org-id"
      licenseKey={process.env.NEXT_PUBLIC_TOUR_KIT_LICENSE_KEY ?? ''}
    >
      {children}
    </LicenseProvider>
  )
}
import { LicenseProvider } from '@tour-kit/license'

function App() {
  return (
    <LicenseProvider
      organizationId="your-polar-org-id"
      licenseKey={import.meta.env.VITE_TOUR_KIT_LICENSE_KEY ?? ''}
    >
      <YourApp />
    </LicenseProvider>
  )
}
PropTypeRequiredDescription
organizationIdstringYesYour Polar organization ID
licenseKeystringYesLicense key with TOURKIT- prefix
onValidate(state: LicenseState) => voidNoCalled after validation completes
onError(error: Error) => voidNoCalled on validation error

Using LicenseGate

Wrap pro components with <LicenseGate> for conditional rendering:

import { LicenseGate } from '@tour-kit/license'

function Dashboard() {
  return (
    <div>
      <BasicFeature />
      <LicenseGate require="pro" fallback={<UpgradePrompt />}>
        <AdvancedAnalytics />
      </LicenseGate>
    </div>
  )
}
PropTypeRequiredDescription
require'pro'YesRequired license tier
fallbackReactNodeNoShown when license is invalid
loadingReactNodeNoShown during validation

<LicenseGate> uses interleaved render-key validation — removing the license check breaks rendering, not just gating.

Watermark Behavior

Without a valid license, pro components still render and function but display an "UNLICENSED" overlay:

  • Semi-transparent overlay with inline styles and high z-index
  • pointer-events: none so the UI remains interactive beneath it
  • Console warning logged in development
  • No crash, no blank screen, full functionality preserved

This follows the same soft-enforcement pattern used by AG Grid and MUI X.

Development Mode

On localhost, 127.0.0.1, and *.local domains, license validation is automatically bypassed when a non-empty licenseKey is configured:

  • No network request to Polar API
  • No activation slot consumed
  • Provider returns { status: 'valid', tier: 'pro', renderKey: 'dev_bypass' }
  • All pro features work without verifying the key

An empty or whitespace-only licenseKey is treated as unlicensed on every host, including localhost. Pro packages still render their UI (soft gate) and a single Tour Kit · Unlicensed · Buy license watermark appears bottom-right — making a missing env var surface locally before it ships.

HostlicenseKeyResult
localhost / 127.0.0.1 / *.localNon-emptyvalid / pro, no Polar call, no watermark
localhost / 127.0.0.1 / *.localEmpty or blankinvalid / free, no Polar call, watermark visible
localhost (no <LicenseProvider>)n/aQuiet — library cannot inspect a key that does not exist
Production-like hostNon-emptyValidated against Polar (cache-aware)
Production-like hostEmpty or blankinvalid / free, no Polar call, watermark visible

The localhost bypass only skips Polar — it does not prove the key is valid. We intentionally do not verify local keys because every validation would consume an activation slot during normal dev.

CI/CD Setup

GitHub Actions

Add NPM_TOKEN to your repository secrets, then reference it in your workflow:

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      # Required for installing restricted @tour-kit/* pro packages
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

Vercel

Add NPM_TOKEN as an environment variable in your Vercel project settings. Vercel automatically reads .npmrc during the build.

Netlify

Add NPM_TOKEN as an environment variable in your Netlify site settings under Build & deploy > Environment.

Hooks Reference

useLicense()

Returns the full license context. Must be used within <LicenseProvider>.

const { state, refresh } = useLicense()
FieldTypeDescription
state.status'valid' | 'invalid' | 'expired' | 'revoked' | 'loading' | 'error'Current license status
state.tier'free' | 'pro'License tier
state.activationsnumberNumber of activated domains
state.maxActivationsnumberMaximum allowed activations
state.domainstring | nullCurrent activated domain
state.expiresAtstring | nullExpiration date (ISO string)
refresh() => Promise<void>Re-validate the license (clears cache)

useIsPro()

Returns true when license tier is 'pro' and status is 'valid'.

const isPro = useIsPro()

FAQ

Do free packages need a license?

No. @tour-kit/core, @tour-kit/react, and @tour-kit/hints are MIT-licensed and work without any license key.

What happens if my key expires?

The watermark appears on pro components and a console warning is logged. Components continue to render and function normally — there is never a crash or blank screen.

How many domains can I activate?

Each license key supports up to 5 domain activations. You can deactivate a domain to free up a slot.

Does localhost count as an activation?

No. When a non-empty licenseKey is set, development environments (localhost, 127.0.0.1, *.local) are automatically bypassed and do not consume an activation slot. An empty key on localhost intentionally shows the unlicensed watermark — see Development Mode.

Can I use one key for staging + production?

Yes. Each environment uses a separate domain activation. For example, staging.example.com and example.com would use 2 of your 5 activation slots.

What is the TOURKIT- prefix?

All userTourKit license keys issued via Polar.sh have a TOURKIT- prefix. This prefix is required for validation to work.