Skip to content

Server Migration

iderex edited this page Sep 15, 2026 · 6 revisions

Server Migration

On the 5.0 line (Jellyfin 12), the settings are five tabs. This page describes the build Latest points at, where every setting is on one page. On the 5.0 line the plugin's entry in the plugin list opens Overview, which shows a card per provider, the SSO-only state and what to do next, and carries no setting of its own; the other four tabs are Providers (both provider workspaces, their editors, the login-button fields and the Configuration check), Accounts (the linked-accounts panel and the account-link export and import), Policies (the provisioning-profile editor) and Server (the configuration export and import, the login-page buttons switch and single logout). So the transfer section this page sends you to is the Server tab.

Moving this plugin's state to a new Jellyfin server, or rebuilding the same one from scratch, without asking anybody to link their account again.

Two downloads carry that state, and neither is a substitute for the other. The order they go back in is not a convention: each step depends on state the one before it puts in place, and the import refuses rather than half-applying if you run one early.

Important

Step 4 does not work on any build published so far, and it fails by reporting success. On every beta from 4.3.0-beta.43 to 4.3.0-beta.61, importing the account-link file answers "Imported" and restores nothing, so a server migrated on one of them has an empty link table and was never told. The fix has landed but is not in any published build yet; when a release carrying #1517 is out, run step 4 again with the same file and the links come back. Until then, keep the file: nothing about it expires, and the restore is the only thing that needs the newer build.

This page has been walked end to end against a scratch server rebuild - a fresh Jellyfin 10.11.11, the accounts recreated, both files imported - and every refusal described below was produced by that run rather than read out of the source. The run is what found the defect above. It is recorded on #1135.

This is the operator's version. The repository carries a code-cited counterpart with a source line beside every claim, and Account-management API carries the full refusal table for the link restore. The two pages move together; if this one disagrees with them, they win.

Take two files off the old server

Both are on the plugin's settings page, in the transfer section, and both have an admin-API equivalent.

File Endpoint What it carries
Configuration GET /sso/Config/Export Every provider's settings, redacted. No secret, no link map.
Account links GET /sso/Config/Links/Export One entry per account link, keyed by Jellyfin username rather than by user id.

The split is deliberate. The configuration export is defined as carrying no link map, so an administrator asks for identity data explicitly instead of receiving it as a side effect of exporting provider settings.

Warning

The link file is personal data. It pairs Jellyfin usernames with each user's subject identifier at the identity provider. It is not redacted and it cannot be. Store and transport it the way you would any other file naming your users.

It is also not the per-subject export one user would ask for. That is a separate per-account read, described on the Account-management API page.

Links pointing at accounts that no longer exist are dropped at export, so a dangling link never travels and the restore cannot differ from the file it claims to apply.

The order, and why each step needs the one before it

  1. Bring the Jellyfin accounts back, under the same usernames. The link import resolves every entry's username against the user database and refuses the whole document if an account it names is missing. It never creates an account, so a backup file cannot bring a user into existence.
  2. Import the configuration export. A merge, not a replace. A provider that exists only on the target is left alone; a provider new to the target arrives with an empty link map and a blank secret.
  3. Re-enter every provider secret and save. The export carries none. A provider that arrived from the file has a blank secret and fails its logins closed until you supply it. On a provider the target already held, a blank incoming secret keeps the stored one rather than wiping it.
  4. Import the account links, last. The import refuses a protocol-and-provider pair this instance does not hold, which is exactly why it cannot run before step 2.

No step half-applies. Both imports resolve the whole document before writing anything, and a single unrestorable entry aborts the import with every refusal collected. Nothing is persisted when an import throws, so a step run too early leaves the server exactly as it was: fix the ordering and run it again. Re-importing a link this instance already holds is not a repoint and succeeds, so a partial migration can be re-run safely.

A successful link import is audited with the total and the per-provider counts, and with no canonical name in the line. Those counts are how you check that what came back matches the file you applied.

