Manifest specification
Schema versions 1, 2 + 3 · revision 2026-09
The PackRelay manifest is a small JSON document that describes every file in a pack along with a cryptographic signature that proves the manifest came from a registered publisher and hasn't been tampered with. Launchers verify the signature before installing any file.
Two versions of this schema are defined. schemaVersion: 1is the original flat shape used by every release as of 2026-05; sections 1–7 below cover it. schemaVersion: 2 adds optional upstream-of-bytes provenance via a top-level sourcescatalog referenced by file entries — documented in section 8. schemaVersion: 3 opens the manifest to games beyond 7 Days to Die, starting with Valheim (section 9). Every version signs identically (Ed25519 over canonical bytes). v1 and v2 manifests are always for 7 Days to Die.
1.A complete example
The simplest way to read the spec is to start with a real manifest. The example below is shortened — a production manifest typically contains hundreds of file entries.
2.Canonicalization & signing
The signature is computed over a canonical JSON serialization of the manifest with the signature field removed. Canonical means:
- UTF-8 encoding.
- Object keys sorted lexicographically.
- No whitespace between tokens (no indentation, no trailing newlines).
- Integers serialized without a decimal point; floats are not used in the manifest schema.
The PackRelay CLI does this for you. If you implement manifest signing yourself, two manifests that differ only in key order or whitespace MUST produce the same signature input.
The algorithm is Ed25519. The signature is computed over the canonical bytes; value is the lowercase hex encoding of the 64-byte signature.
3.Root fields
| Field | Type | Description |
|---|---|---|
| schemaVersion* | integer | Manifest schema version. Currently 1. Bumped only for breaking changes to the manifest shape. |
| name* | string | Globally unique pack identifier. Lowercase, hyphen-separated, 3–50 characters. Used as the URL slug at /packs/<name>. |
| displayName* | string | Human-readable pack name shown in the launcher and on packrelay.cloud. 3–80 characters. |
| version* | string | Semver — MAJOR.MINOR.PATCH, optionally with a pre-release suffix. Bumping any segment forces a sync on connected players. |
| game* | string | Target game identifier. Always "7d2d" in v1 and v2; v3 adds more games (section 9). |
| gameVersion* | string | The game build the pack targets. Launchers warn players when their installed game version doesn't match. |
| publisher* | string | Publisher identity that signed this manifest. Maps to a server-owner account on PackRelay.cloud. |
| publishedAt* | string (ISO 8601) | UTC timestamp of when this version was signed and submitted. The launcher refuses manifests dated in the future. |
| description | string | Long-form pack description in plain text or basic Markdown. Surfaced on the pack detail page. |
| tags | string[] | Up to 5 lowercase tags. Used for /browse filtering and for player discovery. |
| files* | FileEntry[] | Every file in the pack with its content hash and size. See the table below. May be empty — a vanilla pack ships no files and only pins a game version. |
| signature* | Signature | Cryptographic signature over the canonical-JSON form of every other field. |
* required
4.FileEntry
Each element of the files array.
| Field | Type | Description |
|---|---|---|
| path* | string | Path relative to the pack root. Forward slashes only. Must not contain .. or absolute prefixes. |
| sha256* | string (64-char hex) | SHA-256 hash of the file's contents. The launcher refuses any file whose computed hash doesn't match. |
| size* | integer (bytes) | File size in bytes. Used for progress reporting during sync. |
| executable | boolean | Default false. Set true for files that must be marked executable on POSIX targets (rare for 7DTD packs). |
* required
5.Signature
| Field | Type | Description |
|---|---|---|
| algo* | string | Signature algorithm. Currently always "ed25519". Future algorithm support will live behind new values here. |
| publicKeyId* | string | Identifier of the public key that should verify this signature. Format: <publisher>/<key-name>. The key must be registered on PackRelay.cloud and active. |
| value* | string (hex) | Detached signature over the canonical-JSON form of the manifest with the signature field omitted. |
* required
6.Versioning
Pack versions are semver. The launcher considers a version upgrade required when any segment changes — there is no skipping. A player on 1.4.1 joining a server attached to 1.4.2 will sync.
Pre-release suffixes (1.4.2-beta.1) are honored but participating launchers must opt in to pre-release channels via the upcoming public API.
schemaVersionis independent of the pack's version. It bumps only when this specification gets a breaking change to the manifest shape itself.
7.Verification rules
A launcher MUST reject a manifest if any of the following are true:
- The
signature.publicKeyIdis not registered on PackRelay.cloud, or has been revoked. - The Ed25519 verification fails.
publishedAtis in the future relative to a trusted clock source (allow a small skew).- Any
files[].pathescapes the pack root (contains..segments or absolute prefixes). - A downloaded file's computed SHA-256 does not match the manifest entry.
schemaVersionexceeds the version the launcher supports.
8.Schema version 2: source provenance
Schema version 2 is an additive evolution of v1. It adds optional fields that record where a pack's bytes originally came from (Nexus Mods, GitHub Releases, 7DaysToDieMods, or explicitly self-hosted) so a downloader can verify provenance and inherit any trust signals the upstream source provides (e.g. a malware-scan attestation).
Every v1 manifest is a valid v2 manifest minus the new fields. Conversely, a v2 manifest with no sources array and no sourceRef on any file behaves identically to v1. The differences:
schemaVersionchanges from1to2.- Optional top-level
sourcesarray — a normalized catalog of upstream binaries this pack bundles. Up to 500 entries. Many files can share one source entry (a 50-file mod from a single GitHub Release zip = 1 source, 50 files referencing it). - Optional
sourceRefon eachFileEntry— a pointer to asources[].id. Files without asourceRefare treated as self-hosted with no recorded upstream.
The signing contract is identical to v1: the signature covers the canonical-JSON serialization of the manifest minus its signature field. New fields just become part of the signed message.
Example
FileEntry — added fields in v2
| Field | Type | Description |
|---|---|---|
| sourceRef | string (slug) | Pointer to a sources[].id. When set, this file inherits the source's provenance. When omitted, the file is treated as legacy-blob equivalent (self-hosted, no recorded upstream). |
* required
Source — common fields
Every element of the sources array carries these fields. The source discriminator picks which per-variant fields apply (Nexus / GitHub / 7DTM / legacy-blob).
| Field | Type | Description |
|---|---|---|
| id* | string (slug) | Unique-within-this-manifest identifier. Lowercase, hyphen-separated. File entries reference this via sourceRef. |
| source* | "nexus" | "github" | "7dtm" | "legacy-blob" | Discriminator. Picks which other fields are valid on this source object. See the per-variant tables below. |
| discoveredVia | "7dtm" | Catalog through which this source was discovered. Currently only "7dtm" is recognized — recorded for trust signaling (e.g. a github source cataloged on 7DaysToDieMods inherits 7DTM's scan attestation). |
| scanAttestation | ScanAttestation | Malware-scan record from the catalog source that hosts this binary. See ScanAttestation below. |
* required
source: "nexus"
The upstream binary lives on Nexus Mods. Publishing requires the publisher's Premium account API key (Nexus only authorizes file downloads via Premium).
| Field | Type | Description |
|---|---|---|
| game* | string | Nexus game identifier (typically "7daystodie"). Nexus URLs include this segment. |
| modId* | integer | Nexus numeric mod ID. Visible in the Nexus mod page URL (/mods/<modId>). |
| fileId* | integer | Nexus numeric file ID for the specific uploaded archive used for this pack version. |
| version* | string | Author-declared version on the Nexus file entry. |
* required
source: "github"
The upstream binary is a GitHub Release asset. Public repos work without authentication; private repos need a Personal Access Token on the publisher account.
| Field | Type | Description |
|---|---|---|
| owner* | string | GitHub user or organization that owns the source repo: 1-39 letters, digits or single hyphens, not starting or ending with a hyphen. |
| repo* | string | Repository name (without the owner prefix): 1-100 letters, digits, ".", "-" or "_". |
| releaseTag* | string | The release tag the asset was published under (e.g. "v2.4.0"). Pins to an immutable commit + asset combination. |
| assetName* | string | Filename of the release asset (e.g. "BackupMod_v2.4.0.zip"), with no path separators. The launcher never downloads from this URL — bytes come from the blob cache by sha256 — but the field lets a downloader cross-check provenance. |
| sha256 | string (hex) | SHA-256 of the release asset itself (the archive), 64 lowercase hex characters. Distinct from the per-file hashes in files[]: anyone holding the asset can check it is the one this pack was built from. |
* required
source: "7dtm"
The upstream binary is cataloged on 7DaysToDieMods. Many 7DTM-cataloged mods actually host their bytes on GitHub; in that case the publisher chooses either source: "github" with discoveredVia: "7dtm" (preferred, records the catalog reference) or source: "7dtm" (treats 7DTM as the primary source).
| Field | Type | Description |
|---|---|---|
| modSlug* | string | 7DaysToDieMods URL slug (the path segment under 7daystodiemods.com/): lowercase letters, digits and single hyphens. |
| version* | string | Author-declared version on the 7DTM mod page. |
| upstreamUrl* | string (https URL) | The link 7DTM's download button points at for the actual binary. Must be https. Captured at publish time for provenance display; never resolved by the launcher. When it's a GitHub release, prefer source "github" with discoveredVia "7dtm" instead. |
| sha256 | string (hex) | SHA-256 of the archive fetched from upstreamUrl, computed at publish time; 64 lowercase hex characters. |
* required
source: "legacy-blob"
No additional fields. Use when the bytes don't map to any of the recognized upstream sources — the publisher uploaded them directly to PackRelay's blob cache as the original source. Also the implicit behavior for v2 files that omit sourceRef.
ScanAttestation
Optional sub-object on any source variant. Records a malware-scan result from the catalog that hosts the binary. Surfaced in the launcher's pack-detail UI as a trust signal.
| Field | Type | Description |
|---|---|---|
| scannedAt* | string (ISO 8601) | Timestamp the catalog source recorded for the malware scan. |
| scanner* | string | Name of the scanner the catalog used (e.g. "Norton 360", "Windows Defender"). Surfaced in pack-detail UI. |
* required
9.Schema version 3: more games
Schema version 3 is v2 made game-aware. Its game can be any game PackRelay supports, and the game decides where file paths are rooted, which sources a pack may draw on, and which mod loader it runs on. Paths, sources and sourceRef work exactly as in v2, and the signature is computed the same way.
A launcher that only knows 7 Days to Die never sees a v3 manifest for another game: the API only serves one to a client that names the game (?game=valheim), and a launcher MUST refuse a manifest for a game it doesn't support, like any unknown schemaVersion.
Example
Games
"7d2d", 7 Days to Die: paths are rooted atMods/, there is no framework, and sources arenexus,github,7dtmandlegacy-blob. The only game a v1 or v2 manifest can be for."valheim", Valheim: paths are rooted atBepInEx/, the framework isbepinexpack-valheim(BepInExPack), and sources arethunderstore(communityvalheim),nexus(gamevalheim),githubandlegacy-blob.gameVersionis x.y.z, such as0.220.5.
Root fields: changed and added in v3
| Field | Type | Description |
|---|---|---|
| game* | string | Any game in the list above. What else the manifest may hold (sources, framework, paths) follows from it. |
| framework | { id: string, version: string } | The mod loader the pack was built against. Required for a game whose mod loader every pack needs, whenever the pack has files, and for a pack whose requires names one; refused for a game that has none. |
| requires | { serverPlatforms: ("windows" | "linux")[], framework?: string } | What a server needs to run the pack, for a game where that varies: the server platforms that can load it, and the framework its mods run code through. Derived from the files by PackRelay's tools, and signed, so servers and launchers check it themselves. A server refuses a pack that names neither its platform. |
* required
A launcher installs the framework from GET /api/v1/frameworks/<id>/<version>, which lists its files by hash; they download from the same file endpoint as pack files, and are verified the same way. For Valheim that is BepInExPack, laid into the game folder.
source: "thunderstore"
The upstream package is on Thunderstore. PackRelay fetches it once and hosts the files itself, so players never download from Thunderstore. The files are laid out as r2modman lays them out: each package's plugins in plugins/<Namespace-Name>/, its config in the shared config/.
| Field | Type | Description |
|---|---|---|
| community* | string | Thunderstore community slug. Must be the manifest game's own ("valheim"). |
| namespace* | string | The package's team. Letters, digits and underscores. |
| name* | string | The package name. Letters, digits and underscores. |
| version* | string | The package version, strictly x.y.z. |
| sha256* | string (hex) | SHA-256 of the package zip, taken by PackRelay when it fetched the package. Thunderstore publishes no hash of its own, so this one is required. |
* required