Deadlock Mod Manager
Developer Documentation

Build a mod registry

Publish a static catalog or JSON API with taxonomy, variants, website install buttons, release history and updates

A mod registry is a catalog of mods, their descriptions and the files players can download. If you run a mod website or maintain a community collection, you can connect your catalog to Deadlock Mod Manager (DMM) so players can browse and install your mods from its Mods Store.

DMM was built from the start to support sources beyond GameBanana, but websites had no clear protocol to follow when integrating. The native source protocol defines how a site shares its catalog and downloads with the manager.

In DMM, your catalog appears as a mod source. Players add your source once, then use the manager to choose files or variants and download updates. You keep hosting your catalog and mod files; DMM reads them directly from your site.

You can publish a source with JSON files on public HTTPS hosting or provide the same information through an API. You don't need a DMM account, API key, plugin or registration with DMM infrastructure.

Build with an agent

Copy a prompt with the protocol requirements and verification steps.

Read the prompt
Build a mod registry that integrates with Deadlock Mod Manager (DMM).

Work in the current repository and follow its conventions. If a registry already exists, add DMM support to its catalog and publishing workflow. For a new registry, build a compatible mod catalog with a documented way to publish mods. Use the existing stack where possible and choose static hosting or a public JSON API to suit the project.

Require only the source contract DMM needs to browse and install mods. Static JSON files and downloadable artifacts are enough. Website pages, authentication, upload forms, moderation, object storage and social features are optional. Add them only when requested or needed to fit the existing project; a new registry does not need them to integrate with DMM.

Read the current author guide before implementing the protocol:
https://docs.deadlockmods.app/llms.mdx/developer-docs/build-a-mod-registry/content.md

Working examples:
- Static source: https://github.com/deadlock-mod-manager/deadlock-mod-manager/tree/main/examples/mod-source
- API source and taxonomy sync: https://github.com/deadlock-mod-manager/deadlock-mod-manager/tree/main/examples/mod-source-api
- Current taxonomy: https://deadlockmods.app/v1/taxonomy.json

Use the guide and current native-source implementation as the compatibility references. RFC 002 includes unimplemented proposals. There is no published canonical schema package or full conformance validator to depend on.

1. Establish the registry's name, permanent sourceId, public HTTPS origin and hosting approach. Infer these from project configuration; ask for any essential missing choices. Use fictional sample content until real publishing details and mod files are available.

2. Model listings, their current releases and downloadable artifacts with stable, case-sensitive IDs. Include optional descriptions, author names, tags, homepage links and accurate adult-content classifications where available. When assigning heroes or content types, fetch and validate DMM's taxonomy, store its stable IDs separately from display names, and preserve the last good local copy if synchronization fails. A listing accepts a single optional hero and an optional contentType; use the published IDs.

3. Publish a dmm-source descriptor with protocolVersion "1.0", gameId "deadlock", sourceId, name, catalog and networkOrigins. Declare the exact API, download and media origins, including CDN and artifact redirect destinations. The descriptor can live at /.well-known/dmm-source.json or another documented URL. Public source reads must work without login, cookies or tokens; DMM infrastructure registration is unnecessary. Changing any declared origin set requires users to remove, review and add the source again.

4. Implement the complete catalog contract for the chosen approach. Static pages share a snapshotId and paginate with nextUrl. API pages return items and nextCursor, explicitly ending with nextCursor: null; continuations use cursor and limit=100. Keep pagination consistent and listing IDs unique. Linked details must have the same ID and include all metadata because they replace the index record. Available listings need an embedded current release or an explicit releaseUrl, never both. Keep publishing within the guide's request, size and refresh limits. DMM currently searches the downloaded catalog locally.

5. Publish actual installable .vpk, .zip, .rar or .7z artifacts with safe filenames and matching lowercase extensions, within the guide's download and extraction limits. Size and hashes are optional; recommend publishing the byte size and SHA-256 of each exact downloadable file. MD5 is also supported. Each artifact needs exactly one download.url or download.resolverUrl; a resolver returns { "url": "..." } on a declared download origin. Preserve artifact identity when URLs rotate. Change contentRevision when installable bytes or selection meaning changes, and keep it stable for metadata-only edits.

6. Start with installation.mode "manual". Optionally add component plans for mods with known required files or variants, using exact artifact IDs and archive-relative VPK paths. Every component must be required or belong to exactly one bounded choice group, with valid defaults. Preserve saved choices across updates and require a new decision when they no longer fit. Publish explicit unavailable/removed states; a network failure is not a removal signal. Publish complete catalogs so a failed refresh can retain the previous one.

