Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.authforge.cc/llms.txt

Use this file to discover all available pages before exploring further.

Good licensing UX is invisible when things work and helpful when they don’t. These patterns ensure your customers have a smooth experience.

First-run experience

The first time a user launches your app, they need to enter a license key. Make this frictionless:

Desktop apps

Show a clean, focused dialog. Don’t overwhelm with options; just a single input field and an activate button.

Welcome to YourApp

Enter your license key to get started

XXXX-XXXX-XXXX-XXXX
PurchaseActivate
  • Auto-format the input as the user types (add dashes automatically).
  • Accept keys with or without dashes.
  • Show a “Purchase” link for users who don’t have a key yet.
  • Trim whitespace; users often copy keys with trailing spaces.

CLI tools

Support multiple input methods to minimize friction:
# Via flag
yourapp --license-key XXXX-XXXX-XXXX-XXXX

# Via environment variable
export AUTHFORGE_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX
yourapp

# Interactive prompt (if no key found)
yourapp
> No license key found. Enter your key: _

Storing the key locally

After successful validation, save the key locally so users don’t re-enter it on every launch.
import json
import os

CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".yourapp")
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")

def save_license_key(key):
    os.makedirs(CONFIG_DIR, exist_ok=True)
    config = load_config()
    config["licenseKey"] = key
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f)

def load_license_key():
    config = load_config()
    return config.get("licenseKey")
On subsequent launches, load the stored key and authenticate silently:
stored_key = load_license_key()
if stored_key and client.login(stored_key):
    # Authenticated silently; go straight to the app
    run_app()
else:
    # Show the license key dialog
    key = show_license_dialog()
    if client.login(key):
        save_license_key(key)
        run_app()

Settings page

Provide a settings or “About” page where users can see their license status:
FieldValue
License keyA3K9-****-****-QHDT (partially masked)
StatusActive
PlanPro
ExpiresDecember 31, 2026
Devices1 of 2 slots used
[Deactivate]
  • Mask the license key (show first and last groups only).
  • Show the expiry date prominently.
  • Include a “Deactivate” button that clears the stored key and returns to the license dialog.
  • If using license variables, show the plan tier.

Expiration and renewal

When a license is approaching expiration, show gentle reminders:
from datetime import datetime, timedelta

if client.login(license_key):
    expires_at = client.license_variables.get("expiresAt")
    if expires_at:
        expiry = datetime.fromisoformat(expires_at)
        days_left = (expiry - datetime.now()).days

        if days_left <= 7:
            show_banner(
                f"Your license expires in {days_left} days. "
                "Renew now to avoid interruption.",
                action_url="https://yoursite.com/renew"
            )
When the license has expired, show a clear message with a path to renewal:

Your license has expired

Your license expired on December 31, 2026.
Renew to continue using YourApp.

Enter New KeyRenew

Trial mode

Use the Developer API to issue time-limited trial licenses:
// Create a 14-day trial license
const trial = await fetch("https://api.authforge.cc/v1/licenses", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    appId: process.env.AUTHFORGE_APP_ID,
    quantity: 1,
    expiresAt: new Date(Date.now() + 14 * 86400000).toISOString(),
    label: "trial",
  }),
});
In the app, show the remaining trial period:
if is_trial:
    days_left = calculate_days_remaining()
    show_persistent_banner(f"Trial: {days_left} days remaining. Upgrade to keep using YourApp.")

Upgrade prompts

When a user on a lower tier tries to use a gated feature, show a helpful prompt instead of silently failing:
def export_project():
    features = client.license_variables.get("features", "")
    if "export" not in features:
        show_upgrade_dialog(
            title="Export requires Pro",
            message="Upgrade to the Pro plan to unlock project export.",
            action_url="https://yoursite.com/pricing"
        )
        return
    # ... actual export logic

Graceful degradation

If your app has a free tier or limited mode, fall back to it instead of killing the app entirely:
if client.login(license_key):
    run_full_app()
elif has_free_tier:
    show_notice("Running in free mode. Some features are limited.")
    run_limited_app()
else:
    show_error("Invalid license key.")
    exit(1)

Error messages

Always show user-friendly messages. Map internal error codes to helpful text:
ErrorUser sees
invalid_key”Invalid license key. Please check and try again.”
expired”Your license has expired. [Renew now]“
revoked”This license has been deactivated. Contact support.”
hwid_mismatch”This license is in use on another device. Contact support to transfer.”
no_credits”Service temporarily unavailable. Please try again in a few minutes.”
Network error”Couldn’t connect to the license server. Check your internet connection.”
Never show internal error codes like no_credits or invalid_app to end users.

Offline first launch

The initial authentication always requires a network connection. Make this clear:

Connection Required

YourApp needs an internet connection for first-time activation. After that, it works offline.

Retry
After initial activation with LOCAL heartbeat mode, the app works offline until the session token expires (24 hours by default; up to 7 days if the SDK was configured with a longer ttlSeconds).