diff --git a/mobile/lib/features/activity/compose_drafts_provider.dart b/mobile/lib/features/activity/compose_drafts_provider.dart
index b9a079e9a8..b5755f19c8 100644
--- a/mobile/lib/features/activity/compose_drafts_provider.dart
+++ b/mobile/lib/features/activity/compose_drafts_provider.dart
@@ -79,7 +79,13 @@ class ComposeDraftsNotifier extends Notifier<List<ComposeDraft>> {
     _prefsKey = '$_draftsPrefsKey:${config.baseUrl}:$pubkey';
 
     final prefs = ref.read(savedPrefsProvider);
-    final raw = prefs.getString(_prefsKey);
+    final raw = readMigratedPref<String>(
+      prefs,
+      canonicalKey: _prefsKey,
+      legacyKey: '$_draftsPrefsKey:${config.storedOrigin}:$pubkey',
+      read: prefs.getString,
+      write: prefs.setString,
+    );
     if (raw == null) return const [];
     try {
       final decoded = jsonDecode(raw);
diff --git a/mobile/lib/features/search/recent_searches_provider.dart b/mobile/lib/features/search/recent_searches_provider.dart
index 813672a9cc..2fad17832e 100644
--- a/mobile/lib/features/search/recent_searches_provider.dart
+++ b/mobile/lib/features/search/recent_searches_provider.dart
@@ -19,8 +19,16 @@ class RecentSearchesNotifier extends Notifier<List<String>> {
     final pubkey = ref.watch(myPubkeyProvider) ?? 'anon';
     _prefsKey = '$_recentSearchesPrefsKey:${config.baseUrl}:$pubkey';
 
+    final prefs = ref.read(savedPrefsProvider);
     final stored =
-        ref.read(savedPrefsProvider).getStringList(_prefsKey) ?? const [];
+        readMigratedPref<List<String>>(
+          prefs,
+          canonicalKey: _prefsKey,
+          legacyKey: '$_recentSearchesPrefsKey:${config.storedOrigin}:$pubkey',
+          read: prefs.getStringList,
+          write: prefs.setStringList,
+        ) ??
+        const [];
     return List.unmodifiable(
       stored
           .map((query) => query.trim())
diff --git a/mobile/lib/shared/relay/identity_scoped_prefs.dart b/mobile/lib/shared/relay/identity_scoped_prefs.dart
new file mode 100644
index 0000000000..e080b1ca8c
--- /dev/null
+++ b/mobile/lib/shared/relay/identity_scoped_prefs.dart
@@ -0,0 +1,50 @@
+import 'dart:async';
+
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// Reads an identity-scoped preference, migrating values left under a
+/// pre-canonicalization relay origin.
+///
+/// Identity-scoped keys embed the relay origin, and that origin used to be
+/// whatever scheme the onboarding flow happened to persist — `wss://` for an
+/// invite join, `https://` for device pairing. Now that [RelayConfig.baseUrl]
+/// canonicalizes the scheme, an install that joined by invite would compute a
+/// different key than the one its data was written under, leaving that data on
+/// disk but unreachable.
+///
+/// The value under [canonicalKey] wins. Otherwise a value under [legacyKey] is
+/// returned straight away and promoted onto the canonical key in the
+/// background. The legacy entry is removed only once the copy reports success,
+/// so a migration interrupted mid-flight leaves the original readable and the
+/// next read simply retries.
+///
+/// Pass matching accessors for the stored type — `getString`/`setString`, or
+/// `getStringList`/`setStringList`.
+T? readMigratedPref<T>(
+  SharedPreferences prefs, {
+  required String canonicalKey,
+  required String legacyKey,
+  required T? Function(String key) read,
+  required Future<bool> Function(String key, T value) write,
+}) {
+  final canonical = read(canonicalKey);
+  if (canonical != null) return canonical;
+
+  // Pairing-created communities already store an HTTP origin, so the two keys
+  // coincide and there is nothing to migrate.
+  if (canonicalKey == legacyKey) return null;
+
+  final legacy = read(legacyKey);
+  if (legacy == null) return null;
+
+  unawaited(_promote(prefs, legacyKey, write(canonicalKey, legacy)));
+  return legacy;
+}
+
+Future<void> _promote(
+  SharedPreferences prefs,
+  String legacyKey,
+  Future<bool> copy,
+) async {
+  if (await copy) await prefs.remove(legacyKey);
+}
diff --git a/mobile/lib/shared/relay/relay.dart b/mobile/lib/shared/relay/relay.dart
index d168b9a097..bc11325414 100644
--- a/mobile/lib/shared/relay/relay.dart
+++ b/mobile/lib/shared/relay/relay.dart
@@ -1,4 +1,5 @@
 export 'app_lifecycle_provider.dart';
+export 'identity_scoped_prefs.dart';
 export 'media_auth.dart';
 export 'media_image.dart';
 export 'media_upload.dart';
diff --git a/mobile/lib/shared/relay/relay_provider.dart b/mobile/lib/shared/relay/relay_provider.dart
index 97b88dd3df..061dd6cb38 100644
--- a/mobile/lib/shared/relay/relay_provider.dart
+++ b/mobile/lib/shared/relay/relay_provider.dart
@@ -10,12 +10,46 @@ import 'relay_client.dart';
 ///   - `baseUrl` — where the relay lives (used for WS + media upload)
 ///   - `nsec`    — the user's signing key (drives NIP-42 AUTH and event sigs)
 class RelayConfig {
-  final String baseUrl;
+  const RelayConfig({required String baseUrl, this.nsec}) : _baseUrl = baseUrl;
+
+  /// Relay origin exactly as the active community stored it.
+  final String _baseUrl;
 
   /// Nostr secret key (bech32 nsec) for signing events and NIP-42 AUTH.
   final String? nsec;
 
-  const RelayConfig({required this.baseUrl, this.nsec});
+  /// The origin as persisted, before scheme canonicalization.
+  ///
+  /// Exists solely so identity-scoped storage keys written before [baseUrl]
+  /// was canonicalized stay reachable — see [readMigratedPref]. Never use it
+  /// for network I/O; [baseUrl] and [wsUrl] are the addresses to connect to.
+  String get storedOrigin => _baseUrl;
+
+  /// Relay origin as an HTTP(S) URL.
+  ///
+  /// Communities are persisted with whichever scheme their onboarding flow
+  /// used: device pairing stores `https://` (it rejects anything else), while
+  /// an invite join stores the `wss://` relay URL carried by the invite link.
+  /// Every consumer treats this as an HTTP origin — [wsUrl], the `/query`
+  /// endpoint, media upload and Blossom auth — so a `wss://` base silently
+  /// degrades all of them. Folding the websocket schemes back here keeps both
+  /// onboarding paths equivalent, including for already-persisted communities.
+  ///
+  /// Derived rather than normalized in the constructor so that the constructor
+  /// stays `const`: the compile-time fallback below relies on canonicalization
+  /// to keep its identity stable across rebuilds, and Riverpod's default
+  /// `updateShouldNotify` is `previous != next`, which falls back to identity
+  /// here. A fresh instance per rebuild would resubscribe every listener.
+  String get baseUrl {
+    final uri = Uri.tryParse(_baseUrl);
+    if (uri == null) return _baseUrl;
+    final scheme = switch (uri.scheme) {
+      'wss' => 'https',
+      'ws' => 'http',
+      _ => null,
+    };
+    return scheme == null ? _baseUrl : uri.replace(scheme: scheme).toString();
+  }
 
   /// Derive the websocket URL from the HTTP base URL.
   String get wsUrl {
diff --git a/mobile/test/features/activity/compose_drafts_provider_test.dart b/mobile/test/features/activity/compose_drafts_provider_test.dart
index 1f93dcb674..15899e6597 100644
--- a/mobile/test/features/activity/compose_drafts_provider_test.dart
+++ b/mobile/test/features/activity/compose_drafts_provider_test.dart
@@ -34,6 +34,35 @@ void main() {
     return container;
   }
 
+  test(
+    'an invite-joined community keeps its drafts after origin canonicalization',
+    () async {
+      // Written by a build that stored the invite link's wss:// origin
+      // verbatim; RelayConfig now canonicalizes that to https://, so the key
+      // the app computes no longer matches the key on disk.
+      const legacyKey = 'compose_drafts_v1:wss://relay-a.example:pk_a';
+      const canonicalKey = 'compose_drafts_v1:https://relay-a.example:pk_a';
+      SharedPreferences.setMockInitialValues({
+        legacyKey:
+            '[{"key":"ch1","channel_id":"ch1","text":"unsent work",'
+            '"updated_at":1700000000}]',
+      });
+
+      final container = await containerWithPrefs(
+        relayUrl: 'wss://relay-a.example',
+      );
+
+      final drafts = container.read(composeDraftsProvider);
+      expect(drafts, hasLength(1), reason: 'draft survives the upgrade');
+      expect(drafts.single.text, 'unsent work');
+
+      await Future<void>.delayed(Duration.zero);
+      final prefs = await SharedPreferences.getInstance();
+      expect(prefs.getString(canonicalKey), isNotNull);
+      expect(prefs.getString(legacyKey), isNull);
+    },
+  );
+
   test('composeDraftKey separates channel and thread composers', () {
     expect(composeDraftKey('ch1'), 'ch1');
     expect(composeDraftKey('ch1', threadHeadId: 't1'), 'ch1:t1');
diff --git a/mobile/test/features/search/recent_searches_provider_test.dart b/mobile/test/features/search/recent_searches_provider_test.dart
index 30f13c329c..df0a08a8ea 100644
--- a/mobile/test/features/search/recent_searches_provider_test.dart
+++ b/mobile/test/features/search/recent_searches_provider_test.dart
@@ -35,6 +35,27 @@ void main() {
     return container;
   }
 
+  test('an invite-joined community keeps its recent searches after '
+      'origin canonicalization', () async {
+    const legacyKey = 'recent_searches_v1:wss://relay-a.example:pk-a';
+    const canonicalKey = 'recent_searches_v1:https://relay-a.example:pk-a';
+    SharedPreferences.setMockInitialValues({
+      legacyKey: <String>['nostr', 'relays'],
+    });
+
+    final container = await containerWithPrefs(
+      relayUrl: 'wss://relay-a.example',
+      pubkey: 'pk-a',
+    );
+
+    expect(container.read(recentSearchesProvider), ['nostr', 'relays']);
+
+    await Future<void>.delayed(Duration.zero);
+    final prefs = await SharedPreferences.getInstance();
+    expect(prefs.getStringList(canonicalKey), ['nostr', 'relays']);
+    expect(prefs.getStringList(legacyKey), isNull);
+  });
+
   test(
     'normalizes, deduplicates, caps, and persists submitted queries',
     () async {
diff --git a/mobile/test/shared/relay/identity_scoped_prefs_test.dart b/mobile/test/shared/relay/identity_scoped_prefs_test.dart
new file mode 100644
index 0000000000..4fce3f254b
--- /dev/null
+++ b/mobile/test/shared/relay/identity_scoped_prefs_test.dart
@@ -0,0 +1,92 @@
+import 'package:buzz/shared/relay/identity_scoped_prefs.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  const canonical = 'k_v1:https://relay.example:pk';
+  const legacy = 'k_v1:wss://relay.example:pk';
+
+  Future<SharedPreferences> prefsWith(Map<String, Object> values) async {
+    SharedPreferences.setMockInitialValues(values);
+    return SharedPreferences.getInstance();
+  }
+
+  String? readString(SharedPreferences prefs) => readMigratedPref<String>(
+    prefs,
+    canonicalKey: canonical,
+    legacyKey: legacy,
+    read: prefs.getString,
+    write: prefs.setString,
+  );
+
+  test('returns the canonical value when present', () async {
+    final prefs = await prefsWith({canonical: 'new', legacy: 'old'});
+    expect(readString(prefs), 'new');
+  });
+
+  test('falls back to the legacy value and returns it immediately', () async {
+    final prefs = await prefsWith({legacy: 'carried over'});
+    expect(readString(prefs), 'carried over');
+  });
+
+  test('promotes the legacy value onto the canonical key', () async {
+    final prefs = await prefsWith({legacy: 'carried over'});
+    readString(prefs);
+    await Future<void>.delayed(Duration.zero);
+
+    expect(prefs.getString(canonical), 'carried over');
+    expect(prefs.getString(legacy), isNull, reason: 'legacy entry is cleared');
+  });
+
+  test('is idempotent across repeated reads', () async {
+    final prefs = await prefsWith({legacy: 'carried over'});
+    readString(prefs);
+    await Future<void>.delayed(Duration.zero);
+    expect(readString(prefs), 'carried over');
+    await Future<void>.delayed(Duration.zero);
+    expect(prefs.getString(canonical), 'carried over');
+  });
+
+  test('returns null when neither key holds a value', () async {
+    final prefs = await prefsWith({});
+    expect(readString(prefs), isNull);
+  });
+
+  test('does not touch storage when the keys coincide', () async {
+    // A pairing-created community already stores an HTTP origin, so canonical
+    // and legacy are the same string and there is nothing to migrate.
+    SharedPreferences.setMockInitialValues({canonical: 'only'});
+    final prefs = await SharedPreferences.getInstance();
+    final value = readMigratedPref<String>(
+      prefs,
+      canonicalKey: canonical,
+      legacyKey: canonical,
+      read: prefs.getString,
+      write: prefs.setString,
+    );
+    await Future<void>.delayed(Duration.zero);
+
+    expect(value, 'only');
+    expect(prefs.getString(canonical), 'only');
+  });
+
+  test('migrates string lists as well as strings', () async {
+    final prefs = await prefsWith({
+      legacy: <String>['alpha', 'beta'],
+    });
+    final value = readMigratedPref<List<String>>(
+      prefs,
+      canonicalKey: canonical,
+      legacyKey: legacy,
+      read: prefs.getStringList,
+      write: prefs.setStringList,
+    );
+    await Future<void>.delayed(Duration.zero);
+
+    expect(value, ['alpha', 'beta']);
+    expect(prefs.getStringList(canonical), ['alpha', 'beta']);
+    expect(prefs.getStringList(legacy), isNull);
+  });
+}
diff --git a/mobile/test/shared/relay/relay_config_test.dart b/mobile/test/shared/relay/relay_config_test.dart
new file mode 100644
index 0000000000..d0decb3685
--- /dev/null
+++ b/mobile/test/shared/relay/relay_config_test.dart
@@ -0,0 +1,67 @@
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:buzz/shared/relay/relay_provider.dart';
+
+void main() {
+  group('RelayConfig.baseUrl normalization', () {
+    test('folds a wss:// community URL to https://', () {
+      // Invite joins persist the relay URL straight off the invite link, which
+      // deep_link.dart always emits as ws:// or wss://.
+      final config = RelayConfig(baseUrl: 'wss://relay.example.com');
+      expect(config.baseUrl, 'https://relay.example.com');
+    });
+
+    test('folds a ws:// community URL to http://', () {
+      final config = RelayConfig(baseUrl: 'ws://relay.example.com:3000');
+      expect(config.baseUrl, 'http://relay.example.com:3000');
+    });
+
+    test('leaves an https:// community URL untouched', () {
+      // Device pairing rejects anything but https://, so these already conform.
+      final config = RelayConfig(baseUrl: 'https://relay.example.com');
+      expect(config.baseUrl, 'https://relay.example.com');
+    });
+
+    test('leaves an http:// community URL untouched', () {
+      final config = RelayConfig(baseUrl: 'http://localhost:3000');
+      expect(config.baseUrl, 'http://localhost:3000');
+    });
+
+    test('preserves a non-default port', () {
+      final config = RelayConfig(baseUrl: 'wss://relay.example.com:8443');
+      expect(config.baseUrl, 'https://relay.example.com:8443');
+    });
+  });
+
+  group('RelayConfig.wsUrl', () {
+    test('keeps TLS for a relay joined by invite', () {
+      // Regression: a wss:// base used to fall through to the non-https branch
+      // and downgrade to ws://, dialing port 80 — which never connects on a
+      // relay that only serves 443, and drops TLS everywhere else.
+      final config = RelayConfig(baseUrl: 'wss://relay.example.com');
+      expect(config.wsUrl, 'wss://relay.example.com');
+    });
+
+    test('keeps TLS for a relay added by pairing', () {
+      final config = RelayConfig(baseUrl: 'https://relay.example.com');
+      expect(config.wsUrl, 'wss://relay.example.com');
+    });
+
+    test('both onboarding paths agree on the same relay', () {
+      final invited = RelayConfig(baseUrl: 'wss://relay.example.com');
+      final paired = RelayConfig(baseUrl: 'https://relay.example.com');
+      expect(invited.wsUrl, paired.wsUrl);
+      expect(invited.baseUrl, paired.baseUrl);
+    });
+
+    test('stays plaintext for local development', () {
+      final config = RelayConfig(baseUrl: 'http://localhost:3000');
+      expect(config.wsUrl, 'ws://localhost:3000');
+    });
+
+    test('preserves a non-default port', () {
+      final config = RelayConfig(baseUrl: 'wss://relay.example.com:8443');
+      expect(config.wsUrl, 'wss://relay.example.com:8443');
+    });
+  });
+}
