Bump to v0.14.0 — app identifier moves to com.veganostr.Vega

The app identifier changes from `com.hoornet.vega` to `com.veganostr.Vega`.
A reverse-DNS app ID must sit on a domain the project controls; the old one
did not, which blocks publishing on Flathub and leaves Vega's identity
inconsistent across Flathub, winget and the native installers.

The identifier keys every per-app directory, so this migrates existing
installs on first launch: the SQLite cache (vega.db), the embedded relay's
database (relay.db), and webview localStorage (themes, drafts, podcast
subscriptions, article read-state).

Migration runs at the top of run(), before tauri::Builder — not inside
.setup(). Tauri builds the config-defined windows before invoking the setup
hook, and building a webview create_dir_all's <LocalData>/<identifier>, which
on Linux is the same directory that holds vega.db. Migrating from setup()
would therefore always find a non-empty destination, skip, and silently
strand every Linux user's data with no way for a later fix to recover it.

Entries are moved individually rather than renaming the whole tree, so a
destination the webview already created does not block the migration, and a
failed attempt retries on the next launch. An entry that already exists at
the destination is never overwritten — the live copy always wins.

All three per-platform roots are covered: WebView2 keeps localStorage under
%LOCALAPPDATA% on Windows (not %APPDATA%), and WKWebView keeps it under
~/Library/WebKit on macOS, neither of which is the app data dir.

Keys are unaffected — the keychain service name is independent of the
identifier, so users stay logged in.