7. Generate Add source to DMM and Install with DMM links. If the registry has a website, offer them as buttons; otherwise include them in the publishing instructions. Use deadlock-mod-manager://sources/add with url, and deadlock-mod-manager://sources/install with url, source and listing. Encode every parameter value with encodeURIComponent; optional release and artifact values target the current release and a specific artifact. Include a visible descriptor URL and a Download DMM link as fallbacks. Users must still review a new source in DMM.

8. Add screenshots and Markdown descriptions if requested, following the guide's media limits. If release-history browsing is wanted, enable capabilities.releaseHistory and provide releaseHistoryUrl or releaseHistoryUrlTemplate with complete release documents in explicitly terminated cursor pages. History is read-only. Advertise capabilities only where supported; DMM cannot consume authenticated sources or comments, install dependencies or older releases, migrate origins automatically, or download native .dmodpkg artifacts. A website may independently support author sign-in or comments; neither is required for DMM integration.

9. Verify the result with real HTTP requests through every catalog page and artifact download, including linked details, releases, history and resolvers where implemented. Check the downloaded bytes against any supplied size and every supplied supported digest. Test publication of an update and tombstones. Where implemented, test invalid API cursors, taxonomy-fetch failure and component choices. Run the project's relevant tests, lint, formatting, type checks and build. If a DMM build is available, also exercise enrollment, browsing and installation with harmless fixtures; local HTTP enrollment requires a DMM development build. Report any desktop behavior you could not verify.

Deliver the implementation with local run/publish instructions, the descriptor URL, generated DMM links, verification results and remaining limitations. Prepare deployment instructions; deploy only if the user has authorized it.

Choose your integration

ApproachWhat you publishUse it when
Static sourceJSON documents and mod files on HTTPS file hostingYou can generate a complete catalog when publishing mods.
API sourcePublic JSON endpoints implementing the same listing, release and artifact contractYou already have a database-backed site or need fresh download URLs.

Both approaches support the same metadata, installation choices, website buttons and current-release updates. DMM downloads API catalogs and searches their titles and tags locally. It does not call your server's search endpoint.

Which install link should a new source use?

Native sources use deadlock-mod-manager://sources/add and deadlock-mod-manager://sources/install. The older deadlock-mod-manager://install?source=... integration uses a built-in source allowlist and a different manifest. Publishing that manifest alone does not add an independent registry to the Mods Store.

Publish your first mod

The examples use a fictional registry called Example Community.

The static example contains the JSON documents, a harmless VPK, an archive with component choices and an offline checker that needs no packages. Copy it to start with static hosting, or use the runnable API example.

Create this directory on your HTTPS host:

https://mods.example.org/
  dmm-source.json
  catalog.json
  mods/example.json
  files/example.vpk

Publish dmm-source.json:

{
  "format": "dmm-source",
  "protocolVersion": "1.0",
  "sourceId": "my-community",
  "name": "Example Community",
  "gameId": "deadlock",
  "catalog": {
    "kind": "static",
    "indexUrl": "./catalog.json"
  },
  "networkOrigins": {
    "api": ["https://mods.example.org"],
    "downloads": ["https://mods.example.org"],
    "media": []
  }
}

Publish catalog.json. Each static page needs the same nonempty snapshotId:

{
  "snapshotId": "example-1",
  "items": [
    {
      "id": "example",
      "title": "Harmless source example",
      "availability": "available",
      "isNsfw": false,
      "detailUrl": "./mods/example.json"
    }
  ]
}

Publish mods/example.json:

{
  "id": "example",
  "title": "Harmless source example",
  "availability": "available",
  "isNsfw": false,
  "description": "A VPK containing a harmless text notice.",
  "author": "Example Community",
  "hero": "abrams",
  "contentType": "skins",
  "tags": ["example"],
  "release": {
    "id": "release-1",
    "contentRevision": "revision-1",
    "version": "1.0.0",
    "artifacts": [
      {
        "id": "example-vpk",
        "filename": "example.vpk",
        "format": "vpk",
        "download": { "url": "../files/example.vpk" },
        "size": 141,
        "hashes": {
          "sha256": "0ef66746810e2611130e9407a078daeb9657f3716fe5f952bc555276b2af7c5a"
        }
      }
    ],
    "installation": { "mode": "manual" }
  }
}

The size and hash match the repository's fixture VPK. Replace the VPK and update both values before publishing your own mod. Relative URLs resolve against the document containing them: ../files/example.vpk resolves from /mods/example.json to /files/example.vpk.

From a checkout of the repository, validate the static example:

node examples/mod-source/check.mjs examples/mod-source
node --test examples/mod-source/check.test.mjs