On the 5.0 line (Jellyfin 12), from 5.0.0-JF12-beta.82 onward, step 4 answers the numbers. The three lines above describe the 4.3 line, where the endpoint answers 204 and the page prints one fixed sentence.

A successful link import answers the total and the per-provider counts, and audits the same numbers with no canonical name in the line. The answer is 200 with a body:

{
  "Restored": 42,
  "Providers": [{ "Protocol": "OpenID", "Provider": "keycloak", "Links": 42 }]
}

The settings page prints that number for you, so on step 4 you do not have to reach the server log at all. The check is Restored against the number of entries in the file you applied. Both sources carry the same figure and each is good for something different: the answer is what you read while you are still holding the file, and the audit line is the durable copy you still have next week.

Restored counts restored entries, not distinct links, which is what makes that comparison work - a file with 42 entries that all applied answers 42.

A document that restored nothing answers "Restored": 0, and the page says so rather than reporting a plain success. The likeliest cause is the configuration export posted to the link import by mistake. Both documents declare format version 1, so the version check passes and nothing else separates them: the configuration export carries no link entries, so the import finds none, restores none, and is not an error. Take the account-link export from Export Account Links and apply that instead.

What never comes back

Each of these survives neither file, and each is a decision with a reason rather than a gap.

  • Provider secrets and SAML signing keys. Withheld at the JSON boundary, so the document holds no plaintext secret and no encrypted envelope either.
  • The at-rest data key (sso-secret.key). Its own file beside the configuration, never part of the configuration object.
  • Rate-limit tuning. Instance-local operational tuning. A number has no blank-means-keep signal, so importing it would let a partial document silently switch a denial-of-service control off.
  • The SSO-only globals, including the break-glass admin name. Validated on import, never applied: the mode is turned on through its own elevated, audited endpoint, which also runs the per-user sweep.
  • Repointed-user bookkeeping. It describes the user database this instance took a password door away from, not the next one.
  • Logout sessions. Per-session single-logout state; nothing on a rebuilt server has a session to log out.
  • Role-mapped access deadlines. Not a field of the link backup at all.
  • The last-SSO-login stamp. A login instant is an observation. Writing it back would assert a login that never happened on the new server.
  • A provider's last-used redirect-path spelling. Runtime state, meaningless across instances.

Two of those change what you have to plan for rather than only what you know:

  • Re-enter the secrets (step 3), or every provider that arrived from the file fails closed on its first login attempt.
  • A time-limited link comes back unlimited. If you use role-mapped access durations, check those accounts after the restore instead of assuming the expiry travelled.

The three things that actually go wrong

All three answer 400, restore nothing, and name the offending entries by their index in the file you posted rather than by canonical name.

  • A renamed user. The entry names a username no account here holds. Rename the account back, or edit that entry's Username to the new name before importing. The canonical name is the identity; the username is only how the document finds the account.
  • A provider missing on the target. The entry names a protocol-and-provider pair this instance does not hold. Either step 2 was skipped, or the name differs: provider names are matched exactly, character for character, while the protocol is matched case-insensitively.
  • An existing conflicting link. This instance already links that canonical name to a different account, or already binds the link to a different OpenID issuer. Both refusals exist so a backup file cannot silently remap somebody's identity onto another account. Unlink the existing one first, then re-import.

Rolling back the migration itself

A refused import leaves nothing to undo - the walk checked the link table after each of the four refusals and found it unchanged every time.

A successful one is harder to take back than this page used to say, and it matters on the mistake people actually make: importing the wrong file. Re-importing the correct one does not undo it. The link import adds and overwrites but never removes, so the wrong file's entries stay behind, and the correct document is then refused with this instance already links that identity to a different account; unlink it first. That refusal covers the whole document, so one leftover entry blocks every other link from coming back. Unlinking is one call per identity and the refusal points at ten entries at a time by their position in the file. There is no bulk unlink and no replace mode: check the file is the right one before you import it.

Re-importing the SAME file onto the state it produced is safe and does nothing, which the walk also confirmed.

Reversing a bad plugin release is a different operation and is on Rollback.

See also

Clone this wiki locally