diff --git a/packages/desktop-app/src-tauri/src/bin/dev-server.rs b/packages/desktop-app/src-tauri/src/bin/dev-server.rs
index 1ae94a443..9e1e8f503 100644
--- a/packages/desktop-app/src-tauri/src/bin/dev-server.rs
+++ b/packages/desktop-app/src-tauri/src/bin/dev-server.rs
@@ -39,7 +39,7 @@
 
 use std::env;
 use std::path::PathBuf;
-use std::sync::Arc;
+use std::sync::{Arc, RwLock};
 
 use nodespace_core::{DatabaseService, NodeService};
 
@@ -88,9 +88,12 @@ async fn main() -> anyhow::Result<()> {
 
     tracing::info!("✅ Services initialized");
 
+    // Wrap services in RwLock for dynamic database switching during tests
+    let db_arc = Arc::new(RwLock::new(Arc::new(db_service)));
+    let ns_arc = Arc::new(RwLock::new(Arc::new(node_service)));
+
     // Start HTTP server
-    nodespace_app_lib::dev_server::start_server(Arc::new(db_service), Arc::new(node_service), port)
-        .await?;
+    nodespace_app_lib::dev_server::start_server(db_arc, ns_arc, port).await?;
 
     Ok(())
 }
diff --git a/packages/desktop-app/src-tauri/src/dev_server/embedding_endpoints.rs b/packages/desktop-app/src-tauri/src/dev_server/embedding_endpoints.rs
index 8e4da5169..eb580c785 100644
--- a/packages/desktop-app/src-tauri/src/dev_server/embedding_endpoints.rs
+++ b/packages/desktop-app/src-tauri/src/dev_server/embedding_endpoints.rs
@@ -562,16 +562,15 @@ async fn create_container_node(
         mentioned_by: Vec::new(), // Will be computed from node_mentions table
     };
 