The checker supports relative files in one directory tree and validates the example's static subset. It cannot validate the full protocol or parse VPKs. DMM validates your hosted documents again.

Serve JSON as application/json and artifacts as downloadable bytes. In DMM, open Settings > Mods > Mod sources > Add source, paste the full descriptor URL, review it and choose Add. Select your source in the Mods Store and install the example through the normal file-selection flow.

Source descriptor reference

Users supply the descriptor URL themselves or through your website button. You can host the descriptor at /.well-known/dmm-source.json or another path; DMM does not require a fixed discovery path.

FieldTypeRequirement and behavior
formatstringRequired; exactly "dmm-source".
protocolVersionstringRequired; exactly "1.0".
sourceIdstringRequired, permanent ID. Together with the accepted origin, identifies this source.
namestringRequired display name, 1 to 256 UTF-8 bytes.
gameIdstringRequired; exactly "deadlock".
catalogobjectRequired; one of the catalog configurations below.
networkOriginsobjectRequired; accepted API, download and media origins.
iconUrlstring or nullOptional raster icon URL on a declared media origin.
homepageUrlstring or nullOptional website URL.
capabilitiesobject or nullOptional declarations described under Capabilities and limits.

IDs are opaque, case-sensitive strings of 1 to 256 UTF-8 bytes without control characters. Choose permanent IDs; never reuse one for unrelated content. Changing the source's display name does not change its identity. Moving to a new origin does: an identical sourceId elsewhere does not transfer user trust or reconnect installed mods automatically.

Catalog configuration

FieldTypeRequirement and behavior
kindstringRequired; "static" or "api".
indexUrlstringRequired for static; first catalog page URL.
listUrlstringRequired for api; first catalog page URL.
detailUrlTemplatestring or nullOptional for api; fills {listingId} when a listing has no detailUrl.
releaseHistoryUrlTemplatestring or nullOptional for api; fills {listingId} when a listing has no releaseHistoryUrl.
releaseUrlTemplatestring or nullAccepted for api, but current-release ingestion uses the listing's embedded release or explicit releaseUrl. Do not rely on this template to select a release.

A detail document replaces the index listing, so include all metadata you want displayed and use the same id. DMM fetches linked details and current releases during catalog refresh.

Network origins

FieldTypeRequirement and behavior
apistring arrayRequired and nonempty. Allows catalog, detail, release, history and resolver requests.
downloadsstring arrayRequired and nonempty. Allows artifact requests, resolver results and artifact redirect destinations.
mediastring arrayOptional, defaults to []. Allows icons and listing images.

Each list accepts at most 16 origins. An origin is a scheme, host and optional port, for example https://files.example.org; it has no path or query. Lists do not accept wildcard subdomains. Declare every origin you use, including your own host if it serves several roles. Optional homepage links do not authorize file, API or media requests.

Build an API-backed source

You choose the paths on your server. DMM follows the URLs in your descriptor.

Use an API catalog in your descriptor:

{
  "format": "dmm-source",
  "protocolVersion": "1.0",
  "sourceId": "my-community",
  "name": "Example Community",
  "gameId": "deadlock",
  "catalog": {
    "kind": "api",
    "listUrl": "https://mods.example.org/dmm/v1/mods",
    "releaseHistoryUrlTemplate": "https://mods.example.org/dmm/v1/mods/{listingId}/releases"
  },
  "networkOrigins": {
    "api": ["https://mods.example.org"],
    "downloads": ["https://mods.example.org"],
    "media": []
  },
  "capabilities": {
    "search": "local",
    "releaseHistory": true,
    "installationChoices": true
  }
}
RequestResponse
GET /.well-known/dmm-source.jsonDescriptor above.
GET /dmm/v1/modsFirst catalog page.
GET /dmm/v1/mods?cursor=second-page&limit=100Final catalog page.
GET /dmm/v1/mods/exampleListing detail with an embedded release or releaseUrl.
GET /dmm/v1/mods/example/current-releaseOptional release document referenced by releaseUrl.
GET /dmm/v1/mods/example/releasesOptional release-history page.
GET /dmm/v1/files/example-vpk/resolveOptional fresh download URL.

Catalog pagination

For GET /dmm/v1/mods, return:

{
  "items": [
    {
      "id": "example",
      "title": "Harmless source example",
      "availability": "available",
      "isNsfw": false,
      "detailUrl": "https://mods.example.org/dmm/v1/mods/example"
    }
  ],
  "nextCursor": "second-page"
}

For GET /dmm/v1/mods?cursor=second-page&limit=100, return:

{
  "items": [
    {
      "id": "retired-example",
      "availability": "removed"
    }
  ],
  "nextCursor": null
}

