diff --git a/src/common/pinstate.h b/src/common/pinstate.h
index 5fb267168..053e7062f 100644
--- a/src/common/pinstate.h
+++ b/src/common/pinstate.h
@@ -58,7 +58,9 @@ enum class PinState {
      * Also known as "unpinned". Unpinned hydrated files shall be dehydrated
      * as soon as possible.
      *
-     * If a unpinned file becomes hydrated its pin state changes to unspecified.
+     * If a unpinned file becomes hydrated (such as due to an implicit hydration
+     * where the user requested access to the file's data) its pin state changes
+     * to Unspecified.
      */
     OnlineOnly = 2,
 
diff --git a/src/csync/csync.h b/src/csync/csync.h
index 13125cc8e..cc9e1c1a6 100644
--- a/src/csync/csync.h
+++ b/src/csync/csync.h
@@ -142,16 +142,26 @@ enum ItemType {
 
     /** A ItemTypeVirtualFile that wants to be hydrated.
      *
-     * Actions may put this in the db as a request to a future sync.
+     * Actions may put this in the db as a request to a future sync, such as
+     * implicit hydration (when the user wants to access file data) when using
+     * suffix vfs. For pin-state driven hydrations changing the database is
+     * not necessary.
+     *
      * For some vfs plugins the placeholder files on disk may be marked for
-     * dehydration (like with a file attribute) and then the local discovery
+     * (de-)hydration (like with a file attribute) and then the local discovery
      * will return this item type.
+     *
+     * The discovery will also use this item type to mark entries for hydration
+     * if an item's pin state mandates it, such as when encountering a AlwaysLocal
+     * file that is dehydrated.
      */
     ItemTypeVirtualFileDownload = 5,
 
     /** A ItemTypeFile that wants to be dehydrated.
      *
-     * May exist in db or local files, similar to ItemTypeVirtualFileDownload.
+     * Similar to ItemTypeVirtualFileDownload, but there's currently no situation
+     * where it's stored in the database since there is no action that triggers a
+     * file dehydration without changing the pin state.
      */
     ItemTypeVirtualFileDehydration = 6,
 };
diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp
index a409b613c..69ade7872 100644
--- a/src/gui/accountsettings.cpp
+++ b/src/gui/accountsettings.cpp
@@ -762,14 +762,9 @@ void AccountSettings::slotSetCurrentFolderAvailability(PinState state)
     if (!selected.isValid() || !folder)
         return;
 
-    // similar to socket api: set pin state, wipe sub pin-states and sync
+    // similar to socket api: sets pin state recursively and sync
     folder->setNewFilesAreVirtual(state == PinState::OnlineOnly);
-
-    if (state == PinState::AlwaysLocal) {
-        folder->downloadVirtualFile("");
-    } else {
-        folder->dehydrateFile("");
-    }
+    folder->scheduleThisFolderSoon();
 }
 
 void AccountSettings::showConnectionLabel(const QString &message, QStringList errors)
diff --git a/src/gui/application.cpp b/src/gui/application.cpp
index 63edc4b00..5930096b5 100644
--- a/src/gui/application.cpp
+++ b/src/gui/application.cpp
@@ -793,7 +793,7 @@ void Application::openVirtualFile(const QString &filename)
         return;
     }
     QString relativePath = QDir::cleanPath(filename).mid(folder->cleanPath().length() + 1);
