Skip to content

Backup and Restore: What a Backup Keeps

In this page

Every table in the Library’s data store is classified as one of four kinds, in one code registry, and a test fails when a table is not classified. This page explains the kinds and lists every table.

Where this lives in the code

  • src/MediaEngine.Storage/Backup/BackupTableClassification.cs (the registry)
  • src/MediaEngine.Storage/Backup/BackupTableRule.cs and BackupDataKind.cs
  • tests/MediaEngine.Storage.Tests/BackupTableClassificationGuardrailTests.cs (the guardrail)
  • src/MediaEngine.Storage/Backup/PersonalStateBackupRepository.cs, TitleRecognitionReader.cs and BackupArchiveFormat.cs (the exporter that follows the registry, the one place that reads a title’s recognition data, and the file format it writes)
  • src/MediaEngine.Storage/Backup/BackupRestoreRepository.cs (puts an export into the data store), WaitingPersonalStateRepository.cs (the waiting area and the re-attach pass), LiveTitleResolver.cs (finds the live row a recorded title now is), WaitingStateTimePositions.cs (which tables hold time positions) and src/MediaEngine.Domain/Services/RestoredTitleRules.cs (the pure rules for which file is the same title and cut); src/MediaEngine.Api/Services/Backups/WaitingStateReattachHostedService.cs runs the pass
  • src/MediaEngine.Api/Services/DatabaseBackupService.cs (makes a backup file, stages a restore and, before the data store opens, puts the old store aside), RestoreArchiveValidator.cs (the one check every route into a restore uses), RestoreMarkers.cs and PendingRestoreImportService.cs (imports the staged backup into the fresh store at start-up), all under src/MediaEngine.Api/Services/
  • src/MediaEngine.Storage/Backup/UserExportScope.cs (the one map of what a user export holds), UserExportSummary.cs (UserExportReader, which proves a file holds one profile with its sign-in or one household and nothing of anyone else), UserImportRepository.cs (imports the file’s profiles into a chosen household in one transaction), tests/MediaEngine.Storage.Tests/UserExportScopeGuardrailTests.cs (the guardrail) and, in src/MediaEngine.Api/Services/Backups/, UserTransferService.cs, UserImportStagingArea.cs and the routes in src/MediaEngine.Api/Endpoints/UserTransferEndpoints.cs
  • src/MediaEngine.Storage/Backup/IBackupPortableValueCodec.cs (the port that makes a machine-protected value readable for a backup) and src/MediaEngine.Api/Security/DataProtectionBackupPortableValueCodec.cs (the Engine’s implementation for the two-step secret, registered with the exporter in src/MediaEngine.Api/DependencyInjection/TuvimaBackupServiceCollectionExtensions.cs)

What a backup is for

A backup holds what is hard to recreate: your settings, your preferences and your own state (reading and watching positions, bookmarks, favourites). It does not hold your library’s catalogue (titles, matches, enrichment results, artwork), which a rescan rebuilds (apart from the corrections you made to it yourself, such as a title field you locked or a chapter you renamed), and it never holds your media files or folders. Backing those up is a job for your computer’s own backup tool, such as File History, Time Machine or a NAS.

After a restore and a rescan, your own state finds its way back to the right titles, even if a file has since changed. To make that possible, a kept row that points at a catalogue row does not rely on the row’s id (ids do not survive a rescan). It is stored with recognition data instead: the title’s external ids, running time and edition, with the file fingerprint as a fallback.

The four kinds

Configuration is settings and access, kept as rows: accounts, passkeys, profiles, households, grants, view policies and preferences, provider settings without secrets, notification settings, document types, fields and rules, backup settings, plugin approvals, collections, playlists and View galleries a person built, values a person typed into document fields, and files a person chose to keep out of the Library. Rows that point at View items are re-attached by file fingerprint. Password hashes, passkey public keys and two-step data stay, because they are what lets people sign in after a restore. A person’s name and description corrections for a fictional thing or a universe stay too (a fictional thing is found again by its Wikidata id). Collections the Library built (universes and series) are rebuilt, but a person’s own switches on them (shown, featured, display changes) are kept.

Personal state is a person’s own state tied to a title: bookmarks, highlights, reading statistics, audiobook bookmarks and listening history, music play stats, consumption history, reactions, saved items, taste overrides, personal activity and playback positions. It is kept and re-attached to titles by recognition data. Each rule names an owner column (the profile or user the row belongs to), so one profile’s state can be exported on its own.

Catalogue is anything a rescan rebuilds: works, editions, media assets, canonical values, claims, artwork, fingerprints, subtitles, people, provider caches, search indexes, the Needs Review queue, and identity and enrichment jobs, and View items with what the Engine works out about them. It is not kept. The adult-video area’s catalogue is in this group too. The exception is what a person did to it themselves: a person’s own corrections on catalogue rows are kept, in two ways (see “A person’s own choices and corrections” below).

Transient is not kept and not rebuilt from files: sessions and tokens, one-time codes and challenges, run, operation and audit logs, telemetry, device pairing requests, notifications already sent, job queues, backup copy records and data store bookkeeping.

Tables that mix kinds are split in three ways. By column, a rule can name catalogue references (columns that point at catalogue rows, swapped for recognition data), excluded columns (secret or machine-bound columns, never read: a column that may be empty is written as empty, any other is left out of the file) and excluded JSON properties (a named property removed, at any depth, from a JSON column, for example the machine-encrypted destination logins inside the backup settings). By row, a rule can carry a keep condition (for example, only collections a person built, or only provider settings that are not secrets). A rule can also name portable columns (see “Portable sign-in data” below).

A catalogue reference is normally an id. A reference stored as text is treated the same way when it parses as an id; any other text (a Wikidata id, a provider key) is an external key and is kept as written. A reference whose catalogue row no longer exists drops the row, and the count of dropped rows is recorded.

Provider keys and the Engine’s own encryption keys are never kept: provider settings marked secret, the config/secrets and config/.keys folders and plugin secrets are left out. The deliberate exceptions are the two values that are protected with one computer’s key, the two-step sign-in secret and a webhook’s signing secret. They are kept readable so a restore works on a new computer (see “Portable sign-in data”). Nothing in a backup file is treated as a secret: backups are never encrypted, and keeping the file safe is the person’s job.

A person’s own choices and corrections