An API page must include nextCursor; null explicitly ends the complete catalog. An empty catalog is { "items": [], "nextCursor": null }. Missing completion, repeated cursors/URLs, duplicate listing IDs, or a failure on a later page rejects the refresh and retains the previous cached catalog. API pages do not require snapshotId.

DMM requests the exact listUrl first. For each continuation, it appends cursor and limit=100 to that URL. Avoid fixing a cursor or limit in listUrl. Cursors must be nonempty strings, at most 8,192 UTF-8 bytes. Use opaque cursors that keep the catalog view consistent. Publishing during pagination must not cause listings to be skipped or repeated.

Serve the complete catalog without requiring a search term, hero filter, browser cookie, or login. Optional search/filter APIs on your site are separate from DMM's local catalog browsing.

Static catalogs paginate differently: use nextUrl on each non-final page and the same nonempty snapshotId throughout. Omit nextUrl or set it to null to end the static catalog. Do not substitute API cursors for static page links.

Run the API example

The API example uses Node.js 24 or newer and needs no packages:

node examples/mod-source-api/server.mjs examples/mod-source
node --test examples/mod-source-api/server.test.mjs

The server prints http://127.0.0.1:8787/.well-known/dmm-source.json. DMM development builds can add this local HTTP source in Settings; release builds require public HTTPS. The tests start their own short-lived servers, so they can run separately.

The server has both release styles, components, tombstones, history and download resolution. Its downloads use the fixture bytes. It reads the documents on startup, so restart it after editing them. For public hosting, place it behind an HTTPS reverse proxy and set PUBLIC_ORIGIN to your external origin. PORT changes the loopback listener port. Uploads, accounts, publishing and persistence are outside this example.

Listing metadata and taxonomy

FieldTypeRequirement and behavior
idstringRequired stable listing ID within this source.
titlestringRequired unless availability is "removed"; 1 to 1,024 UTF-8 bytes.
availabilitystringRequired; "available", "unavailable", or "removed". Available listings must resolve to an installable current release.
detailUrlstring or nullOptional full listing document, fetched during refresh.
descriptionstring or nullOptional, at most 512 KiB.
descriptionFormatstring or nullOptional "plain" or "markdown"; unknown formats fall back to plain text.
authorstring or nullOptional display name, at most 1,024 UTF-8 bytes.
herostring or nullOptional single stable hero ID from the taxonomy.
contentTypestring or nullOptional stable content-type ID; omitted/null uses the Mods tab.
isNsfwboolean or nullOptional content classification; see below.
tagsstring arrayOptional, defaults to []; at most 100 tags. Included in local search.
imagesstring arrayOptional, defaults to []; at most 32 URLs on declared media origins. Only the first four are fetched during refresh.
homepageUrlstring or nullOptional mod page to open on your website.
updatedAtstring or nullOptional RFC 3339 timestamp, such as "2026-11-01T12:00:00Z".
releaseobject or nullOptional embedded current release. Required for an available listing unless releaseUrl supplies it.
releaseUrlstring or nullOptional current-release document. Do not also embed release.
releaseHistoryUrlstring or nullOptional history endpoint, preferred over the catalog's history URL template.

Do not send DMM's generated remoteId or internal source keys. DMM creates those from the accepted source identity and your listing ID.

Fetch the shared taxonomy

Use the public endpoint:

curl https://deadlockmods.app/v1/taxonomy.json

The endpoint returns the full lists; this abbreviated example shows a few entries:

{
  "format": "dmm-taxonomy",
  "version": 1,
  "heroes": [
    { "id": "abrams", "name": "Abrams" },
    { "id": "lady-geist", "name": "Lady Geist" },
    { "id": "mo-and-krill", "name": "Mo & Krill" }
  ],
  "contentTypes": [
    { "id": "skins", "name": "Skins" },
    { "id": "voice-lines", "name": "Voice Lines" }
  ]
}

Store the id and display the name. IDs are stable; display names can change. Do not derive IDs by lowercasing a display name. Fetch the full taxonomy so a new hero does not require a hand-maintained list in your registry.

Fetch and validate a local copy with the example's sync script:

node examples/mod-source-api/sync-taxonomy.mjs taxonomy.json

It checks format, supported version, nonempty lists, unique kebab-case IDs and nonempty display names before atomically replacing the saved file. A failed HTTP request, invalid document or unsupported version leaves the last good copy intact. Load that copy in your site. Run the script when publishing or on a schedule. The destination's parent directory must exist.

A listing accepts one hero; it has no heroes array. If your mod supports several heroes, choose a representative hero or omit the field and explain the others in the description. An omitted hero is appropriate for a global HUD or map. The taxonomy does not add a multi-hero field or new browsing filters to DMM.

