diff --git a/src/lib/blah/connection/chatServer.ts b/src/lib/blah/connection/chatServer.ts
index 705a706..93868c4 100644
--- a/src/lib/blah/connection/chatServer.ts
+++ b/src/lib/blah/connection/chatServer.ts
@@ -1,5 +1,7 @@
 import { version } from '$app/environment';
 import type { BlahRichText } from '$lib/richText';
+import { messageFromBlah, type Chat, type Message } from '$lib/types';
+import { readable, type Readable } from 'svelte/store';
 import type { BlahKeyPair, BlahSignedPayload } from '../crypto';
 import type { BlahAuth, BlahMessage, BlahRoomInfo, BlahUserJoinMessage } from '../structures';
 import { BlahError } from './error';
@@ -11,14 +13,14 @@ export class BlahChatServerConnection {
 	private static commonHeaders = { 'x-blah-client': `Weblah/${version}` };
 
 	private endpoint: string;
-	private keypair?: BlahKeyPair;
+	private keypair: BlahKeyPair | null;
 
 	private webSocket: WebSocket | null = null;
 	private messageListeners: Map<string, Set<(message: BlahSignedPayload<BlahMessage>) => void>> =
 		new Map();
 	private webSocketRetryTimeout: number | null = null;
 
-	constructor(endpoint: string, keypair?: BlahKeyPair) {
+	constructor(endpoint: string, keypair: BlahKeyPair | null = null) {
 		this.endpoint = endpoint;
 		this.keypair = keypair;
 	}
@@ -147,11 +149,29 @@ export class BlahChatServerConnection {
 		return socket;
 	}
 
+	connect() {
+		if (!this.webSocket) this.webSocket = this.createWebSocket();
+	}
+
+	disconnect() {
+		if (this.webSocketRetryTimeout) clearTimeout(this.webSocketRetryTimeout);
+		this.webSocket?.close();
+		this.webSocket = null;
+	}
+
+	changeKeyPair(keypair: BlahKeyPair | null) {
+		this.keypair = keypair;
+		if (this.webSocket) {
+			this.disconnect();
+			this.connect();
+		}
+	}
+
 	subscribeRoom(
 		roomId: string,
 		onNewMessage: (message: BlahSignedPayload<BlahMessage>) => void
 	): { unsubscribe: () => void } {
-		if (!this.webSocket) this.webSocket = this.createWebSocket();
+		if (!this.webSocket) throw new Error('Must connect to WebSocket before subscribing to rooms');
 
 		const listeners = this.messageListeners.get(roomId) ?? new Set();
 		listeners.add(onNewMessage);
@@ -164,12 +184,43 @@ export class BlahChatServerConnection {
 				if (listeners.size === 0) {
 					this.messageListeners.delete(roomId);
 				}
-				if (this.messageListeners.size === 0) {
-					if (this.webSocketRetryTimeout) clearTimeout(this.webSocketRetryTimeout);
-					this.webSocket?.close();
-					this.webSocket = null;
-				}
 			}
 		};
 	}
+
+	chat(chatId: string): {
+		info: Readable<Chat>;
+		messages: Readable<Message[]>;
+		sendMessage: (brt: BlahRichText) => Promise<void>;
+	} {
+		const info = readable<Chat>(
+			{ server: this.endpoint, id: chatId, name: '', type: 'group' },
+			(set) => {
+				this.fetchRoomInfo(chatId).then((room) => {
+					set({ server: this.endpoint, id: chatId, name: room.title, type: 'group' });
+				});
+			}
+		);
+
+		const messages = readable<Message[]>([], (set, update) => {
+			this.fetchRoomHistory(chatId).then((history) =>
+				update((messages) => [
+					...history.map(messageFromBlah).toSorted((a, b) => a.date.getTime() - b.date.getTime()),
+					...messages
+				])
+			);
+
+			const { unsubscribe } = this.subscribeRoom(chatId, (message) => {
+				update((messages) => [...messages, messageFromBlah(message)]);
+			});
+
+			return unsubscribe;
+		});
+
+		const sendMessage = async (brt: BlahRichText) => {
+			await this.sendMessage(chatId, brt);
+		};
+
+		return { info, messages, sendMessage };
+	}
 }
