diff --git a/assets/jellyfin_icon.svg b/assets/jellyfin_icon.svg
new file mode 100644
index 00000000..a5689016
--- /dev/null
+++ b/assets/jellyfin_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/plex_chevron.svg b/assets/plex_chevron.svg
new file mode 100644
index 00000000..c100a24a
--- /dev/null
+++ b/assets/plex_chevron.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/connection/connection.dart b/lib/connection/connection.dart
new file mode 100644
index 00000000..2fa709af
--- /dev/null
+++ b/lib/connection/connection.dart
@@ -0,0 +1,328 @@
+import '../media/media_backend.dart';
+import '../models/plex/plex_home_user.dart';
+import '../services/plex_auth_service.dart';
+
+/// Identifier of a backend kind a [Connection] points at. Lighter-weight than
+/// [MediaBackend] for places that only care about persistence/auth shape
+/// (e.g. database column values).
+enum ConnectionKind {
+ plex,
+ jellyfin;
+
+ String get id => switch (this) {
+ ConnectionKind.plex => 'plex',
+ ConnectionKind.jellyfin => 'jellyfin',
+ };
+
+ static ConnectionKind fromId(String id) => switch (id) {
+ 'plex' => ConnectionKind.plex,
+ 'jellyfin' => ConnectionKind.jellyfin,
+ _ => throw ArgumentError('Unknown ConnectionKind id: $id'),
+ };
+
+ MediaBackend get backend => switch (this) {
+ ConnectionKind.plex => MediaBackend.plex,
+ ConnectionKind.jellyfin => MediaBackend.jellyfin,
+ };
+}
+
+/// Health snapshot for a connection. Updated by the orchestrator each time a
+/// session is established or refreshed.
+enum ConnectionStatus { unknown, online, offline, authError, disabled }
+
+/// A media server connection — a unit of authentication the user added.
+///
+/// A `PlexAccountConnection` carries one Plex account + its discovered servers + an
+/// optional active Home profile. A `JellyfinConnection` is a single server +
+/// user. Most users only ever add one connection.
+sealed class Connection {
+ String get id;
+ ConnectionKind get kind;
+ String get displayName;
+ ConnectionStatus get status;
+ DateTime get createdAt;
+ DateTime? get lastAuthenticatedAt;
+
+ /// Backend kind as a [MediaBackend] — for UI that branches on backend
+ /// (badges, etc.). Just a passthrough to [kind.backend].
+ MediaBackend get backend => kind.backend;
+
+ /// Primary label shown in connection-list UIs. Plex shows the active
+ /// profile/account name; Jellyfin shows the server name.
+ String get displayLabel;
+
+ /// Secondary line shown beneath [displayLabel] in connection-list UIs.
+ /// Plex: server count; Jellyfin: `userName · baseUrl`. May be null when
+ /// no useful subtitle exists.
+ String? get displaySubtitle;
+
+ /// Backend-specific config payload, persisted as JSON. Each subclass
+ /// defines the schema.
+ Map toConfigJson();
+}
+
+/// A Plex account connection.
+///
+/// Fields here mirror what [PlexAuthService] gathers during PIN OAuth: an
+/// account token (long-lived), the per-device client identifier (so plex.tv
+/// doesn't see a "new device" each launch), and the optional Home user the
+/// user has switched into.
+class PlexAccountConnection extends Connection {
+ @override
+ final String id;
+
+ @override
+ final ConnectionStatus status;
+
+ @override
+ final DateTime createdAt;
+
+ @override
+ final DateTime? lastAuthenticatedAt;
+
+ /// plex.tv account access token.
+ final String accountToken;
+
+ /// Per-device client identifier. Stable across launches.
+ final String clientIdentifier;
+
+ /// Display name shown for this connection (typically the Plex account email
+ /// or username, fallback "Plex").
+ final String accountLabel;
+
+ /// Active Home user, or `null` for the main account.
+ final PlexHomeUser? activeProfile;
+
+ /// Servers discovered for this account (cached). Populated by the auth
+ /// flow and refreshed periodically.
+ final List servers;
+
+ PlexAccountConnection({
+ required this.id,
+ required this.accountToken,
+ required this.clientIdentifier,
+ required this.accountLabel,
+ this.activeProfile,
+ this.servers = const [],
+ this.status = ConnectionStatus.unknown,
+ required this.createdAt,
+ this.lastAuthenticatedAt,
+ });
+
+ @override
+ ConnectionKind get kind => ConnectionKind.plex;
+
+ @override
+ String get displayName => activeProfile != null && activeProfile!.title.isNotEmpty
+ ? '${activeProfile!.title} · $accountLabel'
+ : accountLabel;
+
+ @override
+ String get displayLabel => displayName;
+
+ @override
+ String? get displaySubtitle => servers.length == 1 ? '1 Plex server' : '${servers.length} Plex servers';
+
+ PlexAccountConnection copyWith({
+ String? id,
+ String? accountToken,
+ String? clientIdentifier,
+ String? accountLabel,
+ PlexHomeUser? activeProfile,
+ bool clearActiveProfile = false,
+ List? servers,
+ ConnectionStatus? status,
+ DateTime? createdAt,
+ DateTime? lastAuthenticatedAt,
+ }) {
+ return PlexAccountConnection(
+ id: id ?? this.id,
+ accountToken: accountToken ?? this.accountToken,
+ clientIdentifier: clientIdentifier ?? this.clientIdentifier,
+ accountLabel: accountLabel ?? this.accountLabel,
+ activeProfile: clearActiveProfile ? null : (activeProfile ?? this.activeProfile),
+ servers: servers ?? this.servers,
+ status: status ?? this.status,
+ createdAt: createdAt ?? this.createdAt,
+ lastAuthenticatedAt: lastAuthenticatedAt ?? this.lastAuthenticatedAt,
+ );
+ }
+
+ @override
+ Map toConfigJson() {
+ return {
+ 'accountToken': accountToken,
+ 'clientIdentifier': clientIdentifier,
+ 'accountLabel': accountLabel,
+ 'activeProfile': activeProfile?.toJson(),
+ 'servers': servers.map((s) => s.toJson()).toList(),
+ };
+ }
+
+ factory PlexAccountConnection.fromConfigJson({
+ required String id,
+ required Map json,
+ required ConnectionStatus status,
+ required DateTime createdAt,
+ DateTime? lastAuthenticatedAt,
+ }) {
+ final profileJson = json['activeProfile'];
+ final activeProfile = profileJson is Map ? PlexHomeUser.fromJson(profileJson) : null;
+ final serversJson = json['servers'];
+ final servers = serversJson is List
+ ? serversJson.whereType