Map your categories

Map your site's category names explicitly to these content types. Tags are free-form labels and do not replace taxonomy. Unknown custom content types do not create new desktop tabs and may leave listings out of the expected tab.

Content-type IDDisplay nameDMM tab
skinsSkinsMods
gameplayGameplayMods
hudHUDMods
modelsModelsMods
musicMusicSounds
mapsMapsMaps
ability-soundsAbility SoundsSounds
weapon-soundsWeapon SoundsSounds
voice-linesVoice LinesSounds
kill-soundsKill SoundsSounds
killstreak-musicKillstreak MusicSounds
otherOtherMods

For example, map your site's "Character outfit" category to skins and "Announcer replacements" to voice-lines; keep more specific labels in tags.

Classify adult content

isNsfw: true means adult content. false means you have explicitly classified the listing as non-adult. Missing or null means unknown. When the user hides adult content, DMM also hides unknown classifications. Do not set false automatically on content you have not classified.

Releases and downloadable files

A listing supplies one current release, either embedded as release or fetched from releaseUrl. The release JSON shape is identical in either case.

Release fieldTypeRequirement and behavior
idstringRequired stable release ID within the listing.
contentRevisionstringRequired identity for the installable bytes and selection meaning. Change it when either changes.
versionstring or nullOptional human-readable label; DMM does not require semantic versioning.
publishedAtstring or nullOptional RFC 3339 timestamp.
changelogstring or nullOptional plain text, at most 64 KiB.
artifactsobject arrayRequired, 1 to 100 files with unique IDs and case-insensitively unique filenames.
installationobjectRequired manual or component installation plan.
dependenciesarrayOmit or leave empty. Nonempty dependency plans are rejected by the current client.
Artifact fieldTypeRequirement and behavior
idstringRequired stable artifact ID within the release.
filenamestringRequired safe basename, at most 255 UTF-8 bytes, with a matching lowercase extension.
formatstringRequired; "vpk", "zip", "rar", or "7z".
downloadobjectRequired; exactly one non-null url or resolverUrl.
sizenonnegative integer or nullOptional byte size. Publish the actual artifact size when available.
hashesobject or nullOptional sha256 and/or md5 hex digests. If an object is present, at least one supported digest must be supplied.

Filenames cannot contain paths, control characters, Windows-reserved names or characters, or trailing dots or spaces. Use example.vpk; files/example.vpk and example.VPK are invalid. Specify paths inside an archive in the component plan. Native source downloads do not currently accept .dmodpkg artifacts.

Size and hashes are optional. We recommend publishing the byte size and SHA-256 of the exact uploaded artifact, including the whole archive for ZIP/RAR/7z downloads. This Node.js example streams the file and needs no packages:

import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";

const sha256 = createHash("sha256");
let size = 0;
for await (const bytes of createReadStream(
  "examples/mod-source/files/example.vpk",
)) {
  size += bytes.length;
  sha256.update(bytes);
}
process.stdout.write(
  JSON.stringify({ size, sha256: sha256.digest("hex") }) + "\n",
);

SHA-256 has 64 hexadecimal characters; MD5 has 32. DMM checks every supplied supported hash before extraction or replacement. A mismatch leaves the current installation unchanged. Hashes are optional for native sources, but omitting them prevents verification against a publisher's digest. Do not publish an empty hashes object or invent a digest.

Resolve expiring download URLs

For a stable direct URL, use download.url. For a fresh URL at download time, use download.resolverUrl instead:

{
  "id": "example-vpk",
  "filename": "example.vpk",
  "format": "vpk",
  "download": {
    "resolverUrl": "https://mods.example.org/dmm/v1/files/example-vpk/resolve"
  },
  "size": 141,
  "hashes": {
    "sha256": "0ef66746810e2611130e9407a078daeb9657f3716fe5f952bc555276b2af7c5a"
  }
}

The resolver is a public GET endpoint on a declared API origin. It returns:

{
  "url": "https://mods.example.org/files/example.vpk"
}

The result must use an accepted download origin. For CDN or presigned storage URLs, declare that origin in networkOrigins.downloads too, including each artifact redirect destination. The resolver returns the same artifact bytes; rotating the transport URL does not create a new content revision.

Variants and installation choices

Use installation: { "mode": "manual" } for DMM's existing file-selection flow. Do not include component fields in a manual plan. If saved manual choices no longer fit an update, DMM asks for a new selection before replacing files.

Use mode: "components" when your registry knows the exact VPK files and allowed combinations. An artifact is a downloadable file; a component selects one or more VPK paths from artifacts. Several components can share an archive.

