Extension Registry
The Notur registry is a GitHub-backed index of available extensions. It allows panel administrators to discover, install, and update extensions via the CLI.
Table of Contents
- How the Registry Works
- Registry Index Format
- Extension Manifest Schema
- Publishing Extensions
- The .notur Archive Format
- Signature Verification
- Building a Registry Index
- Self-Hosted Registries
How the Registry Works
The registry is a static JSON file (registry.json) hosted on a GitHub repository. It contains metadata about available extensions -- their IDs, versions, descriptions, repository URLs, and requirements.
Flow
- Sync:
php artisan notur:registry:syncfetchesregistry.jsonfrom the configured registry URL and caches it locally atstorage/notur/registry-cache.json. The cache has a 1-hour TTL. - Search:
php artisan notur:registry:sync --search "analytics"searches the cached (or freshly fetched) index by ID, name, description, and tags. - Install:
php artisan notur:install acme/analyticslooks up the extension in the registry, downloads the.noturarchive from its GitHub release, and installs it. - Update:
php artisan notur:updatecompares installed versions against the registry and offers to update any extensions with newer versions available.
RegistryClient
The Notur\Support\RegistryClient class handles all registry operations:
class RegistryClient
{
// Fetch the raw registry index from the remote URL
public function fetchIndex(): array;
// Search for extensions matching a keyword query
// Searches ID, name, description, and tags
public function search(string $query): array;
// Get metadata for a specific extension by ID
public function getExtension(string $extensionId): ?array;
// Download a .notur archive from the extension's repository
public function download(string $extensionId, string $version, string $targetPath): void;
// Sync the remote index to a local cache file
// Returns the number of extensions in the index
public function syncToCache(string $cachePath): int;
// Load registry data from a local cache file
// Returns null if cache is missing or expired
public function loadFromCache(string $cachePath, bool $ignoreExpiry = false): ?array;
// Check if the local cache is still valid (not expired)
public function isCacheFresh(string $cachePath): bool;
}Configuration is read from config/notur.php:
'registry_url' => 'https://raw.githubusercontent.com/notur/registry/main',
'registry_cache_path' => storage_path('notur/registry-cache.json'),Registry Index Format
The registry index file (registry.json) has the following structure:
{
"version": "1.0",
"updated_at": "2025-01-15T12:00:00Z",
"extensions": [
{
"id": "acme/server-analytics",
"name": "Server Analytics",
"description": "Real-time server analytics and monitoring",
"latest_version": "1.2.0",
"versions": ["1.0.0", "1.1.0", "1.2.0"],
"license": "MIT",
"authors": [
{ "name": "John Doe", "email": "john@example.com" }
],
"requires": {
"notur": "^1.0",
"pterodactyl": "^1.11",
"php": "^8.2"
},
"repository": "https://github.com/acme/server-analytics",
"tags": ["analytics", "monitoring", "server"],
"dependencies": {
"acme/core-lib": "^1.0"
}
}
]
}Extension Entry Fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier in vendor/name format |
name | string | Yes | Human-readable name |
description | string | No | Short description |
latest_version | string | Yes | Latest available version |
versions | string[] | No | All available versions |
license | string | No | SPDX license identifier |
authors | object[] | No | Author objects with name and optional email |
requires | object | No | Version constraints for notur, pterodactyl, php |
repository | string | Yes | GitHub repository URL |
tags | string[] | No | Searchable tags |
dependencies | object | No | Other Notur extensions this depends on |
Extension Manifest Schema
Every extension must include an extension.yaml (or extension.yml) in its root directory. Notur validates manifests against a JSON schema at registry/schema/extension-manifest.schema.json.
Required Fields
notur: "1.0" # Manifest format version
id: "acme/server-analytics" # Unique ID (pattern: ^[a-z0-9\-]+/[a-z0-9\-]+$)
name: "Server Analytics" # Human-readable name
version: "1.0.0" # Semantic version
entrypoint: "Acme\\ServerAnalytics\\ServerAnalyticsExtension" # PHP classFull Manifest Example
notur: "1.0"
id: "acme/server-analytics"
name: "Server Analytics"
version: "1.0.0"
description: "Real-time server analytics and monitoring"
authors:
- name: "John Doe"
email: "john@example.com"
license: "MIT"
requires:
notur: "^1.0"
pterodactyl: "^1.11"
php: "^8.2"
dependencies:
acme/core-lib: "^1.0"
entrypoint: "Acme\\ServerAnalytics\\ServerAnalyticsExtension"
autoload:
psr-4:
"Acme\\ServerAnalytics\\": "src/"
backend:
routes:
api-client: "src/routes/api-client.php"
admin: "src/routes/admin.php"
migrations: "database/migrations"
commands:
- "Acme\\ServerAnalytics\\Console\\SyncCommand"
middleware:
web:
- "Acme\\ServerAnalytics\\Http\\Middleware\\TrackPageView"
events:
"Pterodactyl\\Events\\Server\\Created":
- "Acme\\ServerAnalytics\\Listeners\\InitializeAnalytics"
permissions:
- "analytics.view"
- "analytics.export"
- "analytics.admin"
frontend:
bundle: "resources/frontend/dist/extension.js"
styles: "resources/frontend/dist/extension.css"
slots:
server.subnav:
label: "Analytics"
icon: "chart-bar"
permission: "analytics.view"
dashboard.widgets:
component: "AnalyticsWidget"
order: 10
admin:
views:
settings: "resources/views/admin/settings.blade.php"Publishing Extensions
To publish an extension to the registry:
Step 1: Create a GitHub Repository
Structure your repository with an extension.yaml at the root. See the manifest schema above.
Step 2: Build and Export
# Build the frontend bundle
cd your-extension
bunx webpack --mode production
# Export as a .notur archive
php artisan notur:export /path/to/your-extension
# Output: acme-server-analytics-1.0.0.notur
# Also generated: acme-server-analytics-1.0.0.notur.sha256Step 3: Create a GitHub Release
- Tag your release (e.g.,
v1.0.0). - Attach the
.noturarchive to the release. - Optionally attach the
.sha256checksum and.sigsignature files.
The download URL follows the pattern:
https://github.com/{vendor}/{name}/releases/download/v{version}/{vendor}-{name}-{version}.noturStep 4: Add to the Registry
Add your repository to the registry's configuration file and rebuild the index, or submit a pull request to the registry repository.
The .notur Archive Format
A .notur file is a gzipped tar archive (tar.gz) containing the extension's files.
Contents
acme-server-analytics-1.0.0.notur (tar.gz)
extension.yaml
src/
ServerAnalyticsExtension.php
routes/
api-client.php
Http/
Controllers/
...
database/
migrations/
...
resources/
frontend/
dist/
extension.jsAssociated Files
| File | Purpose |
|---|---|
{name}-{version}.notur | The archive itself (tar.gz) |
{name}-{version}.notur.sha256 | SHA-256 checksum of the archive |
{name}-{version}.notur.sig | Ed25519 signature (optional) |
Creating Archives Manually
cd /path/to/your-extension
tar czf acme-server-analytics-1.0.0.notur \
extension.yaml src/ database/ resources/frontend/dist/ \
--exclude='node_modules' \
--exclude='.git'
sha256sum acme-server-analytics-1.0.0.notur > acme-server-analytics-1.0.0.notur.sha256Or use the notur:export command, which handles this automatically.
Signature Verification
Notur supports Ed25519 signatures on .notur archives to verify their integrity and authenticity.
How It Works
- The publisher signs the archive with their Ed25519 secret key.
- The signature is stored in a
.sigfile alongside the archive. - When
notur.require_signaturesistrue, theInstallCommandverifies the signature using the configured public key before installation. - If verification fails, installation is aborted.
Configuration
In config/notur.php:
// Require valid signatures for all installs
'require_signatures' => true,
// The Ed25519 public key (set via environment variable)
'public_key' => env('NOTUR_PUBLIC_KEY', ''),Generating Keys
Generate a keypair with the built-in command (hex-encoded keys):
php artisan notur:keygenStore the public key in your panel's .env:
NOTUR_PUBLIC_KEY=hex-encoded-public-key-hereSigning Archives
Use the --sign flag when exporting:
php artisan notur:export --signThis reads the secret key from the environment and generates a .sig file alongside the .notur archive.
Building a Registry Index
The registry index builder tool generates a registry.json from extension repositories.
From a Local Directory
php registry/tools/build-index.php /path/to/extensions --output registry.jsonExpected directory layout:
extensions/
vendor1/
extension-a/
extension.yaml
extension-b/
extension.yaml
vendor2/
...From GitHub Repositories
Create a config file listing repositories:
{
"repositories": [
"acme/server-analytics",
"acme/backup-manager",
"notur/hello-world"
]
}Then run:
php registry/tools/build-index.php --config repos.json --output registry.jsonThe tool fetches extension.yaml from each repository's default branch (tries main, then master) and generates the registry index.
Schema Validation
Registry entries and manifests can be validated against the JSON schemas shipped at:
registry/schema/registry-index.schema.jsonregistry/schema/extension-manifest.schema.json
Self-Hosted Registries
To host your own private registry:
- Generate a
registry.jsonusing the build tool. - Host it on a web server, GitHub Pages, or any static file host.
- Configure panels to point to your registry:
// config/notur.php
'registry_url' => 'https://extensions.mycompany.com',- Ensure your extensions'
.noturarchives are downloadable at the expected URL pattern:{repository}/releases/download/v{version}/{vendor}-{name}-{version}.notur
For fully private registries behind authentication, you may need to customize the RegistryClient or configure a Guzzle middleware to add authentication headers.