Skip to main content

License System

Bellamy Book uses a 3-tier licensing system for self-hosted deployments. It supports two modes: MAIN (your deployment that generates license keys) and CONSUMER (customer deployments that activate and enforce licenses). The backend enforces license status and feature limits via middleware and per-feature checks; the admin panel shows or hides features based on the active license.


Modes

ModePurposeWho uses it
MAINGenerate and manage license keys; no activation required on this instanceYou (vendor / main deployment)
CONSUMERActivate a license key; features and limits are enforcedCustomers (self-hosted instances)
  • Configuration: LicenseSystem:Mode or LICENSE_MODE environment variable (MAIN or CONSUMER). Default is CONSUMER.
  • In MAIN mode, license middleware does not block any routes; generation endpoints are available (with Admin role and private key).
  • In CONSUMER mode, activation is required for admin/gated features; user-facing routes can still work when no license is present (see Enforcement).

Tiers and Features

TierMax usersTypical useFeatures
Tier 1 (Free)50Community / trialCore features only; no AI blog, email campaigns, or support tickets
Tier 2 (Professional)500Paid+ AI blog (rate-limited), email campaigns (e.g. 500/month), support tickets
Tier 3 (Enterprise)UnlimitedPremium+ All features unlimited; white label

Feature keys (backend / admin)

  • max_users — Maximum registered users (Count).
  • max_admins — Maximum administrators (Count).
  • ai_blog_generation — AI blog generation: Tier1 = off; Tier2 = rate (e.g. per month); Tier3 = on.
  • email_campaigns — Email campaigns: Tier1 = off; Tier2 = rate (e.g. 500/month); Tier3 = on.
  • support_tickets — Support tickets: Tier1 = off; Tier2/Tier3 = on.
  • advanced_analytics, custom_branding — Tier1 = off; Tier2/Tier3 = on.
  • white_label — Tier3 only.

Controllers (e.g. AI Agent, Email Campaign, Tickets) call ILicenseService.IsFeatureEnabledAsync(featureKey) and CheckFeatureLimitAsync(featureKey) before allowing access. The admin panel uses the license status API to show/hide menu items (e.g. Email Campaigns, Support Tickets, AI Agent).


License Key Format

  • Format: BB-LICENSE-T{TIER}-{TIMESTAMP}-{SIGNATURE}
  • TIER: 1, 2, or 3.
  • TIMESTAMP: Generation date (e.g. yyyyMMdd).
  • SIGNATURE: HMAC-SHA256 (with private key) over metadata (tier, customer, expiry, etc.), truncated/encoded.

Keys are generated only in MAIN mode using a private key (LicenseSystem:PrivateKey or LICENSE_PRIVATE_KEY). The consumer never sees the private key; activation stores a hash of the key and the tier/limits in the database.


Activation (CONSUMER)

  1. Admin opens Admin Panel → Settings → License (e.g. /settings/license).
  2. Enters the license key and submits.
  3. Frontend calls POST /api/license/activate with { "licenseKey": "BB-LICENSE-..." } (no auth required).
  4. Backend parses the key, validates format/signature (and optionally online validation), creates or reactivates the license record, and attaches default feature limits for the tier.
  5. Response includes tier, maxUsers, expiresAt.

If the key is invalid or expired, the API returns an error; no license is stored.


Generation (MAIN)

  1. Admin opens Admin Panel → Settings → License → License Generation (e.g. /settings/license/generation).
  2. Chooses tier, optional expiry, and optional customer info (id, name, email).
  3. Frontend calls POST /api/license/generation/generate (Admin role required) with body:
    • tier (required): Tier1 | Tier2 | Tier3
    • expiresAt (optional): ISO date-time (UTC)
    • customerId, customerName, customerEmail (optional)
  4. Backend requires MAIN mode and a configured private key. It generates the key, stores the license (inactive until consumer activates), and returns the license key to show/copy.

Endpoints:

  • GET /api/license/generation/check-config — Check if private key is configured (MAIN only).
  • GET /api/license/generation/list — List generated licenses (MAIN only, Admin).

Enforcement (CONSUMER)

LicenseEnforcementMiddleware runs on every API request (except exempt routes).

Exempt routes (no license check)

  • /api/license/activate, /api/license/status
  • /health, /swagger

MAIN mode

  • All license checks are skipped; generation and list are available to admins with private key configured.

No license (CONSUMER)

  • Admin/gated routes are blocked with 402 Payment Required: /api/admin/*, /api/users/create, /api/ai-agent, /api/emailcampaign, /api/tickets, /api/settings.
  • User/frontend routes (e.g. posts, feed, users, friendship, story, blog get) are allowed so the app can still be used; only admin features require a license.

Expired license (CONSUMER)

  • Grace period (first 7 days after expiry): User creation and media upload are blocked (402); other operations allowed; header X-License-Status: grace-period can be set.
  • Hard restriction (after 7 days): All write operations are blocked (402, “read-only mode”); read-only routes (e.g. get posts/feed, get user, get story, get blog) are still allowed; header X-License-Status: expired-read-only can be set.

User limit

  • On user creation routes (/api/auth/register, /api/users/create), the middleware checks max_users. If the current user count is at or over the limit, the request is rejected with 403 and a message to upgrade.

Individual features (AI blog, email campaigns, support tickets) are also gated in their controllers via IsFeatureEnabledAsync and CheckFeatureLimitAsync.


API Endpoints

MethodEndpointAuthModeDescription
POST/api/license/activateNoneCONSUMERActivate a license key (body: licenseKey)
GET/api/license/statusNoneBothCurrent license status, tier, limits, mode (isMain, isConsumer)
GET/api/license/validateAdminBothTrigger validation and return isValid
POST/api/license/validate-onlineNoneMAIN onlyValidate a key (body: licenseKey); used by consumer to validate online
POST/api/license/generation/generateAdminMAIN onlyGenerate a new license key (body: tier, expiresAt?, customerId?, customerName?, customerEmail?)
GET/api/license/generation/check-configAdminMAIN onlyCheck if private key is configured
GET/api/license/generation/listAdminMAIN onlyList all generated licenses

Configuration

MAIN (generator)

{
"LicenseSystem": {
"Mode": "MAIN",
"PrivateKey": "<base64-or-secret-string>"
}
}
  • Private key: LicenseSystem:PrivateKey or LICENSE_PRIVATE_KEY. Use a long, random value (e.g. openssl rand -base64 32). Keep it secret; only the MAIN instance should have it.

CONSUMER (customer)

{
"LicenseSystem": {
"Mode": "CONSUMER",
"ValidationEndpoint": "https://your-main-server.com/api/license/validate-online"
}
}
  • ValidationEndpoint (optional): If set, CONSUMER can validate keys against MAIN (e.g. for revocation or online checks). Activation itself stores the key hash and limits locally.

Admin Panel UI

  • CONSUMER: Settings → License shows license activation (enter key, activate). Status (tier, expiry, user count, limits) is shown from GET /api/license/status.
  • MAIN: Settings → License → License Generation shows generate form (tier, expiry, customer) and list of generated licenses. License Management may show the same list.
  • Sidebar and routes (Email Campaigns, Support Tickets, AI Agent) are shown or hidden based on license status and feature flags (e.g. emailCampaigns, supportTickets, aiBlogGeneration) so the UI matches backend enforcement.

Weekly Validation (CONSUMER)

LicenseValidationJob (Quartz) runs weekly (default: Monday midnight). It calls ILicenseService.PerformPeriodicValidationAsync(). In CONSUMER mode this can update validation status (e.g. call MAIN’s validate-online if configured). In MAIN mode the job does nothing. See Scheduled Jobs.