Windows installs v0.14.0 alongside v0.13.2 rather than replacing it, since
the changed identifier reads as a new application. Documented in the README,
CHANGELOG and release notes.
This commit is contained in:
Jure
2026-07-14 12:20:32 +02:00
parent d37a414b82
commit 957557708e
9 changed files with 285 additions and 6 deletions
+8
View File
@@ -69,6 +69,14 @@ jobs:
> **Windows note:** The installer is not yet code-signed. Windows SmartScreen will show an "Unknown publisher" warning — click "More info → Run anyway" to install.
### v0.14.0 — Vega moves to its own app identifier
Vega's internal app identifier changes from `com.hoornet.vega` to `com.veganostr.Vega`, so it is anchored to a domain the project controls. This is a prerequisite for publishing Vega on Flathub and winget, and it makes the app's identity consistent across every platform.
**Your data comes with you.** On first launch, v0.14.0 moves your existing data directory across automatically — cached notes and profiles, the embedded relay's database, your themes, drafts, podcast subscriptions and article read-state. Your keys are stored in the OS keychain and are not affected at all, so you stay logged in.
> **Upgrading on Windows:** because the identifier changed, Windows treats this as a new application, so v0.14.0 installs alongside v0.13.2 instead of replacing it. Your data is carried over — you can uninstall the old "Vega" entry from Add/Remove Programs afterwards. This is a one-time step; future updates will upgrade in place as normal.
### v0.13.2 — Mute-aware search, a tidier compose box & a resizable sidebar
Three community-requested improvements plus a couple of nice touches.
+11
View File
@@ -2,6 +2,17 @@
> Note: entries for v0.12.10 through v0.13.1 live in the [GitHub Releases](https://github.com/hoornet/vega/releases) notes; this file resumes at v0.13.2.
## v0.14.0 — App identifier moves to `com.veganostr.Vega` (2026-07-14)
### Changed
- **App identifier is now `com.veganostr.Vega`** (was `com.hoornet.vega`). Reverse-DNS app IDs must sit on a domain the project controls; Flathub rejects anything else, and this keeps Vega's identity consistent across Flathub, winget and the native installers.
### Migration
- **Existing data is carried across automatically on first launch.** The identifier keys every per-app directory, so v0.14.0 moves the old directories to the new ones before opening anything: the SQLite cache (`vega.db`), the embedded relay's database (`relay.db`), and webview localStorage (themes, drafts, podcast subscriptions, article read-state). All three per-platform roots are covered — WebView2 keeps localStorage under `%LOCALAPPDATA%` on Windows, and WKWebView keeps it under `~/Library/WebKit` on macOS, neither of which is the app data dir.
- The migration only ever moves into a fresh install; an existing directory with data in it is never overwritten.
- **Keys are unaffected.** The OS keychain service name is independent of the app identifier, so you stay logged in.
- **Windows only:** the identifier change makes Windows treat v0.14.0 as a new application, so it installs alongside v0.13.2 rather than replacing it. Data is carried over; remove the old "Vega" entry from Add/Remove Programs. One-time only.
## v0.13.2 — Mute-aware search, collapsing compose box & resizable sidebar (2026-06-15)
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: hoornet <hoornet@users.noreply.github.com>
pkgname=vega-nostr
pkgver=0.13.2
pkgver=0.14.0
pkgrel=1
pkgdesc="Cross-platform Nostr desktop client with Lightning integration"
arch=('x86_64')
+12
View File
@@ -1,5 +1,17 @@
# Vega
> ## ⚠️ v0.14.0 changes Vega's app identifier — please read before upgrading
>
> Vega's internal app identifier changes from `com.hoornet.vega` to **`com.veganostr.Vega`** in v0.14.0. Sorry for the disruption — here is exactly what it means and why it has to happen now.
>
> **Your data is migrated for you.** On first launch, v0.14.0 moves your existing data across automatically: cached notes and profiles, the embedded relay's database, and your themes, drafts, podcast subscriptions and article read-state. Your keys live in the OS keychain, which is not tied to the identifier, so **you stay logged in.** If a folder already exists at the new location, Vega leaves it alone rather than overwriting anything.
>
> **Windows users — one manual step.** Because the identifier changed, Windows treats v0.14.0 as a *new* application, so it installs **alongside** v0.13.2 instead of replacing it. Your data is still carried over. Once v0.14.0 is running, uninstall the old **Vega** entry from Add/Remove Programs. This is a one-time step — future updates upgrade in place as normal.
>
> **Why this is necessary, and why now.** A reverse-DNS app identifier is supposed to sit on a domain its owner controls. `com.hoornet.vega` does not — it was a placeholder from Vega's earliest days, and it blocks publishing Vega on **Flathub**, which rejects app IDs on domains you cannot prove you own. Flathub is how most Linux users discover and install software, and there is currently almost no Nostr client there at all. Making the identifier `com.veganostr.Vega` also gives Vega one consistent identity across Flathub, winget and the native installers.
>
> The identifier keys every per-app directory, so changing it is inherently disruptive — and it only gets worse the longer it waits. Vega's install base is still small, so this is the cheapest this change will ever be. Doing it now means it never has to be done to a larger group of people later.
A cross-platform desktop Nostr client built with Tauri 2.0 + React + TypeScript. Polished UI, deep Lightning integration, and first-class support for long-form writing.
**Website:** [veganostr.com](https://veganostr.com) · **Download:** [Releases](https://github.com/hoornet/vega/releases) · **AUR:** `vega-nostr-git`
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "vega",
"private": true,
"version": "0.13.2",
"version": "0.14.0",
"type": "module",
"scripts": {
"dev": "vite",
+2 -1
View File
@@ -5429,8 +5429,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vega"
version = "0.13.2"
version = "0.14.0"
dependencies = [
"dirs",
"futures-util",
"hex",
"keyring",
+4 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vega"
version = "0.13.2"
version = "0.14.0"
description = "Cross-platform Nostr desktop client with Lightning integration"
authors = ["hoornet"]
edition = "2021"
@@ -19,6 +19,9 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["devtools", "tray-icon"] }
# Resolves the per-platform data/config roots before Tauri exists — the identifier
# migration must run before tauri::Builder, so it cannot use app.path().
dirs = "6"
tauri-plugin-opener = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
+244
View File
@@ -9,6 +9,110 @@ use tauri::{
mod relay;
// ── Data directory migration ────────────────────────────────────────────────
// The app identifier changed from `com.hoornet.vega` to `com.veganostr.Vega` in
// v0.14.0 so it is anchored to a domain we control (Flathub requires this). The
// identifier keys every per-app directory, so without this the SQLite cache, the
// embedded relay database and the webview's localStorage would all be stranded
// and existing users would open a factory-fresh app.
const LEGACY_IDENTIFIER: &str = "com.hoornet.vega";
const APP_IDENTIFIER: &str = "com.veganostr.Vega";
/// Move the contents of `<parent>/com.hoornet.vega` into `<parent>/com.veganostr.Vega`.
///
/// Entry-by-entry rather than a single directory rename, because the destination is
/// *not* reliably absent: Tauri creates `<LocalData>/<identifier>` while building the
/// window, and the webview populates it. A whole-directory rename would fail against
/// that, and treating "destination is non-empty" as "already migrated" would silently
/// strand the user's data (see `run()`).
///
/// An entry that already exists at the destination is never overwritten — real data
/// always wins over the legacy copy — so this is safe to retry on every launch.
fn migrate_dir(new_dir: &std::path::Path) {
let Some(parent) = new_dir.parent() else {
return;
};
let legacy_dir = parent.join(LEGACY_IDENTIFIER);
if legacy_dir == new_dir || !legacy_dir.is_dir() {
return;
}
// Fast path: destination not created yet, so move the whole tree in one atomic
// rename. Same parent means same filesystem, so this can't hit EXDEV, and the
// data dir can be hundreds of MB — a deep copy would stall startup.
if !new_dir.exists() && std::fs::rename(&legacy_dir, new_dir).is_ok() {
eprintln!(
"[migrate] moved {} -> {}",
legacy_dir.display(),
new_dir.display()
);
return;
}
std::fs::create_dir_all(new_dir).ok();
let Ok(entries) = std::fs::read_dir(&legacy_dir) else {
return;
};
let (mut moved, mut left) = (0usize, 0usize);
for entry in entries.flatten() {
let dest = new_dir.join(entry.file_name());
if dest.exists() {
left += 1; // Never clobber: whatever is already there is the live copy.
continue;
}
match std::fs::rename(entry.path(), &dest) {
Ok(()) => moved += 1,
Err(e) => {
left += 1;
eprintln!("[migrate] could not move {}: {e}", entry.path().display());
}
}
}
// Fully drained: drop the empty legacy dir so this stops running. If anything was
// left behind, keep it — a later launch retries, and nothing is ever lost.
if left == 0 {
std::fs::remove_dir(&legacy_dir).ok();
}
eprintln!(
"[migrate] {}: moved {moved} entries, left {left}",
legacy_dir.display()
);
}
/// Carry a pre-v0.14.0 install's data across the identifier change.
///
/// Must run before `tauri::Builder` — see the call site in `run()`.
///
/// Covers every root the identifier keys, because localStorage does not live in the
/// same place on each platform:
/// - Linux — WebKitGTK stores it under the data dir alongside SQLite.
/// - Windows — WebView2 keeps `EBWebView/` under the *local* data dir (`%LOCALAPPDATA%`),
/// a different root from `%APPDATA%`; migrating only one of them loses it.
/// - macOS — WKWebView stores website data in `~/Library/WebKit/<bundle-id>`, outside
/// the app data dir entirely.
///
/// These roots coincide on some platforms (on Linux data == local data; on Windows and
/// macOS data == config). The duplicate call is a no-op: the first one drains the legacy
/// directory, the second finds nothing to do.
fn migrate_legacy_data_dirs() {
// `dirs` is what Tauri's own path resolver uses, so these resolve identically to
// app_data_dir()/app_local_data_dir()/app_config_dir() — but without needing an App.
for base in [dirs::data_dir(), dirs::data_local_dir(), dirs::config_dir()]
.into_iter()
.flatten()
{
migrate_dir(&base.join(APP_IDENTIFIER));
}
#[cfg(target_os = "macos")]
if let Some(home) = dirs::home_dir() {
migrate_dir(&home.join("Library").join("WebKit").join(APP_IDENTIFIER));
}
}
// ── OS keychain ─────────────────────────────────────────────────────────────
// Keep legacy keyring service name so existing users don't lose their keys
@@ -449,6 +553,13 @@ fn install_info() -> serde_json::Value {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Before tauri::Builder, not inside .setup(). Tauri builds the config-defined
// windows *before* invoking the setup hook, and building a webview creates (and
// populates) <LocalData>/<identifier> — which on Linux is the very directory
// holding vega.db, relay.db and localStorage. Migrating from inside setup() would
// therefore always find a non-empty destination and skip, stranding user data.
migrate_legacy_data_dirs();
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_updater::Builder::new().build())
@@ -575,3 +686,136 @@ pub fn run() {
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(test)]
mod tests {
use super::{migrate_dir, APP_IDENTIFIER, LEGACY_IDENTIFIER};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
/// Isolated stand-in for a per-platform data root (e.g. ~/.local/share).
fn scratch_root() -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let root =
std::env::temp_dir().join(format!("vega-migrate-test-{}-{}", std::process::id(), n));
fs::remove_dir_all(&root).ok();
fs::create_dir_all(&root).unwrap();
root
}
fn write(path: &Path, body: &str) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, body).unwrap();
}
fn read(path: &Path) -> String {
fs::read_to_string(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()))
}
/// A pre-v0.14.0 install: SQLite cache, embedded relay db, and WebKit localStorage,
/// laid out the way they actually are on disk (relay.db sits directly in the data dir).
fn seed_legacy(root: &Path) -> PathBuf {
let legacy = root.join(LEGACY_IDENTIFIER);
write(&legacy.join("vega.db"), "cached-notes");
write(&legacy.join("relay.db"), "relay-events");
write(&legacy.join("localstorage").join("ls.db"), "themes+drafts");
legacy
}
#[test]
fn moves_legacy_data_into_a_fresh_install() {
let root = scratch_root();
let legacy = seed_legacy(&root);
let new = root.join(APP_IDENTIFIER);
migrate_dir(&new);
assert_eq!(read(&new.join("vega.db")), "cached-notes");
assert_eq!(read(&new.join("relay.db")), "relay-events");
assert_eq!(
read(&new.join("localstorage").join("ls.db")),
"themes+drafts",
"localStorage carries themes, drafts and podcast subs — it must survive"
);
assert!(!legacy.exists(), "drained legacy dir should be removed");
fs::remove_dir_all(&root).ok();
}
/// The regression that shipped-and-was-caught: Tauri creates and populates
/// <LocalData>/<identifier> while building the window, so on Linux the destination
/// already exists — and holds webview files — by the time we look at it. Treating
/// "non-empty destination" as "already migrated" stranded 100% of user data.
#[test]
fn migrates_even_when_the_webview_already_created_the_destination() {
let root = scratch_root();
let legacy = seed_legacy(&root);
let new = root.join(APP_IDENTIFIER);
// What WebKitGTK leaves behind on a first v0.14.0 launch:
write(&new.join("hsts-storage.sqlite"), "webview");
write(&new.join("WebKitCache").join("blob"), "webview");
migrate_dir(&new);
assert_eq!(
read(&new.join("vega.db")),
"cached-notes",
"user data must migrate despite the destination being non-empty"
);
assert_eq!(read(&new.join("relay.db")), "relay-events");
assert_eq!(read(&new.join("localstorage").join("ls.db")), "themes+drafts");
assert_eq!(read(&new.join("hsts-storage.sqlite")), "webview");
assert!(!legacy.exists());
fs::remove_dir_all(&root).ok();
}
#[test]
fn never_overwrites_data_already_at_the_destination() {
let root = scratch_root();
let legacy = seed_legacy(&root);
let new = root.join(APP_IDENTIFIER);
write(&new.join("vega.db"), "live-data");
migrate_dir(&new);
assert_eq!(
read(&new.join("vega.db")),
"live-data",
"the live copy always wins over the legacy one"
);
// Non-conflicting entries still come across.
assert_eq!(read(&new.join("relay.db")), "relay-events");
assert!(
legacy.join("vega.db").exists(),
"the un-moved legacy file is kept, never deleted"
);
fs::remove_dir_all(&root).ok();
}
#[test]
fn is_safe_to_run_again_after_a_successful_migration() {
let root = scratch_root();
seed_legacy(&root);
let new = root.join(APP_IDENTIFIER);
migrate_dir(&new);
migrate_dir(&new); // second launch
assert_eq!(read(&new.join("vega.db")), "cached-notes");
fs::remove_dir_all(&root).ok();
}
#[test]
fn does_nothing_for_a_brand_new_user() {
let root = scratch_root();
let new = root.join(APP_IDENTIFIER);
migrate_dir(&new);
assert!(!new.exists(), "must not fabricate a data dir for a new user");
fs::remove_dir_all(&root).ok();
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Vega",
"version": "0.13.2",
"identifier": "com.hoornet.vega",
"version": "0.14.0",
"identifier": "com.veganostr.Vega",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",