Table of Contents
- License Management System
- Overview
- Core Concepts
- Web UI
- API Endpoints
- Authenticated (requires token + repo admin)
- Public (no auth required)
- Purchase Webhook
- Validation Endpoint
- Update Feed Integration
- Domain Enforcement
- Channel System
- Enabling the Licensing System
- Key Heartbeat
- Release Tag Enforcement
- Permissions
- Joomla Standard Compliance
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.
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.
Repo: MokoGitea
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-30 | Claude (Opus 4.6) | Initial version |
| 1.1 | 2026-05-31 | Claude (Opus 4.6) | Add feature toggle, unified update system, tag enforcement, heartbeat tracking |
| 1.2 | 2026-05-31 | Claude (Opus 4.6) | Add permissions (TypeLicenses unit), renewal, auto-domain, custom keys, UI/UX cleanup |
Joomla Standard Compliance
MokoGitea's update feed follows the Joomla update server specification. Key alignment with the Akeeba Backup Pro reference implementation:
Download Key (<dlid>)
Extensions declare <dlid prefix="dlid=" suffix=""/> in their package manifest (pkg_*.xml). This tells Joomla to show a "Download Key" field in System > Update Sites. The key is appended as ?dlid=MOKO-XXXX-... to download requests.
MokoGitea serves the <downloadkey> element in updates.xml when RequireKey is enabled:
<downloadkey prefix="dlid=" suffix=""/>
The server accepts keys via three query parameters: key, download_key, or dlid.
XML Fields
| Field | Description | Example |
|---|---|---|
<name> |
Extension display name | Package - MokoWaaS |
<element> |
Joomla element identifier | pkg_mokowaas |
<type> |
Extension type | package, component, plugin |
<version> |
Semantic version | 02.29.04 |
<downloads> |
Download URL(s) with type/format | .zip attachment URL |
<tags> |
Release channel | stable, release-candidate |
<targetplatform> |
Compatible Joomla versions | name="joomla" version="(5|6)\..*" |
<sha256> |
Checksum for integrity | SHA-256 of zip |
<sha512> |
Checksum (optional) | SHA-512 of zip |
<php_minimum> |
Minimum PHP version | 7.4 |
<infourl> |
Release info page | Link to release on Gitea |
<changelogurl> |
Changelog file | CHANGELOG.md raw URL |
<downloadkey> |
Download key config | prefix="dlid=" suffix="" |
Platform Enforcement
Feed endpoints are gated by the repo's update platform setting:
| Platform Setting | /updates.xml |
/updates/dolibarr.json |
|---|---|---|
joomla |
Served | 404 |
dolibarr |
404 | Served |
both |
Served | Served |
| (empty/default) | Served | 404 |