Changelog: comprehensive entry covering all features, security fixes, platform feeds, UI changes, and settings restructure. Wiki: all 7 platform feeds now listed as Production. Revision 1.5 added covering sub-orgs, visibility modes, settings pages, and security hardening. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
17 KiB
License Management System
MokoGitea includes a built-in commercial license management system for organizations that sell extensions (Joomla, Dolibarr, etc.). It handles license key generation, validation, update feed gating, domain enforcement, and payment integration.
Overview
The license system operates at two levels:
- Organization-level: Manages packages and keys shared across all repos in an org
- Repository-level: Same features scoped to a single repo, plus update feed URLs
Core Concepts
License Packages
A package defines a subscription tier (e.g., "Pro Annual", "Basic Monthly"). Each package specifies:
| Field | Description |
|---|---|
| Name | Human-readable name (e.g., "Pro Annual") |
| Duration | Key validity in days. 0 = lifetime |
| Max Sites | Maximum unique domains. 0 = unlimited |
| Channels | Which update streams this package grants access to |
| Active | Whether the package is enabled |
License Keys
Keys are generated from packages. Format: MOKO-XXXX-XXXX-XXXX-XXXX.
Important: Keys are hashed (SHA-256) before storage. The full key is only shown once at creation time. Store it securely.
Each key can have:
- Licensee name/email for customer tracking
- Domain restriction (comma-separated allowed domains)
- Max sites override (0 = use package default)
- Custom expiry date
Master Package
Every org/repo automatically gets a "Master (Internal)" package with an internal key. This provides unlimited access to all channels and cannot be edited or deleted. Master keys can only be revoked.
Web UI
Repository Licenses Page
/{owner}/{repo}/licenses
- View all packages and issued keys
- Create new packages with channel selection
- Generate keys from packages (auto-generated or custom key for admins)
- Renew expired keys (sync icon, extends by package duration)
- Edit packages and keys (pencil icon)
- Revoke keys (X icon, with confirmation modal)
- Delete packages (trash icon, site admins only, with confirmation modal)
- Copy update feed URLs (clipboard button with tooltip feedback)
- Copy newly created keys (clipboard button in success message)
Organization Licenses Page
/{org}/-/licenses
Same features as repo level, scoped to the organization. Packages and keys created here are shared across all repos in the org.
API Endpoints
All endpoints are under /api/v1/repos/{owner}/{repo}/.
Authenticated (requires token + repo admin)
| Method | Path | Description |
|---|---|---|
| GET | /license-packages |
List all packages |
| POST | /license-packages |
Create a package |
| GET | /license-keys |
List all keys |
| POST | /license-keys |
Create a key (returns full key) |
| PATCH | /license-keys/{id} |
Edit a key |
| DELETE | /license-keys/{id} |
Delete a key |
| GET | /license-keys/{id}/usage |
Get usage logs (last 100) |
| POST | /license-keys/purchase |
Create key from payment webhook |
Public (no auth required)
| Method | Path | Description |
|---|---|---|
| POST | /license-keys/validate |
Validate a key + domain |
Purchase Webhook
POST /api/v1/repos/{owner}/{repo}/license-keys/purchase
Designed for payment system integration (Stripe, PayPal, etc.):
{
"package_id": 1,
"licensee_name": "John Doe",
"licensee_email": "john@example.com",
"domain": "example.com",
"payment_ref": "stripe_pi_xxx"
}
The payment_ref field provides idempotency -- if a key already exists with that reference, the existing key is returned instead of creating a duplicate.
Response includes the full raw_key (only on first creation).
Validation Endpoint
POST /api/v1/repos/{owner}/{repo}/license-keys/validate
For client-side license checks:
{
"key": "MOKO-XXXX-XXXX-XXXX-XXXX",
"domain": "example.com"
}
Response:
{
"valid": true,
"package_name": "Pro Annual",
"channels": "[\"stable\",\"release-candidate\"]",
"expires_at": "2027-05-30T00:00:00Z",
"sites_used": 2,
"max_sites": 5
}
Update Feed Integration
Joomla (updates.xml)
GET /{owner}/{repo}/updates.xml
When RequireKey is enabled for a repo, the XML feed includes a <downloadkey> element:
<update>
<name>MokoOnyx</name>
<version>3.10.9</version>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/.../mokoonyx.zip</downloadurl>
</downloads>
<downloadkey prefix="&dlid=" suffix=""/>
</update>
This tells Joomla to show a Download Key field in System > Update Sites. The key is sent as &dlid=MOKO-XXXX-... on download requests.
The server accepts keys via three query parameters: key, download_key, or dlid.
Dolibarr (dolibarr.json)
GET /{owner}/{repo}/updates/dolibarr.json
Uses the same license key validation as the Joomla XML feed. All platforms share the same licensing system. Invalid or missing keys receive an empty response.
Domain Enforcement
When a key has a DomainRestriction set (comma-separated domains):
- The domain from the request is checked against the allowed list
- Comparison is case-insensitive
- Master/internal keys bypass domain checks
Auto-Association (Lock-on-First-Use)
When a key has no DomainRestriction set and a domain is provided during validation:
- The domain is automatically appended to the key's
DomainRestriction - On subsequent heartbeats, additional domains can be added (up to
MaxSites) - Once
MaxSitesis reached, no new domains are accepted - This provides zero-config domain locking — the first site to use a key "claims" it
Site Limit Enforcement
When MaxSites is set (on key or package):
- The system counts unique domains from usage records
- If the requesting domain is already known, it's allowed through
- If the domain is new and the limit is reached, the request is rejected
- Error:
site limit reached (N/N) - Auto-association also respects this limit — domains won't be added past the cap
Channel System
Channels control which update streams a license key grants access to. Available channels come from the org's Update Stream configuration:
Default Joomla streams: stable, release-candidate, beta, alpha, development
Channels are selected via checkboxes when creating/editing a package. They're stored as a JSON array (e.g., ["stable","release-candidate"]).
A package with no channels selected grants access to all channels.
Enabling the Licensing System
Licensing is off by default and must be enabled explicitly.
Organization Level
Go to Organization Settings > Licenses & Update Streams and check:
- Enable licensing system -- shows the Licenses tab for all org members
- Require license key for update feeds -- gates update XML/JSON feeds behind key validation
Repository Level
Go to Repository Settings > Advanced and check:
- Enable licensing system -- shows the Licenses tab for this repo
- Require license key for update feed access -- gates this repo's update feeds
Either org-level or repo-level enabling is sufficient to show the Licenses tab.
Feed Visibility
Controls what unauthenticated users see when accessing update feed URLs:
| Mode | Update Feed | Release Page | Download Files |
|---|---|---|---|
| Public | Full feed with download URLs | Public | Public |
| No-download | Version info only (URLs stripped) | Public (notes visible) | "Sign in to download" |
| Hidden | Empty feed | Redirects to login | Login required |
The no-download mode is ideal for commercial extensions: customers see that updates exist (motivating purchase/renewal) but cannot download without signing in. Release notes and changelogs remain publicly readable.
Key Heartbeat
Every time a license key is successfully validated (via update feed or API), the LastHeartbeatUnix timestamp is updated. This is shown as "Last Seen" in the keys table and included in API responses as last_heartbeat.
Use this to identify inactive keys or monitor customer usage patterns.
Release Tag Enforcement
When licensing is enabled, release tags with prerelease suffixes (e.g., -rc, -beta, -alpha, -dev) must match a configured update stream suffix. This ensures the update feed generator can correctly categorize releases.
- Tags without prerelease suffixes (e.g.,
v1.0.0) are always allowed (stable stream) - Tags with suffixes must match a stream (e.g.,
v1.0.0-rc1matches therelease-candidatestream with suffix-rc) - When licensing is disabled, any tag format is accepted
Permissions
Licenses use Gitea's unit permission system (TypeLicenses). Teams can be granted Read, Write, or Admin access.
| Action | Required Permission |
|---|---|
| View licenses page | Unit Read (or org member) |
| Create/edit packages | Unit Write |
| Generate/revoke/renew keys | Unit Write |
| Edit keys | Unit Write |
| Set custom key value | Site admin or org owner |
| Delete packages | Site admin only |
| Edit/delete master package | Blocked (not allowed) |
| Edit master keys | Blocked (only revoke allowed) |
Note: Admin-level teams implicitly have admin access to all units (including Licenses) even without explicit TeamUnit records. This means existing admin teams automatically gain license access.
Key Renewal
The Renew action extends a key's expiration by the package's DurationDays:
- If the key is still valid, renewal extends from the current expiry date
- If the key has already expired, renewal extends from now
- Renewal also re-activates revoked keys
- For lifetime packages (DurationDays=0), renewal extends by 365 days
Custom Keys
Site admins and org owners can set a custom key value instead of auto-generating. Use cases:
- Migrating from an existing licensing system
- Keys that match external records or payment systems
- Deterministic keys for testing
The custom key is hashed identically to auto-generated keys — no security difference.
Package Archiving
Packages can be archived instead of permanently deleted. This is the recommended workflow for end-of-life packages.
How archiving works:
- Archived packages are hidden from the main list
- Existing keys from archived packages continue to work (validation, heartbeats, update feeds)
- No new keys can be generated from an archived package
- Archived packages appear in a collapsible "Archived Packages" section at the bottom
- Admins can restore (unarchive) a package at any time
- Only site admins and org owners can permanently delete a package
Permanent deletion removes the package record entirely. Any keys that reference it will become orphaned and fail validation. Use with caution.
Step-by-Step: Setting Up Licensing for a Joomla Extension
1. Enable Licensing
Organization level (recommended — applies to all repos):
- Go to your org → Settings → Licensing & Update Streams
- Check Enable licensing for this organization
- Check Require license key for all update feeds
- Click Save
Repository level (per-repo override):
- Go to your repo → Settings → scroll to Licensing & Updates
- Check Enable licensing for this repository
- Select the Update Feed Format (Joomla, Dolibarr, or Both)
- Check Require license key for update feeds
- Click Save Changes
2. Configure Extension Metadata
In Organization Settings → Licensing & Update Streams, under Extension Metadata:
- Set the Platform (Joomla, Dolibarr, WordPress, etc.)
- Set the Extension Name (e.g.,
pkg_mokowaas) — this becomes the<element>in the XML feed - Set the Display Name (e.g., "Package - MokoWaaS") — shown in Joomla update manager
- Set the Extension Type (component, module, plugin, package, template, library)
- Set the Target Version regex (e.g.,
(5|6)\..*for Joomla 5 and 6) - Set the PHP Minimum if applicable (e.g.,
8.1) - Set Maintainer and Maintainer URL
3. Create a License Package
- Navigate to the Licenses tab (repo or org level)
- Click New Package
- Fill in:
- Name: e.g., "Pro Annual"
- Duration: e.g.,
365days (0 = lifetime) - Max Sites: e.g.,
3(0 = unlimited) - Channels: check which update streams this package grants access to
- Click Create License Package
4. Generate a License Key
- On the Licenses page, find your package row
- Click the + button to generate a key
- The full key (e.g.,
MOKO-A1B2-C3D4-E5F6-G7H8) is shown in a success banner - Copy the key and send it to your customer
5. Set Up the Client (Joomla Example)
In the customer's Joomla admin:
- Go to System → Update Sites
- Add or edit the update site URL:
https://git.mokoconsulting.tech/{org}/{repo}/updates.xml - If prompted for a Download Key, enter the license key
- Joomla will automatically append
&dlid=MOKO-XXXX-...to download requests
6. Monitor Usage
- The Last Seen column shows when each key last checked for updates
- Click the edit (pencil) button to view/modify key details
- Domain restrictions are auto-populated on first use (lock-on-first-use)
Step-by-Step: Payment Integration (Stripe/PayPal Webhook)
1. Create a Webhook Endpoint
Configure your payment provider to POST to:
POST https://git.mokoconsulting.tech/api/v1/repos/{org}/{repo}/license-keys/purchase
With headers:
Authorization: token YOUR_API_TOKEN
Content-Type: application/json
2. Webhook Payload
{
"package_id": 1,
"licensee_name": "John Doe",
"licensee_email": "john@example.com",
"domain": "example.com",
"payment_ref": "stripe_pi_abc123"
}
The payment_ref provides idempotency — duplicate webhooks return the existing key instead of creating duplicates.
3. Response
On first creation:
{
"id": 42,
"raw_key": "MOKO-A1B2-C3D4-E5F6-G7H8",
"package_id": 1,
"licensee_name": "John Doe",
...
}
Send the raw_key to the customer via email.
Step-by-Step: Validating a Key (Client-Side)
Your extension can validate its own key against the server:
POST https://git.mokoconsulting.tech/api/v1/repos/{org}/{repo}/license-keys/validate
Content-Type: application/json
{"key": "MOKO-A1B2-C3D4-E5F6-G7H8", "domain": "example.com"}
Response:
{
"valid": true,
"package_name": "Pro Annual",
"channels": "[\"stable\",\"release-candidate\"]",
"expires_at": "2027-06-01T00:00:00Z",
"sites_used": 1,
"max_sites": 3
}
This endpoint is public — no authentication required. Use it in your extension's license check flow.
Supported Platforms
The update feed system currently supports:
| Platform | Feed URL | Format | Status |
|---|---|---|---|
| Joomla | /{repo}/updates.xml |
XML with <downloadkey> |
Production |
| Dolibarr | /{repo}/updates/dolibarr.json |
JSON | Production |
| WordPress | /{repo}/updates/wordpress.json |
PUC-compatible JSON | Production |
| Composer | /{repo}/updates/packages.json |
packages.json | Production |
| PrestaShop | /{repo}/updates/prestashop.xml |
Module update XML | Production |
| Drupal | /{repo}/updates/drupal.xml |
Update status XML | Production |
| WHMCS | /{repo}/updates/whmcs.json |
Module update JSON | Production |
| Changelog | /{repo}/changelog.xml |
Joomla changelog XML | Production |
All platforms share the same licensing backend — the same keys, packages, and validation work across all feed formats.
Repo: MokoGitea
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-30 | Jonathan Miller (@jmiller) | Initial version |
| 1.1 | 2026-05-31 | Jonathan Miller (@jmiller) | Add feature toggle, unified update system, tag enforcement, heartbeat tracking |
| 1.2 | 2026-05-31 | Jonathan Miller (@jmiller) | Add permissions (TypeLicenses unit), renewal, auto-domain, custom keys, UI/UX cleanup |
| 1.3 | 2026-06-01 | Jonathan Miller (@jmiller) | Add package archiving, expanded delete permissions, migration v340, API renew, step-by-step guides |
| 1.4 | 2026-06-02 | Jonathan Miller (@jmiller) | WordPress feed, feed visibility modes, download gating, RepoScope enforcement, API package CRUD, settings API, combolist channel picker, double confirmation modals, extension metadata in repo settings, domain lock timer, Joomla-standard tags, SHA256 in XML, changelog XML, no-download release page mode |
| 1.5 | 2026-06-02 | Jonathan Miller (@jmiller) | All 7 platform feeds (Composer, PrestaShop, Drupal, WHMCS), enterprise sub-org hierarchy, three-level repo visibility (Public/Private/Hidden), styled 403 page with login form, separate licensing/advanced settings pages, icons on all navbars, manual stream mapping, configurable key prefix, feed always public, xorm column name fixes, security hardening |