Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Nexus Mods

Overview

modde integrates with Nexus Mods for authentication, browsing, searching, downloading, installing, and update-checking mods. The integration uses both the v1 REST API (https://api.nexusmods.com/v1) and the v2 GraphQL API (https://api.nexusmods.com/v2/graphql). A Nexus Mods account with an API key is required for every authenticated call, and automated CDN downloads additionally require a Premium subscription — see Premium and what free accounts get.

This page covers the full surface: the six-source key-resolution chain, browse/search/trending feeds, rate-limit behaviour, single-mod installs, Nexus Collections, the nxm:// handler, and update checking with its safety guardrails.

Authentication

The six-source resolution chain

When modde needs a Nexus credential, it walks an ordered chain and uses the first source that yields a non-empty key. Higher entries win over lower ones — a configured config file beats an environment variable, which beats the keyring, and so on.

PrioritySourceHow it is setBest for
1OAuth tokenOAuth2 PKCE flow, stored in the keyring (modde/nexus-oauth-token); used only while unexpired (60 s skew buffer)Future interactive desktop login
2modde config filemodde nexus auth writes ~/.config/modde/nexus_api_key (mode 0600)Single-user workstations
3NEXUS_API_KEYEnvironment variableCI, containers, one-off shells
4System keyringsecret-service over D-Bus, service modde, key nexus-api-keyDesktop sessions with an unlocked keyring
5NEXUS_API_KEY_FILEEnvironment variable pointing at a file whose contents are the keysops-nix / agenix / systemd credentials
6Legacy settings.tomlnexus_api_key field in modde’s settings fileBackward compatibility only

If none of the six yields a key, modde fails with:

No Nexus API key found. Set NEXUS_API_KEY env var or run `modde nexus auth`.

Every key is trimmed of surrounding whitespace before use, and empty/whitespace-only sources are skipped (they do not short-circuit the chain). The OAuth entry at priority 1 is checked first but falls through to the API-key chain when no token is stored or the stored token has expired.

Note on the keyring vs. the config file. Priority 2 (the modde nexus auth config file) and priority 4 (the system keyring) are distinct. The current modde nexus auth flow writes the mode-0600 config file at ~/.config/modde/nexus_api_key; the keyring slot is also read if present (e.g. written by another tool or a future flow). Because the config file sits above the keyring in the chain, a key written by modde nexus auth always takes precedence.

Recommendation per environment

EnvironmentRecommended sourceWhy
NixOS / home-managerNEXUS_API_KEY_FILE via programs.modde.nexus.apiKeyFileDeclarative, secret stays out of the Nix store, sops-nix/agenix compatible
Single-user desktopmodde nexus auth (config file)One command, restrictive permissions, survives reboots
CI / ephemeral containersNEXUS_API_KEY env varNo persistent state, easy to inject as a secret
Shared / multi-user hostNEXUS_API_KEY_FILE with per-user file permissionsAvoids leaking the key into a shared keyring or shell history

Saving your API key (modde nexus auth)

modde nexus auth

This stores your personal API key so that subsequent commands can authenticate non-interactively. Get your key from Nexus Mods → My Account → API Keys.

For NixOS/home-manager setups, point modde at a secret file. The home-manager module wires apiKeyFile to NEXUS_API_KEY_FILE, which is priority 5 in the chain:

programs.modde = {
  enable = true;
  nexus.apiKeyFile = "/run/secrets/nexus-api-key";
};

The file contents are the raw key; modde trims trailing newlines. Because the path is read at runtime, the secret never enters the Nix store. This is compatible with sops-nix, agenix, and systemd credentials. See the Home-Manager module reference for the full option.

Checking status (modde nexus status)

modde nexus status

This calls the v1 users/validate.json endpoint and reports the resolved account name and whether it has Premium. Use it to confirm both that your key is valid and that automated downloads will work.

Premium and what free accounts get

Automated CDN downloads — the download_link.json endpoint that returns a time-limited CDN URL — are Premium-only. Before generating any download link, modde validates the account and refuses non-Premium keys with:

Nexus Premium is required for automated downloads.
Please upgrade at https://next.nexusmods.com/premium
CapabilityFree accountPremium account
modde nexus status / account validationYesYes
Browse / search / trending / updated feeds (REST + GraphQL)YesYes
Mod metadata, file listings, collection manifestsYesYes
Update checking (modde update check)YesYes
Endorse / track / untrack modsYesYes
Automated CDN download (install mod, update apply, nxm handle, collection installs)NoYes

Free accounts can still drive a nxm:// “Download with modde” link when the link carries its own short-lived key/expires pair issued by the website (see the nxm:// handler), because that path does not require the Premium download_link.json call. Everything that resolves a CDN URL through the API requires Premium.

Browsing and searching

modde queries the v2 GraphQL endpoint first for browse/search, because it returns richer per-tile data (thumbnails, summaries, endorsement and download counts) in a single round-trip. If the GraphQL schema shifts or the endpoint errors, modde transparently falls back to the v1 REST equivalent so the UI keeps rendering.

FeedGraphQL (preferred)REST fallback (v1)
Trendingbrowse_feed_gql(domain, Trending)GET /games/{domain}/mods/trending.json
Monthly topbrowse_feed_gql(domain, MonthlyTop)GET /games/{domain}/mods/updated.json?period=1m
Full-text searchsearch_mods_gql(domain, term, page)GET /games/{domain}/mods/search.json?search=…&page=…
Collections feed/searchcollections_feed_gql(domain, term)GET /games/{domain}/collections.json?search=…

Additional v1 REST endpoints back the rest of the integration:

PurposeEndpoint
Mod detailsGET /games/{domain}/mods/{id}.json
File listingGET /games/{domain}/mods/{id}/files.json
Recently updatedGET /games/{domain}/mods/updated.json?period={1d|1w|1m}
Collection by slugGET /collections/{slug}.json (game domain auto-discovered)
Collection revisionGET /games/{domain}/collections/{slug}/revisions/{rev}.json
Endorse / abstainPOST …/mods/{id}/endorse.json · …/abstain.json
Tracked modsGET/POST/DELETE /user/tracked_mods.json

The mod-image gallery is fetched via a small dedicated GraphQL query (modImages), falling back to the single picture_url from the v1 mod response when GraphQL is unavailable.

The base URLs honour MODDE_NEXUS_BASE_URL and MODDE_NEXUS_GRAPHQL_URL for pointing the client at a mock server in tests. Production never sets them.

Rate-limit awareness

Nexus enforces hourly and daily request limits. modde reads the response headers on every v1 call and reacts:

  • x-rl-hourly-remaining — when fewer than 10 requests remain in the hour, modde logs a warning so you can slow down before hitting the wall.
  • HTTP 429 — a hard rate-limit error. modde surfaces it as Nexus API rate limit exceeded. Please wait before retrying. Respect the Retry-After header the server returns and wait before re-running.

Update checking is designed to be frugal here: mods are grouped by game domain and modde issues exactly one updated_mods call per domain regardless of how many mods you track in that game (see Checking for updates).

Installing a single mod

modde install mod https://www.nexusmods.com/skyrimspecialedition/mods/12345 --profile my-skyrim

The pipeline:

  1. Resolve the mod, pick the latest MAIN file, and generate a CDN download link (Premium-gated).
  2. Download the archive into the store, then extract into a temporary staging tree (not the store) for analysis.
  3. Hash the archive (xxhash64) and record it on the install plan.
  4. Analyze the staged tree to detect the install method (simple copy, FOMOD, BAIN, …).
  5. Move files into the canonical store layout store/{domain}_{mod}_{file}/, or, when more input is needed, keep the staged copy and route you to a wizard.

The pipeline is idempotent: if store/{domain}_{mod}_{file}/ already exists it returns AlreadyStaged and skips the download. The same code path backs both the CLI and the UI Browse Nexus → Install button.

Non-interactive FOMOD installs

If the mod uses a FOMOD installer, supply a declarative config to answer its choices without a wizard:

modde install mod <url> --profile my-skyrim --fomod-config my-choices.toml

The config can be TOML or JSON. Without it, a FOMOD mod is staged and marked pending_user_input so a wizard can run later without re-downloading. See the FOMOD guide for authoring --fomod-config files.

When detection fails

If modde cannot classify the archive, it writes a dossier (mod name, author, version, Nexus URL, and a probe trace) next to the staged files and marks the install unknown. This is the same dossier the skill tooling consumes to add bespoke handling.

Nexus Collections

A Nexus Collection is a curated, version-pinned set of mods. modde resolves them in two steps, which lets you install by slug alone without knowing the game domain up front:

  1. DiscoverGET /collections/{slug}.json returns the collection’s game domain and its latest published revision number.
  2. Fetch the manifestGET /games/{domain}/collections/{slug}/revisions/{rev}.json returns the full, pinned manifest.

From the CLI

# Install the latest published revision
modde install nexus-collection <slug> --profile my-collection

# Pin a specific revision/version
modde install nexus-collection <slug> --version 7 --profile my-collection

When --version is omitted, modde uses the latest published revision discovered in step 1. When it is supplied, step 1 is still used to learn the game domain, but the given revision is fetched directly.

Declaratively (home-manager)

programs.modde.profiles.my-collection = {
  game = "skyrim-se";
  nexusCollection = {
    slug = "collection-slug";
    version = "1.0.0";
  };
};

nexusCollection is mutually exclusive with wabbajackList. See the Home-Manager module reference.

Version pinning and the profile lock

Installing a collection locks the profile with a NexusCollection lock reason. The lock preserves the curator’s intended load order and prevents modde update apply from silently drifting the profile away from the pinned revision. To update a locked profile you must opt in explicitly — see the update guardrails.

nxm:// protocol handler

Nexus’s “Download with [manager]” buttons emit nxm:// URIs. To make modde the system handler:

modde nxm install

This registers an XDG desktop entry for the nxm:// scheme. Clicking a download link on the Nexus website then hands the URI to modde, which fetches the file. You can also handle a URI by hand:

modde nxm handle "nxm://skyrimspecialedition/mods/12345/files/67890?key=abc&expires=123" \
  --profile my-skyrim

The URI carries the game domain, mod ID, file ID, and a short-lived key/expires pair the website mints for the download. --profile routes the result into a specific profile.

Checking for updates

modde update check

# Check for updates in the last week (default period)
modde update check --profile my-skyrim

# Check updates from the last day or month
modde update check --profile my-skyrim --period 1d
modde update check --profile my-skyrim --period 1m

--period accepts 1d, 1w (default), or 1m. Only enabled mods that carry Nexus metadata (nexus_mod_id + nexus_game_domain + installed_timestamp) are considered — mods installed via modde install mod and Nexus Collections record this automatically. modde groups the tracked mods by game domain, makes one updated_mods call per domain, and reports a mod as updatable when its latest_file_update timestamp is newer than your local installed_timestamp.

modde update check --mods checks your Nexus-tracked profile mods. Without --mods (and with no profile mods to check) the same command surface also covers modde’s own product update check.

modde update apply

The companion command downloads and installs the latest MAIN file for each updatable mod and rewrites the profile entries to point at the fresh (mod_id, file_id) pair. Pair check and apply in a scheduled task to keep a profile current.

# Preview only — list what would change, download nothing
modde update apply --profile my-skyrim --dry-run

# Apply non-breaking updates
modde update apply --profile my-skyrim

Safety guardrails

update apply is deliberately conservative because auto-updating can corrupt a curated setup:

  • Locked-profile guard. Wabbajack, Nexus Collection, and TOML-import profiles ship a pinned, authoritative load order. apply refuses to run on a locked profile and tells you to either pass --confirm-locked (acknowledging the drift) or run modde profile unlock <name> first. With --confirm-locked it proceeds but warns that the locked source will not be re-verified afterward.
  • Breaking-semver guard. Updates that look like a major version bump (e.g. 1.x → 2.0) are flagged breaking. They are skipped unless you pass --accept-breaking, and even then each breaking mod prompts for an interactive y/N confirmation. --yes assumes “yes” to those per-mod prompts but still refuses any breaking update unless --accept-breaking is also set. Version strings that can’t be parsed fall through as “not breaking”.
# Update a locked Collection profile, accepting major bumps non-interactively
modde update apply --profile my-collection --confirm-locked --accept-breaking --yes

See also