diff --git a/src/lib/chatServers.ts b/src/lib/chatServers.ts
new file mode 100644
index 0000000..1761a5d
--- /dev/null
+++ b/src/lib/chatServers.ts
@@ -0,0 +1,63 @@
+import { persisted } from 'svelte-persisted-store';
+import { get } from 'svelte/store';
+import { BlahChatServerConnection } from './blah/connection/chatServer';
+import { BlahKeyPair, type EncodedBlahKeyPair } from './blah/crypto';
+import { currentKeyPair } from './keystore';
+
+export const chatServers = persisted<string[]>('weblah-chat-servers', ['https://blah.oxa.li/api']);
+
+class ChatServerConnectionPool {
+	private connections: Map<string, BlahChatServerConnection> = new Map();
+	private keypair: BlahKeyPair | null = null;
+
+	constructor() {
+		chatServers.subscribe(this.onChatServersChange.bind(this));
+		currentKeyPair.subscribe(this.onKeyPairChange.bind(this));
+	}
+
+	connectAll(keypair?: BlahKeyPair) {
+		for (const endpoint of get(chatServers)) {
+			const connection = new BlahChatServerConnection(endpoint, keypair);
+			this.connections.set(endpoint, connection);
+			connection.connect();
+		}
+	}
+	disconnectAll() {
+		for (const connection of this.connections.values()) {
+			connection.disconnect();
+		}
+		this.connections.clear();
+	}
+
+	private async onKeyPairChange(encodedKeyPair: EncodedBlahKeyPair) {
+		this.keypair = await BlahKeyPair.fromEncoded(encodedKeyPair);
+		for (const connection of this.connections.values()) {
+			connection.changeKeyPair(this.keypair);
+		}
+	}
+
+	private async onChatServersChange(newChatServers: string[]) {
+		// Disconnect from chat servers that are no longer in the list
+		for (const [endpoint, connection] of this.connections.entries()) {
+			if (!newChatServers.includes(endpoint)) {
+				connection.disconnect();
+				this.connections.delete(endpoint);
+			}
+		}
+
+		// Connect to chat servers that are in the list but not yet connected
+		for (const endpoint of newChatServers) {
+			if (!this.connections.has(endpoint)) {
+				const connection = new BlahChatServerConnection(endpoint, this.keypair);
+				this.connections.set(endpoint, connection);
+				connection.connect();
+			}
+		}
+	}
+
+	getConnection(endpoint: string): BlahChatServerConnection | null {
+		return this.connections.get(endpoint) ?? null;
+	}
+}
+
+export const chatServerConnectionPool = new ChatServerConnectionPool();
diff --git a/src/routes/(app)/chats/[chatId]/+page.svelte b/src/routes/(app)/chats/[chatId]/+page.svelte
deleted file mode 100644
index fc6f94c..0000000
--- a/src/routes/(app)/chats/[chatId]/+page.svelte
+++ /dev/null
@@ -1,68 +0,0 @@
-<script lang="ts">
-	import { page } from '$app/stores';
-	import BgPattern from '$lib/components/BgPattern.svelte';
-	import { messageFromBlah, type Chat, type Message } from '$lib/types';
-	import { onDestroy } from 'svelte';
-	import ChatHeader from './ChatHeader.svelte';
-	import ChatHistory from './ChatHistory.svelte';
-	import ChatInput from './ChatInput.svelte';
-	import { BlahChatServerConnection } from '$lib/blah/connection/chatServer';
-	import { currentKeyPair } from '$lib/keystore';
-	import { BlahKeyPair, type EncodedBlahKeyPair } from '$lib/blah/crypto';
-	import { browser } from '$app/environment';
-
-	const roomId = $page.params.chatId;
-
-	let chat: Chat = {
-		id: roomId,
-		name: '',
-		type: 'group'
-	};
-	let messages: Message[] = [];
-	let server: BlahChatServerConnection;
-	let unsubscribe: () => void = () => {};
-
-	async function initConnection(encodedKeyPair?: EncodedBlahKeyPair) {
-		messages = [];
-		const keyPair = encodedKeyPair ? await BlahKeyPair.fromEncoded(encodedKeyPair) : undefined;
-		server = new BlahChatServerConnection('https://blah.oxa.li/api', keyPair);
-		unsubscribe();
-		unsubscribe = server.subscribeRoom(roomId, (message) => {
-			messages = [...messages, messageFromBlah(message)];
-		}).unsubscribe;
-		return server;
-	}
-
-	async function loadChatInfo(server: BlahChatServerConnection) {
-		const room = await server.fetchRoomInfo(roomId);
-		chat = {
-			id: roomId,
-			name: room.title,
-			type: 'group'
-		};
-	}
-
-	async function loadChatHistory(server: BlahChatServerConnection) {
-		const history = await server.fetchRoomHistory(roomId);
-		messages = [
-			...history.map(messageFromBlah).toSorted((a, b) => a.date.getTime() - b.date.getTime()),
-			...messages
-		];
-	}
-
-	async function loadChat(server: BlahChatServerConnection) {
-		return await Promise.allSettled([loadChatInfo(server), loadChatHistory(server)]);
-	}
-
-	$: if (browser) initConnection($currentKeyPair).then((server) => loadChat(server));
-
-	onDestroy(() => unsubscribe());
-</script>
-
-<div class="flex h-full w-full flex-col justify-stretch">
-	<ChatHeader {chat} outsideUnreadCount={263723} />
-	<BgPattern class="flex-1" pattern="charlieBrown">
-		<ChatHistory {messages} mySenderId={$currentKeyPair?.id} />
-	</BgPattern>
-	<ChatInput {roomId} {server} />
-</div>
diff --git a/src/routes/(app)/chats/[server]/[chatId]/+page.svelte b/src/routes/(app)/chats/[server]/[chatId]/+page.svelte
new file mode 100644
index 0000000..6a3ff89
--- /dev/null
+++ b/src/routes/(app)/chats/[server]/[chatId]/+page.svelte
@@ -0,0 +1,45 @@
+<script lang="ts">
+	import { page } from '$app/stores';
+	import BgPattern from '$lib/components/BgPattern.svelte';
+	import { messageFromBlah, type Chat, type Message } from '$lib/types';
+	import { onDestroy } from 'svelte';
+	import ChatHeader from './ChatHeader.svelte';
+	import ChatHistory from './ChatHistory.svelte';
+	import ChatInput from './ChatInput.svelte';
+	import { BlahChatServerConnection } from '$lib/blah/connection/chatServer';
+	import { currentKeyPair } from '$lib/keystore';
+	import { BlahKeyPair, type EncodedBlahKeyPair } from '$lib/blah/crypto';
+	import { browser } from '$app/environment';
+	import { chatServerConnectionPool } from '$lib/chatServers';
+	import ServiceMessage from '$lib/components/ServiceMessage.svelte';
+	import ChatPage from './ChatPage.svelte';
+
+	$: roomId = $page.params.chatId;
+
+	let serverEndpoint: string = '';
+	$: {
+		const endpointString = decodeURIComponent($page.params.server);
+		serverEndpoint = endpointString.startsWith('http')
+			? endpointString
+			: `https://${endpointString}`;
+	}
+
+	let server: BlahChatServerConnection | null;
+	$: {
+		if (browser) {
+			server = chatServerConnectionPool.getConnection(serverEndpoint);
+		}
+	}
+</script>
+
+<div class="flex h-full w-full flex-col items-center justify-center">
+	{#if server}
+		{@const { info, messages, sendMessage } = server.chat(roomId)}
+		<ChatPage {info} {messages} on:sendMessage={sendMessage} />
+	{:else}
+		<ServiceMessage>
+			To view this chat, you need to connect to chat server
+			<span class="font-semibold">{serverEndpoint}</span>.
+		</ServiceMessage>
+	{/if}
+</div>
diff --git a/src/routes/(app)/chats/[chatId]/ChatHeader.svelte b/src/routes/(app)/chats/[server]/[chatId]/ChatHeader.svelte
similarity index 94%
rename from src/routes/(app)/chats/[chatId]/ChatHeader.svelte
rename to src/routes/(app)/chats/[server]/[chatId]/ChatHeader.svelte
index 0cc31e0..98cd57b 100644
--- a/src/routes/(app)/chats/[chatId]/ChatHeader.svelte
+++ b/src/routes/(app)/chats/[server]/[chatId]/ChatHeader.svelte
@@ -4,7 +4,7 @@
 	import type { Chat } from '$lib/types';
 	import { AvatarBeam } from 'svelte-boring-avatars';
 
-	export let chat: Chat;
+	export let info: Chat;
 	export let outsideUnreadCount = 0;
 </script>
 
@@ -28,10 +28,10 @@
 		<span class="sr-only">Back</span>
 	</Button>
 	<div class="flex flex-1 flex-col justify-center text-center sm:order-2 sm:text-start">
-		<h3 class="truncate text-sm font-semibold">{chat.name}</h3>
+		<h3 class="truncate text-sm font-semibold">{info.name}</h3>
 	</div>
 	<div class="sm:order-1">
-		<AvatarBeam size={30} name={chat.name} />
+		<AvatarBeam size={30} name={info.id} />
 	</div>
 
 	<a class="absolute inset-y-0 start-0 hidden w-2 cursor-default sm:block" href="/">
diff --git a/src/routes/(app)/chats/[chatId]/ChatHistory.svelte b/src/routes/(app)/chats/[server]/[chatId]/ChatHistory.svelte
similarity index 100%
rename from src/routes/(app)/chats/[chatId]/ChatHistory.svelte
rename to src/routes/(app)/chats/[server]/[chatId]/ChatHistory.svelte
diff --git a/src/routes/(app)/chats/[chatId]/ChatInput.svelte b/src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte
similarity index 68%
rename from src/routes/(app)/chats/[chatId]/ChatInput.svelte
rename to src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte
index 53fe00b..144c099 100644
--- a/src/routes/(app)/chats/[chatId]/ChatInput.svelte
+++ b/src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte
@@ -1,19 +1,16 @@
 <script lang="ts">
-	import type { BlahChatServerConnection } from '$lib/blah/connection/chatServer';
-	import { BlahError } from '$lib/blah/connection/error';
 	import Button from '$lib/components/Button.svelte';
 	import RichTextInput from '$lib/components/RichTextInput.svelte';
-	import { deltaToBlahRichText } from '$lib/richText';
+	import { deltaToBlahRichText, type BlahRichText } from '$lib/richText';
+	import { createEventDispatcher } from 'svelte';
 	import type { Delta, Editor } from 'typewriter-editor';
 
-	export let roomId: string;
-	export let server: BlahChatServerConnection | undefined;
-
 	let editor: Editor | undefined;
 	let delta: Delta;
 	let plainText: string = '';
 	let form: HTMLFormElement | null = null;
-	let sendDisabled = false;
+
+	const dispatch = createEventDispatcher<{ sendMessage: BlahRichText }>();
 
 	function onKeyboardSubmit() {
 		editor?.select(null);
@@ -21,27 +18,13 @@
 	}
 
 	async function submit() {
-		if (!server || plainText.trim() === '') return;
+		if (plainText.trim() === '') return;
 
 		const brt = deltaToBlahRichText(delta);
-		sendDisabled = true;
-		try {
-			await server.sendMessage(roomId, brt);
-		} catch (e) {
-			console.log(e);
-			if (e instanceof BlahError && e.statusCode === 403) {
-				// TODO: Actual error handling
-				await server.joinRoom(roomId);
-				await server.sendMessage(roomId, brt);
-			} else {
-				throw e;
-			}
-		}
-		sendDisabled = false;
+		dispatch('sendMessage', brt);
+
 		plainText = '';
 	}
-
-	$: sendDisabled = !server;
 </script>
 
 <form
@@ -75,7 +58,7 @@
 		keyboardSubmitMethod="enter"
 		on:keyboardSubmit={onKeyboardSubmit}
 	/>
-	<Button class="p-1.5" variant="primary" type="submit" disabled={sendDisabled}>
+	<Button class="p-1.5" variant="primary" type="submit">
 		<svg
 			xmlns="http://www.w3.org/2000/svg"
 			viewBox="0 0 24 24"
diff --git a/src/routes/(app)/chats/[chatId]/ChatMessage.svelte b/src/routes/(app)/chats/[server]/[chatId]/ChatMessage.svelte
similarity index 100%
rename from src/routes/(app)/chats/[chatId]/ChatMessage.svelte
rename to src/routes/(app)/chats/[server]/[chatId]/ChatMessage.svelte
diff --git a/src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte b/src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte
new file mode 100644
index 0000000..3cd4006
--- /dev/null
+++ b/src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte
@@ -0,0 +1,25 @@
+<script lang="ts">
+	import type { Readable } from 'svelte/store';
+
+	import type { Chat, Message } from '$lib/types';
+	import BgPattern from '$lib/components/BgPattern.svelte';
+	import { currentKeyPair } from '$lib/keystore';
+
+	import ChatHeader from './ChatHeader.svelte';
+	import ChatHistory from './ChatHistory.svelte';
+	import ChatInput from './ChatInput.svelte';
+	import type { BlahRichText } from '$lib/richText';
+
+	export let info: Readable<Chat>;
+	export let messages: Readable<Message[]>;
+
+	type $$Events = {
+		sendMessage: BlahRichText;
+	};
+</script>
+
+<ChatHeader info={$info} outsideUnreadCount={263723} />
+<BgPattern class="flex-1" pattern="charlieBrown">
+	<ChatHistory messages={$messages} mySenderId={$currentKeyPair?.id} />
+</BgPattern>
+<ChatInput on:sendMessage />