diff --git a/Cargo.lock b/Cargo.lock
index a0fad2ad34..f825fa779d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,6 +23,17 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "1.1.3"
@@ -541,6 +552,12 @@ version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
+[[package]]
+name = "base64ct"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+
 [[package]]
 name = "bezier_easing"
 version = "0.1.1"
@@ -802,6 +819,26 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bzip2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
 [[package]]
 name = "cairo-rs"
 version = "0.18.5"
@@ -1021,6 +1058,7 @@ dependencies = [
  "md5",
  "mp4",
  "nix 0.29.0",
+ "num_cpus",
  "objc",
  "objc2-app-kit",
  "png",
@@ -1035,6 +1073,7 @@ dependencies = [
  "specta",
  "specta-typescript",
  "swift-rs",
+ "sysinfo 0.32.1",
  "tauri",
  "tauri-build",
  "tauri-nspanel",
@@ -1068,6 +1107,7 @@ dependencies = [
  "whisper-rs",
  "windows 0.60.0",
  "windows-sys 0.59.0",
+ "zip 0.6.6",
 ]
 
 [[package]]
@@ -1526,6 +1566,16 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "82bc2f84c0baaa09299da3a03864491549685912c1e338a54211e00589dc1e4c"
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
 [[package]]
 name = "clang-sys"
 version = "1.8.1"
@@ -1738,6 +1788,12 @@ dependencies = [
  "tiny-keccak",
 ]
 
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
 [[package]]
 name = "convert_case"
 version = "0.4.0"
@@ -2317,6 +2373,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
  "crypto-common",
+ "subtle",
 ]
 
 [[package]]
@@ -3602,6 +3659,15 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
 
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "home"
 version = "0.5.11"
@@ -4026,6 +4092,15 @@ dependencies = [
  "cfb",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "inquire"
 version = "0.7.5"
@@ -5769,6 +5844,17 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "password-hash"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
 [[package]]
 name = "paste"
 version = "1.0.15"
@@ -5781,6 +5867,18 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
 
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest",
+ "hmac",
+ "password-hash",
+ "sha2",
+]
+
 [[package]]
 name = "pbr"
 version = "1.1.1"
@@ -6976,7 +7074,7 @@ dependencies = [
  "objc",
  "pipewire",
  "rand 0.8.5",
- "sysinfo",
+ "sysinfo 0.30.13",
  "thiserror 2.0.12",
  "windows 0.58.0",
  "windows-capture",
@@ -8048,6 +8146,20 @@ dependencies = [
  "windows 0.52.0",
 ]
 
+[[package]]
+name = "sysinfo"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+ "memchr",
+ "ntapi",
+ "rayon",
+ "windows 0.54.0",
+]
+
 [[package]]
 name = "system-configuration"
 version = "0.6.1"
@@ -8595,7 +8707,7 @@ dependencies = [
  "tokio",
  "url",
  "windows-sys 0.60.2",
- "zip",
+ "zip 4.3.0",
 ]
 
 [[package]]
@@ -11228,6 +11340,26 @@ dependencies = [
  "syn 2.0.104",
 ]
 
+[[package]]
+name = "zip"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
+dependencies = [
+ "aes",
+ "byteorder",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "crossbeam-utils",
+ "flate2",
+ "hmac",
+ "pbkdf2",
+ "sha1",
+ "time",
+ "zstd",
+]
+
 [[package]]
 name = "zip"
 version = "4.3.0"
@@ -11246,6 +11378,35 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
 
+[[package]]
+name = "zstd"
+version = "0.11.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "5.0.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.15+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
 [[package]]
 name = "zune-core"
 version = "0.4.12"
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index 7c49e88664..2415a54bd6 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -77,6 +77,9 @@ clipboard-rs = "0.2.2"
 whisper-rs = "0.11.0"
 lazy_static = "1.4.0"
 log = "0.4.20"
+num_cpus = "1.16.0"
+sysinfo = "0.32.1"
+zip = "0.6"
 
 cap-audio = { path = "../../../crates/audio" }
 cap-camera = { path = "../../../crates/camera", features = ["serde", "specta"] }
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 38c15d50ea..715d437ab9 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -17,6 +17,7 @@ mod permissions;
 mod platform;
 mod presets;
 mod recording;
+mod system_info;
 mod target_select_overlay;
 mod tray;
 mod upload;
@@ -1972,6 +1973,10 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
             captions::export_captions_srt,
             target_select_overlay::open_target_select_overlays,
             target_select_overlay::close_target_select_overlays,
+            system_info::get_logs_and_system_info,
+            system_info::get_log_files,
+            system_info::get_last_recording_zip,
+            system_info::get_recording_zip,
         ])
         .events(tauri_specta::collect_events![
             RecordingOptionsChanged,
diff --git a/apps/desktop/src-tauri/src/system_info.rs b/apps/desktop/src-tauri/src/system_info.rs
new file mode 100644
index 0000000000..b752463d85
--- /dev/null
+++ b/apps/desktop/src-tauri/src/system_info.rs
@@ -0,0 +1,418 @@
+use base64::prelude::*;
+use serde::{Deserialize, Serialize};
+use specta::Type;
+use std::fs;
+use std::io::{Read, Write};
+use std::path::PathBuf;
+use tauri::{AppHandle, Manager};
+use zip::ZipWriter;
+use zip::write::FileOptions;
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct SystemInfo {
+    pub os: String,
+    pub os_version: String,
+    pub arch: String,
+    pub cpu_cores: u32,
+    pub memory_gb: f64,
+    pub displays: Vec<DisplayInfo>,
+    pub cameras: Vec<String>,
+    pub microphones: Vec<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct DisplayInfo {
+    pub width: u32,
+    pub height: u32,
+    pub scale_factor: f64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct RecordingLog {
+    pub id: String,
+    pub timestamp: String,
+    pub duration_seconds: Option<f64>,
+    pub error: Option<String>,
+    pub log_content: Option<String>,
+    pub log_file_path: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct LogsAndSystemInfo {
+    pub system_info: SystemInfo,
+    pub recent_logs: Vec<RecordingLog>,
+    pub app_version: String,
+}
+
+pub async fn get_system_info() -> Result<SystemInfo, String> {
+    let os = std::env::consts::OS.to_string();
+
+    #[cfg(target_os = "macos")]
+    let os_version = {
+        use std::process::Command;
+        Command::new("sw_vers")
+            .arg("-productVersion")
+            .output()
+            .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
+            .unwrap_or_else(|_| "Unknown".to_string())
+    };
+
+    #[cfg(target_os = "windows")]
+    let os_version = {
+        use std::process::Command;
+        Command::new("cmd")
+            .args(&["/C", "ver"])
+            .output()
+            .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
+            .unwrap_or_else(|_| "Unknown".to_string())
+    };
+
+    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
+    let os_version = "Unknown".to_string();
+
+    let arch = std::env::consts::ARCH.to_string();
+    let cpu_cores = num_cpus::get() as u32;
+
+    let memory_gb = {
+        use sysinfo::System;
+        let sys = System::new_all();
+        sys.total_memory() as f64 / (1024.0 * 1024.0 * 1024.0)
+    };
+
+    let displays = get_display_info().await;
+    let cameras = get_camera_list().await;
+    let microphones = get_microphone_list().await;
+
+    Ok(SystemInfo {
+        os,
+        os_version,
+        arch,
+        cpu_cores,
+        memory_gb,
+        displays,
+        cameras,
+        microphones,
+    })
+}
+
+async fn get_display_info() -> Vec<DisplayInfo> {
+    #[cfg(target_os = "macos")]
+    {
+        use core_graphics::display::CGDisplay;
+        CGDisplay::active_displays()
+            .map(|displays| {
+                displays
+                    .into_iter()
+                    .map(|display_id| {
+                        let display = CGDisplay::new(display_id);
+                        let bounds = display.bounds();
+                        DisplayInfo {
+                            width: bounds.size.width as u32,
+                            height: bounds.size.height as u32,
+                            scale_factor: 1.0,
+                        }
+                    })
+                    .collect()
+            })
+            .unwrap_or_default()
+    }
+
+    #[cfg(not(target_os = "macos"))]
+    {
+        vec![DisplayInfo {
+            width: 1920,
+            height: 1080,
+            scale_factor: 1.0,
+        }]
+    }
+}
+
+async fn get_camera_list() -> Vec<String> {
+    let cameras = crate::recording::list_cameras();
+    cameras
+        .into_iter()
+        .map(|c| c.display_name().to_string())
+        .collect()
+}
+
+async fn get_microphone_list() -> Vec<String> {
+    match crate::list_audio_devices().await {
+        Ok(devices) => devices,
+        Err(_) => vec!["Unable to query microphones".to_string()],
+    }
+}
+
+pub async fn get_recent_recording_logs(
+    app: &AppHandle,
+    count: usize,
+) -> Result<Vec<RecordingLog>, String> {
+    let recordings_dir = app
+        .path()
+        .app_data_dir()
+        .map_err(|e| e.to_string())?
+        .join("recordings");
+
+    if !recordings_dir.exists() {
+        return Ok(vec![]);
+    }
+
+    let mut recordings: Vec<(PathBuf, std::time::SystemTime)> = vec![];
+
+    if let Ok(entries) = fs::read_dir(&recordings_dir) {
+        for entry in entries.flatten() {
+            let path = entry.path();
+            if path.is_dir() {
+                if let Ok(metadata) = entry.metadata() {
+                    if let Ok(modified) = metadata.modified() {
+                        recordings.push((path, modified));
+                    }
+                }
+            }
+        }
+    }
+
+    recordings.sort_by(|a, b| b.1.cmp(&a.1));
+    recordings.truncate(count);
+
+    let mut logs = vec![];
+
+    for (recording_path, _) in recordings {
+        let recording_id = recording_path
+            .file_name()
+            .and_then(|n| n.to_str())
+            .unwrap_or("unknown")
+            .to_string();
+
+        let meta_path = recording_path.join("recording-meta.json");
+        let log_path = recording_path.join("recording-logs.log");
+
+        let mut log = RecordingLog {
+            id: recording_id.clone(),
+            timestamp: chrono::Local::now().to_rfc3339(),
+            duration_seconds: None,
+            error: None,
+            log_content: None,
+            log_file_path: None,
+        };
+
+        if meta_path.exists() {
+            if let Ok(meta_content) = fs::read_to_string(&meta_path) {
+                if let Ok(meta_json) = serde_json::from_str::<serde_json::Value>(&meta_content) {
+                    if let Some(duration) = meta_json.get("duration").and_then(|d| d.as_f64()) {
+                        log.duration_seconds = Some(duration);
+                    }
+                    if let Some(timestamp) = meta_json.get("timestamp").and_then(|t| t.as_str()) {
+                        log.timestamp = timestamp.to_string();
+                    }
+                }
+            }
+        }
+
+        if log_path.exists() {
+            log.log_file_path = Some(log_path.to_string_lossy().to_string());
+        }
+
+        logs.push(log);
+    }
+
+    Ok(logs)
+}
+
+#[tauri::command]
+#[specta::specta]
+pub async fn get_logs_and_system_info(app: AppHandle) -> Result<LogsAndSystemInfo, String> {
+    let system_info = get_system_info().await?;
+    let recent_logs = get_recent_recording_logs(&app, 3).await?;
+    let app_version = app.package_info().version.to_string();
+
+    Ok(LogsAndSystemInfo {
+        system_info,
+        recent_logs,
+        app_version,
+    })
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct LogFile {
+    pub name: String,
+    pub content: String,
+}
+
+#[tauri::command]
+#[specta::specta]
+pub async fn get_log_files(paths: Vec<String>) -> Result<Vec<LogFile>, String> {
+    let mut log_files = vec![];
+
+    for path_str in paths {
+        let path = PathBuf::from(&path_str);
+        if path.exists() {
+            if let Ok(content) = fs::read(&path) {
+                let base64_content = base64::prelude::BASE64_STANDARD.encode(&content);
+                let file_name = path
+                    .file_name()
+                    .and_then(|n| n.to_str())
+                    .unwrap_or("recording.log")
+                    .to_string();
+
+                let parent_dir_name = path
+                    .parent()
+                    .and_then(|p| p.file_name())
+                    .and_then(|n| n.to_str())
+                    .unwrap_or("unknown");
+
+                let full_name = format!("{}/{}", parent_dir_name, file_name);
+
+                log_files.push(LogFile {
+                    name: full_name,
+                    content: base64_content,
+                });
+            }
+        }
+    }
+
+    Ok(log_files)
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct LastRecording {
+    pub name: String,
+    pub content: String,
+    pub size_mb: f64,
+}
+
+#[tauri::command]
+#[specta::specta]
+pub async fn get_recording_zip(
+    app: AppHandle,
+    recording_path: Option<String>,
+) -> Result<Option<LastRecording>, String> {
+    let recording_path = if let Some(path) = recording_path {
+        PathBuf::from(path)
+    } else {
+        // If no path provided, get the last recording
+        let recordings_dir = app
+            .path()
+            .app_data_dir()
+            .map_err(|e| e.to_string())?
+            .join("recordings");
+
+        if !recordings_dir.exists() {
+            return Ok(None);
+        }
+
+        // Find the most recent recording directory
+        let mut recordings: Vec<(PathBuf, std::time::SystemTime)> = vec![];
+
+        if let Ok(entries) = fs::read_dir(&recordings_dir) {
+            for entry in entries.flatten() {
+                let path = entry.path();
+                if path.is_dir() && path.extension().and_then(|s| s.to_str()) == Some("cap") {
+                    if let Ok(metadata) = entry.metadata() {
+                        if let Ok(modified) = metadata.modified() {
+                            recordings.push((path, modified));
+                        }
+                    }
+                }
+            }
+        }
+
+        if recordings.is_empty() {
+            return Ok(None);
+        }
+
+        // Sort by modification time, most recent first
+        recordings.sort_by(|a, b| b.1.cmp(&a.1));
+        recordings[0].0.clone()
+    };
+
+    if !recording_path.exists() {
+        return Err(format!(
+            "Recording path does not exist: {:?}",
+            recording_path
+        ));
+    }
+
+    // Create a zip file in memory
+    let mut zip_buffer = Vec::new();
+    {
+        let mut zip = ZipWriter::new(std::io::Cursor::new(&mut zip_buffer));
+        let options = FileOptions::default()
+            .compression_method(zip::CompressionMethod::Deflated)
+            .unix_permissions(0o755);
+
+        // Add all files from the recording directory to the zip
+        add_dir_to_zip(&mut zip, &recording_path, "", &options)?;
+
+        zip.finish()
+            .map_err(|e| format!("Failed to finish zip: {}", e))?;
+    }
+
+    let size_mb = zip_buffer.len() as f64 / (1024.0 * 1024.0);
+
+    let recording_name = recording_path
+        .file_name()
+        .and_then(|n| n.to_str())
+        .unwrap_or("recording")
+        .to_string();
+
+    Ok(Some(LastRecording {
+        name: format!("{}.zip", recording_name),
+        content: base64::prelude::BASE64_STANDARD.encode(&zip_buffer),
+        size_mb,
+    }))
+}
+
+#[tauri::command]
+#[specta::specta]
+pub async fn get_last_recording_zip(app: AppHandle) -> Result<Option<LastRecording>, String> {
+    get_recording_zip(app, None).await
+}
+
+fn add_dir_to_zip<W: Write + std::io::Seek>(
+    zip: &mut ZipWriter<W>,
+    dir_path: &PathBuf,
+    prefix: &str,
+    options: &FileOptions,
+) -> Result<(), String> {
+    let entries = fs::read_dir(dir_path).map_err(|e| format!("Failed to read directory: {}", e))?;
+
+    for entry in entries {
+        let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
+        let path = entry.path();
+        let name = path
+            .file_name()
+            .and_then(|n| n.to_str())
+            .ok_or_else(|| "Invalid file name".to_string())?;
+
+        let zip_path = if prefix.is_empty() {
+            name.to_string()
+        } else {
+            format!("{}/{}", prefix, name)
+        };
+
+        if path.is_dir() {
+            // Add directory to zip
+            zip.add_directory(&zip_path, *options)
+                .map_err(|e| format!("Failed to add directory to zip: {}", e))?;
+
+            // Recursively add directory contents
+            add_dir_to_zip(zip, &path, &zip_path, options)?;
+        } else {
+            // Add file to zip
+            zip.start_file(&zip_path, *options)
+                .map_err(|e| format!("Failed to start file in zip: {}", e))?;
+
+            let mut file =
+                fs::File::open(&path).map_err(|e| format!("Failed to open file: {}", e))?;
+
+            let mut buffer = Vec::new();
+            file.read_to_end(&mut buffer)
+                .map_err(|e| format!("Failed to read file: {}", e))?;
+
+            zip.write_all(&buffer)
+                .map_err(|e| format!("Failed to write file to zip: {}", e))?;
+        }
+    }
+
+    Ok(())
+}
diff --git a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx
index 95412ae0a1..af1980e483 100644
--- a/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx
+++ b/apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx
@@ -1,27 +1,178 @@
 import { Button } from "@cap/ui-solid";
 import { action, useAction, useSubmission } from "@solidjs/router";
 import { getVersion } from "@tauri-apps/api/app";
+import { convertFileSrc } from "@tauri-apps/api/core";
 import { type as ostype } from "@tauri-apps/plugin-os";
-import { createSignal } from "solid-js";
-
+import { createSignal, For, onMount, Show } from "solid-js";
+import { clientEnv } from "~/utils/env";
+import { commands } from "~/utils/tauri";
 import { apiClient, protectedHeaders } from "~/utils/web-api";
 
 const sendFeedbackAction = action(async (feedback: string) => {
-	const response = await apiClient.desktop.submitFeedback({
-		body: { feedback, os: ostype() as any, version: await getVersion() },
-		headers: await protectedHeaders(),
-	});
+	const logsAndInfo = await commands.getLogsAndSystemInfo();
+
+	const formData = new URLSearchParams();
+	formData.append("feedback", feedback);
+	formData.append("os", ostype() as any);
+	formData.append("version", await getVersion());
+	formData.append("systemInfo", JSON.stringify(logsAndInfo.system_info));
+
+	const headers = await protectedHeaders();
+	const response = await fetch(
+		`${clientEnv.VITE_SERVER_URL}/api/desktop/feedback`,
+		{
+			method: "POST",
+			headers: {
+				...headers,
+				"Content-Type": "application/x-www-form-urlencoded",
+			},
+			body: formData,
+		},
+	);
 
 	if (response.status !== 200) throw new Error("Failed to submit feedback");
-	return response.body;
+	return await response.json();
 });
 
 export default function FeedbackTab() {
 	const [feedback, setFeedback] = createSignal("");
+	const [isSubmittingLogs, setIsSubmittingLogs] = createSignal(false);
+	const [logsResult, setLogsResult] = createSignal<{
+		success?: boolean;
+		error?: string;
+	}>({});
+	const [isSubmittingRecording, setIsSubmittingRecording] = createSignal(false);
+	const [recordingResult, setRecordingResult] = createSignal<{
+		success?: boolean;
+		error?: string;
+	}>({});
+	const [selectedRecording, setSelectedRecording] = createSignal<string | null>(
+		null,
+	);
+	const [recordings, setRecordings] = createSignal<
+		Array<{ path: string; name: string; thumbnailPath: string }>
+	>([]);
+	const [isLoadingRecordings, setIsLoadingRecordings] = createSignal(false);
+	const [showRecordingSelector, setShowRecordingSelector] = createSignal(false);
 
 	const submission = useSubmission(sendFeedbackAction);
 	const sendFeedback = useAction(sendFeedbackAction);
 
+	const loadRecordings = async () => {
+		setIsLoadingRecordings(true);
+		try {
+			const result = await commands.listRecordings().catch(() => [] as const);
+			const recentRecordings = result.slice(0, 10).map(([path, meta]) => ({
+				path,
+				name: meta.pretty_name,
+				thumbnailPath: `${path}/screenshots/display.jpg`,
+			}));
+			setRecordings(recentRecordings);
+			if (recentRecordings.length > 0) {
+				setSelectedRecording(recentRecordings[0].path);
+			}
+		} catch (error) {
+			console.error("Error loading recordings:", error);
+			setRecordings([]);
+		} finally {
+			setIsLoadingRecordings(false);
+		}
+	};
+
+	const sendRecording = async () => {
+		const recordingPath = selectedRecording();
+		if (!recordingPath) return;
+
+		setIsSubmittingRecording(true);
+		setRecordingResult({});
+
+		try {
+			const recordingZip = await commands.getRecordingZip(recordingPath);
+
+			if (!recordingZip) {
+				setRecordingResult({ error: "Failed to compress recording" });
+				return;
+			}
+
+			const logsAndInfo = await commands.getLogsAndSystemInfo();
+
+			const response = await apiClient.desktop.submitRecording({
+				body: {
+					systemInfo: logsAndInfo.system_info,
+					appVersion: logsAndInfo.app_version,
+					recording: {
+						name: recordingZip.name,
+						content: recordingZip.content,
+						size_mb: recordingZip.size_mb,
+					},
+				},
+				headers: await protectedHeaders(),
+			});
+
+			if (response.status !== 200) {
+				throw new Error(`Failed to send recording: ${response.status}`);
+			}
+
+			setRecordingResult({ success: true });
+			setShowRecordingSelector(false);
+		} catch (error) {
+			if (error instanceof Error) {
+				setRecordingResult({ error: error.message });
+			} else {
+				setRecordingResult({ error: "Failed to send recording" });
+			}
+		} finally {
+			setIsSubmittingRecording(false);
+		}
+	};
+
+	const sendLogs = async () => {
+		setIsSubmittingLogs(true);
+		setLogsResult({});
+
+		try {
+			const logsAndInfo = await commands.getLogsAndSystemInfo();
+
+			const logFilePaths = logsAndInfo.recent_logs
+				.filter((log) => log.log_file_path)
+				.map((log) => log.log_file_path as string);
+
+			let logFiles: Array<{ name: string; content: string }> = [];
+			if (logFilePaths.length > 0) {
+				logFiles = await commands.getLogFiles(logFilePaths);
+			}
+
+			const response = await apiClient.desktop.submitLogs({
+				body: {
+					systemInfo: logsAndInfo.system_info,
+					recentLogs: logsAndInfo.recent_logs,
+					appVersion: logsAndInfo.app_version,
+					logFiles: logFiles,
+				},
+				headers: await protectedHeaders(),
+			});
+
+			if (response.status !== 200) {
+				throw new Error(
+					`Failed to send logs: ${response.status} - ${JSON.stringify(
+						response.body,
+					)}`,
+				);
+			}
+
+			setLogsResult({ success: true });
+		} catch (error) {
+			console.error("Detailed error sending logs:", error);
+			if (error instanceof Error) {
+				setLogsResult({ error: `Error: ${error.message}` });
+			} else {
+				setLogsResult({ error: `Unknown error: ${JSON.stringify(error)}` });
+			}
+		} finally {
+			setIsSubmittingLogs(false);
+		}
+	};
+
 	return (
 		<div class="flex flex-col w-full h-full">
 			<div class="flex-1 custom-scroll">
@@ -72,6 +223,196 @@ export default function FeedbackTab() {
 							</Button>
 						</fieldset>
 					</form>
+
+					{/* Send Logs Section */}
+					<div class="mt-8 pt-6 border-t border-gray-2">
+						<h3 class="text-md font-medium text-gray-12 mb-2">
+							Send Diagnostic Logs
+						</h3>
+						<p class="text-sm text-gray-10 mb-4">
+							Send diagnostic information and logs from your recent recordings
+							to help us troubleshoot issues.
+						</p>
+
+						<div class="p-3 bg-gray-2 rounded-md mb-4">
+							<p class="text-sm text-gray-11 mb-2">This will send:</p>
+							<ul class="text-sm text-gray-10 space-y-1 list-disc list-inside">
+								<li>System information (OS, hardware, displays)</li>
+								<li>Camera and microphone device list</li>
+								<li>Logs from your 3 most recent recordings</li>
+								<li>Cap app version information</li>
+							</ul>
+						</div>
+
+						{logsResult().error && (
+							<p class="text-sm text-red-400 mb-3">{logsResult().error}</p>
+						)}
+
+						{logsResult().success && (
+							<p class="text-sm text-green-400 mb-3">
+								Logs sent successfully! Thank you for helping us improve Cap.
+							</p>
+						)}
+
+						<Button
+							onClick={sendLogs}
+							size="md"
+							disabled={isSubmittingLogs()}
+							class="bg-primary text-primary"
+						>
+							{isSubmittingLogs() ? "Sending..." : "Send Logs to Cap Team"}
+						</Button>
+					</div>
+
+					{/* Send Recording Section */}
+					<div class="mt-8 pt-6 border-t border-gray-2">
+						<h3 class="text-md font-medium text-gray-12 mb-2">
+							Send Recording
+						</h3>
+						<p class="text-sm text-gray-10 mb-4">
+							Send a recording to help us debug specific issues. The entire
+							recording folder will be compressed and uploaded securely.
+						</p>
+
+						<div class="p-3 bg-gray-2 rounded-md mb-4">
+							<p class="text-sm text-gray-11 mb-2">⚠️ Important:</p>
+							<ul class="text-sm text-gray-10 space-y-1 list-disc list-inside">
+								<li>
+									This will send your entire recording including video/audio to
+									the Cap team.
+								</li>
+								<li>
+									Large recordings may take some time to compress and upload.
+								</li>
+								<li>Only use this when requested by the Cap team.</li>
+							</ul>
+						</div>
+
+						<Show when={!showRecordingSelector()}>
+							<div class="flex gap-2 items-center">
+								<Button
+									onClick={async () => {
+										await loadRecordings();
+										if (recordings().length > 0) {
+											setShowRecordingSelector(true);
+											// Scroll to bottom after a small delay to ensure DOM is updated
+											setTimeout(() => {
+												const container =
+													document.querySelector(".custom-scroll");
+												if (container) {
+													container.scrollTo({
+														top: container.scrollHeight,
+														behavior: "smooth",
+													});
+												}
+											}, 100);
+										} else {
+											setRecordingResult({ error: "No recordings found" });
+										}
+									}}
+									size="md"
+									disabled={isLoadingRecordings()}
+									class="bg-primary text-primary"
+								>
+									{isLoadingRecordings() ? (
+										<IconLucideLoaderCircle class="animate-spin size-4" />
+									) : (
+										"Select Recording to Send"
+									)}
+								</Button>
+							</div>
+						</Show>
+
+						<Show when={showRecordingSelector()}>
+							<div class="space-y-3">
+								<div class="max-h-60 overflow-y-auto border border-gray-3 rounded-md">
+									<For each={recordings()}>
+										{(recording, index) => (
+											<div
+												class={`flex items-center gap-3 p-3 hover:bg-gray-3 cursor-pointer transition-colors ${
+													selectedRecording() === recording.path
+														? "bg-gray-3"
+														: ""
+												} ${
+													index() !== recordings().length - 1
+														? "border-b border-gray-3"
+														: ""
+												}`}
+												onClick={() => setSelectedRecording(recording.path)}
+											>
+												<input
+													type="radio"
+													name="recording"
+													checked={selectedRecording() === recording.path}
+													class="text-primary"
+												/>
+												<img
+													class="object-cover rounded size-10"
+													alt="Recording thumbnail"
+													src={convertFileSrc(recording.thumbnailPath)}
+													onError={(e) => {
+														e.currentTarget.style.display = "none";
+													}}
+												/>
+												<span class="text-sm text-gray-12">
+													{recording.name}
+												</span>
+											</div>
+										)}
+									</For>
+								</div>
+
+								<Show when={isSubmittingRecording()}>
+									<div class="p-3 bg-blue-500/10 border border-blue-500/20 rounded-md">
+										<div class="flex items-center gap-2 text-sm text-blue-400">
+											<IconLucideLoaderCircle class="animate-spin size-4" />
+											<span>Uploading recording...</span>
+										</div>
+										<p class="mt-1 text-xs text-gray-10">
+											Please keep this window open while we upload your
+											recording.
+										</p>
+									</div>
+								</Show>
+
+								{recordingResult().error && (
+									<p class="text-sm text-red-400">{recordingResult().error}</p>
+								)}
+
+								{recordingResult().success && (
+									<p class="text-sm">
+										Recording sent successfully. The Cap team will review it and
+										get back to you as soon as possible.
+									</p>
+								)}
+
+								<div class="flex gap-2">
+									<Button
+										onClick={sendRecording}
+										size="md"
+										disabled={isSubmittingRecording() || !selectedRecording()}
+										class="bg-primary text-primary"
+									>
+										{isSubmittingRecording()
+											? "Uploading..."
+											: "Send Selected Recording"}
+									</Button>
+									<Show when={!isSubmittingRecording()}>
+										<Button
+											onClick={() => {
+												setShowRecordingSelector(false);
+												setSelectedRecording(null);
+											}}
+											size="md"
+											class="bg-gray-3 text-gray-12"
+										>
+											Cancel
+										</Button>
+									</Show>
+								</div>
+							</div>
+						</Show>
+					</div>
 				</div>
 			</div>
 		</div>
diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts
index cd7ad47057..e54759d7dc 100644
--- a/apps/desktop/src/utils/tauri.ts
+++ b/apps/desktop/src/utils/tauri.ts
@@ -302,6 +302,20 @@ export const commands = {
 	async closeTargetSelectOverlays(): Promise<null> {
 		return await TAURI_INVOKE("close_target_select_overlays");
 	},
+	async getLogsAndSystemInfo(): Promise<LogsAndSystemInfo> {
+		return await TAURI_INVOKE("get_logs_and_system_info");
+	},
+	async getLogFiles(paths: string[]): Promise<LogFile[]> {
+		return await TAURI_INVOKE("get_log_files", { paths });
+	},
+	async getLastRecordingZip(): Promise<LastRecording | null> {
+		return await TAURI_INVOKE("get_last_recording_zip");
+	},
+	async getRecordingZip(
+		recordingPath: string | null,
+	): Promise<LastRecording | null> {
+		return await TAURI_INVOKE("get_recording_zip", { recordingPath });
+	},
 };
 
 /** user-defined events **/
@@ -515,6 +529,11 @@ export type Cursors =
 	| { [key in string]: CursorMeta };
 export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType };
 export type DisplayId = string;
+export type DisplayInfo = {
+	width: number;
+	height: number;
+	scale_factor: number;
+};
 export type DownloadProgress = { progress: number; message: string };
 export type EditorStateChanged = { playhead_position: number };
 export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato";
@@ -548,7 +567,7 @@ export type GeneralSettingsStore = {
 	windowTransparency?: boolean;
 	postStudioRecordingBehaviour?: PostStudioRecordingBehaviour;
 	mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour;
-	customCursorCapture2?: boolean;
+	custom_cursor_capture2?: boolean;
 	serverUrl?: string;
 	recordingCountdown?: number | null;
 	enableNativeCameraPreview: boolean;
@@ -588,11 +607,18 @@ export type HotkeysConfiguration = { show: boolean };
 export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } };
 export type InstantRecordingMeta = { fps: number; sample_rate: number | null };
 export type JsonValue<T> = [T];
+export type LastRecording = { name: string; content: string; size_mb: number };
 export type LayoutMode = "default" | "cameraOnly" | "hideCamera";
 export type LayoutSegment = { start: number; end: number; mode?: LayoutMode };
+export type LogFile = { name: string; content: string };
 export type LogicalBounds = { position: LogicalPosition; size: LogicalSize };
 export type LogicalPosition = { x: number; y: number };
 export type LogicalSize = { width: number; height: number };
+export type LogsAndSystemInfo = {
+	system_info: SystemInfo;
+	recent_logs: RecordingLog[];
+	app_version: string;
+};
 export type MainWindowRecordingStartBehaviour = "close" | "minimise";
 export type ModelIDType = string;
 export type Mp4ExportSettings = {
@@ -655,6 +681,14 @@ export type RecordingEvent =
 	| { variant: "Started" }
 	| { variant: "Stopped" }
 	| { variant: "Failed"; error: string };
+export type RecordingLog = {
+	id: string;
+	timestamp: string;
+	duration_seconds: number | null;
+	error: string | null;
+	log_content: string | null;
+	log_file_path: string | null;
+};
 export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & {
 	platform?: Platform | null;
 	pretty_name: string;
@@ -738,6 +772,16 @@ export type StereoMode = "stereo" | "monoL" | "monoR";
 export type StudioRecordingMeta =
 	| { segment: SingleSegment }
 	| { inner: MultipleSegments };
+export type SystemInfo = {
+	os: string;
+	os_version: string;
+	arch: string;
+	cpu_cores: number;
+	memory_gb: number;
+	displays: DisplayInfo[];
+	cameras: string[];
+	microphones: string[];
+};
 export type TargetUnderCursor = {
 	display_id: DisplayId | null;
 	window: WindowUnderCursor | null;
diff --git a/apps/web/app/api/desktop/[...route]/root.ts b/apps/web/app/api/desktop/[...route]/root.ts
index b6633acafd..3e0a37bd9d 100644
--- a/apps/web/app/api/desktop/[...route]/root.ts
+++ b/apps/web/app/api/desktop/[...route]/root.ts
@@ -1,5 +1,6 @@
 import * as crypto from "node:crypto";
 import { db } from "@cap/database";
+import { nanoId } from "@cap/database/helpers";
 import { organizations, users } from "@cap/database/schema";
 import { buildEnv, serverEnv } from "@cap/env";
 import { stripe, userIsPro } from "@cap/utils";
@@ -8,6 +9,7 @@ import { eq } from "drizzle-orm";
 import { Hono } from "hono";
 import { PostHog } from "posthog-node";
 import { z } from "zod";
+import { createBucketProvider } from "@/utils/s3";
 import { withAuth } from "../../utils";
 
 export const app = new Hono().use(withAuth);
@@ -20,27 +22,58 @@ app.post(
 			feedback: z.string(),
 			os: z.union([z.literal("macos"), z.literal("windows")]).optional(),
 			version: z.string().optional(),
+			systemInfo: z
+				.string()
+				.optional()
+				.transform((val) => {
+					if (!val) return undefined;
+					try {
+						return JSON.parse(val);
+					} catch {
+						return undefined;
+					}
+				})
+				.pipe(
+					z
+						.object({
+							os: z.string(),
+							os_version: z.string(),
+							arch: z.string(),
+							cpu_cores: z.number(),
+							memory_gb: z.number(),
+							displays: z.array(
+								z.object({
+									width: z.number(),
+									height: z.number(),
+									scale_factor: z.number(),
+								}),
+							),
+							cameras: z.array(z.string()),
+							microphones: z.array(z.string()),
+						})
+						.optional(),
+				),
 		}),
 	),
 	async (c) => {
-		const { feedback, os, version } = c.req.valid("form");
+		const { feedback, os, version, systemInfo } = c.req.valid("form");
 
 		try {
 			const discordWebhookUrl = serverEnv().DISCORD_FEEDBACK_WEBHOOK_URL;
 			if (!discordWebhookUrl)
 				throw new Error("Discord webhook URL is not configured");
 
+			let messageContent = `New feedback from ${c.get("user").email}:\n${feedback}`;
+
+			if (version) {
+				messageContent += `\nCap version: ${version}`;
+			}
+
 			const response = await fetch(discordWebhookUrl, {
 				method: "POST",
 				headers: { "Content-Type": "application/json" },
 				body: JSON.stringify({
-					content: [
-						`New feedback from ${c.get("user").email}:`,
-						feedback,
-						os && version && `${os} v${version}`,
-					]
-						.filter(Boolean)
-						.join("\n"),
+					content: messageContent,
 				}),
 			});
 
@@ -59,6 +92,284 @@ app.post(
 	},
 );
 
+app.post(
+	"/recording",
+	zValidator(
+		"json",
+		z.object({
+			systemInfo: z.object({
+				os: z.string(),
+				os_version: z.string(),
+				arch: z.string(),
+				cpu_cores: z.number(),
+				memory_gb: z.number(),
+				displays: z.array(
+					z.object({
+						width: z.number(),
+						height: z.number(),
+						scale_factor: z.number(),
+					}),
+				),
+				cameras: z.array(z.string()),
+				microphones: z.array(z.string()),
+			}),
+			appVersion: z.string(),
+			recording: z.object({
+				name: z.string(),
+				content: z.string(),
+				size_mb: z.number(),
+			}),
+		}),
+	),
+	async (c) => {
+		const { systemInfo, appVersion, recording } = c.req.valid("json");
+		const user = c.get("user");
+
+		try {
+			const bucket = await createBucketProvider();
+			const timestamp = new Date().toISOString().split("T")[0];
+			const recordingKey = `debug-recordings/${user.id}/${timestamp}-${nanoId()}.zip`;
+
+			const buffer = Buffer.from(recording.content, "base64");
+
+			await bucket.putObject(recordingKey, buffer, {
+				contentType: "application/zip",
+			});
+
+			const downloadUrl = await bucket.getSignedObjectUrl(recordingKey);
+
+			const discordWebhookUrl = serverEnv().DISCORD_LOGS_WEBHOOK_URL;
+			if (!discordWebhookUrl)
+				throw new Error("Discord webhook URL is not configured");
+
+			const formattedMessage = {
+				content: `📹 **Recording Submission from ${user.email}**`,
+				embeds: [
+					{
+						title: "Recording Details",
+						color: 5814783,
+						fields: [
+							{
+								name: "File",
+								value: recording.name,
+								inline: true,
+							},
+							{
+								name: "Size",
+								value: `${recording.size_mb.toFixed(2)} MB`,
+								inline: true,
+							},
+							{
+								name: "App Version",
+								value: appVersion,
+								inline: true,
+							},
+							{
+								name: "OS",
+								value: `${systemInfo.os} ${systemInfo.os_version}`,
+								inline: true,
+							},
+							{
+								name: "Architecture",
+								value: systemInfo.arch,
+								inline: true,
+							},
+							{
+								name: "Hardware",
+								value: `${systemInfo.cpu_cores} cores, ${systemInfo.memory_gb.toFixed(1)} GB RAM`,
+								inline: true,
+							},
+							{
+								name: "Download Link",
+								value: `[Download Recording (valid for 7 days)](${downloadUrl})`,
+								inline: false,
+							},
+						],
+					},
+				],
+			};
+
+			const response = await fetch(discordWebhookUrl, {
+				method: "POST",
+				headers: { "Content-Type": "application/json" },
+				body: JSON.stringify(formattedMessage),
+			});
+
+			if (!response.ok)
+				throw new Error(
+					`Failed to send notification to Discord: ${response.statusText}`,
+				);
+
+			return c.json({
+				success: true,
+				message: "Recording uploaded successfully",
+			});
+		} catch (error) {
+			console.error("Error submitting recording:", error);
+			return c.json({ error: "Failed to submit recording" }, { status: 500 });
+		}
+	},
+);
+
+app.post(
+	"/logs",
+	zValidator(
+		"json",
+		z.object({
+			systemInfo: z.object({
+				os: z.string(),
+				os_version: z.string(),
+				arch: z.string(),
+				cpu_cores: z.number(),
+				memory_gb: z.number(),
+				displays: z.array(
+					z.object({
+						width: z.number(),
+						height: z.number(),
+						scale_factor: z.number(),
+					}),
+				),
+				cameras: z.array(z.string()),
+				microphones: z.array(z.string()),
+			}),
+			recentLogs: z.array(
+				z.object({
+					id: z.string(),
+					timestamp: z.string(),
+					duration_seconds: z.number().nullable(),
+					error: z.string().nullable(),
+					log_content: z.string().nullable(),
+					log_file_path: z.string().nullable().optional(),
+				}),
+			),
+			appVersion: z.string(),
+			logFiles: z
+				.array(
+					z.object({
+						name: z.string(),
+						content: z.string(),
+					}),
+				)
+				.optional(),
+		}),
+	),
+	async (c) => {
+		const { systemInfo, appVersion, logFiles } = c.req.valid("json");
+
+		try {
+			const discordWebhookUrl = serverEnv().DISCORD_LOGS_WEBHOOK_URL;
+			if (!discordWebhookUrl)
+				throw new Error("Discord webhook URL is not configured");
+
+			const formattedMessage = {
+				content: `🔧 **Logs Report from ${c.get("user").email}**`,
+				embeds: [
+					{
+						title: "System Information",
+						color: 5814783,
+						fields: [
+							{
+								name: "OS",
+								value: `${systemInfo.os} ${systemInfo.os_version}`,
+								inline: true,
+							},
+							{
+								name: "Architecture",
+								value: systemInfo.arch,
+								inline: true,
+							},
+							{
+								name: "App Version",
+								value: appVersion,
+								inline: true,
+							},
+							{
+								name: "CPU Cores",
+								value: systemInfo.cpu_cores.toString(),
+								inline: true,
+							},
+							{
+								name: "Memory",
+								value: `${systemInfo.memory_gb.toFixed(1)} GB`,
+								inline: true,
+							},
+							{
+								name: "Displays",
+								value: systemInfo.displays
+									.map((d) => `${d.width}x${d.height}`)
+									.join(", "),
+								inline: false,
+							},
+							{
+								name: "Cameras",
+								value:
+									systemInfo.cameras.slice(0, 3).join("\n") || "None detected",
+								inline: false,
+							},
+							{
+								name: "Microphones",
+								value:
+									systemInfo.microphones.slice(0, 3).join("\n") ||
+									"None detected",
+								inline: false,
+							},
+						],
+					},
+				] as Array<{
+					title: string;
+					color: number;
+					fields?: {
+						name: string;
+						value: string;
+						inline: boolean;
+					}[];
+					description?: string;
+				}>,
+			};
+
+			if (logFiles && logFiles.length > 0) {
+				const formData = new FormData();
+
+				formData.append("payload_json", JSON.stringify(formattedMessage));
+
+				logFiles.forEach((file, index) => {
+					const buffer = Buffer.from(file.content, "base64");
+					const blob = new Blob([buffer], { type: "text/plain" });
+					formData.append(`files[${index}]`, blob, file.name);
+				});
+
+				const response = await fetch(discordWebhookUrl, {
+					method: "POST",
+					body: formData,
+				});
+
+				if (!response.ok)
+					throw new Error(
+						`Failed to send logs to Discord: ${response.statusText}`,
+					);
+			} else {
+				const response = await fetch(discordWebhookUrl, {
+					method: "POST",
+					headers: { "Content-Type": "application/json" },
+					body: JSON.stringify(formattedMessage),
+				});
+
+				if (!response.ok)
+					throw new Error(
+						`Failed to send logs to Discord: ${response.statusText}`,
+					);
+			}
+
+			return c.json({
+				success: true,
+				message: "Logs submitted successfully",
+			});
+		} catch (error) {
+			return c.json({ error: "Failed to submit logs" }, { status: 500 });
+		}
+	},
+);
+
 app.get("/org-custom-domain", async (c) => {
 	const user = c.get("user");
 
@@ -72,7 +383,6 @@ app.get("/org-custom-domain", async (c) => {
 			.leftJoin(organizations, eq(users.activeOrganizationId, organizations.id))
 			.where(eq(users.id, user.id));
 
-		// Ensure custom domain has https:// prefix
 		let customDomain = result?.customDomain ?? null;
 		if (
 			customDomain &&
diff --git a/packages/database/auth/auth-options.tsx b/packages/database/auth/auth-options.tsx
index b728e0fab1..85e0961b2a 100644
--- a/packages/database/auth/auth-options.tsx
+++ b/packages/database/auth/auth-options.tsx
@@ -117,7 +117,7 @@ export const authOptions = (): NextAuthOptions => {
 					httpOnly: true,
 					sameSite: "none",
 					path: "/",
-					secure: process.env.NODE_ENV === "production",
+					secure: true,
 				},
 			},
 		},
diff --git a/packages/env/server.ts b/packages/env/server.ts
index 678825bc51..d4a3ae95a2 100644
--- a/packages/env/server.ts
+++ b/packages/env/server.ts
@@ -41,6 +41,7 @@ function createServerEnv() {
 			STRIPE_SECRET_KEY_LIVE: z.string().optional(),
 			STRIPE_WEBHOOK_SECRET: z.string().optional(),
 			DISCORD_FEEDBACK_WEBHOOK_URL: z.string().optional(),
+			DISCORD_LOGS_WEBHOOK_URL: z.string().optional(),
 			OPENAI_API_KEY: z.string().optional(),
 			GROQ_API_KEY: z.string().optional(),
 			INTERCOM_SECRET: z.string().optional(),
diff --git a/packages/web-api-contract/src/desktop.ts b/packages/web-api-contract/src/desktop.ts
index c0942de1d3..fe13845a99 100644
--- a/packages/web-api-contract/src/desktop.ts
+++ b/packages/web-api-contract/src/desktop.ts
@@ -54,11 +54,104 @@ const protectedContract = c.router(
 				feedback: z.string(),
 				os: z.union([z.literal("macos"), z.literal("windows")]),
 				version: z.string(),
+				systemInfo: z
+					.object({
+						os: z.string(),
+						os_version: z.string(),
+						arch: z.string(),
+						cpu_cores: z.number(),
+						memory_gb: z.number(),
+						displays: z.array(
+							z.object({
+								width: z.number(),
+								height: z.number(),
+								scale_factor: z.number(),
+							}),
+						),
+						cameras: z.array(z.string()),
+						microphones: z.array(z.string()),
+					})
+					.optional(),
 			}),
 			responses: {
 				200: z.object({ success: z.boolean() }),
 			},
 		},
+		submitRecording: {
+			method: "POST",
+			path: "/desktop/recording",
+			body: z.object({
+				systemInfo: z.object({
+					os: z.string(),
+					os_version: z.string(),
+					arch: z.string(),
+					cpu_cores: z.number(),
+					memory_gb: z.number(),
+					displays: z.array(
+						z.object({
+							width: z.number(),
+							height: z.number(),
+							scale_factor: z.number(),
+						}),
+					),
+					cameras: z.array(z.string()),
+					microphones: z.array(z.string()),
+				}),
+				appVersion: z.string(),
+				recording: z.object({
+					name: z.string(),
+					content: z.string(),
+					size_mb: z.number(),
+				}),
+			}),
+			responses: {
+				200: z.object({ success: z.boolean(), message: z.string() }),
+			},
+		},
+		submitLogs: {
+			method: "POST",
+			path: "/desktop/logs",
+			body: z.object({
+				systemInfo: z.object({
+					os: z.string(),
+					os_version: z.string(),
+					arch: z.string(),
+					cpu_cores: z.number(),
+					memory_gb: z.number(),
+					displays: z.array(
+						z.object({
+							width: z.number(),
+							height: z.number(),
+							scale_factor: z.number(),
+						}),
+					),
+					cameras: z.array(z.string()),
+					microphones: z.array(z.string()),
+				}),
+				recentLogs: z.array(
+					z.object({
+						id: z.string(),
+						timestamp: z.string(),
+						duration_seconds: z.number().nullable(),
+						error: z.string().nullable(),
+						log_content: z.string().nullable(),
+						log_file_path: z.string().nullable().optional(),
+					}),
+				),
+				appVersion: z.string(),
+				logFiles: z
+					.array(
+						z.object({
+							name: z.string(),
+							content: z.string(),
+						}),
+					)
+					.optional(),
+			}),
+			responses: {
+				200: z.object({ success: z.boolean(), message: z.string() }),
+			},
+		},
 		getUserPlan: {
 			method: "GET",
 			path: "/desktop/plan",
