Skip to content

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

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

  1. Sync: php artisan notur:registry:sync fetches registry.json from the configured registry URL and caches it locally at storage/notur/registry-cache.json. The cache has a 1-hour TTL.
  2. Search: php artisan notur:registry:sync --search "analytics" searches the cached (or freshly fetched) index by ID, name, description, and tags.
  3. Install: php artisan notur:install acme/analytics looks up the extension in the registry, downloads the .notur archive from its GitHub release, and installs it.
  4. Update: php artisan notur:update compares 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:

php
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:

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:

json
{
    "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

FieldTypeRequiredDescription
idstringYesUnique identifier in vendor/name format
namestringYesHuman-readable name
descriptionstringNoShort description
latest_versionstringYesLatest available version
versionsstring[]NoAll available versions
licensestringNoSPDX license identifier
authorsobject[]NoAuthor objects with name and optional email
requiresobjectNoVersion constraints for notur, pterodactyl, php
repositorystringYesGitHub repository URL
tagsstring[]NoSearchable tags
dependenciesobjectNoOther 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

yaml
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 class

Full Manifest Example

yaml
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

bash
# 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.sha256

Step 3: Create a GitHub Release

  1. Tag your release (e.g., v1.0.0).
  2. Attach the .notur archive to the release.
  3. Optionally attach the .sha256 checksum and .sig signature files.

The download URL follows the pattern:

https://github.com/{vendor}/{name}/releases/download/v{version}/{vendor}-{name}-{version}.notur

Step 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.js

Associated Files

FilePurpose
{name}-{version}.noturThe archive itself (tar.gz)
{name}-{version}.notur.sha256SHA-256 checksum of the archive
{name}-{version}.notur.sigEd25519 signature (optional)

Creating Archives Manually

bash
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.sha256

Or 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

  1. The publisher signs the archive with their Ed25519 secret key.
  2. The signature is stored in a .sig file alongside the archive.
  3. When notur.require_signatures is true, the InstallCommand verifies the signature using the configured public key before installation.
  4. If verification fails, installation is aborted.

Configuration

In config/notur.php:

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):

bash
php artisan notur:keygen

Store the public key in your panel's .env:

NOTUR_PUBLIC_KEY=hex-encoded-public-key-here

Signing Archives

Use the --sign flag when exporting:

bash
php artisan notur:export --sign

This 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

bash
php registry/tools/build-index.php /path/to/extensions --output registry.json

Expected directory layout:

extensions/
  vendor1/
    extension-a/
      extension.yaml
    extension-b/
      extension.yaml
  vendor2/
    ...

From GitHub Repositories

Create a config file listing repositories:

json
{
    "repositories": [
        "acme/server-analytics",
        "acme/backup-manager",
        "notur/hello-world"
    ]
}

Then run:

bash
php registry/tools/build-index.php --config repos.json --output registry.json

The 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.json
  • registry/schema/extension-manifest.schema.json

Self-Hosted Registries

To host your own private registry:

  1. Generate a registry.json using the build tool.
  2. Host it on a web server, GitHub Pages, or any static file host.
  3. Configure panels to point to your registry:
php
// config/notur.php
'registry_url' => 'https://extensions.mycompany.com',
  1. Ensure your extensions' .notur archives 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.

Released under the MIT License.