diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index 4c10bf5e66..050df181fb 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -877,6 +877,7 @@ dependencies = [
  "reqwest 0.13.4",
  "rodio",
  "rubato",
+ "rusqlite",
  "serde",
  "serde_json",
  "serde_yaml",
@@ -2333,6 +2334,18 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
 
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
 [[package]]
 name = "fastrand"
 version = "2.4.1"
@@ -3062,6 +3075,15 @@ dependencies = [
  "foldhash 0.2.0",
 ]
 
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
 [[package]]
 name = "heapify"
 version = "0.2.0"
@@ -4185,6 +4207,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "libsqlite3-sys"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.15"
@@ -7275,6 +7307,20 @@ dependencies = [
  "windowfunctions",
 ]
 
+[[package]]
+name = "rusqlite"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
+dependencies = [
+ "bitflags 2.11.1",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
 [[package]]
 name = "rust-ini"
 version = "0.21.3"
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index 5f2f84a384..89f467fc92 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -79,6 +79,7 @@ png = "0.18"
 zip = "8"
 sherpa-onnx = "1.12"
 regex = "1"
+rusqlite = "0.37"
 axum = "0.8"
 rodio = "0.22"
 earshot = "1.0"
diff --git a/desktop/src-tauri/src/commands/legacy_storage.rs b/desktop/src-tauri/src/commands/legacy_storage.rs
new file mode 100644
index 0000000000..e1cbd334c2
--- /dev/null
+++ b/desktop/src-tauri/src/commands/legacy_storage.rs
@@ -0,0 +1,289 @@
+use std::path::{Path, PathBuf};
+
+use rusqlite::{Connection, OpenFlags};
+use serde::Serialize;
+
+const BUZZ_RELEASE_IDENTIFIER_PREFIX: &str = "xyz.block.buzz.app";
+const SPROUT_RELEASE_IDENTIFIER: &str = "xyz.block.sprout.app";
+const BUZZ_DEV_IDENTIFIER_PREFIX: &str = "xyz.block.buzz.app.dev";
+const SPROUT_DEV_IDENTIFIER_PREFIX: &str = "xyz.block.sprout.app.dev";
+
+const SPROUT_WORKSPACES_KEY: &str = "sprout-workspaces";
+const SPROUT_ACTIVE_WORKSPACE_KEY: &str = "sprout-active-workspace-id";
+const SPROUT_ONBOARDING_COMPLETE_PREFIX: &str = "sprout-onboarding-complete.v1:";
+
+#[derive(Debug, Default, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LegacyWorkspaceStorage {
+    workspaces: Option<String>,
+    active_workspace_id: Option<String>,
+    onboarding_completions: Vec<LegacyOnboardingCompletion>,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LegacyOnboardingCompletion {
+    pubkey: String,
+    value: String,
+}
+
+fn legacy_identifier(current_identifier: &str) -> Option<String> {
+    if current_identifier.starts_with(BUZZ_DEV_IDENTIFIER_PREFIX) {
+        Some(current_identifier.replacen(
+            BUZZ_DEV_IDENTIFIER_PREFIX,
+            SPROUT_DEV_IDENTIFIER_PREFIX,
+            1,
+        ))
+    } else if current_identifier.starts_with(BUZZ_RELEASE_IDENTIFIER_PREFIX) {
+        Some(current_identifier.replacen(
+            BUZZ_RELEASE_IDENTIFIER_PREFIX,
+            SPROUT_RELEASE_IDENTIFIER,
+            1,
+        ))
+    } else {
+        None
+    }
+}
+
+#[cfg(target_os = "macos")]
+fn legacy_webkit_data_root(identifier: &str) -> Option<PathBuf> {
+    dirs::home_dir().map(|home| {
+        home.join("Library")
+            .join("WebKit")
+            .join(identifier)
+            .join("WebsiteData")
+    })
+}
+
+#[cfg(not(target_os = "macos"))]
+fn legacy_webkit_data_root(_identifier: &str) -> Option<PathBuf> {
+    None
+}
+
+fn collect_local_storage_databases(root: &Path, databases: &mut Vec<PathBuf>) {
+    let Ok(entries) = std::fs::read_dir(root) else {
+        return;
+    };
+
+    for entry in entries.flatten() {
+        let path = entry.path();
+        if path.is_dir() {
+            collect_local_storage_databases(&path, databases);
+        } else if path.file_name().and_then(|name| name.to_str()) == Some("localstorage.sqlite3") {
+            databases.push(path);
+        }
+    }
+}
+
+fn decode_webkit_local_storage_value(bytes: &[u8]) -> Option<String> {
+    if bytes.is_empty() {
+        return Some(String::new());
+    }
+
+    if bytes.len() % 2 == 0 {
+        let has_utf16_ascii_shape = bytes.chunks_exact(2).any(|chunk| chunk[1] == 0);
+        if has_utf16_ascii_shape {
+            let utf16: Vec<u16> = bytes
+                .chunks_exact(2)
+                .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
+                .collect();
+            if let Ok(value) = String::from_utf16(&utf16) {
+                return Some(value.trim_end_matches('\0').to_string());
+            }
+        }
+    }
+
+    String::from_utf8(bytes.to_vec()).ok()
+}
+
+fn read_legacy_workspace_storage_db(path: &Path) -> Result<LegacyWorkspaceStorage, String> {
+    let conn = Connection::open_with_flags(
+        path,
+        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
+    )
+    .map_err(|e| format!("open legacy localStorage db: {e}"))?;
+
+    let mut stmt = conn
+        .prepare(
+            "SELECT key, value FROM ItemTable \
+             WHERE key = ?1 OR key = ?2 OR key LIKE ?3",
+        )
+        .map_err(|e| format!("prepare legacy localStorage query: {e}"))?;
+    let mut rows = stmt
+        .query([
+            SPROUT_WORKSPACES_KEY,
+            SPROUT_ACTIVE_WORKSPACE_KEY,
+            &format!("{SPROUT_ONBOARDING_COMPLETE_PREFIX}%"),
+        ])
+        .map_err(|e| format!("query legacy localStorage: {e}"))?;
+
+    let mut result = LegacyWorkspaceStorage::default();
+    while let Some(row) = rows
+        .next()
+        .map_err(|e| format!("read legacy localStorage row: {e}"))?
+    {
+        let key: String = row
+            .get(0)
+            .map_err(|e| format!("read legacy localStorage key: {e}"))?;
+        let value_bytes: Vec<u8> = row
+            .get(1)
+            .map_err(|e| format!("read legacy localStorage value: {e}"))?;
+        let Some(value) = decode_webkit_local_storage_value(&value_bytes) else {
+            continue;
+        };
+
+        if key == SPROUT_WORKSPACES_KEY {
+            result.workspaces = Some(value);
+        } else if key == SPROUT_ACTIVE_WORKSPACE_KEY {
+            result.active_workspace_id = Some(value);
+        } else if let Some(pubkey) = key.strip_prefix(SPROUT_ONBOARDING_COMPLETE_PREFIX) {
+            result
+                .onboarding_completions
+                .push(LegacyOnboardingCompletion {
+                    pubkey: pubkey.to_string(),
+                    value,
+                });
+        }
+    }
+
+    Ok(result)
+}
+
+fn merge_legacy_workspace_storage(
+    target: &mut LegacyWorkspaceStorage,
+    source: LegacyWorkspaceStorage,
+) {
+    if target.workspaces.is_none() {
+        target.workspaces = source.workspaces;
+    }
+    if target.active_workspace_id.is_none() {
+        target.active_workspace_id = source.active_workspace_id;
+    }
+    target
+        .onboarding_completions
+        .extend(source.onboarding_completions);
+}
+
+/// Return workspace-scoped localStorage values from the legacy Sprout WebKit
+/// data directory so the frontend can seed Buzz localStorage before first
+/// render. This is separate from `migrate_legacy_app_data_dir`: Tauri app data
+/// migration copies files such as `identity.key`, but WebKit localStorage lives
+/// under `~/Library/WebKit/<identifier>/...` on macOS and is not included in the
+/// app data directory.
+#[tauri::command]
+pub fn get_legacy_workspace_storage(
+    app: tauri::AppHandle,
+) -> Result<LegacyWorkspaceStorage, String> {
+    let Some(identifier) = legacy_identifier(&app.config().identifier) else {
+        return Ok(LegacyWorkspaceStorage::default());
+    };
+    let Some(root) = legacy_webkit_data_root(&identifier) else {
+        return Ok(LegacyWorkspaceStorage::default());
+    };
+    if !root.exists() {
+        return Ok(LegacyWorkspaceStorage::default());
+    }
+
+    let mut databases = Vec::new();
+    collect_local_storage_databases(&root, &mut databases);
+
+    let mut result = LegacyWorkspaceStorage::default();
+    for database in databases {
+        match read_legacy_workspace_storage_db(&database) {
+            Ok(storage) => merge_legacy_workspace_storage(&mut result, storage),
+            Err(error) => eprintln!(
+                "buzz-desktop: legacy-local-storage-migration: {}: {error}",
+                database.display()
+            ),
+        }
+    }
+
+    Ok(result)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn legacy_identifier_maps_release_identifier() {
+        assert_eq!(
+            legacy_identifier("xyz.block.buzz.app"),
+            Some("xyz.block.sprout.app".to_string())
+        );
+    }
+
+    #[test]
+    fn legacy_identifier_maps_dev_worktree_identifier() {
+        assert_eq!(
+            legacy_identifier("xyz.block.buzz.app.dev.my-branch"),
+            Some("xyz.block.sprout.app.dev.my-branch".to_string())
+        );
+    }
+
+    #[test]
+    fn decode_webkit_local_storage_value_reads_utf16le() {
+        let bytes: Vec<u8> = "true".encode_utf16().flat_map(u16::to_le_bytes).collect();
+        assert_eq!(
+            decode_webkit_local_storage_value(&bytes).as_deref(),
+            Some("true")
+        );
+    }
+
+    #[test]
+    fn decode_webkit_local_storage_value_reads_utf8_fallback() {
+        assert_eq!(
+            decode_webkit_local_storage_value(b"plain utf8").as_deref(),
+            Some("plain utf8")
+        );
+    }
+
+    #[test]
+    fn read_legacy_workspace_storage_db_reads_workspace_keys() {
+        let dir = tempfile::tempdir().unwrap();
+        let path = dir.path().join("localstorage.sqlite3");
+        let conn = Connection::open(&path).unwrap();
+        conn.execute(
+            "CREATE TABLE ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)",
+            [],
+        )
+        .unwrap();
+
+        fn utf16le(value: &str) -> Vec<u8> {
+            value.encode_utf16().flat_map(u16::to_le_bytes).collect()
+        }
+
+        conn.execute(
+            "INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
+            (
+                SPROUT_WORKSPACES_KEY,
+                utf16le("[{\"relayUrl\":\"wss://relay.example.com\"}]"),
+            ),
+        )
+        .unwrap();
+        conn.execute(
+            "INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
+            (SPROUT_ACTIVE_WORKSPACE_KEY, utf16le("workspace-1")),
+        )
+        .unwrap();
+        conn.execute(
+            "INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
+            (
+                format!("{SPROUT_ONBOARDING_COMPLETE_PREFIX}abc123"),
+                utf16le("true"),
+            ),
+        )
+        .unwrap();
+        drop(conn);
+
+        let storage = read_legacy_workspace_storage_db(&path).unwrap();
+        assert_eq!(
+            storage.workspaces.as_deref(),
+            Some("[{\"relayUrl\":\"wss://relay.example.com\"}]")
+        );
+        assert_eq!(storage.active_workspace_id.as_deref(), Some("workspace-1"));
+        assert_eq!(storage.onboarding_completions.len(), 1);
+        assert_eq!(storage.onboarding_completions[0].pubkey, "abc123");
+        assert_eq!(storage.onboarding_completions[0].value, "true");
+    }
+}
diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs
index 2f7c343dca..559577bf7b 100644
--- a/desktop/src-tauri/src/commands/mod.rs
+++ b/desktop/src-tauri/src/commands/mod.rs
@@ -10,6 +10,7 @@ mod engrams;
 mod export_util;
 mod identity;
 mod identity_archive;
+mod legacy_storage;
 mod media;
 mod media_download;
 mod media_transcode;
@@ -37,6 +38,7 @@ pub use dms::*;
 pub use engrams::*;
 pub use identity::*;
 pub use identity_archive::*;
+pub use legacy_storage::*;
 pub use media::*;
 pub use media_download::*;
 #[cfg(feature = "mesh-llm")]
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 47b0e3e6bb..10a8f98a0f 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -690,6 +690,7 @@ pub fn run() {
             search_users,
             get_presence,
             get_default_relay_url,
+            get_legacy_workspace_storage,
             is_shared_identity,
             get_relay_ws_url,
             get_relay_http_url,
diff --git a/desktop/src/features/workspaces/legacyWorkspaceStorage.test.mjs b/desktop/src/features/workspaces/legacyWorkspaceStorage.test.mjs
new file mode 100644
index 0000000000..83c1de781a
--- /dev/null
+++ b/desktop/src/features/workspaces/legacyWorkspaceStorage.test.mjs
@@ -0,0 +1,153 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { applyLegacyWorkspaceStorage } from "./legacyWorkspaceStorage.ts";
+
+function createMemoryStorage(initial = {}) {
+  const values = new Map(Object.entries(initial));
+  return {
+    getItem(key) {
+      return values.has(key) ? values.get(key) : null;
+    },
+    setItem(key, value) {
+      values.set(key, String(value));
+    },
+    removeItem(key) {
+      values.delete(key);
+    },
+    clear() {
+      values.clear();
+    },
+    key(index) {
+      return Array.from(values.keys())[index] ?? null;
+    },
+    get length() {
+      return values.size;
+    },
+  };
+}
+
+const legacyWorkspaces = JSON.stringify([
+  {
+    id: "legacy-workspace",
+    name: "Existing relay",
+    relayUrl: "wss://relay.example.com",
+    addedAt: "2026-06-12T00:00:00.000Z",
+  },
+]);
+
+const currentWorkspaces = JSON.stringify([
+  {
+    id: "current-workspace",
+    name: "Current relay",
+    relayUrl: "wss://current.example.com",
+    addedAt: "2026-06-12T00:00:00.000Z",
+  },
+]);
+
+const localhostWorkspaces = JSON.stringify([
+  {
+    id: "local-workspace",
+    name: "Local Dev",
+    relayUrl: "ws://localhost:3000",
+    addedAt: "2026-06-12T00:00:00.000Z",
+  },
+]);
+
+test("applyLegacyWorkspaceStorage seeds missing workspaces and active workspace", () => {
+  const storage = createMemoryStorage();
+
+  applyLegacyWorkspaceStorage(
+    {
+      workspaces: legacyWorkspaces,
+      activeWorkspaceId: "legacy-workspace",
+      onboardingCompletions: [],
+    },
+    storage,
+  );
+
+  assert.equal(storage.getItem("buzz-workspaces"), legacyWorkspaces);
+  assert.equal(storage.getItem("buzz-active-workspace-id"), "legacy-workspace");
+});
+
+test("applyLegacyWorkspaceStorage preserves existing non-local Buzz workspaces", () => {
+  const storage = createMemoryStorage({
+    "buzz-workspaces": currentWorkspaces,
+    "buzz-active-workspace-id": "current-workspace",
+  });
+
+  applyLegacyWorkspaceStorage(
+    {
+      workspaces: legacyWorkspaces,
+      activeWorkspaceId: "legacy-workspace",
+      onboardingCompletions: [],
+    },
+    storage,
+  );
+
+  assert.equal(storage.getItem("buzz-workspaces"), currentWorkspaces);
+  assert.equal(
+    storage.getItem("buzz-active-workspace-id"),
+    "current-workspace",
+  );
+});
+
+test("applyLegacyWorkspaceStorage replaces broken localhost first-run workspace", () => {
+  const storage = createMemoryStorage({
+    "buzz-workspaces": localhostWorkspaces,
+    "buzz-active-workspace-id": "local-workspace",
+  });
+
+  applyLegacyWorkspaceStorage(
+    {
+      workspaces: legacyWorkspaces,
+      activeWorkspaceId: "legacy-workspace",
+      onboardingCompletions: [],
+    },
+    storage,
+  );
+
+  assert.equal(storage.getItem("buzz-workspaces"), legacyWorkspaces);
+  assert.equal(storage.getItem("buzz-active-workspace-id"), "legacy-workspace");
+});
+
+test("applyLegacyWorkspaceStorage treats trailing-slash localhost as broken", () => {
+  const storage = createMemoryStorage({
+    "buzz-workspaces": JSON.stringify([
+      {
+        id: "local-workspace",
+        name: "Local Dev",
+        relayUrl: "ws://localhost:3000/",
+        addedAt: "2026-06-12T00:00:00.000Z",
+      },
+    ]),
+    "buzz-active-workspace-id": "local-workspace",
+  });
+
+  applyLegacyWorkspaceStorage(
+    {
+      workspaces: legacyWorkspaces,
+      activeWorkspaceId: "legacy-workspace",
+      onboardingCompletions: [],
+    },
+    storage,
+  );
+
+  assert.equal(storage.getItem("buzz-workspaces"), legacyWorkspaces);
+  assert.equal(storage.getItem("buzz-active-workspace-id"), "legacy-workspace");
+});
+
+test("applyLegacyWorkspaceStorage migrates onboarding completion keys", () => {
+  const storage = createMemoryStorage();
+
+  applyLegacyWorkspaceStorage(
+    {
+      workspaces: null,
+      activeWorkspaceId: null,
+      onboardingCompletions: [{ pubkey: "abc123", value: "true" }],
+    },
+    storage,
+  );
+
+  assert.equal(storage.getItem("buzz-onboarding-complete.v1:abc123"), "true");
+});
diff --git a/desktop/src/features/workspaces/legacyWorkspaceStorage.ts b/desktop/src/features/workspaces/legacyWorkspaceStorage.ts
new file mode 100644
index 0000000000..492e05d755
--- /dev/null
+++ b/desktop/src/features/workspaces/legacyWorkspaceStorage.ts
@@ -0,0 +1,138 @@
+import { invokeTauri } from "@/shared/api/tauri";
+
+const BUZZ_WORKSPACES_KEY = "buzz-workspaces";
+const BUZZ_ACTIVE_WORKSPACE_KEY = "buzz-active-workspace-id";
+const BUZZ_ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX =
+  "buzz-onboarding-complete.v1:";
+const LOCAL_DEV_RELAY_URLS = new Set([
+  "ws://localhost:3000",
+  "ws://127.0.0.1:3000",
+]);
+
+type LegacyWorkspaceStorageSnapshot = {
+  workspaces: string | null;
+  activeWorkspaceId: string | null;
+  onboardingCompletions: Array<{
+    pubkey: string;
+    value: string;
+  }>;
+};
+
+type StoredWorkspace = {
+  relayUrl?: unknown;
+};
+
+function parseWorkspaceList(raw: string | null): StoredWorkspace[] | null {
+  if (!raw) {
+    return null;
+  }
+
+  try {
+    const parsed: unknown = JSON.parse(raw);
+    return Array.isArray(parsed) ? (parsed as StoredWorkspace[]) : null;
+  } catch {
+    return null;
+  }
+}
+
+function normalizeRelayUrl(relayUrl: string) {
+  return relayUrl.trim().replace(/\/$/, "");
+}
+
+function hasOnlyLocalDevWorkspace(raw: string | null): boolean {
+  const workspaces = parseWorkspaceList(raw);
+  return (
+    workspaces?.length === 1 &&
+    typeof workspaces[0]?.relayUrl === "string" &&
+    LOCAL_DEV_RELAY_URLS.has(normalizeRelayUrl(workspaces[0].relayUrl))
+  );
+}
+
+function hasNonLocalCurrentWorkspaces(raw: string | null): boolean {
+  const workspaces = parseWorkspaceList(raw);
+  return (
+    workspaces !== null &&
+    workspaces.length > 0 &&
+    !hasOnlyLocalDevWorkspace(raw)
+  );
+}
+
+function shouldWriteLegacyWorkspaces({
+  currentWorkspacesRaw,
+  legacyWorkspacesRaw,
+}: {
+  currentWorkspacesRaw: string | null;
+  legacyWorkspacesRaw: string | null;
+}) {
+  const legacyWorkspaces = parseWorkspaceList(legacyWorkspacesRaw);
+  if (!legacyWorkspaces || legacyWorkspaces.length === 0) {
+    return false;
+  }
+
+  return !hasNonLocalCurrentWorkspaces(currentWorkspacesRaw);
+}
+
+export function applyLegacyWorkspaceStorage(
+  legacyStorage: LegacyWorkspaceStorageSnapshot,
+  storage: Storage = window.localStorage,
+): void {
+  const currentWorkspacesRaw = storage.getItem(BUZZ_WORKSPACES_KEY);
+  const shouldWriteWorkspaces = shouldWriteLegacyWorkspaces({
+    currentWorkspacesRaw,
+    legacyWorkspacesRaw: legacyStorage.workspaces,
+  });
+
+  if (shouldWriteWorkspaces && legacyStorage.workspaces) {
+    storage.setItem(BUZZ_WORKSPACES_KEY, legacyStorage.workspaces);
+  }
+
+  const currentActiveWorkspaceId = storage.getItem(BUZZ_ACTIVE_WORKSPACE_KEY);
+  if (
+    legacyStorage.activeWorkspaceId &&
+    (!currentActiveWorkspaceId || shouldWriteWorkspaces)
+  ) {
+    storage.setItem(BUZZ_ACTIVE_WORKSPACE_KEY, legacyStorage.activeWorkspaceId);
+  }
+
+  for (const completion of legacyStorage.onboardingCompletions) {
+    const key = `${BUZZ_ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX}${completion.pubkey}`;
+    if (storage.getItem(key) === null) {
+      storage.setItem(key, completion.value);
+    }
+  }
+}
+
+/**
+ * Seed Buzz localStorage from legacy Sprout WebKit localStorage before the app
+ * renders providers that read workspace state. The native command reads the old
+ * app identifier's WebKit SQLite database; this frontend step writes only when
+ * Buzz does not already have workspace state, except for the known broken
+ * Sprout→Buzz first-run handoff that created a single localhost workspace.
+ */
+export async function migrateLegacyWorkspaceStorageBeforeRender(): Promise<void> {
+  if (typeof window === "undefined") {
+    return;
+  }
+
+  const currentWorkspacesRaw = window.localStorage.getItem(BUZZ_WORKSPACES_KEY);
+  const hasCurrentActiveWorkspace = window.localStorage.getItem(
+    BUZZ_ACTIVE_WORKSPACE_KEY,
+  );
+  if (
+    currentWorkspacesRaw &&
+    hasCurrentActiveWorkspace &&
+    !hasOnlyLocalDevWorkspace(currentWorkspacesRaw)
+  ) {
+    return;
+  }
+
+  try {
+    applyLegacyWorkspaceStorage(
+      await invokeTauri<LegacyWorkspaceStorageSnapshot>(
+        "get_legacy_workspace_storage",
+      ),
+    );
+  } catch (error) {
+    console.warn("Failed to read legacy Sprout workspace storage.", error);
+  }
+}
diff --git a/desktop/src/features/workspaces/ui/WelcomeSetup.tsx b/desktop/src/features/workspaces/ui/WelcomeSetup.tsx
index e4ad2ea8e8..05a85ff0ba 100644
--- a/desktop/src/features/workspaces/ui/WelcomeSetup.tsx
+++ b/desktop/src/features/workspaces/ui/WelcomeSetup.tsx
@@ -31,6 +31,14 @@ type WelcomeSetupProps = {
 };
 
 const DEFAULT_WORKSPACE_HANDOFF_MIN_MS = 200;
+const LOCAL_DEV_RELAY_URLS = new Set([
+  "ws://localhost:3000",
+  "ws://127.0.0.1:3000",
+]);
+
+function isLocalDevRelayUrl(relayUrl: string) {
+  return LOCAL_DEV_RELAY_URLS.has(relayUrl.trim().replace(/\/$/, ""));
+}
 
 function wait(ms: number) {
   return new Promise<void>((resolve) => {
@@ -97,6 +105,12 @@ export function WelcomeSetup({
         setError("Please enter a workspace URL.");
         return;
       }
+      if (!workspaceName && isLocalDevRelayUrl(trimmedUrl)) {
+        setError("Enter your relay URL to join a workspace.");
+        setTransitionMode("forward");
+        setPage("create-workspace");
+        return;
+      }
 
       const handoffStartedAt = performance.now();
       flushSync(() => {
@@ -220,20 +234,22 @@ export function WelcomeSetup({
             </p>
 
             <div className="mt-8 flex w-full flex-col gap-3">
-              <Button
-                className="h-10 w-full"
-                aria-disabled={isConnecting}
-                onClick={() => {
-                  if (isConnecting) {
-                    return;
-                  }
-                  setError(null);
-                  void handleConnect(defaultRelayUrl);
-                }}
-                type="button"
-              >
-                Continue with Block Inc. workspace
-              </Button>
+              {isLocalDevRelayUrl(defaultRelayUrl) ? null : (
+                <Button
+                  className="h-10 w-full"
+                  aria-disabled={isConnecting}
+                  onClick={() => {
+                    if (isConnecting) {
+                      return;
+                    }
+                    setError(null);
+                    void handleConnect(defaultRelayUrl);
+                  }}
+                  type="button"
+                >
+                  Continue with Block Inc. workspace
+                </Button>
+              )}
 
               <Button
                 className="h-10 w-full"
diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx
index ca89f1ed98..cf41ee4b26 100644
--- a/desktop/src/main.tsx
+++ b/desktop/src/main.tsx
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
 import { App } from "@/app/App";
 import "@/shared/styles/globals.css";
 import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
+import { migrateLegacyWorkspaceStorageBeforeRender } from "@/features/workspaces/legacyWorkspaceStorage";
 import { WorkspacesProvider } from "@/features/workspaces/useWorkspaces";
 import { ThemeProvider } from "@/shared/theme/ThemeProvider";
 import { EmojiBurstProvider } from "@/shared/ui/EmojiBurstProvider";
@@ -78,6 +79,7 @@ async function installE2eBridgeIfConfigured() {
 async function bootstrap() {
   configureDevE2eBridgeFromUrl();
   await installE2eBridgeIfConfigured();
+  await migrateLegacyWorkspaceStorageBeforeRender();
   renderApp();
 }
 
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index b98dc9e800..2a99941fb0 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -6149,6 +6149,12 @@ export function maybeInstallE2eTauriMocks() {
         return getRelayWsUrl(activeConfig);
       case "get_default_relay_url":
         return getRelayWsUrl(activeConfig);
+      case "get_legacy_workspace_storage":
+        return {
+          workspaces: null,
+          activeWorkspaceId: null,
+          onboardingCompletions: [],
+        };
       case "get_relay_http_url":
         return getRelayHttpUrl(activeConfig);
       case "discover_acp_providers":
diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts
index b4c4ce6791..9e2fab8ee0 100644
--- a/desktop/tests/e2e/onboarding.spec.ts
+++ b/desktop/tests/e2e/onboarding.spec.ts
@@ -582,7 +582,11 @@ test("first-run default workspace handoff gives immediate stepper feedback", asy
     {
       profileReadDelayMs: 2_000,
     },
-    { skipOnboardingSeed: true, skipWorkspaceSeed: true },
+    {
+      relayWsUrl: "wss://default.example.com",
+      skipOnboardingSeed: true,
+      skipWorkspaceSeed: true,
+    },
   );
   await page.goto("/");
 
@@ -621,6 +625,7 @@ test("first-run default workspace handoff gives immediate stepper feedback", asy
 
 test("welcome can continue using an existing Nostr key", async ({ page }) => {
   await installMockBridge(page, undefined, {
+    relayWsUrl: "wss://default.example.com",
     skipOnboardingSeed: true,
     skipWorkspaceSeed: true,
   });
@@ -662,6 +667,9 @@ test("welcome presents custom workspace setup as joining a workspace", async ({
   });
   await page.goto("/");
 
+  await expect(
+    page.getByRole("button", { name: "Continue with Block Inc. workspace" }),
+  ).toHaveCount(0);
   await page.getByRole("button", { name: "Join a workspace" }).click();
 
   await expect(
diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts
index 3ef71af372..a4d4489dc1 100644
--- a/desktop/tests/helpers/bridge.ts
+++ b/desktop/tests/helpers/bridge.ts
@@ -446,6 +446,7 @@ export async function installMockBridge(
   page: Page,
   mock?: MockBridgeOptions,
   options?: {
+    relayWsUrl?: string;
     skipOnboardingSeed?: boolean;
     skipWorkspaceSeed?: boolean;
     seedPreviewFeatures?: boolean;
@@ -454,6 +455,7 @@ export async function installMockBridge(
   await installBridge(page, {
     mode: "mock",
     mock,
+    relayWsUrl: options?.relayWsUrl,
     skipOnboardingSeed: options?.skipOnboardingSeed,
     skipWorkspaceSeed: options?.skipWorkspaceSeed,
     seedPreviewFeatures: options?.seedPreviewFeatures,
