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
| Mode | Purpose | Who uses it |
|---|---|---|
| MAIN | Generate and manage license keys; no activation required on this instance | You (vendor / main deployment) |
| CONSUMER | Activate a license key; features and limits are enforced | Customers (self-hosted instances) |
- Configuration:
LicenseSystem:ModeorLICENSE_MODEenvironment variable (MAINorCONSUMER). Default isCONSUMER. - 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
| Tier | Max users | Typical use | Features |
|---|---|---|---|
| Tier 1 (Free) | 50 | Community / trial | Core features only; no AI blog, email campaigns, or support tickets |
| Tier 2 (Professional) | 500 | Paid | + AI blog (rate-limited), email campaigns (e.g. 500/month), support tickets |
| Tier 3 (Enterprise) | Unlimited | Premium | + 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)
- Admin opens Admin Panel → Settings → License (e.g.
/settings/license). - Enters the license key and submits.
- Frontend calls
POST /api/license/activatewith{ "licenseKey": "BB-LICENSE-..." }(no auth required). - 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.
- Response includes
tier,maxUsers,expiresAt.
If the key is invalid or expired, the API returns an error; no license is stored.
Generation (MAIN)
- Admin opens Admin Panel → Settings → License → License Generation (e.g.
/settings/license/generation). - Chooses tier, optional expiry, and optional customer info (id, name, email).
- Frontend calls
POST /api/license/generation/generate(Admin role required) with body:tier(required):Tier1|Tier2|Tier3expiresAt(optional): ISO date-time (UTC)customerId,customerName,customerEmail(optional)
- 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-periodcan 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-onlycan 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
| Method | Endpoint | Auth | Mode | Description |
|---|---|---|---|---|
| POST | /api/license/activate | None | CONSUMER | Activate a license key (body: licenseKey) |
| GET | /api/license/status | None | Both | Current license status, tier, limits, mode (isMain, isConsumer) |
| GET | /api/license/validate | Admin | Both | Trigger validation and return isValid |
| POST | /api/license/validate-online | None | MAIN only | Validate a key (body: licenseKey); used by consumer to validate online |
| POST | /api/license/generation/generate | Admin | MAIN only | Generate a new license key (body: tier, expiresAt?, customerId?, customerName?, customerEmail?) |
| GET | /api/license/generation/check-config | Admin | MAIN only | Check if private key is configured |
| GET | /api/license/generation/list | Admin | MAIN only | List all generated licenses |
Configuration
MAIN (generator)
{
"LicenseSystem": {
"Mode": "MAIN",
"PrivateKey": "<base64-or-secret-string>"
}
}
- Private key:
LicenseSystem:PrivateKeyorLICENSE_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.
Related
- Admin Panel — License Management vs License Activation, feature flags
- AI Blog Generation — aiBlogGeneration gate
- Email Campaigns — emailCampaigns gate
- Support Tickets — supportTickets gate
- Scheduled Jobs — LicenseValidationJob
- Understanding the Architecture — where license fits in the request path