-    state
-        .node_service
+    let node_service = state.node_service.read().unwrap().clone();
+    node_service
         .create_node(container_node)
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
 
     // If mentioned_by is provided, create mention relationship
     if let Some(mentioning_node_id) = input.mentioned_by {
-        state
-            .node_service
+        node_service
             .create_mention(&mentioning_node_id, &node_id)
             .await
             .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
@@ -607,8 +606,8 @@ async fn create_node_mention(
     State(state): State<AppState>,
     Json(payload): Json<CreateMentionRequest>,
 ) -> Result<StatusCode, HttpError> {
-    state
-        .node_service
+    let node_service = state.node_service.read().unwrap().clone();
+    node_service
         .create_mention(&payload.mentioning_node_id, &payload.mentioned_node_id)
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
diff --git a/packages/desktop-app/src-tauri/src/dev_server/mod.rs b/packages/desktop-app/src-tauri/src/dev_server/mod.rs
index 07dd88f56..1f35eb0ce 100644
--- a/packages/desktop-app/src-tauri/src/dev_server/mod.rs
+++ b/packages/desktop-app/src-tauri/src/dev_server/mod.rs
@@ -33,7 +33,7 @@ use axum::{
     http::{header, Method},
     Router,
 };
-use std::sync::Arc;
+use std::sync::{Arc, RwLock};
 use tower_http::cors::{Any, CorsLayer};
 
 use nodespace_core::{DatabaseService, NodeService};
@@ -54,10 +54,15 @@ mod http_error;
 pub use http_error::HttpError;
 
 /// Application state shared across all endpoints
+///
+/// Uses RwLock to allow dynamic database switching for test isolation.
+/// Each test can call /api/database/init with a unique database path,
+/// and the init endpoint will replace the NodeService with a new instance
+/// connected to the test database.
 #[derive(Clone)]
 pub struct AppState {
-    pub db: Arc<DatabaseService>,
-    pub node_service: Arc<NodeService>,
+    pub db: Arc<RwLock<Arc<DatabaseService>>>,
+    pub node_service: Arc<RwLock<Arc<NodeService>>>,
 }
 
 /// Create the main application router with all endpoint modules
@@ -137,8 +142,8 @@ fn cors_layer() -> CorsLayer {
 ///
 /// Returns error if server fails to bind or start.
 pub async fn start_server(
-    db: Arc<DatabaseService>,
-    node_service: Arc<NodeService>,
+    db: Arc<RwLock<Arc<DatabaseService>>>,
+    node_service: Arc<RwLock<Arc<NodeService>>>,
     port: u16,
 ) -> anyhow::Result<()> {
     let state = AppState { db, node_service };
diff --git a/packages/desktop-app/src-tauri/src/dev_server/node_endpoints.rs b/packages/desktop-app/src-tauri/src/dev_server/node_endpoints.rs
index 93d4103d8..63ccc8971 100644
--- a/packages/desktop-app/src-tauri/src/dev_server/node_endpoints.rs
+++ b/packages/desktop-app/src-tauri/src/dev_server/node_endpoints.rs
@@ -22,6 +22,7 @@ use axum::{
     Router,
 };
 use serde::{Deserialize, Serialize};
+use std::sync::Arc;
 
 use crate::commands::nodes::CreateNodeInput;
 use crate::dev_server::{AppState, HttpError};
@@ -88,7 +89,7 @@ async fn health_check() -> Json<HealthStatus> {
 /// The actual database initialization happens when the dev-server binary starts.
 /// Tests should ensure the dev-server is running before calling this endpoint.
 async fn init_database(
-    State(_state): State<AppState>,
+    State(state): State<AppState>,
     Query(params): Query<InitDbQuery>,
 ) -> Result<Json<InitDbResponse>, HttpError> {
     use std::path::PathBuf;
@@ -120,13 +121,26 @@ async fn init_database(
         .ok_or_else(|| HttpError::new("Invalid database path", "PATH_ERROR"))?
         .to_string();
 
-    // Actually initialize the database using the existing DatabaseService
-    use nodespace_core::DatabaseService;
-    let _ = DatabaseService::new(db_path.clone())
+    // Create new DatabaseService and NodeService for this database
+    use nodespace_core::{DatabaseService, NodeService};
+    let new_db = DatabaseService::new(db_path.clone())
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "DATABASE_INIT_ERROR"))?;
 
-    tracing::info!("📦 Database initialized at: {}", db_path_str);
+    let new_node_service = NodeService::new(new_db.clone())
+        .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_INIT_ERROR"))?;
+
+    // Replace the services in AppState using RwLock
+    {
+        let mut db_lock = state.db.write().unwrap();
+        *db_lock = Arc::new(new_db);
+    }
+    {
+        let mut ns_lock = state.node_service.write().unwrap();
+        *ns_lock = Arc::new(new_node_service);
+    }
+
+    tracing::info!("🔄 Database SWAPPED to: {}", db_path_str);
 
     Ok(Json(InitDbResponse {
         db_path: db_path_str,
@@ -192,11 +206,15 @@ async fn create_node(
         mentioned_by: Vec::new(),
     };
 
-    state
-        .node_service
+    // Access node_service through RwLock
+    let node_service = state.node_service.read().unwrap().clone();
+    node_service
         .create_node(full_node)
         .await
-        .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
+        .map_err(|e| {
+            tracing::error!("❌ Node creation failed for {}: {:?}", node.id, e);
+            HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR")
+        })?;
 
     tracing::debug!("✅ Created node: {}", node.id);
 
@@ -218,8 +236,8 @@ async fn get_node(
     State(state): State<AppState>,
     Path(id): Path<String>,
 ) -> Result<Json<Option<Node>>, HttpError> {
-    let node = state
-        .node_service
+    let node_service = state.node_service.read().unwrap().clone();
+    let node = node_service
         .get_node(&id)
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
@@ -249,8 +267,8 @@ async fn update_node(
     Path(id): Path<String>,
     Json(update): Json<NodeUpdate>,
 ) -> Result<StatusCode, HttpError> {
-    state
-        .node_service
+    let node_service = state.node_service.read().unwrap().clone();
+    node_service
         .update_node(&id, update)
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
@@ -275,8 +293,8 @@ async fn delete_node(
     State(state): State<AppState>,
     Path(id): Path<String>,
 ) -> Result<StatusCode, HttpError> {
-    state
-        .node_service
+    let node_service = state.node_service.read().unwrap().clone();
+    node_service
         .delete_node(&id)
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
@@ -301,8 +319,8 @@ async fn get_children(
     State(state): State<AppState>,
     Path(parent_id): Path<String>,
 ) -> Result<Json<Vec<Node>>, HttpError> {
-    let children = state
-        .node_service
+    let node_service = state.node_service.read().unwrap().clone();
+    let children = node_service
         .get_children(&parent_id)
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_SERVICE_ERROR"))?;
diff --git a/packages/desktop-app/src-tauri/src/dev_server/query_endpoints.rs b/packages/desktop-app/src-tauri/src/dev_server/query_endpoints.rs
index a3e2ece5e..10b47b2da 100644
--- a/packages/desktop-app/src-tauri/src/dev_server/query_endpoints.rs
+++ b/packages/desktop-app/src-tauri/src/dev_server/query_endpoints.rs
@@ -145,8 +145,8 @@ async fn query_nodes_simple(
 
     // Execute query with timing
     let start = std::time::Instant::now();
-    let nodes = state
-        .node_service
+    let node_service = state.node_service.read().unwrap().clone();
+    let nodes = node_service
         .query_nodes(filter.clone())
         .await
         .map_err(|e| HttpError::from_anyhow(e.into(), "NODE_QUERY_ERROR"))?;
diff --git a/packages/desktop-app/src/lib/services/reactive-node-service.svelte.ts b/packages/desktop-app/src/lib/services/reactive-node-service.svelte.ts
index d10aa0a6a..e4c4ec79a 100644
--- a/packages/desktop-app/src/lib/services/reactive-node-service.svelte.ts
+++ b/packages/desktop-app/src/lib/services/reactive-node-service.svelte.ts
@@ -374,15 +374,15 @@ export function createReactiveNodeService(events: NodeManagerEvents) {
       beforeSiblingId = afterNodeId;
     }
 
-    // Determine origin_node_id - inherit from parent or use own id if no parent
+    // Determine origin_node_id - inherit from parent or use 'root' if no parent
     let rootId: string;
     if (newParentId) {
       const parent = sharedNodeStore.getNode(newParentId);
       // Inherit origin_node_id from parent, or use parent's id if parent has no origin_node_id
       rootId = parent?.containerNodeId || newParentId;
     } else {
-      // No parent means this node is the root
-      rootId = nodeId;
+      // No parent means this node is at root level
+      rootId = 'root';
     }
 
     // Create Node with unified type system
diff --git a/packages/desktop-app/src/lib/services/shared-node-store.ts b/packages/desktop-app/src/lib/services/shared-node-store.ts
index 5d8416765..2cc49137d 100644
--- a/packages/desktop-app/src/lib/services/shared-node-store.ts
+++ b/packages/desktop-app/src/lib/services/shared-node-store.ts
@@ -288,7 +288,10 @@ export class SharedNodeStore {
       this.metrics.updateCount++;
 
       // Phase 2.4: Persist to database (unless skipped)
-      if (!options.skipPersistence && source.type !== 'database') {
+      // IMPORTANT: Skip viewer-sourced updates - BaseNodeViewer handles persistence with debouncing
+      // Only persist updates from other sources (e.g., MCP server in the future)
+      // TODO: Refactor BaseNodeViewer to use SharedNodeStore for all persistence (#TBD)
+      if (!options.skipPersistence && source.type !== 'database' && source.type !== 'viewer') {
         // Skip persisting empty text nodes - they exist in UI but not in database
         const isEmptyTextNode =
           updatedNode.nodeType === 'text' && updatedNode.content.trim() === '';
@@ -368,7 +371,10 @@ export class SharedNodeStore {
     }
 
     // Phase 2.4: Persist to database
-    if (!skipPersistence && source.type !== 'database') {
+    // IMPORTANT: Skip viewer-sourced updates - BaseNodeViewer handles persistence with debouncing
+    // Only persist updates from other sources (e.g., MCP server in the future)
+    // TODO: Refactor BaseNodeViewer to use SharedNodeStore for all persistence (#TBD)
+    if (!skipPersistence && source.type !== 'database' && source.type !== 'viewer') {
       // Skip persisting empty text nodes - they exist in UI but not in database
       // until user adds content (backend validation requires non-empty content)
       const isEmptyTextNode = node.nodeType === 'text' && node.content.trim() === '';
@@ -1238,6 +1244,28 @@ export class SharedNodeStore {
   clearTestErrors(): void {
     this.testErrors = [];
   }
+
+  /**
+   * Reset store state (for testing only)
+   * @internal
+   */
+  __resetForTesting(): void {
+    this.nodes.clear();
+    this.persistedNodeIds.clear();
+    this.subscriptions.clear();
+    this.wildcardSubscriptions.clear();
+    this.pendingUpdates.clear();
+    this.versions.clear();
+    this.testErrors = [];
+    this.metrics = {
+      updateCount: 0,
+      avgUpdateTime: 0,
+      maxUpdateTime: 0,
+      subscriptionCount: 0,
+      conflictCount: 0,
+      rollbackCount: 0
+    };
+  }
 }
 
 // ============================================================================
diff --git a/packages/desktop-app/src/tests/integration/node-ordering.test.ts b/packages/desktop-app/src/tests/integration/node-ordering.test.ts
index 81e7faf21..a58f3f2aa 100644
--- a/packages/desktop-app/src/tests/integration/node-ordering.test.ts
+++ b/packages/desktop-app/src/tests/integration/node-ordering.test.ts
@@ -30,6 +30,7 @@
 
 import { describe, it, expect, beforeEach } from 'vitest';
 import { createReactiveNodeService } from '$lib/services/reactive-node-service.svelte';
+import { sharedNodeStore } from '$lib/services/shared-node-store';
 import { createTestNode } from '../helpers';
 
 describe('Node Ordering Integration Tests', () => {
@@ -43,6 +44,8 @@ describe('Node Ordering Integration Tests', () => {
   };
 
   beforeEach(() => {
+    // Reset singleton state between tests to prevent contamination
+    sharedNodeStore.__resetForTesting();
     nodeService = createReactiveNodeService(mockEvents);
   });
 
