Skip to content

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:

Terminal window
bun add @jrtilak-recall/marketplace-interface

Import 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";

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, use the contract schema source as the source of truth.

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));
FieldRequiredPurpose
nameYesHuman-readable marketplace name.
descriptionNoShort explanation of the marketplace. May be null.
iconUrlNoMarketplace icon URL. May be null.
homepageUrlNoPublic marketplace homepage. May be null.
namespaceYesUnique identifier used to prevent conflicts between marketplaces.
baseUrlYesBase URL used to resolve relative route templates.
urlsYesObject containing all required marketplace route templates.
urls.listPluginsYesRoute for listing and searching plugins.
urls.getPluginByNameYesRoute for reading one plugin.
urls.getPluginVersionYesRoute 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.

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.0.1",
"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
}
}
]

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.

FieldRequiredPurpose
nameYesUnique plugin package name.
displayNameYesHuman-readable plugin name.
descriptionNoShort plugin description. May be null.
authorYesPlugin author information.
homepageUrlNoPlugin homepage or documentation URL. May be null.
latestVersionYesLatest version available from this marketplace.
totalDownloadsNoMarketplace download count. Defaults to 0 when omitted during validation.
createdAtNoCreation timestamp. May be null.
updatedAtNoLast update timestamp. May be null.
categoryNoMarketplace-defined plugin category. May be null.
iconUrlNoPlugin icon URL. May be null.
publisherYesInformation about the account that published the plugin.
publisher.usernameYesPublisher account name.
publisher.isVerifiedYesWhether the marketplace has verified the publisher.

Recall derives the client-side plugin ID as <marketplace-namespace>:<plugin-name>. The server does not include this id field in its response.

The getPluginByName route returns one plugin using the same response shape described above.

GET https://market.recall.jrtilak.dev/api/plugins/%40recall%2Fdefault-theme

The <plugin-name> value must be URL encoded. For example, @recall/default-theme becomes %40recall%2Fdefault-theme.

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.0.1

Example response:

{
"version": "0.0.1",
"size": 923,
"downloadUrl": "https://market.recall.jrtilak.dev/api/plugins/%40recall%2Fdefault-theme/0.0.1/plugin.zip",
"manifestVersion": "0.0.1",
"permissions": ["ui.theme.static.write"],
"main": null,
"theme": "theme.json",
"createdAt": "2026-06-15T05:42:36.877Z"
}
FieldRequiredPurpose
versionYesPlugin version represented by this response.
sizeYesDownload archive size in bytes.
downloadUrlYesURL of the plugin zip archive.
manifestVersionYesRecall plugin manifest version used by the plugin.
permissionsYesPermissions requested by the plugin. Use an empty array when none are required.
mainNoPath to the compiled plugin entry point. May be null.
themeNoPath to the plugin theme file. May be null.
createdAtYesTimestamp for when this version was published.

downloadUrl may be a marketplace route, external object-storage URL, or a short-lived signed URL.

The downloaded zip must contain the plugin files at its root:

plugin.zip
manifest.json
index.js
theme.json

Do not wrap the files in an additional top-level directory:

plugin.zip
plugin-folder/
manifest.json
index.js

Only include files required by the plugin. A theme-only plugin may omit index.js, while a code plugin may omit theme.json.

See the contract schema source for exact validation constraints and the latest contract.