The static example's example-choices.zip contains:

base/notice.vpk
blue/choice.vpk
red/choice.vpk

Require the base and exactly one color with this installation plan:

{
  "mode": "components",
  "components": [
    {
      "id": "base",
      "label": "Required notice",
      "files": [{ "artifactId": "choices-archive", "path": "base/notice.vpk" }]
    },
    {
      "id": "blue",
      "label": "Blue notice",
      "files": [{ "artifactId": "choices-archive", "path": "blue/choice.vpk" }]
    },
    {
      "id": "red",
      "label": "Red notice",
      "files": [{ "artifactId": "choices-archive", "path": "red/choice.vpk" }]
    }
  ],
  "requiredComponents": ["base"],
  "choiceGroups": [
    {
      "id": "color",
      "label": "Notice color",
      "minSelections": 1,
      "maxSelections": 1,
      "componentIds": ["blue", "red"],
      "defaultComponentIds": ["blue"]
    }
  ]
}

See the complete choices listing for the release and artifact hash.

ObjectFields and rules
Installationmode, components, requiredComponents, choiceGroups. Component mode requires 1 to 500 components. Required IDs and choice groups default to empty arrays if omitted.
ComponentRequired id, display label and nonempty files array, with at most 500 files. Labels accept up to 1,024 UTF-8 bytes.
Component fileRequired artifactId referencing a release artifact, and exact path to a VPK inside it. Use the artifact filename as the path for a direct VPK.
Choice groupRequired id, label, minSelections, maxSelections, componentIds, defaultComponentIds. At most 100 groups per plan.

Every component must belong to the required set or exactly one choice group. Required components cannot also be choices. Bounds must satisfy 0 <= minSelections <= maxSelections <= componentIds.length; defaults must be unique members and satisfy those bounds. For an optional choice, set minSelections: 0 and use defaultComponentIds: [] if nothing is preselected.

Paths use /, must end in .vpk, and cannot contain empty segments, . or .., backslashes, colons or control characters. Keep paths within 4,096 UTF-8 bytes. The source cannot run installation scripts or choose arbitrary game-file destinations.

Defaults suggest choices for the first installation. DMM keeps saved user choices on updates. If a later release removes a saved component, the user must choose again, even if you publish a different default.

Add website buttons

deadlock-mod-manager://sources/add?url=<encoded descriptor URL>
deadlock-mod-manager://sources/install?url=<encoded descriptor URL>&source=<encoded sourceId>&listing=<encoded listing ID>
ParameterRequiredMeaning
urlBoth linksFull descriptor URL, including its path.
sourceInstallExact descriptor sourceId.
listingInstallListing ID within your source.
releaseNoPins the current release ID; an older/different release is refused, not substituted.
artifactNoTargets one artifact of the current release. Component choices can still require a picker.

Percent-encode each value, including opaque IDs. DMM's query parser treats an unencoded = or & as a separator and does not interpret + as a space. Use encodeURIComponent for each value:

const descriptorUrl = "https://mods.example.org/.well-known/dmm-source.json";
const sourceId = "my-community";

function sourceLink(action, params) {
  const query = Object.entries(params)
    .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
    .join("&");
  return `deadlock-mod-manager://sources/${action}?${query}`;
}

const addLink = sourceLink("add", { url: descriptorUrl });
const installLink = sourceLink("install", {
  url: descriptorUrl,
  source: sourceId,
  listing: "example",
  release: "release-1",
  artifact: "example-vpk",
});

document.getElementById("add-source").href = addLink;
document.getElementById("install-mod").href = installLink;

Place matching anchors in your page before running the script:

<a id="add-source">Add source to DMM</a>
<a id="install-mod">Install with DMM</a>
<a href="https://deadlockmods.app/download">Download DMM</a>
<a href="https://mods.example.org/.well-known/dmm-source.json"
  >Source descriptor</a
>

The add link opens a preview where the user chooses Add. An install link first enrolls an unknown source with user review, or asks before re-enabling a disabled source. It rejects a descriptor whose sourceId does not match the link. For an enabled source, it opens the listing and starts the existing download/install flow. If the mod is already in the library, it opens its page. If the listing is missing from the cache, DMM refreshes once to find just-published mods.

Manual releases with several files need a picker unless artifact specifies one. Component releases include required components and the component holding the targeted artifact; unresolved choice groups still open the picker. Keep variant artifacts separate if you want a link to an artifact to identify a single variant; targeting an archive shared by several choices can be ambiguous.

Links carry identities, never download URLs. DMM still fetches artifacts from your catalog and the origins the user accepted. Keep the descriptor URL and a Download DMM link visible because a custom-scheme link may do nothing when DMM is not installed.