-    folder->downloadVirtualFile(relativePath);
+    folder->implicitlyHydrateFile(relativePath);
     QString normalName = filename.left(filename.size() - virtualFileExt.size());
     auto con = QSharedPointer<QMetaObject::Connection>::create();
     *con = QObject::connect(folder, &Folder::syncFinished, [con, normalName] {
diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp
index 633cc9209..8ca941f28 100644
--- a/src/gui/folder.cpp
+++ b/src/gui/folder.cpp
@@ -590,58 +590,36 @@ void Folder::slotWatchedPathChanged(const QString &path)
     scheduleThisFolderSoon();
 }
 
-void Folder::downloadVirtualFile(const QString &_relativepath)
+void Folder::implicitlyHydrateFile(const QString &relativepath)
 {
-    qCInfo(lcFolder) << "Download virtual file: " << _relativepath;
-    auto relativepath = _relativepath.toUtf8();
+    qCInfo(lcFolder) << "Implicitly hydrate virtual file:" << relativepath;
 
     // Set in the database that we should download the file
     SyncJournalFileRecord record;
-    _journal.getFileRecord(relativepath, &record);
-    if (!record.isValid() && !relativepath.isEmpty())
+    _journal.getFileRecord(relativepath.toUtf8(), &record);
+    if (!record.isValid()) {
+        qCInfo(lcFolder) << "Did not find file in db";
         return;
-    if (record._type == ItemTypeVirtualFile) {
-        record._type = ItemTypeVirtualFileDownload;
-        _journal.setFileRecord(record);
-        // Make sure we go over that file during the discovery even if
-        // no actual remote discovery would be necessary
-        _journal.schedulePathForRemoteDiscovery(relativepath);
-    } else if (record._type == ItemTypeDirectory || relativepath.isEmpty()) {
-        _journal.markVirtualFileForDownloadRecursively(relativepath);
-    } else {
-        qCWarning(lcFolder) << "Invalid existing record " << record._type << " for file " << _relativepath;
+    }
+    if (!record.isVirtualFile()) {
+        qCInfo(lcFolder) << "The file is not virtual";
+        return;
+    }
+    record._type = ItemTypeVirtualFileDownload;
+    _journal.setFileRecord(record);
+
+    // Change the file's pin state if it's contradictory to being hydrated
+    // (suffix-virtual file's pin state is stored at the hydrated path)
+    QString pinPath = relativepath;
+    if (_vfs->mode() == Vfs::WithSuffix && pinPath.endsWith(_vfs->fileSuffix()))
+        pinPath.chop(_vfs->fileSuffix().size());
+    const auto pin = _vfs->pinState(pinPath);
+    if (pin && *pin == PinState::OnlineOnly) {
+        _vfs->setPinState(pinPath, PinState::Unspecified);
     }
 
-    // Schedule a sync (Folder man will start the sync in a few ms)
-    slotScheduleThisFolder();
-}
-
-void Folder::dehydrateFile(const QString &_relativepath)
-{
-    qCInfo(lcFolder) << "Dehydrating file: " << _relativepath;
-    auto relativepath = _relativepath.toUtf8();
-
-    auto markForDehydration = [&](SyncJournalFileRecord rec) {
-        if (rec._type != ItemTypeFile)
-            return;
-        rec._type = ItemTypeVirtualFileDehydration;
-        _journal.setFileRecord(rec);
-        _localDiscoveryTracker->addTouchedPath(relativepath);
-    };
-
-    SyncJournalFileRecord record;
-    _journal.getFileRecord(relativepath, &record);
-    if (!record.isValid() && !relativepath.isEmpty())
-        return;
-    if (record._type == ItemTypeFile) {
-        markForDehydration(record);
-    } else if (record._type == ItemTypeDirectory || relativepath.isEmpty()) {
-        _journal.getFilesBelowPath(relativepath, markForDehydration);
-    } else {
-        qCWarning(lcFolder) << "Invalid existing record " << record._type << " for file " << _relativepath;
-    }
-
-    // Schedule a sync (Folder man will start the sync in a few ms)
+    // Add to local discovery
+    schedulePathForLocalDiscovery(relativepath);
     slotScheduleThisFolder();
 }
 
@@ -684,7 +662,12 @@ bool Folder::newFilesAreVirtual() const
 
 void Folder::setNewFilesAreVirtual(bool enabled)
 {
-    _vfs->setPinState(QString(), enabled ? PinState::OnlineOnly : PinState::AlwaysLocal);
+    const auto newPin = enabled ? PinState::OnlineOnly : PinState::AlwaysLocal;
+    _vfs->setPinState(QString(), newPin);
+
+    // We don't actually need discovery, but it's important to recurse
+    // into all folders, so the changes can be applied.
+    slotNextSyncFullLocalDiscovery();
 }
 
 bool Folder::supportsSelectiveSync() const
diff --git a/src/gui/folder.h b/src/gui/folder.h
index d836952de..a88ead00d 100644
--- a/src/gui/folder.h
+++ b/src/gui/folder.h
@@ -332,21 +332,21 @@ public slots:
     void slotWatchedPathChanged(const QString &path);
 
     /**
-     * Mark a virtual file as being ready for download, and start a sync.
-     * relativepath is the path to the file (including the extension)
-     * Passing a folder means that all contained virtual items shall be downloaded.
-     * A relative path of "" downloads everything.
-     */
-    void downloadVirtualFile(const QString &relativepath);
-
-    /**
-     * Turn a regular file into a dehydrated placeholder.
+     * Mark a virtual file as being requested for download, and start a sync.
      *
-     * relativepath is the path to the file
-     * It's allowed to pass a path to a folder: all contained files will be dehydrated.
-     * A relative path of "" dehydrates everything.
+     * "implicit" here means that this download request comes from the user wanting
+     * to access the file's data. The user did not change the file's pin state.
+     * If the file is currently OnlineOnly its state will change to Unspecified.
+     *
+     * The download request is stored by setting ItemTypeVirtualFileDownload
+     * in the database. This is necessary since the hydration is not driven by
+     * the pin state.
+     *
+     * relativepath is the folder-relative path to the file (including the extension)
+     *
+     * Note, passing directories is not supported. Files only.
      */
-    void dehydrateFile(const QString &relativepath);
+    void implicitlyHydrateFile(const QString &relativepath);
 
     /** Adds the path to the local discovery list
      *
diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp
index 3e6291815..b96619d40 100644
--- a/src/gui/socketapi.cpp
+++ b/src/gui/socketapi.cpp
@@ -729,8 +729,9 @@ void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketLi
         auto pinPath = data.folderRelativePathNoVfsSuffix();
         data.folder->vfs().setPinState(pinPath, PinState::AlwaysLocal);
 
-        // Trigger the recursive download
-        data.folder->downloadVirtualFile(data.folderRelativePath);
+        // Trigger sync
+        data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
+        data.folder->scheduleThisFolderSoon();
     }
 }
 
@@ -748,8 +749,9 @@ void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener
         auto pinPath = data.folderRelativePathNoVfsSuffix();
         data.folder->vfs().setPinState(pinPath, PinState::OnlineOnly);
 
-        // Trigger recursive dehydration
-        data.folder->dehydrateFile(data.folderRelativePath);
+        // Trigger sync
+        data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
+        data.folder->scheduleThisFolderSoon();
     }
 }
 
diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp
index 20f566f0d..5cb62bbc1 100644
--- a/src/libsync/discovery.cpp
+++ b/src/libsync/discovery.cpp
@@ -88,7 +88,9 @@ void ProcessDirectoryJob::process()
             auto name = pathU8.isEmpty() ? rec._path : QString::fromUtf8(rec._path.constData() + (pathU8.size() + 1));
             if (rec.isVirtualFile() && isVfsWithSuffix())
                 chopVirtualFileSuffix(name);
-            entries[name].dbEntry = rec;
+            auto &dbEntry = entries[name].dbEntry;
+            dbEntry = rec;
+            setupDbPinStateActions(dbEntry);
         })) {
         dbError();
         return;
@@ -1456,4 +1458,30 @@ void ProcessDirectoryJob::computePinState(PinState parentState)
     }
 }
 
+void ProcessDirectoryJob::setupDbPinStateActions(SyncJournalFileRecord &record)
+{
+    // Only suffix-vfs uses the db for pin states.
+    // Other plugins will set localEntry._type according to the file's pin state.
+    if (!isVfsWithSuffix())
+        return;
+
+    QByteArray pinPath = record._path;
+    if (record.isVirtualFile()) {
+        const auto suffix = _discoveryData->_syncOptions._vfs->fileSuffix().toUtf8();
+        if (pinPath.endsWith(suffix))
+            pinPath.chop(suffix.size());
+    }
+    auto pin = _discoveryData->_statedb->internalPinStates().rawForPath(pinPath);
+    if (!pin || *pin == PinState::Inherited)
+        pin = _pinState;
+
+    // OnlineOnly hydrated files want to be dehydrated
+    if (record._type == ItemTypeFile && *pin == PinState::OnlineOnly)
+        record._type = ItemTypeVirtualFileDehydration;
+
+    // AlwaysLocal dehydrated files want to be hydrated
+    if (record._type == ItemTypeVirtualFile && *pin == PinState::AlwaysLocal)
+        record._type = ItemTypeVirtualFileDownload;
+}
+
 }
diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h
index 6cc059bbd..20d27b73d 100644
--- a/src/libsync/discovery.h
+++ b/src/libsync/discovery.h
@@ -198,13 +198,25 @@ private:
       */
     bool runLocalQuery();
 
-    /** Sets _pinState
+    /** Sets _pinState, the directory's pin state
      *
      * If the folder exists locally its state is retrieved, otherwise the
      * parent's pin state is inherited.
      */
     void computePinState(PinState parentState);
 
+    /** Adjust record._type if the db pin state suggests it.
+     *
+     * If the pin state is stored in the database (suffix vfs only right now)
+     * its effects won't be seen in localEntry._type. Instead the effects
+     * should materialize in dbEntry._type.
+     *
+     * This function checks whether the combination of file type and pin
+     * state suggests a hydration or dehydration action and changes the
+     * _type field accordingly.
+     */
+    void setupDbPinStateActions(SyncJournalFileRecord &record);
+
     QueryMode _queryServer = QueryMode::NormalQuery;
     QueryMode _queryLocal = QueryMode::NormalQuery;
 
@@ -244,7 +256,7 @@ private:
     PathTuple _currentFolder;
     bool _childModified = false; // the directory contains modified item what would prevent deletion
     bool _childIgnored = false; // The directory contains ignored item that would prevent deletion
-    PinState _pinState = PinState::Unspecified; // The directory's pin-state, see setParentPinState()
+    PinState _pinState = PinState::Unspecified; // The directory's pin-state, see computePinState()
 
 signals:
     void finished();
diff --git a/test/testsyncvirtualfiles.cpp b/test/testsyncvirtualfiles.cpp
index b32889505..afb737c6e 100644
--- a/test/testsyncvirtualfiles.cpp
+++ b/test/testsyncvirtualfiles.cpp
@@ -64,8 +64,9 @@ QSharedPointer<Vfs> setupVfs(FakeFolder &folder)
     auto suffixVfs = QSharedPointer<Vfs>(createVfsFromPlugin(Vfs::WithSuffix).release());
     folder.switchToVfs(suffixVfs);
 
-    // Using this directly doesn't recursively unpin everything
-    folder.syncJournal().internalPinStates().setForPath("", PinState::OnlineOnly);
+    // Using this directly doesn't recursively unpin everything and instead leaves
+    // the files in the hydration that that they start with
+    folder.syncJournal().internalPinStates().setForPath("", PinState::Unspecified);
 
     return suffixVfs;
 }
@@ -923,7 +924,7 @@ private slots:
         setPin("online", PinState::OnlineOnly);
         setPin("unspec", PinState::Unspecified);
 
-        // Test 1: root is OnlineOnly
+        // Test 1: root is Unspecified
         fakeFolder.remoteModifier().insert("file1");
         fakeFolder.remoteModifier().insert("online/file1");
         fakeFolder.remoteModifier().insert("local/file1");
@@ -935,7 +936,7 @@ private slots:
         QVERIFY(fakeFolder.currentLocalState().find("local/file1"));
         QVERIFY(fakeFolder.currentLocalState().find("unspec/file1.nextcloud"));
 
-        // Test 2: root is AlwaysLocal
+        // Test 2: change root to AlwaysLocal
         setPin("", PinState::AlwaysLocal);
 
         fakeFolder.remoteModifier().insert("file2");
@@ -949,8 +950,32 @@ private slots:
         QVERIFY(fakeFolder.currentLocalState().find("local/file2"));
         QVERIFY(fakeFolder.currentLocalState().find("unspec/file2.nextcloud"));
 
-        // file1 is unchanged
+        // root file1 was hydrated due to its new pin state
+        QVERIFY(fakeFolder.currentLocalState().find("file1"));
+
+        // file1 is unchanged in the explicitly pinned subfolders
+        QVERIFY(fakeFolder.currentLocalState().find("online/file1.nextcloud"));
+        QVERIFY(fakeFolder.currentLocalState().find("local/file1"));
+        QVERIFY(fakeFolder.currentLocalState().find("unspec/file1.nextcloud"));
+
+        // Test 3: change root to OnlineOnly
+        setPin("", PinState::OnlineOnly);
+
+        fakeFolder.remoteModifier().insert("file3");
+        fakeFolder.remoteModifier().insert("online/file3");
+        fakeFolder.remoteModifier().insert("local/file3");
+        fakeFolder.remoteModifier().insert("unspec/file3");
+        QVERIFY(fakeFolder.syncOnce());
+
+        QVERIFY(fakeFolder.currentLocalState().find("file3.nextcloud"));
+        QVERIFY(fakeFolder.currentLocalState().find("online/file3.nextcloud"));
+        QVERIFY(fakeFolder.currentLocalState().find("local/file3"));
+        QVERIFY(fakeFolder.currentLocalState().find("unspec/file3.nextcloud"));
+
+        // root file1 was dehydrated due to its new pin state
         QVERIFY(fakeFolder.currentLocalState().find("file1.nextcloud"));
+
+        // file1 is unchanged in the explicitly pinned subfolders
         QVERIFY(fakeFolder.currentLocalState().find("online/file1.nextcloud"));
         QVERIFY(fakeFolder.currentLocalState().find("local/file1"));
         QVERIFY(fakeFolder.currentLocalState().find("unspec/file1.nextcloud"));