A rescan rebuilds the catalogue, so anything a person changed on a catalogue row would be lost. Two kinds of rule keep it:

  • Choices are single columns on a row that a rescan recreates: a favourite flag on a View item, a display change typed on a title, a switch on a collection the Library built. The rule names the choice columns (and, when they sit on a side table such as a View item’s metadata row, the column that says whose choice the row is, otherwise the row’s own id); the exporter writes them to data/choices/<table>.json as a reference to the row (recognised by its external ids, or its file fingerprint) plus the values, and only for rows where a choice differs from its default. A choice column that points at another catalogue row (the series a title was placed in) is written as a reference too. A column can be gated by a sibling flag: it counts only while the flag is set, so a value the Engine read itself (a capture date from the file, a match source the person never locked) is never written. A Configuration table that keeps only some of its rows (collections) can name choice columns as well; they apply to the rows its keep condition leaves out.
  • Corrections are whole rows a person made on a Catalogue table: a title field they locked or edited, a renamed audiobook chapter, a name they gave to a face in a photo, a cover they chose from a provider. The rule carries a correction condition; matching rows are written to data/corrections/<table>.json like personal rows, with every catalogue reference turned into a reference to a title and machine-only columns left out. A row whose required reference no longer exists is dropped and counted. A correction or choice on a withheld title (one a backup may not name: a media asset in a Vault library, a work or edition reached from one, or a View item in a Vault image library) is left out and counted separately, and the title is not written for it. A profile backup holds no corrections or Library choices, because they belong to the Library.

Structural moves are not kept: a locked or rejected match, a locked series placement, which episodes a file covers, or an edition moved to another title. A rescan sorts these out, and it usually fixes what the move was for. Only what a person typed or picked stays.

The reader accepts a choices or corrections file only for a table whose rule allows it. Restoring them (re-applying each correction to the rescanned title) is part of restore, not of the export.

Artwork a person chose is kept only when it has a stable outside identity: a cover picked from a provider is found again by provider and image address (entity_assets) or by the artwork’s provider, source address and provider reference (entity_artwork_links). Artwork a person uploaded is never kept, because artwork files are not in a backup.

Portable sign-in data

The two-step sign-in secret is stored sealed with this computer’s Data Protection key, so on its own it would be useless on a new computer. The same goes for a webhook’s signing secret (application_webhooks.secret_ciphertext, sealed for that webhook’s id; the column cannot be empty, so a restored webhook row needs it). The account_two_step rule names secret_protected as a portable column, and the application_webhooks rule names secret_ciphertext: the exporter asks the Engine’s codec to unseal it and writes it readable, marked as portable in the file, and a restore seals it again with the new computer’s key. Plainly: backups are never encrypted, so the backup file can be used to sign in as you; keeping it safe is your job. If the key is gone and the secret cannot be unsealed, it is kept as it is, counted in the backup’s contents, and works only on this computer. Without a codec registered, portable columns are written unchanged. The codec handles exactly these two columns, with the same protector and purpose the sign-in code and the webhook code use (it is given the row’s values because a webhook secret is sealed for the webhook’s id), and refuses any other.

One person or one household

A user file moves people to another Library (backup and restore spec D19, users spec D9 and D16). It is the same archive format with the same matcher and re-attach pass, but its scope is one profile with the sign-in that opens it (BackupScope.ForAccount) or a whole household (BackupScope.ForHousehold), not the whole Library. The profile holds the person (roles, access, limits, PIN, preferences and personal state); the user is only the sign-in attached to it.

Whole backups carry the people. A full backup holds the users (accounts, with profile_id), profiles (roles, content limits), households (including is_default), profile_feature_grants, profile_library_hidden, library_profile_grants and profile_admin_protections as configuration, so a restore into a fresh Library brings every user back opening their own profile with the same password, roles and access (PeopleBackupRoundTripTests). A backup or user file made with another data structure (a different database_epoch in its manifest, or none) is refused on every route with one sentence, “This backup is from an older version of the Library and can’t be restored. Make a new backup with this version.” (a file from a later version says it was made by a different version); a user file says “This file is from an older version of the Library and can’t be imported. Export again with this version.” (users spec D11).

What a user file holds. The map in UserExportScope is the single list of tables and the filter for each:

  • Sign-ins: for each profile that has one, the user row, its password hash, its two-step secret (written readable through the portable-value codec, exactly as in a backup, but only in the exporting person’s own sign-in; see below), passkeys, outside sign-in links and dismissed notices.
  • Profiles: each profile (one for a person, every profile of the household for a household), PIN hashes, grid, sequence and View preferences and View policies (a View preference that points at another person’s profile is left out), Vault settings (without who last changed them), playback settings and Read/Watch/Listen/View access (profile_feature_grants). The household row is held only for its name, a suggestion for a new household. Roles are written as 0.
  • Their own state: every Personal state table, filtered by its owner column to those profiles (the owner is a BLOB id in most tables and lower-case text in the reader tables), and the items of their bookmark packs.
  • What they built: collections and playlists their profiles own (owner_kind = 'Profile'), with their items, placements and audience limited to those profiles.

What the file leaves out, each with a reason in UserExportScope.NotInAUserExport: every other profile and sign-in, hidden libraries (library ids differ between Libraries), administrator settings, invitations, paired devices, sessions and recovery codes, server and Library settings, the catalogue, choices and corrections on catalogue rows, and typed catalogue corrections. View galleries are left out too: they hang off a View space and View items that belong to folders configured on one Library. A test fails when a Configuration or Personal state table is in neither list, so a new table forces a decision.

The file. tuvima-user-<yyyyMMdd-HHmmss>.zip for a person, tuvima-household-<yyyyMMdd-HHmmss>.zip for a household. Its manifest.json has kind user and scope profile or household, plus profile_count, sign_in_count, two_step_left_out and the export counts (no email and no name); data/contents.json says user or household and names the account or household id only. A user file is never a backup of the Library: RestoreArchiveValidator refuses kind user on every route into a restore with “This file is one user’s data, not a backup of the Library. Use Import a user instead.”, and the import refuses any other file with “This file isn’t a user export.”. A user file is read with its own limits (200 entries, 64 MB an entry, 256 MB in all), accepts only tables in the map, holds at most eight profiles, and every row must prove it belongs to one of the file’s sign-ins or profiles (UserRowCheck), so a crafted file cannot write somebody else’s rows. The manifest’s scope is a claim: a file whose data holds another scope is refused.