Publish and maintain updates

Keep listing, artifact and component IDs stable for the same content role. Use contentRevision to identify the bytes and installation meaning. Description edits and transport URL changes leave it unchanged.

ChangeWhat to publish
Title, description, author, tags or screenshotsUpdate listing metadata; keep the installed content revision unchanged.
Expiring URL rotation for identical bytes on accepted originsUpdate the URL/resolver; URL changes alone are not a new mod version.
A change to the declared API, download or media originsUpdate the descriptor and tell users to remove, review and add the source again.
New bytes, artifact identities, filenames, sizes/hashes or installation selectionsPublish a new contentRevision and accurate artifact evidence. Give a new release its own release ID.
A saved component or archive path disappearsExpect user selection before replacement; keep the previous installation usable until the update succeeds.
Temporary lack of installable contentSet availability: "unavailable"; do not pretend a request error means removal.
Listing removed from your servicePublish an explicit "removed" tombstone with its permanent listing ID.

DMM rejects changed artifact evidence or installation meaning under an unchanged release ID and content revision. A version label or updatedAt alone does not prove that different bytes are installed. Keep old artifacts available long enough for in-progress downloads; a pinned link to a non-current release is still refused by the current client.

DMM checks the descriptor again on refresh. Any change to its declared API, download or media origin sets blocks refresh until the user removes the source, reviews the new descriptor and adds it again. This includes adding a CDN origin. Re-adding the same accepted origin and sourceId reconnects the source to its local files and provenance. Changing a URL within an already accepted origin does not require this review; moving the source identity to another origin does not reconnect it automatically.

For static publishing, upload artifact and versioned detail files first, then complete catalog pages, and atomically replace the entry index last. All pages must describe the same snapshot. Version detail URLs when necessary so an old index cannot accidentally read a partially published new release.

For API publishing, provide a consistent view across cursor pages and linked documents. Do not return only the first page or silently truncate the catalog to stay below client limits. A failed or incomplete refresh keeps the previous complete catalog and records an error; there is no automatic refresh retry or conditional HTTP validator support to rely on.

If you configure a detail template, tombstone detail URLs must return the matching removed listing too. An HTTP 404 on a linked detail fails the refresh. A minimal tombstone is:

{
  "id": "retired-example",
  "availability": "removed"
}

Disabling or removing a source stops its downloads and remote updates. Local files, source provenance and local enable/disable/remove operations remain. Re-adding the same origin and source ID reconnects that identity. Local profiles retain provenance, but portable source-aware profile import/sharing and automatic origin migration are not implemented.

Descriptions, media and release history

Descriptions and screenshots

Descriptions default to plain text. Set descriptionFormat: "markdown" for GitHub-style tables, task lists, code and links. DMM opens links in the browser and drops inline Markdown images; provide screenshots through images instead. An unknown description format falls back to plain text.

Use PNG, JPEG, WebP or GIF for listing images. Source icons also accept ICO; their URL must have a recognized raster extension. Media responses need the correct image MIME type and matching raster bytes. SVG and HTML are not accepted. Each response is limited to 2 MiB. Only the first four listing image URLs are fetched during refresh; keep source icon bytes at or below 128 KiB for retention. DMM omits media that fails to load and keeps the catalog available.

Release history

Set capabilities.releaseHistory: true to show a read-only Changelog tab. Supply releaseHistoryUrl on the listing, or releaseHistoryUrlTemplate in an API catalog. DMM fetches history when the tab opens, independently of catalog refresh.

A history page contains complete release documents, including artifacts and installation. Use the fields in the release reference:

{
  "items": [
    {
      "id": "release-1",
      "contentRevision": "revision-1",
      "version": "1.0.0",
      "publishedAt": "2026-11-01T12:00:00Z",
      "changelog": "First example release.",
      "artifacts": [
        {
          "id": "example-vpk",
          "filename": "example.vpk",
          "format": "vpk",
          "download": { "url": "https://mods.example.org/files/example.vpk" },
          "size": 141,
          "hashes": {
            "sha256": "0ef66746810e2611130e9407a078daeb9657f3716fe5f952bc555276b2af7c5a"
          }
        }
      ],
      "installation": { "mode": "manual" }
    }
  ],
  "nextCursor": null
}

Paginate with nonempty nextCursor strings and explicitly end with null. Continuation requests use cursor and limit=100; release IDs must be unique throughout the history. The client accepts at most 20 history pages and 500 releases. Changelogs are plain text. History does not enable older-release installation, and a history failure does not block installing the current release.

Hosting and troubleshooting

