Marketplace
The FreeSDN Marketplace is a curated directory of installable plugins served from a remote registry. It provides browsing, version history, ratings, reviews, and one-click install - all gated behind Ed25519 catalog signing and SHA-256 download verification.
This page covers how browsing and installing works, the security model behind catalog signing, how to sign your own catalog for a private or third-party registry, and what the review system does.
How the Marketplace fits into the plugin system
Section titled “How the Marketplace fits into the plugin system”The Marketplace is one path to getting a plugin onto your instance. The other path is
direct ZIP upload (POST /api/v1/plugins/install). Both end up calling the same
PluginLoader.install_plugin pipeline - the Marketplace simply adds a catalog layer
on top: it knows where to download the ZIP, what version history exists, and what
SHA-256 hash to expect.
The API lives at /api/v1/marketplace/plugins. The Swagger reference is at
/api/v1/docs (unavailable in production; accessible in non-production environments when ENABLE_DOCS=true).
Browsing the catalog
Section titled “Browsing the catalog”These endpoints are read-only and require no authentication - they are effectively public. No login or session is required to browse the catalog:
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/marketplace/plugins | Paginated browse - filter by category, sort by downloads / rating / newest / name |
GET | /api/v1/marketplace/plugins/featured | Up to six featured plugins |
GET | /api/v1/marketplace/plugins/categories | Category list with counts |
GET | /api/v1/marketplace/plugins/{slug} | Plugin detail by slug |
GET | /api/v1/marketplace/plugins/{slug}/versions | Full version history |
GET | /api/v1/marketplace/plugins/{slug}/reviews | Reviews, paginated (up to 50 per page) |
Browse parameters
Section titled “Browse parameters”GET /api/v1/marketplace/plugins accepts:
| Parameter | Values | Notes |
|---|---|---|
q | string (≤ 128 chars) | Substring search on name and description |
category | string (≤ 64 chars) | Filter by category slug |
sort | downloads, rating, newest, name | Default: downloads |
page | integer ≥ 1 | Pagination cursor |
per_page | 1-100 | Items per page |
Available categories: monitoring, security, automation, integration, analytics,
device, reporting.
What the catalog cannot tell you
Section titled “What the catalog cannot tell you”The remote registry feed can set: name, description, author, version, download URL,
checksum_sha256, tags, screenshots, icon, banner, and minimum version requirements.
The registry feed cannot set - and these values are ignored from the feed:
is_verified(whether FreeSDN has verified the publisher)is_featuredstatus(draft / published / suspended / deprecated)download_count- Database IDs or timestamps
This prevents a compromised or third-party registry from self-declaring “Verified by FreeSDN” status on a plugin. Those fields are controlled exclusively by your local database.
Syncing the catalog
Section titled “Syncing the catalog”Before you can browse, your instance needs catalog data. Sync pulls from the remote registry and upserts the catalog into your local database:
POST /api/v1/marketplace/plugins/syncRequires: super_admin.
Sync enforces the following:
- HTTPS only; no redirects; DNS-resolved to a public IP (DNS-rebind-safe); streamed response capped at 5 MB.
- JSON parsed - 502 on malformed JSON.
- Catalog signature verified (see the section below).
- Only the allowlisted fields above are written to the database.
First-party seed data
Section titled “First-party seed data”If the remote registry is unreachable, FreeSDN falls back to a first-party seed file at
backend/app/data/marketplace_seed.json. This file is trusted by provenance (it ships with
the application source), not by signature check.
Catalog signature verification
Section titled “Catalog signature verification”This is the most security-critical part of the Marketplace.
How signing works
Section titled “How signing works”The catalog is signed with an Ed25519 key pair. The publisher signs a
canonicalized JSON representation of the catalog (keys sorted, compact, signature
field removed). The signature is a detached hex string delivered alongside the catalog
JSON.
During sync, FreeSDN recanonicalizes the fetched catalog and verifies the signature against the pinned public key before touching the database.
The three signing postures
Section titled “The three signing postures”MARKETPLACE_PUBLISHER_PUBLIC_KEY set? | MARKETPLACE_ALLOW_UNSIGNED set? | Behavior |
|---|---|---|
| Yes | - | Signature required and verified against pinned key. Mismatch → 403. |
| No | No (default) | Sync refused - 403. This is the default. |
| No | Yes (1, true, or yes) | Unsigned catalog accepted with a loud warning logged. |
Setting up key pinning
Section titled “Setting up key pinning”Set the MARKETPLACE_PUBLISHER_PUBLIC_KEY environment variable to the Ed25519 public
key in hex. For the official FreeSDN registry, retrieve this key from the release notes
or the project’s public signing manifest, then pin it in your deployment’s environment:
# .env.pro / .env.maxMARKETPLACE_PUBLISHER_PUBLIC_KEY=<hex-encoded-ed25519-public-key>You cannot pin a key at runtime - it must be set before the process starts, because the variable is read at sync time.
Installing a plugin from the Marketplace
Section titled “Installing a plugin from the Marketplace”POST /api/v1/marketplace/plugins/{slug}/installRequires: super_admin.
What happens when you trigger a marketplace install:
- The catalog entry for
{slug}is looked up in your local database (sync first if it is missing). - The
download_urlfrom the catalog is fetched over HTTPS with DNS pinning, no redirects, and a 50 MB streaming cap. - The downloaded bytes are SHA-256 hashed and compared against
checksum_sha256from the catalog entry. Mismatch → install aborted. - The verified archive is passed to
PluginLoader.install_plugin- the same ZIP pipeline used for direct uploads: zip-bomb protection (200 MB uncompressed cap), zip-slip rejection, manifest parse, optional hash-pinned dependency install, class load,on_installhook. - The plugin record is persisted and the
on_installhook is called. Note: unlike direct ZIP upload, marketplace install does not automatically start the plugin for active organizations - operators must enable or restart it after install. - An audit row is written (tag:
["plugin","supply-chain","marketplace"]). - The catalog’s
download_countis incremented. - HTTP 201 on success.
Signing your own catalog
Section titled “Signing your own catalog”Use this when you operate a private registry or want to publish plugins outside the official FreeSDN registry. The signing tool ships with the backend:
backend/scripts/sign_marketplace_catalog.pyGenerate a key pair
Section titled “Generate a key pair”# Requires: Python ≥ 3.11 with the cryptography packagepython scripts/sign_marketplace_catalog.py keygen# Outputs PRIVATE_KEY_HEX and PUBLIC_KEY_HEX to stdout.# Copy PUBLIC_KEY_HEX into MARKETPLACE_PUBLISHER_PUBLIC_KEY.# Store PRIVATE_KEY_HEX offline - never commit it.Store the private key outside your repository and outside your deployment containers.
The hex public key goes into MARKETPLACE_PUBLISHER_PUBLIC_KEY on every FreeSDN
instance that should trust your catalog.
Sign a catalog file
Section titled “Sign a catalog file”Your catalog file must be valid JSON matching the expected schema (an array of plugin objects). To sign:
python scripts/sign_marketplace_catalog.py sign \ --key <PRIVATE_KEY_HEX> \ --in plugins.json \ --out plugins-signed.jsonThe tool:
- Reads your catalog JSON.
- Removes any existing
signaturefield. - Produces a canonicalized representation (keys sorted, compact, no whitespace).
- Signs that canonical bytes with the Ed25519 private key.
- Writes a new JSON file identical to the input but with a
signaturefield added at the top level (hex-encoded signature).
Point instances at your registry
Section titled “Point instances at your registry”MARKETPLACE_REGISTRY_URL=https://your-registry.example.com/plugins.jsonMARKETPLACE_PUBLISHER_PUBLIC_KEY=<your-public-key-hex>Both variables are read at sync time.
Re-signing after catalog updates
Section titled “Re-signing after catalog updates”Every time you add, remove, or update a plugin entry in your catalog file, you must re-sign it. Serving a stale or unsigned catalog causes sync to fail with 403 on any instance with key pinning enabled.
Reviews and ratings
Section titled “Reviews and ratings”Any active user can submit one review per plugin. Super_admin authority is not required.
POST /api/v1/marketplace/plugins/{slug}/reviewsRequest body:
| Field | Type | Constraints |
|---|---|---|
rating | integer | 1-5, required |
title | string | ≤ 200 characters, optional |
body | string | ≤ 4000 characters, optional |
Constraints:
- One review per user per plugin. A second submission returns 409.
- After each new review the plugin’s aggregate
ratingandrating_countare recomputed. - Reviews are public reads - no authentication is required to list them via
GET /api/v1/marketplace/plugins/{slug}/reviews.
Environment variable reference
Section titled “Environment variable reference”| Variable | Default | Purpose |
|---|---|---|
MARKETPLACE_REGISTRY_URL | https://registry.freesdn.org/plugins.json | Remote catalog URL for sync |
MARKETPLACE_PUBLISHER_PUBLIC_KEY | (empty) | Hex Ed25519 public key to require for signature verification |
MARKETPLACE_ALLOW_UNSIGNED | (unset / false) | Set 1, true, or yes to accept unsigned catalogs - logs a loud warning |
Troubleshooting sync
Section titled “Troubleshooting sync”Sync returns 403 - “Catalog signature verification failed”
You have MARKETPLACE_PUBLISHER_PUBLIC_KEY set and the fetched catalog’s signature does
not match. Causes:
- The catalog file was updated but not re-signed.
- You are pointing at the wrong registry URL.
- Your key was rotated but the instance env was not updated.
Check that MARKETPLACE_REGISTRY_URL resolves to the intended host, that your public
key hex matches the signing private key, and that the catalog was signed after its last
edit.
Sync returns 403 - “Unsigned catalogs not permitted”
You have no key pinned and MARKETPLACE_ALLOW_UNSIGNED is not set. Either pin a key
or explicitly opt in to unsigned operation.
Sync returns 502
The remote URL returned malformed JSON (invalid UTF-8 or a JSON parse error). Check
that MARKETPLACE_REGISTRY_URL points to a valid JSON catalog. The seed fallback does
not apply in this case - the 502 is returned immediately.
Sync succeeds but shows only first-party plugins (seed data)
The remote registry was unreachable (network error, DNS failure, or timeout). FreeSDN
fell back to the bundled first-party seed file at backend/app/data/marketplace_seed.json.
Only first-party plugins are available until the registry is reachable again. If no
seed file is present, the response is 503 instead.
Install returns a checksum mismatch error
The downloaded archive’s SHA-256 does not match the catalog entry. This is either a corrupted download or a catalog / archive out of sync. Do not retry without investigating - a mismatch is a supply-chain signal.
Next steps
Section titled “Next steps”- Plugins Overview - the two-tier trust model and what SDK plugins can and cannot do
- Getting Started - scaffold and locally install your first plugin
- Packaging and Publishing - build a distributable ZIP and prepare a catalog entry
- Manifest Reference - every
plugin.yamlfield includingpython_dependenciespinning rules