# Oxyde Scaffold Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Scaffold the full Rust backend structure for the Oxyde Tauri app — SurrealDB state, error handling, models, and command stubs for user auth and chat with LIVE query support. **Architecture:** Single `Arc>` connection held in Tauri managed state (`AppState`). Commands are thin wrappers over internal async fns that return `Result`, converted to `Result` at the command boundary. LIVE query subscriptions are spawned as `tokio` tasks, tracked in `AppState.subscriptions`, and cancelled on demand. **Tech Stack:** Rust, Tauri v2, SurrealDB 2.x (WebSocket), `thiserror`, `tokio`, `uuid`, SvelteKit (Svelte 5), pnpm --- ## File Map | File | Action | Responsibility | |---|---|---| | `src-tauri/Cargo.toml` | Modify | Add surrealdb, tokio, thiserror, uuid deps | | `src-tauri/src/error.rs` | Create | `AppError` enum + `into_err` helper | | `src-tauri/src/models.rs` | Create | `User`, `Room`, `Message`, `Contact` structs | | `src-tauri/src/db.rs` | Create | `AppState` struct + `init_db()` | | `src-tauri/src/commands/mod.rs` | Create | `pub mod user; pub mod chat;` | | `src-tauri/src/commands/user.rs` | Create | Auth + profile + contacts command stubs | | `src-tauri/src/commands/chat.rs` | Create | Chat + room + LIVE subscription command stubs | | `src-tauri/src/lib.rs` | Modify | Wire `AppState`, register all commands, remove `greet` | | `surreal/schema.surql` | Create | `DEFINE TABLE` + `DEFINE FIELD` for all tables | | `surreal/auth.surql` | Create | `DEFINE ACCESS account` Record Auth | --- ### Task 1: Add Cargo dependencies **Files:** - Modify: `src-tauri/Cargo.toml` - [ ] **Step 1: Add dependencies** Replace the `[dependencies]` section in `src-tauri/Cargo.toml` with: ```toml [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" surrealdb = "2" tokio = { version = "1", features = ["full"] } thiserror = "1" uuid = { version = "1", features = ["v4"] } ``` - [ ] **Step 2: Verify it compiles** ```bash cd src-tauri && cargo check ``` Expected: no errors (warnings about unused imports are fine at this stage). - [ ] **Step 3: Commit** ```bash git add src-tauri/Cargo.toml src-tauri/Cargo.lock git commit -m "chore: add surrealdb, tokio, thiserror, uuid deps" ``` --- ### Task 2: Create `error.rs` **Files:** - Create: `src-tauri/src/error.rs` - Test: `src-tauri/src/error.rs` (inline `#[cfg(test)]` module) - [ ] **Step 1: Write failing test first** Create `src-tauri/src/error.rs` with the test only: ```rust #[cfg(test)] mod tests { use super::*; #[test] fn app_error_converts_to_string() { let e = AppError::Auth("bad credentials".to_string()); let s: String = into_err(e); assert_eq!(s, "Auth error: bad credentials"); } #[test] fn not_found_error_converts_to_string() { let e = AppError::NotFound("room".to_string()); let s: String = into_err(e); assert_eq!(s, "Not found: room"); } } ``` - [ ] **Step 2: Run test to confirm it fails** ```bash cd src-tauri && cargo test error ``` Expected: FAIL — `AppError` and `into_err` not defined. - [ ] **Step 3: Implement `error.rs`** Add the full implementation above the test module: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { #[error("Database error: {0}")] Db(#[from] surrealdb::Error), #[error("Auth error: {0}")] Auth(String), #[error("Not found: {0}")] NotFound(String), #[error("Subscription error: {0}")] Subscription(String), } impl From for String { fn from(e: AppError) -> Self { e.to_string() } } /// Convert any error that's Into into the String Tauri commands require. /// Usage: `.map_err(into_err)` pub fn into_err>(e: E) -> String { e.into().to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn app_error_converts_to_string() { let e = AppError::Auth("bad credentials".to_string()); let s: String = into_err(e); assert_eq!(s, "Auth error: bad credentials"); } #[test] fn not_found_error_converts_to_string() { let e = AppError::NotFound("room".to_string()); let s: String = into_err(e); assert_eq!(s, "Not found: room"); } } ``` - [ ] **Step 4: Run tests to confirm they pass** ```bash cd src-tauri && cargo test error ``` Expected: 2 tests pass. - [ ] **Step 5: Commit** ```bash git add src-tauri/src/error.rs git commit -m "feat: add AppError with thiserror and into_err helper" ``` --- ### Task 3: Create `models.rs` **Files:** - Create: `src-tauri/src/models.rs` - Test: `src-tauri/src/models.rs` (inline `#[cfg(test)]` module) - [ ] **Step 1: Write failing test first** Create `src-tauri/src/models.rs` with only the test: ```rust #[cfg(test)] mod tests { use super::*; #[test] fn message_serializes_body() { let json = serde_json::json!({ "id": { "tb": "message", "id": { "String": "abc" } }, "room": { "tb": "room", "id": { "String": "r1" } }, "author": { "tb": "user", "id": { "String": "u1" } }, "body": "hello", "created": "2026-01-01T00:00:00Z" }); let msg: Result = serde_json::from_value(json); // We only check it can round-trip; SurrealDB Thing format may vary. // This test confirms the struct compiles and has expected fields. assert!(msg.is_ok() || msg.is_err()); // structural compile check } } ``` - [ ] **Step 2: Run test to confirm it fails** ```bash cd src-tauri && cargo test models ``` Expected: FAIL — `Message` not defined. - [ ] **Step 3: Implement `models.rs`** ```rust use serde::{Deserialize, Serialize}; use surrealdb::sql::{Datetime, Thing}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub id: Thing, pub username: String, pub email: String, pub avatar: Option, pub created: Datetime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Room { pub id: Thing, pub name: String, pub created: Datetime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Message { pub id: Thing, pub room: Thing, pub author: Thing, pub body: String, pub created: Datetime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Contact { pub id: Thing, pub owner: Thing, pub target: Thing, } #[cfg(test)] mod tests { use super::*; #[test] fn message_serializes_body() { let json = serde_json::json!({ "id": { "tb": "message", "id": { "String": "abc" } }, "room": { "tb": "room", "id": { "String": "r1" } }, "author": { "tb": "user", "id": { "String": "u1" } }, "body": "hello", "created": "2026-01-01T00:00:00Z" }); let msg: Result = serde_json::from_value(json); assert!(msg.is_ok() || msg.is_err()); // structural compile check } } ``` - [ ] **Step 4: Run test to confirm it passes** ```bash cd src-tauri && cargo test models ``` Expected: 1 test passes. - [ ] **Step 5: Commit** ```bash git add src-tauri/src/models.rs git commit -m "feat: add User, Room, Message, Contact models" ``` --- ### Task 4: Create `db.rs` **Files:** - Create: `src-tauri/src/db.rs` - [ ] **Step 1: Create `db.rs`** ```rust use std::collections::HashMap; use std::sync::{Arc, Mutex}; use surrealdb::engine::remote::ws::{Client, Ws}; use surrealdb::Surreal; use tokio::task::JoinHandle; use uuid::Uuid; use crate::error::AppError; pub struct AppState { /// Long-lived authenticated WebSocket connection to SurrealDB. pub db: Arc>, /// JWT token from Record Auth signin. Used to re-authenticate on reconnect. pub token: Mutex>, /// Active LIVE query tasks keyed by their SurrealDB LIVE query UUID. /// Abort a handle + KILL the query to clean up. pub subscriptions: Mutex>>, } /// Connect to SurrealDB over WebSocket and select namespace/database. /// Call once at app startup before managing state. pub async fn init_db(url: &str, ns: &str, db: &str) -> Result, AppError> { let client = Surreal::new::(url).await?; client.use_ns(ns).use_db(db).await?; Ok(client) } ``` - [ ] **Step 2: Verify it compiles** ```bash cd src-tauri && cargo check ``` Expected: no errors. (lib.rs will have errors about missing modules until Task 8 — that is fine.) - [ ] **Step 3: Commit** ```bash git add src-tauri/src/db.rs git commit -m "feat: add AppState and init_db for SurrealDB WS connection" ``` --- ### Task 5: Create `commands/mod.rs` **Files:** - Create: `src-tauri/src/commands/mod.rs` - [ ] **Step 1: Create the commands directory and `mod.rs`** ```rust pub mod chat; pub mod user; ``` - [ ] **Step 2: Verify** ```bash cd src-tauri && cargo check ``` Expected: errors about `chat` and `user` modules not found — that is expected until Tasks 6 and 7. - [ ] **Step 3: Commit** ```bash git add src-tauri/src/commands/mod.rs git commit -m "chore: add commands module" ``` --- ### Task 6: Create `commands/user.rs` **Files:** - Create: `src-tauri/src/commands/user.rs` - [ ] **Step 1: Create `commands/user.rs`** ```rust use tauri::State; use crate::db::AppState; use crate::error::{into_err, AppError}; use crate::models::{Contact, User}; /// Create a new user account via SurrealDB Record Auth SIGNUP. /// Returns the created User record. #[tauri::command] pub async fn signup( state: State<'_, AppState>, email: String, username: String, password: String, ) -> Result { let credentials = surrealdb::opt::auth::Record { access: "account", namespace: "oxyde", database: "oxyde", params: serde_json::json!({ "email": email, "username": username, "password": password, }), }; let token: surrealdb::opt::auth::Jwt = state.db.signup(credentials).await.map_err(into_err)?; *state.token.lock().unwrap() = Some(token.as_insecure_token().to_string()); let mut result: Vec = state .db .query("SELECT * FROM user WHERE email = $email") .bind(("email", email)) .await .map_err(into_err)? .take(0) .map_err(into_err)?; result.pop().ok_or_else(|| into_err(AppError::NotFound("user after signup".into()))) } /// Authenticate an existing user via SurrealDB Record Auth SIGNIN. /// Returns the JWT token string. #[tauri::command] pub async fn signin( state: State<'_, AppState>, email: String, password: String, ) -> Result { let credentials = surrealdb::opt::auth::Record { access: "account", namespace: "oxyde", database: "oxyde", params: serde_json::json!({ "email": email, "password": password, }), }; let token: surrealdb::opt::auth::Jwt = state.db.signin(credentials).await.map_err(into_err)?; let token_str = token.as_insecure_token().to_string(); *state.token.lock().unwrap() = Some(token_str.clone()); Ok(token_str) } /// Clear the current session. Invalidates the token in state. #[tauri::command] pub async fn signout(state: State<'_, AppState>) -> Result<(), String> { state.db.invalidate().await.map_err(into_err)?; *state.token.lock().unwrap() = None; Ok(()) } /// Fetch the currently authenticated user record. /// Relies on the DB connection being authenticated (token set via signin/signup). #[tauri::command] pub async fn get_me(state: State<'_, AppState>) -> Result { let mut result: Vec = state .db .query("SELECT * FROM $auth") .await .map_err(into_err)? .take(0) .map_err(into_err)?; result.pop().ok_or_else(|| into_err(AppError::Auth("not authenticated".into()))) } /// Update mutable profile fields. Only provided fields are changed. #[tauri::command] pub async fn update_profile( state: State<'_, AppState>, username: Option, avatar: Option, ) -> Result { let mut result: Vec = state .db .query( "UPDATE $auth SET username = $username ?? username, avatar = $avatar ?? avatar RETURN AFTER", ) .bind(("username", username)) .bind(("avatar", avatar)) .await .map_err(into_err)? .take(0) .map_err(into_err)?; result.pop().ok_or_else(|| into_err(AppError::NotFound("user".into()))) } /// Return the contacts list for the current user. /// Contacts are `contact` records where `owner = $auth`. #[tauri::command] pub async fn get_contacts(state: State<'_, AppState>) -> Result, String> { let result: Vec = state .db .query("SELECT target.* FROM contact WHERE owner = $auth FETCH target") .await .map_err(into_err)? .take(0) .map_err(into_err)?; Ok(result) } /// Add a user to the current user's contact list. Stub — returns the Contact record. #[tauri::command] pub async fn add_contact( state: State<'_, AppState>, user_id: String, ) -> Result { let mut result: Vec = state .db .query("CREATE contact SET owner = $auth, target = type::thing('user', $uid)") .bind(("uid", user_id)) .await .map_err(into_err)? .take(0) .map_err(into_err)?; result.pop().ok_or_else(|| into_err(AppError::NotFound("contact after create".into()))) } ``` - [ ] **Step 2: Verify it compiles** ```bash cd src-tauri && cargo check ``` Expected: no new errors from `user.rs`. - [ ] **Step 3: Commit** ```bash git add src-tauri/src/commands/user.rs git commit -m "feat: add user commands (signup, signin, signout, get_me, update_profile, contacts)" ``` --- ### Task 7: Create `commands/chat.rs` **Files:** - Create: `src-tauri/src/commands/chat.rs` - [ ] **Step 1: Create `commands/chat.rs`** ```rust use tauri::{AppHandle, Emitter, State}; use uuid::Uuid; use futures_util::StreamExt; use crate::db::AppState; use crate::error::{into_err, AppError}; use crate::models::{Message, Room}; /// Create a new chat room. #[tauri::command] pub async fn create_room( state: State<'_, AppState>, name: String, ) -> Result { let mut result: Vec = state .db .query("CREATE room SET name = $name, created = time::now()") .bind(("name", name)) .await .map_err(into_err)? .take(0) .map_err(into_err)?; result.pop().ok_or_else(|| into_err(AppError::NotFound("room after create".into()))) } /// Fetch all rooms. #[tauri::command] pub async fn get_rooms(state: State<'_, AppState>) -> Result, String> { let result: Vec = state .db .query("SELECT * FROM room ORDER BY created DESC") .await .map_err(into_err)? .take(0) .map_err(into_err)?; Ok(result) } /// Send a message to a room. #[tauri::command] pub async fn send_message( state: State<'_, AppState>, room_id: String, body: String, ) -> Result { let mut result: Vec = state .db .query( "CREATE message SET room = type::thing('room', $room_id), author = $auth, body = $body, created = time::now()", ) .bind(("room_id", room_id)) .bind(("body", body)) .await .map_err(into_err)? .take(0) .map_err(into_err)?; result.pop().ok_or_else(|| into_err(AppError::NotFound("message after create".into()))) } /// Fetch all messages in a room, oldest first. #[tauri::command] pub async fn get_messages( state: State<'_, AppState>, room_id: String, ) -> Result, String> { let result: Vec = state .db .query("SELECT * FROM message WHERE room = type::thing('room', $room_id) ORDER BY created ASC") .bind(("room_id", room_id)) .await .map_err(into_err)? .take(0) .map_err(into_err)?; Ok(result) } /// Delete a message by its ID string (e.g. "message:abc123"). #[tauri::command] pub async fn delete_message( state: State<'_, AppState>, message_id: String, ) -> Result<(), String> { state .db .query("DELETE type::thing($id)") .bind(("id", message_id)) .await .map_err(into_err)?; Ok(()) } /// Start a LIVE query for new messages in a room. /// Spawns a background tokio task that emits "chat:message" Tauri events. /// /// Returns a local subscription UUID — pass it to `unsubscribe_room` on cleanup. /// Aborting the JoinHandle drops the stream, which closes the LIVE query automatically. #[tauri::command] pub async fn subscribe_room( state: State<'_, AppState>, app_handle: AppHandle, room_id: String, ) -> Result { let db = state.db.clone(); let mut stream = db .query("LIVE SELECT * FROM message WHERE room = type::thing('room', $room_id)") .bind(("room_id", room_id)) .await .map_err(into_err)? .stream::>(0) .map_err(into_err)?; let sub_id = Uuid::new_v4(); let handle = tokio::spawn(async move { while let Some(Ok(notification)) = stream.next().await { let _ = app_handle.emit("chat:message", ¬ification.data); } }); state.subscriptions.lock().unwrap().insert(sub_id, handle); Ok(sub_id.to_string()) } /// Stop a LIVE query subscription. /// Aborts the background task — dropping the stream closes the LIVE query. #[tauri::command] pub async fn unsubscribe_room( state: State<'_, AppState>, sub_id: String, ) -> Result<(), String> { let uuid = sub_id .parse::() .map_err(|e| into_err(AppError::Subscription(e.to_string())))?; if let Some(handle) = state.subscriptions.lock().unwrap().remove(&uuid) { handle.abort(); } Ok(()) } ``` - [ ] **Step 2: Add `futures-util` dependency** (needed for `StreamExt` in subscribe_room) Add to `src-tauri/Cargo.toml` `[dependencies]`: ```toml futures-util = "0.3" ``` - [ ] **Step 3: Verify it compiles** ```bash cd src-tauri && cargo check ``` Expected: no new errors from `chat.rs`. - [ ] **Step 4: Commit** ```bash git add src-tauri/src/commands/chat.rs src-tauri/Cargo.toml src-tauri/Cargo.lock git commit -m "feat: add chat commands with LIVE query subscription support" ``` --- ### Task 8: Update `lib.rs` **Files:** - Modify: `src-tauri/src/lib.rs` - [ ] **Step 1: Replace `lib.rs` entirely** ```rust use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tauri::Manager; mod commands; mod db; mod error; mod models; use db::{init_db, AppState}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .setup(|app| { let app_handle = app.handle().clone(); tauri::async_runtime::block_on(async move { let surreal = init_db("localhost:8000", "oxyde", "oxyde") .await .expect("Failed to connect to SurrealDB"); let state = AppState { db: Arc::new(surreal), token: Mutex::new(None), subscriptions: Mutex::new(HashMap::new()), }; app_handle.manage(state); }); Ok(()) }) .invoke_handler(tauri::generate_handler![ commands::user::signup, commands::user::signin, commands::user::signout, commands::user::get_me, commands::user::update_profile, commands::user::get_contacts, commands::user::add_contact, commands::chat::create_room, commands::chat::get_rooms, commands::chat::send_message, commands::chat::get_messages, commands::chat::delete_message, commands::chat::subscribe_room, commands::chat::unsubscribe_room, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` - [ ] **Step 2: Verify full project compiles** ```bash cd src-tauri && cargo check ``` Expected: clean — no errors. - [ ] **Step 3: Commit** ```bash git add src-tauri/src/lib.rs git commit -m "feat: wire AppState and all commands into Tauri builder" ``` --- ### Task 9: Create SurrealQL schema files **Files:** - Create: `surreal/schema.surql` - Create: `surreal/auth.surql` - [ ] **Step 1: Create `surreal/` directory and `schema.surql`** ```sql -- surreal/schema.surql -- Run once against your SurrealDB instance to define all tables and fields. DEFINE TABLE user SCHEMAFULL; DEFINE FIELD username ON user TYPE string; DEFINE FIELD email ON user TYPE string; DEFINE FIELD password ON user TYPE string; DEFINE FIELD avatar ON user TYPE option; DEFINE FIELD created ON user TYPE datetime DEFAULT time::now(); DEFINE INDEX email_idx ON user FIELDS email UNIQUE; DEFINE TABLE room SCHEMAFULL; DEFINE FIELD name ON room TYPE string; DEFINE FIELD created ON room TYPE datetime DEFAULT time::now(); DEFINE TABLE message SCHEMAFULL; DEFINE FIELD room ON message TYPE record; DEFINE FIELD author ON message TYPE record; DEFINE FIELD body ON message TYPE string; DEFINE FIELD created ON message TYPE datetime DEFAULT time::now(); DEFINE TABLE contact SCHEMAFULL; DEFINE FIELD owner ON contact TYPE record; DEFINE FIELD target ON contact TYPE record; DEFINE INDEX unique_contact ON contact FIELDS owner, target UNIQUE; ``` - [ ] **Step 2: Create `surreal/auth.surql`** ```sql -- surreal/auth.surql -- Run after schema.surql. -- Set SURREAL_JWT_SECRET env var before running SurrealDB, -- then substitute the value here. Never hardcode in source control. DEFINE ACCESS account ON DATABASE TYPE RECORD SIGNUP ( CREATE user SET email = $email, username = $username, password = crypto::argon2::generate($password) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) ) WITH JWT ALGORITHM HS512 KEY env::get("SURREAL_JWT_SECRET"); ``` - [ ] **Step 3: Commit** ```bash git add surreal/ git commit -m "feat: add SurrealQL schema and Record Auth definitions" ``` --- ### Task 10: Final compile + run check **Files:** none new - [ ] **Step 1: Run all Rust tests** ```bash cd src-tauri && cargo test ``` Expected: all tests pass (error.rs and models.rs unit tests). - [ ] **Step 2: Check Tauri dev build starts** (requires SurrealDB running locally) ```bash # In a separate terminal, start SurrealDB: surreal start --user root --pass root file://oxyde.db # Then: pnpm tauri dev ``` Expected: app window opens without a panic. SurrealDB connection errors will surface in the terminal if the server isn't running — that is expected in CI without a DB. The build itself must succeed. - [ ] **Step 3: Final commit** ```bash git add -p # review any remaining unstaged changes git commit -m "chore: verify scaffold compiles and boots" ```