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.
| Priority | Source | How it is set | Best for |
|---|---|---|---|
| 1 | OAuth token | OAuth2 PKCE flow, stored in the keyring (modde/nexus-oauth-token); used only while unexpired (60 s skew buffer) | Future interactive desktop login |
| 2 | modde config file | modde nexus auth writes ~/.config/modde/nexus_api_key (mode 0600) | Single-user workstations |
| 3 | NEXUS_API_KEY | Environment variable | CI, containers, one-off shells |
| 4 | System keyring | secret-service over D-Bus, service modde, key nexus-api-key | Desktop sessions with an unlocked keyring |
| 5 | NEXUS_API_KEY_FILE | Environment variable pointing at a file whose contents are the key | sops-nix / agenix / systemd credentials |
| 6 | Legacy settings.toml | nexus_api_key field in modde’s settings file | Backward 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 authconfig file) and priority 4 (the system keyring) are distinct. The currentmodde nexus authflow writes the mode-0600config 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 bymodde nexus authalways takes precedence.
Recommendation per environment
| Environment | Recommended source | Why |
|---|---|---|
| NixOS / home-manager | NEXUS_API_KEY_FILE via programs.modde.nexus.apiKeyFile | Declarative, secret stays out of the Nix store, sops-nix/agenix compatible |
| Single-user desktop | modde nexus auth (config file) | One command, restrictive permissions, survives reboots |
| CI / ephemeral containers | NEXUS_API_KEY env var | No persistent state, easy to inject as a secret |
| Shared / multi-user host | NEXUS_API_KEY_FILE with per-user file permissions | Avoids 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.
Using sops-nix (declarative, recommended on NixOS)
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
| Capability | Free account | Premium account |
|---|---|---|
modde nexus status / account validation | Yes | Yes |
| Browse / search / trending / updated feeds (REST + GraphQL) | Yes | Yes |
| Mod metadata, file listings, collection manifests | Yes | Yes |
Update checking (modde update check) | Yes | Yes |
| Endorse / track / untrack mods | Yes | Yes |
Automated CDN download (install mod, update apply, nxm handle, collection installs) | No | Yes |
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.
| Feed | GraphQL (preferred) | REST fallback (v1) |
|---|---|---|
| Trending | browse_feed_gql(domain, Trending) | GET /games/{domain}/mods/trending.json |
| Monthly top | browse_feed_gql(domain, MonthlyTop) | GET /games/{domain}/mods/updated.json?period=1m |
| Full-text search | search_mods_gql(domain, term, page) | GET /games/{domain}/mods/search.json?search=…&page=… |
| Collections feed/search | collections_feed_gql(domain, term) | GET /games/{domain}/collections.json?search=… |
Additional v1 REST endpoints back the rest of the integration:
| Purpose | Endpoint |
|---|---|
| Mod details | GET /games/{domain}/mods/{id}.json |
| File listing | GET /games/{domain}/mods/{id}/files.json |
| Recently updated | GET /games/{domain}/mods/updated.json?period={1d|1w|1m} |
| Collection by slug | GET /collections/{slug}.json (game domain auto-discovered) |
| Collection revision | GET /games/{domain}/collections/{slug}/revisions/{rev}.json |
| Endorse / abstain | POST …/mods/{id}/endorse.json · …/abstain.json |
| Tracked mods | GET/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_URLandMODDE_NEXUS_GRAPHQL_URLfor 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 asNexus API rate limit exceeded. Please wait before retrying.Respect theRetry-Afterheader 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:
- Resolve the mod, pick the latest MAIN file, and generate a CDN download link (Premium-gated).
- Download the archive into the store, then extract into a temporary staging tree (not the store) for analysis.
- Hash the archive (
xxhash64) and record it on the install plan. - Analyze the staged tree to detect the install method (simple copy, FOMOD, BAIN, …).
- 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:
- Discover —
GET /collections/{slug}.jsonreturns the collection’s game domain and its latest published revision number. - Fetch the manifest —
GET /games/{domain}/collections/{slug}/revisions/{rev}.jsonreturns 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 --modschecks 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.
applyrefuses to run on a locked profile and tells you to either pass--confirm-locked(acknowledging the drift) or runmodde profile unlock <name>first. With--confirm-lockedit 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 interactivey/Nconfirmation.--yesassumes “yes” to those per-mod prompts but still refuses any breaking update unless--accept-breakingis 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
- Download backends — every transport modde can fetch from, and the resume/queue model
- Wabbajack modlists — the other major Nexus-backed install surface
- FOMOD installers — authoring
--fomod-configfiles - Home-Manager module reference —
nexus.apiKeyFile,nexusCollection - Parity audit — what is
DonevsPartial