Build a Marketplace Server
A Recall marketplace can use any framework, database, storage provider, authentication system, or deployment platform. Recall only requires the server to expose a compatible API.
For TypeScript projects, install the official package to use its types and Zod schemas:
bun add @jrtilak-recall/marketplace-interfaceImport marketplace contracts from the server-specific entry point:
import { MarketplaceInfoSchema, PluginListResponseSchema, PluginResponseSchema, PluginVersionResponseSchema, type MarketplaceInfoInput, type PluginListResponseInput, type PluginResponseInput, type PluginVersionResponseInput,} from "@jrtilak-recall/marketplace-interface/server";Implementation checklist
Section titled “Implementation checklist”A production marketplace should keep its submission and response boundaries small and explicit:
- Validate discovery, list, detail, and version responses with the shared schemas before returning them.
- Authenticate publish and delete operations, enforce publisher ownership, and make published package versions immutable.
- Bound archive size, expanded size, file count, manifest size, and entry size.
Reject unsafe paths and require
manifest.jsonat the archive root. - Validate the declared entry. Parse theme entries with
ThemeSchema; store JavaScript entries without executing them on the marketplace server. - Save the exact validated manifest with each version and require its name and version to match the requested plugin and version.
- Keep private credentials in secrets, return generic internal errors, and apply rate limits to authentication and upload routes.
- Keep metadata and archive writes consistent. If one write fails, clean up only the resources created by that submission.
For Cloudflare Workers, D1 can hold accounts and plugin metadata while R2 holds the archives. The binding names are implementation-defined, but must match the names used by the Worker:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-recall-marketplace", "main": "src/index.ts", "compatibility_date": "2026-07-12", "compatibility_flags": ["nodejs_compat"], "d1_databases": [ { "binding": "DB", "database_name": "recall-marketplace", "database_id": "<D1_DATABASE_ID>", "migrations_dir": "drizzle" } ], "r2_buckets": [ { "binding": "PLUGIN_BUCKET", "bucket_name": "recall-marketplace-storage" } ]}Keep the R2 bucket private and serve archives through the marketplace route.
Use encrypted Worker secrets for authentication keys instead of placing them
in vars or source control. D1 migrations must be applied separately from a
Worker deployment.
The Input types represent values your server can return before schema defaults
are applied. For example, PluginResponseInput allows totalDownloads to be
omitted because PluginResponseSchema defaults it to 0.
The examples below come from the official Recall Marketplace. For detailed field validation, see Marketplace Interface and Plugin Schema.
Marketplace entry point
Section titled “Marketplace entry point”Recall starts with one marketplace base URL:
GET https://market.recall.jrtilak.dev/api/The response identifies the marketplace and tells Recall which route templates to use:
{ "name": "Default Marketplace", "description": "Official marketplace for Recall plugins.", "baseUrl": "https://market.recall.jrtilak.dev/api/", "namespace": "default", "urls": { "listPlugins": "plugins?q=<query>", "getPluginByName": "plugins/<plugin-name>", "getPluginVersion": "plugins/<plugin-name>/<plugin-version>" }}In a TypeScript server, define the response with MarketplaceInfoInput and
optionally validate it before returning it:
const marketplace: MarketplaceInfoInput = { name: "Default Marketplace", description: "Official marketplace for Recall plugins.", namespace: "default", baseUrl: "https://market.recall.jrtilak.dev/api/", urls: { listPlugins: "plugins?q=<query>", getPluginByName: "plugins/<plugin-name>", getPluginVersion: "plugins/<plugin-name>/<plugin-version>", },};
return Response.json(MarketplaceInfoSchema.parse(marketplace));| Field | Required | Purpose |
|---|---|---|
name | Yes | Human-readable marketplace name. |
description | No | Short explanation of the marketplace. May be null. |
iconUrl | No | Marketplace icon URL. May be null. |
homepageUrl | No | Public marketplace homepage. May be null. |
namespace | Yes | Unique identifier used to prevent conflicts between marketplaces. |
baseUrl | Yes | Base URL used to resolve relative route templates. |
urls | Yes | Object containing all required marketplace route templates. |
urls.listPlugins | Yes | Route for listing and searching plugins. |
urls.getPluginByName | Yes | Route for reading one plugin. |
urls.getPluginVersion | Yes | Route for reading install metadata for a plugin version. |
Route templates may be absolute URLs or relative to baseUrl. Recall replaces
these supported placeholders:
<query>with an optional search query.<plugin-name>with the URL-encoded plugin package name.<plugin-version>with the requested version.
List plugins
Section titled “List plugins”The listPlugins route returns an array of available plugins. The <query>
placeholder is optional and can be used to filter the list.
GET https://market.recall.jrtilak.dev/api/plugins?q=Example response:
[ { "name": "@recall/default-theme", "displayName": "Default Theme", "description": "The default theme for Recall.", "author": "jrtilak <https://jrtilak.dev>", "homepageUrl": null, "latestVersion": "0.1.0", "totalDownloads": 8, "createdAt": "2026-06-15T05:42:36.877Z", "updatedAt": "2026-06-15T05:42:36.877Z", "category": "theme", "iconUrl": null, "publisher": { "username": "jrtilak", "isVerified": true } }]Plugin response fields
Section titled “Plugin response fields”The list route returns an array of these objects. The plugin detail route returns one object with the same fields.
Use PluginListResponseInput for the list response and PluginResponseInput
for a single plugin response. Validate them with PluginListResponseSchema and
PluginResponseSchema when runtime checking is required.
| Field | Required | Purpose |
|---|---|---|
name | Yes | Unique plugin package name. |
displayName | Yes | Human-readable plugin name. |
description | No | Short plugin description. May be null. |
author | Yes | Plugin author information. |
homepageUrl | No | Plugin homepage or documentation URL. May be null. |
latestVersion | Yes | Latest version available from this marketplace. |
totalDownloads | No | Marketplace download count. Defaults to 0 when omitted during validation. |
createdAt | No | Creation timestamp. May be null. |
updatedAt | No | Last update timestamp. May be null. |
category | No | Marketplace-defined plugin category. May be null. |
iconUrl | No | Plugin icon URL. May be null. |
publisher | Yes | Information about the account that published the plugin. |
publisher.username | Yes | Publisher account name. |
publisher.isVerified | Yes | Whether the marketplace has verified the publisher. |
Recall derives the client-side plugin ID as
<publisher>:<marketplace-namespace>:<plugin-name>. The server does not include
this id field in its response.
Get a plugin
Section titled “Get a plugin”The getPluginByName route returns one plugin using the same response shape
described above.
GET https://market.recall.jrtilak.dev/api/plugins/%40recall%2Fdefault-themeThe <plugin-name> value must be URL encoded. For example,
@recall/default-theme becomes %40recall%2Fdefault-theme.
Get a plugin version
Section titled “Get a plugin version”The getPluginVersion route returns the information Recall needs to install a
specific version.
Use PluginVersionResponseInput for this response and
PluginVersionResponseSchema for runtime validation.
GET https://market.recall.jrtilak.dev/api/plugins/%40recall%2Fdefault-theme/0.1.0Example response:
{ "version": "0.1.0", "size": 923, "downloadUrl": "https://market.recall.jrtilak.dev/api/plugins/%40recall%2Fdefault-theme/0.1.0/plugin.zip", "manifest": { "name": "@recall/default-theme", "displayName": "Default Theme", "version": "0.1.0", "author": "jrtilak <https://jrtilak.dev>", "recall": { "manifestVersion": "0.1", "permissions": ["registry.theme"], "entry": { "runtime": "theme", "file": "theme.json" } } }, "createdAt": "2026-06-15T05:42:36.877Z"}| Field | Required | Purpose |
|---|---|---|
version | Yes | Plugin version represented by this response. |
size | Yes | Download archive size in bytes. |
downloadUrl | Yes | Relative or HTTP(S) URL of the plugin zip archive. |
manifest | Yes | Validated manifest snapshot for this exact plugin version. |
createdAt | Yes | Timestamp for when this version was published. |
downloadUrl may be a marketplace route, external object-storage URL, or a
short-lived signed URL. version must match manifest.version; manifest fields
follow the Plugin Schema.
Plugin archive
Section titled “Plugin archive”The downloaded zip must contain the plugin files at its root:
plugin.zip manifest.json index.js theme.jsonDo not wrap the files in an additional top-level directory:
plugin.zip plugin-folder/ manifest.json index.jsOnly include files required by the plugin. A theme-only plugin may omit
index.js, while a code plugin may omit theme.json.
See Marketplace Interface for exact response validation and client behavior. Changes to the official Cloudflare implementation are listed in the Marketplace Server changelog.