Who may export. Only the server administrator, signed in at the Dashboard (users spec D16): their own profile and its sign-in (POST /access/me/export), any user (POST /access/accounts/{accountId}/export) or a whole household (POST /access/households/{householdId}/export). Everyone else, household administrators included, gets 403 on all three. A household that does not exist gets “That household isn’t in this Library.”. Each route asks a stale sign-in to confirm it is them and records account.exported or household.exported.

Who gets the two-step secret. A person exporting themselves carries their own two-step secret, portable through the codec. Anyone exporting someone else’s sign-in leaves theirs out: a file for another person has no account_two_step row, and a household file keeps only the exporting person’s own (BackupScope.ForHousehold(id, keepTwoStepForAccount), BackupScope.ForAccount(id, leaveOutTwoStep)). The test is whose sign-in it is against the caller’s own account id, not which route was used. The manifest says two_step_left_out: true when any sign-in lost its secret, and the import preview carries two_step_to_set_up so the administrator is told; those people arrive with two-step off and set it up again from Account > Security. The flag is a claim, not proof: a file for one person that says the secret was left out yet holds a two-step row is refused. Recovery codes are never in the file.

Importing (users spec D9). Only the server administrator imports. They upload the file (POST /access/accounts/import/inspect), which checks it the same way a restore is checked, keeps it for an hour in the backup folder’s user-imports staging area under an operation id only that administrator can use, and returns the preview and the choices (the profiles in the file, each sign-in with whether its email is used here, the households that exist with how many people they hold, whether a second household can be added). Nothing is written until POST /access/accounts/import/{operationId} with the choices, which imports in one write transaction. A refused import keeps the file so a choice can be changed and tried again; a successful one removes it. The choices:

  • Where: target_household_id (an existing household) or new_household_name (made by the import, never the default household), exactly one. All profiles arrive in that household (never household_id NULL). At most eight people fit (Household.MaximumProfiles); a new household is a second household only once every server administrator has an email and a password or passkey (D10, the same check as everywhere else), and needs new_household_administrator_profile_id: an imported profile that keeps its sign-in and is not a Kids profile (D15). An existing household gets no administrator from the import.
  • Sign-ins: each sign-in keeps its password hash, passkeys and outside sign-in links, and gets the file’s email unless emails names another (a changed email is never a reason to refuse: the preview says which emails are used here and the administrator types another). without_sign_in lists sign-ins to leave out: their profile arrives with no user and nothing about how they signed in. An email used here, a passkey already registered here, an outside sign-in linked to someone here, or a two-step secret that was not made readable for any computer refuses the import (or that sign-in is left out), with nothing written.
  • Roles are never carried. is_server_admin and is_household_admin are forced to 0 whatever the file says, a profile role of Administrator becomes a standard user and Kids stays Kids, except the one profile chosen to look after a household the import creates. Vault access is switched off (access_mode off).
  • Access: Read, Watch, Listen and View arrive as the file had them only when keep_access is true (off by default, so they arrive with none); hidden libraries never arrive (library ids differ between Libraries), so everything in the person’s areas is visible.
  • New ids: an id that already exists here (a household exported from this very Library, for example) is replaced by a new one everywhere it is written; nothing existing is overwritten. Rows are written with a plain INSERT, so any other clash fails the whole import.
  • Every column a rule excludes (a profile’s avatar_image_path, a collection’s cover, background, banner and logo artwork paths, and ids that point at someone else) is emptied whatever the file says, and the reader of every route refuses a file that fills one. A collection’s parent is kept only when the parent is in the same file. Collections arrive private and not featured; an audience limited to selected profiles stays limited to the imported profiles.
  • The two-step secret is sealed again with this computer’s key. A file with no two-step row imports normally, with two-step off.
  • Rows with no title go live. Rows that point at titles wait in waiting_personal_state under a new backup_restores row (named after the uploaded file), and attach as their titles are found, exactly as in a restore.

Refusals are plain sentences with “Nothing was imported.” (409): the email is used (“Someone in this Library already signs in with {email}.”), the household is full (“A household can have up to 8 people, and this would be more.”), a second household before the administrator has a password (“Set an email and password for your own sign-in first. A Library with several households must not be open to anyone who reaches it.”), a new household with nobody to look after it, and the outside sign-in or passkey clashes. The same checks run again inside the import’s transaction, so a clash that appears after the preview is still refused. A user who is not in this Library is 404 (“That user isn’t in this Library.”); a problem on this computer (a key that cannot be read) is 500 with “The file couldn’t be made on this computer.” and an Error log line, never the internal message. A second upload by the same administrator replaces and deletes their earlier kept file.

Deleting a user removes only the sign-in (users spec D5); deleting a profile removes its sign-in and everything personal in it (D7, D8), so the same file can be imported again afterwards.

What a backup file holds