Public sources need HTTPS with public DNS/IP addresses, without embedded URL credentials, login pages, cookies or OAuth. Local/private addresses and HTTP are accepted only through development-build enrollment in the current UI. Use the local API example to develop, then test your public HTTPS deployment.

JSON and media requests do not follow redirects. Link directly to the final descriptor, API and media URLs. Artifacts can follow up to five redirects, provided every destination is on an accepted download origin. Declare CDN origins before enrollment; a descriptor ID does not grant access to other hosts. Browser CORS configuration is not needed for catalog/download fetching because Rust makes these requests.

Current request budgets

LimitMaximum
JSON response10 MiB
Catalog pages100
Listings per complete catalog10,000
JSON documents per refresh, including pages, details and releases1,000
Normalized catalog, including retained images64 MiB
Catalog ingestionTwo minutes
History pages / releases20 / 500
Complete release-history fetch30 seconds
Downloadable artifact2 GiB
Extracted bytes per archive100 times archive size, clamped to 256 MiB through 16 GiB

Linked details and releases cost extra requests for every listing. For larger catalogs, embed complete records and current releases in catalog pages to reduce requests. Stay within both request and byte budgets; pagination alone does not remove them. History has its own budget and is not fetched on every refresh. The catalog-ingestion timeout starts after descriptor and source-icon fetching. Small archives get a 256 MiB extraction allowance. Larger archives can expand to 100 times their compressed size, up to 16 GiB per archive.

Common failures

SymptomCheck
Source rejected on enrollmentExact format/version/game, public HTTPS descriptor, final URL without a redirect, and nonempty API/download origin lists.
Listing is missing with adult content hiddenPublish a truthful isNsfw classification; missing/null is unknown and hidden.
Listing is missing from its expected tabUse a published contentType ID, not a display label or custom category name.
Refresh fails after page oneExplicit API nextCursor: null, unique IDs, stable cursors, consistent static snapshot IDs, and linked details/releases within request budgets.
Refresh reports changed network originsThe declared origin sets changed. Remove the source, review its new descriptor and add it again.
Detail is rejectedIts ID must match the index record, and an available listing must resolve to a current release. Do not combine embedded release and releaseUrl.
Download rejected after redirect/resolutionEvery artifact destination must be in networkOrigins.downloads; the resolver itself belongs in api.
Integrity check failsCalculate the digest and size from the exact uploaded bytes, not the VPK inside an archive or the HTML download page.
Icon/screenshots are absentAccepted media origin, direct raster URL, matching MIME/bytes, supported format and size.
Update needs a new selectionSaved component IDs or archive paths no longer fit; defaults do not replace the user's previous choice.
Install button failsEncode every parameter value, pass the full descriptor URL and exact IDs, and target the current release.

Capabilities and limits

Capability fieldAccepted values/defaultWhat this client does
search"local", "server", "none", or omitted/nullSearches cached titles/tags locally. Advertising "server" does not make DMM call a search endpoint.
releaseHistoryboolean, default falseEnables the read-only Changelog tab using the declared history URL/template.
installationChoicesboolean, default falseDeclares choice support. The actual installation plan is validated from each release.
changelogboolean, default falseAccepted declaration; use releaseHistory and history documents for the Changelog tab.
commentsboolean, default falseAccepted but not implemented.
dependenciesboolean, default falseAccepted but dependency installation is not implemented; nonempty release dependency arrays are rejected.

supportedFilters and supportedSorts are not consumed by the current client. Adding them or unknown optional descriptor fields does not create new UI. Custom sources do not inherit GameBanana mirrors, comments, vote/download statistics, moderation or author enrichment.

Mapped adapters, authenticated sources, equivalence links, automatic origin migration, installing older releases, version holds, portable source-aware profile sharing/import and progressive all-source search remain unimplemented. There is no published canonical schema package, OpenAPI contract or full conformance validator yet. The mod-source RFC includes proposals for features that are still unimplemented.

Publishing checklist

  • Serve your descriptor, complete catalog and referenced documents on public HTTPS without authentication or API/media redirects.
  • Keep permanent source/listing IDs and accurate content revisions.
  • Publish installable artifacts with safe filenames and within download and extraction limits. Include accurate size and SHA-256 when available.
  • Use current taxonomy IDs and truthful adult-content classifications.
  • Test catalog completion and artifact downloads. Check linked details, component selections, resolvers and CDN/media origins where used.
  • Publish and install an update; verify saved choices still fit or require a clear new selection before replacement.
  • Test removal and a failed refresh; existing local files and the previous complete catalog should remain usable.
  • Offer encoded Add source and Install buttons alongside a visible descriptor URL and Download DMM link.

For implementation details, read the native source types, validation and catalog reader.

On this page