A backup is one zip file, tuvima-backup-<time>.zip, with three parts:

  • manifest.json: schema_version 3.0, kind settings-and-personal-state, created_at, database_epoch (which data structure it was made on), includes_secrets and includes_keys (both false), leaves_out (media files, folders, artwork, catalogue, adult-video catalogue, secrets) and the counts of what it holds (profiles, configuration rows, personal rows, correction rows, choice rows, titles, titles recognised by file only). It holds no Vault field and names no title.
  • config/**: the safe configuration files, as before (never config/secrets, config/.keys, backups, .bak and .tmp files, restore markers).
  • data/**: the export described above, written by BackupArchiveFormat.

There is no copy of the data store file and no write lock: the export reads its own consistent snapshot, so a backup does not pause the Library. A backup of the earlier layout (manifest version 1.0 or 2.0, no kind, or a database/library.db entry) is refused everywhere with “This backup is from an older version of the Library and can’t be restored. Make a new backup with this version.”; a backup whose data cannot be read is refused with “This backup file is damaged and can’t be restored.”.

How a restore runs

One check (RestoreArchiveValidator) is used by scheduling a restore, the monthly restore check, the test restore, a setup upload and a restore from a destination. It applies the zip safety rules (unsafe or duplicate paths, forbidden secret entries, compression ratio, entry count, size), then reads the manifest, then reads the whole data/ part once. Scheduling copies the checked zip into a staging folder pending-<id> in the backup folder, unpacks its config/ files beside it and writes .restore-pending.json in the configuration folder.

Scheduling gives the restore an id and writes it into the marker. At the next start-up, before the data store opens, DatabaseBackupService.ApplyPendingRestore checks the staged zip again, writes where the current data store will go (the pre-restore copy) into the marker, and only then puts the store aside, whole (the file and its -wal and -shm files), as <db>.pre-restore-<time>.bak. Leftover -wal or -shm files of a store that is no longer there are put aside too (<db>.orphaned-<time>), so SQLite never replays them into the new store. If any move, or the copying of the configuration that follows (each replaced file is kept as .pre-restore-<time>.bak), fails, everything is put back (the data store and the configuration files already replaced), the pending marker stays and the Engine stops with the error, so the Library is left exactly as it was. Otherwise the marker is replaced with .restore-import.json, which carries the restore id and the pre-restore copy. The Engine builds a fresh data store, and PendingRestoreImportService (run once after the Engine is built, before anything is served) reads the staged zip and imports it as described below, then deletes the staged folder and the marker and logs one line of counts. The import is recorded under the restore id, so if the Engine stopped after the import committed but before the clean-up, the next start sees the id, imports nothing twice, cleans up and logs that. A clean-up that fails after the import committed is only a warning (“Restore finished; couldn’t remove the staged copy”). If the import itself fails, it logs an error naming where the previous data store was put aside, read from the marker (never its contents), renames the marker to .restore-import-failed.json so it is not retried on every start, keeps the staged folder, and the Engine carries on with the fresh store. Your current Library is therefore never deleted by a restore.

Backup screen actions

GET /system/backups/state gives the Dashboard counts only: titles a restore can match by exact file only (PersonalStateBackupRepository.CountTitlesRecognisedByFileOnly), the waiting period, how many restored rows still wait (WaitingPersonalStateRepository.CountWaiting, the Vault’s included, never named) and the latest restore (GetLatestRestore). PUT /system/backups/waiting-period validates the days against MissingFileRetentionLimits (7 to 3650) and saves missing_file_retention_days through the maintenance configuration. The restore test and a setup upload return a nullable contents read from the manifest’s counts (profiles, configuration_rows, personal, correction and choice rows added together, titles, titles_recognised_by_file_only); it is a total and names nothing.

Restore and re-attach

A restore imports an export into the fresh data store in one write transaction, so it either completes or leaves the data store as it was. It does three things.

  • Settings go straight back. A Configuration row that points at no title is written into its table, replacing the built-in row a fresh data store already holds (the default profile, the provider list). Values keep their storage class. A portable value (the two-step secret) is sealed again with this computer’s key through the Engine’s codec; with no codec the restore refuses rather than write it unsealed. A personal row that points at no title (a taste override) is written only if the Library does not already have it.
  • State that points at a title waits. Every row, in any group, that holds a reference to a title is parked in waiting_personal_state with the recognition data of its titles (ids, running time, edition, file fingerprints, name and year, copied into the row). Each row records when it started waiting. The waiting period (missing_file_retention_days in config/maintenance.json, 90 days by default; Safer deleting reuses the same setting) is read live when rows are cleared, so changing the setting affects rows already waiting. A row that hangs off a waiting row (the items of a bookmark pack) travels with it. A backup taken while rows still wait keeps them, with the time they started waiting, so restoring it does not restart the clock. Rows about a title a backup may not name (the Vault) are marked withheld and are not counted unless asked.
  • A record of the restore is kept in backup_restores with counts of rows put straight back, rows that started waiting, rows that have since attached, rows that yielded to a row the person made since the restore, and rows cleared.

A pass over the waiting area looks every titled row up in the Library, oldest first, and writes the row back only when every title it holds resolves to exactly one candidate. The Engine runs a pass after start-up, whenever the ingestion queue goes from working to idle, and at least once a day (which is also when rows that have waited longer than the waiting period are cleared). A title resolves like this, and never by guessing:

  • A file: the exact same file fingerprint is the same file. Otherwise the works that share an id with it (a Wikidata id, IMDb, TMDB and so on; for an episode or track, the parent found by its ids and the child at the same position) are the candidates, and each of their original files is compared with the shared matcher used for bookmark packs. Ids come in two levels (the list is RestoredTitleRules.EditionLevelIdKeys): title-level ids name the title (Wikidata id, IMDb, TMDB, a MusicBrainz release group), edition-level ids name one edition of it (ISBN, ASIN, Audible and Apple Books ids, a MusicBrainz release, an edition’s Wikidata id). When both sides share a title-level id, those ids decide same title or wrong title, and an edition-level id that disagrees (a hardcover and an ebook of the same book) only makes it another edition, which is treated as a different cut. When they share no title-level id, an agreeing edition-level id means the same edition and a disagreeing one a wrong title. The running time must be within the same tolerance, and the edition label must agree. Exactly one file of the same cut is an exact match; with none, exactly one file of a different cut or edition is a different-cut match; anything else keeps waiting. A file with no ids and a different fingerprint is never matched.
  • A work or an edition: exactly one work with the same ids (an edition also needs the same label or edition id). With no id, a work is found through an exact file fingerprint.
  • A collection by its Wikidata id, or, for a collection a person built, by its exact name if that is unique. A person or a fictional thing by its Wikidata id (a person with none by exact name, if unique). A View item by the fingerprint of its file. Artwork by its provider and source address or provider reference. Anything else has nothing to recognise it by and waits until it is cleared.

On a different cut, a row that holds a time position in seconds (an audiobook or video bookmark, a listening history entry) is written only when it names a chapter the live file also has; the position then moves to the start of that chapter (a backup does not record how far into the chapter the position was, so it lands at the chapter’s start; this is a known limitation). A position with no chapter name keeps waiting. The saved timing of a file (user_states) attaches on a different cut with its progress percent, but without the seconds that belonged to the other cut: position_seconds and duration_seconds are removed from its extended_properties and every other key stays; on the exact same cut it comes back whole. A row with no time position (a progress percent, a reaction, a saved item, a preference) attaches on a different cut as well. The tables that hold seconds are listed once in WaitingStateTimePositions, and a test fails if a kept table gains a seconds column that is not listed there or named as a length rather than a position.

Writing a row back never overwrites what the person did since the restore. Settings and personal state are written only if the Library does not already have the row; when it does, the waiting row yields: it is removed, together with the rows that hang off it (the items of a bookmark pack), and counted as yielded, not as attached. A choice (a favourite flag, a display change) is written only for the columns still at their default. A typed correction is written as a new row (with a new id if its own is taken), replacing what a provider put in the same place but not a correction the person made again since; a typed claim on a title also sets the title’s winning value, exactly as saving it in Editing Mode does, so it wins over the provider. A row that cannot be written yet (a choice on a row the rescan has not made, a person who has since been removed) keeps waiting.

A run looks at every waiting row once, in batches of 200, however many there are: the write lock is free between batches, there is a pause of 100 ms after each so playback and ingestion writes are not starved, and the run stops early, to carry on when the ingestion queue is idle again, if ingestion becomes busy. The Engine logs one line per run that re-attached, yielded or cleared anything (counts only, never a title), and one Warning per run when rows could not be written, with the number of rows and the table names (never a title); per-row detail is at Debug. The lookups by file fingerprint, id kind and value and edition Wikidata id each use an index; fingerprints are compared in lower case because they are stored that way. Search showing waiting items under the Missing filter, and the restore summary on the Backup screen, come with the screens in the next step.

Finding waiting items in search (D18). Global search (GET /display/search, UniversalSearchReadService) also lists the searching profile’s own waiting titles as a file-missing section (“File missing”), through WaitingPersonalStateRepository.SearchWaitingTitles. The query reads waiting_personal_state for the active human profile only (profile_id, indexed), group_kind = 'personal' and is_withheld = 0 (so never a Vault title), and only titles whose target is a Work, Edition or Media Asset (never a View item: search code names no View table). It matches the title’s name or parent name case-insensitively, lists each (name, parent, year, media type) once, at most 10. A row carries no link or artwork (DetailRoute is empty) and MatchReason is “File missing”. The section sits after all others, is never the top result, and is counted only in its own TotalCount, not the response total. Nothing extra is needed when a title returns: its waiting rows are removed by the re-attach pass, so it stops appearing.

Backup contents (manifest)

See “What a backup file holds” above. The restore screens come in the next step.

Configuration tables (64)

TableWhyPoints at the catalogueLeft outOnly rows where
accountsSign-in accounts and their administrator and household settings.nonenoneall rows
account_credentialsPassword hashes, needed to sign in after a restore.nonenoneall rows
account_external_loginsLinks between an account and an outside sign-in provider.nonenoneall rows
account_passkeysPasskey public keys, needed to sign in after a restore.nonenoneall rows
account_two_stepTwo-step sign-in data, needed to sign in after a restore; the secret is protected with this computer’s Data Protection key, so the export writes it readable (portable column secret_protected), or keeps it as it is when it cannot be read.nonenoneall rows
account_feature_grantsWhich features an account may use.nonenoneall rows
account_profile_grantsWhich profiles an account may use.nonenoneall rows
account_notice_dismissalsNotices an account has dismissed, so they do not return.nonenoneall rows
grant_admin_protectionsOptional extra lock (PIN hash and lock settings) on an administrator grant.nonenoneall rows
profile_feature_grantsWhich features a profile may use.nonenoneall rows
profile_library_hiddenWhich libraries an administrator hid from a profile.nonenoneall rows
library_profile_grantsWhich profiles can see a Selected-profiles library.nonenoneall rows
profile_admin_protectionsOptional extra lock (lock settings) on a profile’s administrator rights.nonenoneall rows
householdsHouseholds that group accounts and profiles.nonenoneall rows
profilesPeople using the Library, with their display, content limit and navigation settings.noneavatar_image_pathall rows
profile_credentialsProfile PIN hashes, needed to switch profile after a restore.nonenoneall rows
profile_grid_preferencesHow a profile sorts each grid.nonenoneall rows
profile_hero_settingsHow a profile wants the Home hero to behave.nonenoneall rows
profile_hero_plugin_switchesWhich plugins’ Home slides a profile switched off.nonenoneall rows
profile_notice_dismissalsNotices a profile has dismissed, so they do not return.nonenoneall rows
profile_sequence_preferencesWhether a profile shows missing items in a series or sequence.container_key (other, text)noneall rows
profile_view_policiesWhat a profile may do in View (access, sharing, documents).nonenoneall rows
profile_view_preferencesA profile’s View display preferences.nonenoneall rows
user_playback_settingsA profile’s playback settings.nonenoneall rows
client_devicesPaired devices and their capabilities (their tokens are not kept, so a device signs in again).nonenoneall rows
applicationsRegistered outside applications and what kind of access each has.nonenoneall rows
application_client_bindingsWhich client ids belong to which application.nonenoneall rows
application_credentialsHashed application credentials, which are access settings.nonenoneall rows
application_permission_grantsWhich permissions an application has been granted.nonenoneall rows
application_webhooksWebhook destinations an application has registered; the signing secret is protected with this computer’s Data Protection key, so the export writes it readable (portable column secret_ciphertext), or keeps it as it is when it cannot be read.nonenoneall rows
approval_recordsApprovals an administrator has given, with the statement approved.nonenoneall rows
plugin_lore_sourcesLore sources a person approved or rejected; sources only discovered but never decided on are rebuilt.nonenoneapproved_at IS NOT NULL OR rejected_at IS NOT NULL
plugin_library_limitsThe libraries an administrator limited a plugin or source to.nonenoneall rows
metadata_providersThe list of metadata providers and whether each is switched on.nonenoneall rows
provider_configProvider settings; rows marked secret are left out of a backup.nonenoneis_secret = 0
notification_settingsNotification channels and rules.nonenoneall rows
backup_settingsBackup schedule, destination and retention settings; the machine-encrypted destination logins are left out.nonethe EncryptedSecret property inside value_jsonall rows
doc_typesDocument types a person defined.nonenoneall rows
doc_field_definitionsCustom document fields a person defined.nonenoneall rows
doc_field_choicesThe choices offered by a document field.nonenoneall rows
doc_correspondentsCorrespondents (senders and recipients) a person defined for documents.nonenoneall rows
doc_rulesRules that file documents automatically.nonenoneall rows
doc_rule_conditionsThe conditions of a document filing rule.nonenoneall rows
doc_rule_actionsThe actions of a document filing rule.nonenoneall rows
tagsTags a person created for View items.nonenoneall rows
view_personal_spacesEach profile’s private View space.nonenoneall rows
view_sourcesFolders and devices a View space reads from.nonenoneall rows
view_source_policiesWhether a View source appears in the timeline.nonenoneall rows
view_devicesDevices that back up into a View space.nonenoneall rows
view_storage_labelsNames a person gave to View storage locations.nonenoneall rows
view_shared_libraryWhich library a household shares in View.nonenoneall rows
view_galleriesGalleries a person built in View.cover_item_id (local item), soundtrack_playlist_id (collection)noneall rows
view_gallery_itemsThe View items in a person-built gallery.item_id (local item)noneall rows
view_gallery_sharesWho a person-built View gallery is shared with.nonenoneall rows
doc_field_valuesValues a person entered in custom document fields.item_id (local item)noneall rows
local_item_tag_linksWhich tags a person put on which View item.item_id (local item)noneall rows
doc_item_detailsFiling details a person entered for a document (machine suggestions are rebuilt).item_id (local item)suggestions_jsonall rows
excluded_source_filesFiles a person removed from the library and chose to keep out, so a rescan does not add them back.nonetitle, excluded_by_nameall rows
collectionsCollections and playlists a person built; collections built from providers (universes and series) are rebuilt, but a person’s switches on them are kept: choice columns is_enabled, is_featured, display_overrides_json, for the rows not kept here.universe_id (other), parent_collection_id (collection)cover_artwork_path, background_artwork_path, banner_artwork_path, logo_artwork_pathcollection_type IN ('Custom', 'Playlist', 'PlaylistFolder', 'Smart')
collection_itemsThe titles in a person-built collection.work_id (work)nonecollection_id IN (SELECT id FROM collections WHERE collection_type IN ('Custom', 'Playlist', 'PlaylistFolder', 'Smart'))
collection_placementsWhere a person-built collection appears.nonenonecollection_id IN (SELECT id FROM collections WHERE collection_type IN ('Custom', 'Playlist', 'PlaylistFolder', 'Smart'))
collection_profile_audienceWhich profiles can see a person-built collection.nonenonecollection_id IN (SELECT id FROM collections WHERE collection_type IN ('Custom', 'Playlist', 'PlaylistFolder', 'Smart'))
collection_view_sourcesThe gallery or rule feeding a person-built collection.nonenonecollection_id IN (SELECT id FROM collections WHERE collection_type IN ('Custom', 'Playlist', 'PlaylistFolder', 'Smart'))
fictional_entity_user_overridesA person’s name and description corrections for a fictional thing, found again by its Wikidata id.entity_id (fictional thing)noneall rows
narrative_root_user_overridesA person’s name and description corrections for a universe (already keyed by its Wikidata id).nonenoneall rows
folder_first_checksThe first check of each newly added folder: a pause waiting for an administrator’s choice, and which folders have already passed, so a restore neither releases a paused folder nor starts the check again.nonenoneall rows
backup_restoresWhich backups were restored and when, with counts of what was put back and what is still waiting.nonenoneall rows
waiting_personal_stateState from a restored backup that is still waiting for its title, kept with the time it started waiting, so a backup taken before it attaches does not lose it or restart its waiting period.nonenoneall rows

Personal state tables (16)

TableWhyOwner columnPoints at the catalogueLeft out
reader_bookmarksBookmarks a person placed in a book.user_idasset_id (media asset)none
reader_highlightsHighlights and notes a person made in a book.user_idasset_id (media asset)none
reader_statisticsA person’s reading time and speed per book.user_idasset_id (media asset)none
audiobook_bookmarksBookmarks a person placed in an audiobook.profile_idwork_id (work), asset_id (media asset)none
audiobook_listen_historyWhat a person listened to in audiobooks.profile_idwork_id (work), asset_id (media asset)none
music_play_statsPlay counts and last played time per album or track.profile_idwork_id (work)none
consumption_historyWhat a person watched, read or listened to, with progress.profile_idasset_id (media asset)none
personal_activityA person’s own activity feed (what they played and for how long).profile_idwork_id (work), asset_id (media asset)artwork_key
player_device_statesWhere a person stopped on each device (playback positions).profile_idasset_id (media asset), work_id (work)player_session_id
user_statesA person’s progress and extended state per file.user_idasset_id (media asset)none
user_taste_profilesA person’s taste profile.user_idnonenone
profile_reactionsA person’s reactions (likes and dislikes) to titles and other items.profile_identity_id (other)none
profile_saved_itemsItems a person saved (favourites and watch later).profile_identity_id (other)none
profile_taste_overridesGenres and tastes a person switched on or off.profile_idnonenone
profile_work_preferencesA person’s own tags and preferences for a title.profile_idwork_id (work)none
profile_person_preferencesA person’s own tags and preferences for a creator or performer.profile_idperson_id (person)none

Catalogue tables (72)

The rows a rescan rebuilds are not kept; the tables below say where a person’s own choices or corrections on them are. The search indexes (search_index, local_item_search, artwork_asset_search) are listed; their hidden storage tables are not.

TableWhy
worksTitles; rebuilt by a rescan, but display changes a person typed are kept: choice column display_overrides_json. Matching and placement choices (locked or rejected matches, a locked series placement) are left to the rescan.
editionsEditions of a title; rebuilt by a rescan.
media_assetsThe files themselves; rebuilt by a rescan.
media_asset_chaptersChapters read from a file.
media_asset_coverageWhich part of a work a file covers; rebuilt by a rescan, including coverage a person set by hand (moves are left to the rescan).
media_artwork_writebackArtwork write-back state for files.
media_inspection_holdsInspection holds on files.
playback_inspection_cacheCached file inspection results.
playback_segmentsIntro, credits and recap segments found in files.
audio_fingerprintsAudio fingerprints computed from files.
file_hash_cacheCached file hashes.
canonical_valuesWinning metadata values, rebuilt from claims.
canonical_value_arraysWinning list-type metadata values, rebuilt from claims.
metadata_claimsWhat each provider said about a title; rebuilt by enrichment. Current claims a person made themselves are kept as corrections: is_current = 1 and either locked by a person or written by the manual-edit provider. entity_id is a reference (work, edition, person or file).
bridge_idsExternal ids found for a title.
entity_eventsIdentity and enrichment history of a title.
entity_field_changesField changes recorded in a title’s history.
entity_relationshipsRelationships between titles, people and characters from Wikidata.
entity_relationship_qualifiersDetails of those relationships.
entity_capability_statesWhat enrichment has done for each title.
identity_jobsIdentification and enrichment jobs.
unmatched_assetsFiles the Engine could not match, with when each is tried again and whether it was kept as it is.
retail_match_candidatesRetail match candidates for an identification job.
wikidata_bridge_candidatesWikidata candidates for an identification job.
deferred_enrichment_queueTitles waiting for enrichment.
enrichment_refresh_scheduleWhen each title is next refreshed.
ai_feature_artifactsResults of optional AI features.
pending_person_signalsPeople spotted in files, waiting to be matched.
review_queueNeeds Review items; a rescan raises them again.
artwork_assetsArtwork files known to the Library.
artwork_asset_contextWhere each artwork file is used.
artwork_asset_searchSearch index over artwork.
entity_artwork_linksWhich artwork belongs to which title or person. A cover a person picked from a provider is kept as a correction (is_user_override = 1): entity_id is a reference to the title or person, and artwork_asset_id is a reference to the artwork, found again by its provider, source address and provider reference (never its content hash, because artwork files are not in a backup). A link to an uploaded artwork, or one with no source address and no provider reference, cannot be found again, so the correction condition leaves it out (it is simply absent, not counted as dropped).
entity_assetsImages attached to titles and people. A cover a person chose from a provider is kept as a correction (is_user_override = 1, a provider other than user_upload, an http image address), found again by provider and image address; entity_id is a reference, and the row id, local file paths and export flags are left out. Uploads are not kept.
image_cacheCached downloaded images (not kept: a cache row has no owner to attach a choice to).
image_cache_sourcesWhere cached images came from.
character_portraitsCharacter portraits.
character_performer_linksWhich performer plays which character.
fictional_entitiesCharacters, places and other fictional things from Wikidata.
fictional_entity_work_linksWhere a fictional thing appears.
narrative_rootsUniverses and franchises from Wikidata.
personsCreators and performers; rebuilt from files and providers, but a person’s display changes are kept: choice column display_overrides_json.
person_aliasesPseudonyms of people.
person_group_membersMembers of bands and groups.
person_media_linksWhich person worked on which file.
person_rolesThe roles a person has.
collection_relationshipsRelationships of provider-built collections.
series_manifest_hydrationsSeries information fetched from providers.
series_manifest_itemsThe items of a series fetched from providers.
series_membersSeries membership fetched from providers.
tv_episode_creditsEpisode credits from providers.
audiobook_chapter_title_overridesChapter titles; the ones a person renamed are kept as corrections (title_source = 'Override', with work_id and asset_id as references), the ones read from the file are rebuilt.
subtitle_index_assetsSubtitle indexing state per file.
subtitle_index_tracksSubtitle tracks that were indexed.
subtitle_linesIndexed subtitle text.
subtitle_name_tagsCharacters found in subtitle text.
text_tracksSubtitle and lyric tracks found for files.
text_track_rejectionsLyrics and subtitle results a person marked wrong for a file, so they are not picked again.
local_itemsPhotos, documents and other View items found on disk; a person’s own choices on them (favourite, hidden, archived, trashed, capture date they set) are kept: choice columns favorite, hidden, archived_at, trashed_at, captured_at_user_override, captured_at (the date only while the person set it).
local_filesFiles behind View items.
local_file_sourcesWhere View files were found.
local_item_filesWhich files belong to which View item.
local_item_metadataMetadata read from View items; a place a person set on a photo and a description they wrote are kept as choices on the item named by item_id: choice columns description, location_user_override, and the place columns latitude, longitude, location_name, location_city, location_region, location_country, location_country_code, location_source (only while location_user_override is set). The embedded coordinates, camera and lens details and document text are never kept.
local_item_annotationsLabels and detections found on View items; a name a person gave, or a detection they reviewed, is kept as a correction (source = 'user_assignment' OR reviewed_at IS NOT NULL, item_id as a reference), without the machine’s own details (confidence, model_name, model_version, provenance_json).
local_item_searchSearch index over View items.
local_item_search_keysRow numbers linking View items to their search index entries.
doc_item_field_sourcesWhere document field values came from (tied to View items).
view_shared_assetsView items promoted to the shared library.
personal_activity_artworkCached pictures for the activity feed.
provider_response_cacheCached provider answers.
resolver_cacheCached identification answers.
search_results_cacheCached search answers.
qid_labelsCached Wikidata labels.
search_indexSearch index over titles.

Transient tables (68)

None of these are kept.

TableWhy
auth_sessionsSign-in sessions.
client_tokensDevice and application tokens.
plugin_display_contributionsLines and badges plugins publish for the Library’s pages; each plugin publishes them again when it next runs.
plugin_outboxMessages a plugin queued to send to an outside service; a queued share is worth little after a restore.
plugin_profile_connectionsAccounts a person linked to a plugin for their own profile; the values are protected with this computer’s key, so each person links again after a restore.
service_credentialsService credentials; they are issued again.
account_invitationsOne-time invitation tokens.
password_recovery_codesOne-time recovery codes.
password_reset_challengesOne-time reset links.
two_step_challengesOne-time sign-in challenges.
setup_codesOne-time setup codes.
grant_admin_unlocksTemporary administrator unlocks.
profile_admin_unlocksTemporary administrator unlocks on a profile.
device_pairing_requestsPairing requests from devices.
authorization_audit_eventsAccess change audit log.
identity_audit_eventsSign-in audit log.
system_activitySystem activity log.
transaction_logChange log.
ingestion_logIngestion run log.
folder_organise_logFolder organise run log.
folder_kind_changesPlans of the change-what-this-folder-is jobs.
scheduled_task_runsScheduled task run log.
media_operationsMedia operation queue.
media_operation_eventsMedia operation log.
media_file_write_intentsPending file write-back jobs.
folder_first_check_filesThe files counted in a folder’s first check.
file_write_runsWhen the file write-back schedule last ran.
encode_jobsEncode job queue.
alignment_jobsBook and audiobook alignment job queue.
adaptive_hls_packagesStreaming copies prepared for playback.
offline_variantsOffline copies prepared for devices.
device_downloadsWhat each device has downloaded.
device_photo_downloadsWhich photos each device has downloaded.
playback_telemetry_sessionsPlayback telemetry.
audiobook_listen_active_segmentsAudiobook listening in progress.
music_play_active_segmentsMusic playing in progress.
import_uploadsResumable uploads into the Import folder in progress, with the hidden working file each is written into.
import_upload_piecesWhich pieces of each upload into the Import folder have arrived.
player_sessionsLive player sessions.
player_queue_itemsLive player queues.
companion_sessionsCompanion screen sessions.
personal_status_commandsRecent status changes kept for undo.
ai_usage_recordsAI usage telemetry.
provider_healthProvider health telemetry.
provider_connection_checksProvider connection checks.
application_eventsApplication event outbox.
application_webhook_attemptsWebhook delivery attempts.
application_webhook_deliveriesWebhook delivery queue.
plugin_event_cursorsHow far each plugin has read events.
plugin_recordsPlugin working data.
plugin_activityWhat plugins did and how they failed; fills again as they run.
plugin_lore_entitiesLore entities found by plugins.
plugin_lore_relationshipsLore relationships found by plugins.
media_editor_commitsEditor save records, kept for repeat requests.
media_editor_commit_itemsItems of editor save records.
media_editor_commit_artworkArtwork of editor save records.
media_editor_edition_artwork_commitsEdition artwork save records.
media_editor_preferred_artwork_commitsPreferred artwork save records.
media_editor_music_pairing_commitsMusic pairing save records.
media_editor_music_pairing_commit_itemsItems of music pairing save records.
view_shared_contributionsShared library contribution requests.
view_shared_contribution_itemsItems of shared library contribution requests.
view_shared_contribution_eventsShared library contribution log.
view_shared_transfersShared library transfers in progress.
view_intake_filesPhotos and videos waiting in, or refused from, the Import area; the files themselves are never backed up.
view_duplicate_groupsDuplicate photo groups found by the cleanup job; found again by the next run.
view_tidy_jobsProgress of Tidy this folder runs; a restored library is simply tidied again.
view_tidy_movesThe rename ledger of a Tidy this folder run; it only matters while the run is going.
view_tidy_schedulesWhich folders are tidied every night; set again after a restore.
notificationsNotifications already sent.
notification_channel_stateNotification channel health.
notification_rate_marksNotification rate limiting marks.
backup_copiesRecords of backup copies already made.
ui_settings_cacheCached Dashboard settings.
onboarding_workflowsFirst-run setup progress.
onboarding_stepsFirst-run setup steps.
onboarding_sessionsFirst-run setup sessions.
onboarding_restore_operationsRestore operations started from first-run setup.
data_store_upgradesWhich data store upgrade steps have run.
storage_metadataData store bookkeeping.

The Vault

The Vault keeps its settings and each person’s own state in a backup, and never its catalogue (a rescan rebuilds it). There is no Vault-specific backup step: the Vault folds into the one mechanism above. A Vault video that a person’s state points at is written to titles.json only as its ids, its Stash-box record (stash_box_id, written server|record), its running time, its edition and its file fingerprints, flagged withheld, with no name, parent name or year; the same Stash-box record is read from the live video, so after a rescan a Vault bookmark finds the same video even when the file (and so its fingerprint) is different, and with no Stash-box record the exact file fingerprint still decides. A person’s corrections and choices on a Vault title are left out. A restore brings the Vault libraries back empty, with Vault mode settings, PIN hashes and grants. The saved Vault bookmarks wait like any other state, marked withheld, and are counted only when asked (the Vault settings page asks, through GET /api/v1/vault/restored-bookmarks, which counts the withheld waiting rows and never names a title). The earlier Vault-only scrub, restore note and bookmark re-link (and the vault_pending_bookmarks table, dropped by upgrade step 0040) are gone.

TableKindWhy
profile_nsfw_settingsConfigurationHow each profile’s Vault mode is protected and when it turns off.
vault_librariesConfigurationWhich libraries are Vault libraries and how they look things up.
vault_section_grantsConfigurationWhich Vault sections each profile may open.
bookmark_packsPersonal stateBookmark packs a person imported for a title.
bookmark_pack_itemsPersonal stateThe bookmarks inside a pack (owned through the pack’s profile).
nsfw_session_unlocksTransientTemporary Vault mode sessions.
vault_movesTransientMove to Vault jobs; a job in flight is not worth restoring, the files are the record.
vault_move_itemsTransientThe items of a Move to Vault job.
vault_activityCatalogueQuiet list of Vault ingestion activity.
vault_artworkCatalogueVault artwork.
vault_asset_performersCatalogueVault performers per video.
vault_asset_studiosCatalogueVault studios per video.
vault_asset_tagsCatalogueVault tags per video.
vault_field_sourcesCatalogueWhere each Vault field came from.
vault_file_fingerprintsCatalogueVault file fingerprints.
vault_matchesCatalogueVault lookup matches.
vault_performersCatalogueVault performers.
vault_studiosCatalogueVault studios.
vault_tagsCatalogueVault tags.
vault_galleriesCatalogueVault image galleries.
vault_gallery_itemsCatalogueImages inside Vault galleries.
vault_image_foldersCatalogueVault image folders.
vault_image_itemsCatalogueVault images.
vault_image_librariesCatalogueVault image libraries.
vault_image_tagsCatalogueVault image tags.

How to add a table

When you add a table through a numbered data store upgrade step, add one line for it in BackupTableClassification, choosing Configuration, Personal state, Catalogue or Transient, with a one-sentence reason. The guardrail test fails and names the table if you forget. Then:

  • For Configuration or Personal state, list every column that holds the id of a catalogue row as a catalogue reference, and every secret or machine-bound column as an excluded column. Personal state also names its owner column.
  • If only some rows are kept, give the rule a keep condition.
  • If a secret sits inside a JSON column, name the JSON property to remove. If a person’s own choice sits on a Catalogue row, name the choice columns on that Catalogue rule (and gate a column by its flag when only a person-set value counts). If a person makes whole rows on a Catalogue table, give it a correction condition, its catalogue references and its machine-only excluded columns. If a Configuration column is sealed with this computer’s key, name it as a portable column and extend the Engine’s codec (it refuses anything it does not know).
  • Add the table to the matching list on this page. A second test fails if a classified table is missing here.
  • If unsure whether a column holds a secret, exclude it and ask.