diff --git a/auth/auth.go b/auth/auth.go
index c737c584..58539f74 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -76,14 +76,15 @@ type UserPrefs struct {
 }
 
 type UserSubscription struct {
+	ID      string `json:"id"`
 	BaseURL string `json:"base_url"`
 	Topic   string `json:"topic"`
 }
 
 type UserNotificationPrefs struct {
-	Sound       string `json:"sound"`
-	MinPriority string `json:"min_priority"`
-	DeleteAfter int    `json:"delete_after"`
+	Sound       string `json:"sound,omitempty"`
+	MinPriority int    `json:"min_priority,omitempty"`
+	DeleteAfter int    `json:"delete_after,omitempty"`
 }
 
 // Grant is a struct that represents an access control entry to a topic
diff --git a/server/server.go b/server/server.go
index 2879b92a..40150e46 100644
--- a/server/server.go
+++ b/server/server.go
@@ -40,6 +40,7 @@ import (
 		auto-refresh tokens from UI
 		pricing page
 		home page
+		reserve topics
 
 
 
@@ -80,16 +81,18 @@ var (
 	authPathRegex          = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
 	publishPathRegex       = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
 
-	webConfigPath    = "/config.js"
-	userStatsPath    = "/user/stats" // FIXME get rid of this in favor of /user/account
-	userTokenPath    = "/user/token"
-	userAccountPath  = "/user/account"
-	matrixPushPath   = "/_matrix/push/v1/notify"
-	staticRegex      = regexp.MustCompile(`^/static/.+`)
-	docsRegex        = regexp.MustCompile(`^/docs(|/.*)$`)
-	fileRegex        = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
-	disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
-	urlRegex         = regexp.MustCompile(`^https?://`)
+	webConfigPath               = "/config.js"
+	userStatsPath               = "/user/stats" // FIXME get rid of this in favor of /user/account
+	userTokenPath               = "/user/token"
+	userAccountPath             = "/user/account"
+	userSubscriptionPath        = "/user/subscription"
+	userSubscriptionDeleteRegex = regexp.MustCompile(`^/user/subscription/([-_A-Za-z0-9]{16})$`)
+	matrixPushPath              = "/_matrix/push/v1/notify"
+	staticRegex                 = regexp.MustCompile(`^/static/.+`)
+	docsRegex                   = regexp.MustCompile(`^/docs(|/.*)$`)
+	fileRegex                   = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
+	disallowedTopics            = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
+	urlRegex                    = regexp.MustCompile(`^https?://`)
 
 	//go:embed site
 	webFs        embed.FS
@@ -325,6 +328,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.handleUserAccount(w, r, v)
 	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == userAccountPath {
 		return s.handleUserAccountUpdate(w, r, v)
+	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == userSubscriptionPath {
+		return s.handleUserSubscriptionAdd(w, r, v)
+	} else if r.Method == http.MethodDelete && userSubscriptionDeleteRegex.MatchString(r.URL.Path) {
+		return s.handleUserSubscriptionDelete(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
 		return s.handleMatrixDiscovery(w)
 	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
@@ -461,10 +468,12 @@ type userPlanResponse struct {
 }
 
 type userAccountResponse struct {
-	Username string            `json:"username"`
-	Role     string            `json:"role,omitempty"`
-	Plan     *userPlanResponse `json:"plan,omitempty"`
-	Settings *auth.UserPrefs   `json:"settings,omitempty"`
+	Username      string                      `json:"username"`
+	Role          string                      `json:"role,omitempty"`
+	Plan          *userPlanResponse           `json:"plan,omitempty"`
+	Language      string                      `json:"language,omitempty"`
+	Notification  *auth.UserNotificationPrefs `json:"notification,omitempty"`
+	Subscriptions []*auth.UserSubscription    `json:"subscriptions,omitempty"`
 }
 
 func (s *Server) handleUserAccount(w http.ResponseWriter, r *http.Request, v *visitor) error {
@@ -474,7 +483,17 @@ func (s *Server) handleUserAccount(w http.ResponseWriter, r *http.Request, v *vi
 	if v.user != nil {
 		response.Username = v.user.Name
 		response.Role = string(v.user.Role)
-		response.Settings = v.user.Prefs
+		if v.user.Prefs != nil {
+			if v.user.Prefs.Language != "" {
+				response.Language = v.user.Prefs.Language
+			}
+			if v.user.Prefs.Notification != nil {
+				response.Notification = v.user.Prefs.Notification
+			}
+			if v.user.Prefs.Subscriptions != nil {
+				response.Subscriptions = v.user.Prefs.Subscriptions
+			}
+		}
 	} else {
 		response = &userAccountResponse{
 			Username: auth.Everyone,
@@ -516,12 +535,83 @@ func (s *Server) handleUserAccountUpdate(w http.ResponseWriter, r *http.Request,
 		if newPrefs.Notification.DeleteAfter > 0 {
 			prefs.Notification.DeleteAfter = newPrefs.Notification.DeleteAfter
 		}
-		// ...
+		if newPrefs.Notification.Sound != "" {
+			prefs.Notification.Sound = newPrefs.Notification.Sound
+		}
+		if newPrefs.Notification.MinPriority > 0 {
+			prefs.Notification.MinPriority = newPrefs.Notification.MinPriority
+		}
 	}
-	// ...
 	return s.auth.ChangeSettings(v.user)
 }
 
+func (s *Server) handleUserSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	if v.user == nil {
+		return errors.New("no user")
+	}
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
+	body, err := util.Peek(r.Body, 4096)               // FIXME
+	if err != nil {
+		return err
+	}
+	defer r.Body.Close()
+	var newSubscription auth.UserSubscription
+	if err := json.NewDecoder(body).Decode(&newSubscription); err != nil {
+		return err
+	}
+	if v.user.Prefs == nil {
+		v.user.Prefs = &auth.UserPrefs{}
+	}
+	newSubscription.ID = "" // Client cannot set ID
+	for _, subscription := range v.user.Prefs.Subscriptions {
+		if newSubscription.BaseURL == subscription.BaseURL && newSubscription.Topic == subscription.Topic {
+			newSubscription = *subscription
+			break
+		}
+	}
+	if newSubscription.ID == "" {
+		newSubscription.ID = util.RandomString(16)
+		v.user.Prefs.Subscriptions = append(v.user.Prefs.Subscriptions, &newSubscription)
+		if err := s.auth.ChangeSettings(v.user); err != nil {
+			return err
+		}
+	}
+	if err := json.NewEncoder(w).Encode(newSubscription); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *Server) handleUserSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	if v.user == nil {
+		return errors.New("no user")
+	}
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
+	matches := userSubscriptionDeleteRegex.FindStringSubmatch(r.URL.Path)
+	if len(matches) != 2 {
+		return errHTTPInternalErrorInvalidFilePath // FIXME
+	}
+	subscriptionID := matches[1]
+	if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil {
+		return nil
+	}
+	newSubscriptions := make([]*auth.UserSubscription, 0)
+	for _, subscription := range v.user.Prefs.Subscriptions {
+		if subscription.ID != subscriptionID {
+			newSubscriptions = append(newSubscriptions, subscription)
+		}
+	}
+	if len(newSubscriptions) < len(v.user.Prefs.Subscriptions) {
+		v.user.Prefs.Subscriptions = newSubscriptions
+		if err := s.auth.ChangeSettings(v.user); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
 	r.URL.Path = webSiteDir + r.URL.Path
 	util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
diff --git a/web/src/app/Api.js b/web/src/app/Api.js
index ced005f3..6046a600 100644
--- a/web/src/app/Api.js
+++ b/web/src/app/Api.js
@@ -8,7 +8,7 @@ import {
     topicUrlJsonPollWithSince,
     userAccountUrl,
     userTokenUrl,
-    userStatsUrl
+    userStatsUrl, userSubscriptionUrl, userSubscriptionDeleteUrl
 } from "./utils";
 import userManager from "./UserManager";
 
@@ -186,6 +186,35 @@ class Api {
             throw new Error(`Unexpected server response ${response.status}`);
         }
     }
+
+    async userSubscriptionAdd(baseUrl, token, payload) {
+        const url = userSubscriptionUrl(baseUrl);
+        const body = JSON.stringify(payload);
+        console.log(`[Api] Adding user subscription ${url}: ${body}`);
+        const response = await fetch(url, {
+            method: "POST",
+            headers: maybeWithBearerAuth({}, token),
+            body: body
+        });
+        if (response.status !== 200) {
+            throw new Error(`Unexpected server response ${response.status}`);
+        }
+        const subscription = await response.json();
+        console.log(`[Api] Subscription`, subscription);
+        return subscription;
+    }
+
+    async userSubscriptionDelete(baseUrl, token, remoteId) {
+        const url = userSubscriptionDeleteUrl(baseUrl, remoteId);
+        console.log(`[Api] Removing user subscription ${url}`);
+        const response = await fetch(url, {
+            method: "DELETE",
+            headers: maybeWithBearerAuth({}, token)
+        });
+        if (response.status !== 200) {
+            throw new Error(`Unexpected server response ${response.status}`);
+        }
+    }
 }
 
 const api = new Api();
diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js
index f6573489..f793ddf7 100644
--- a/web/src/app/SubscriptionManager.js
+++ b/web/src/app/SubscriptionManager.js
@@ -18,17 +18,43 @@ class SubscriptionManager {
     }
 
     async add(baseUrl, topic) {
+        const id = topicUrl(baseUrl, topic);
+        const existingSubscription = await this.get(id);
+        if (existingSubscription) {
+            return existingSubscription;
+        }
         const subscription = {
             id: topicUrl(baseUrl, topic),
             baseUrl: baseUrl,
             topic: topic,
             mutedUntil: 0,
-            last: null
+            last: null,
+            remoteId: null
         };
         await db.subscriptions.put(subscription);
         return subscription;
     }
 
+    async syncFromRemote(remoteSubscriptions) {
+        // Add remote subscriptions
+        let remoteIds = [];
+        for (let i = 0; i < remoteSubscriptions.length; i++) {
+            const remote = remoteSubscriptions[i];
+            const local = await this.add(remote.base_url, remote.topic);
+            await this.setRemoteId(local.id, remote.id);
+            remoteIds.push(remote.id);
+        }
+
+        // Remove local subscriptions that do not exist remotely
+        const localSubscriptions = await db.subscriptions.toArray();
+        for (let i = 0; i < localSubscriptions.length; i++) {
+            const local = localSubscriptions[i];
+            if (local.remoteId && !remoteIds.includes(local.remoteId)) {
+                await this.remove(local.id);
+            }
+        }
+    }
+
     async updateState(subscriptionId, state) {
         db.subscriptions.update(subscriptionId, { state: state });
     }
@@ -139,6 +165,12 @@ class SubscriptionManager {
         });
     }
 
+    async setRemoteId(subscriptionId, remoteId) {
+        await db.subscriptions.update(subscriptionId, {
+            remoteId: remoteId
+        });
+    }
+
     async pruneNotifications(thresholdTimestamp) {
         await db.notifications
             .where("time").below(thresholdTimestamp)
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index 2ed023fe..e98ac77a 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -21,6 +21,8 @@ export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topi
 export const userStatsUrl = (baseUrl) => `${baseUrl}/user/stats`;
 export const userTokenUrl = (baseUrl) => `${baseUrl}/user/token`;
 export const userAccountUrl = (baseUrl) => `${baseUrl}/user/account`;
+export const userSubscriptionUrl = (baseUrl) => `${baseUrl}/user/subscription`;
+export const userSubscriptionDeleteUrl = (baseUrl, id) => `${baseUrl}/user/subscription/${id}`;
 export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
 export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
 export const expandSecureUrl = (url) => `https://${url}`;
diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js
index ffd08751..c79aec9c 100644
--- a/web/src/components/ActionBar.js
+++ b/web/src/components/ActionBar.js
@@ -32,7 +32,6 @@ import Button from "@mui/material/Button";
 const ActionBar = (props) => {
     const { t } = useTranslation();
     const location = useLocation();
-    const username = session.username();
     let title = "ntfy";
     if (props.selected) {
         title = topicDisplayName(props.selected);
@@ -112,9 +111,12 @@ const SettingsIcons = (props) => {
     };
 
     const handleUnsubscribe = async (event) => {
-        console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`);
+        console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`, props.subscription);
         handleClose(event);
         await subscriptionManager.remove(props.subscription.id);
+        if (session.exists() && props.subscription.remoteId) {
+            await api.userSubscriptionDelete("http://localhost:2586", session.token(), props.subscription.remoteId);
+        }
         const newSelected = await subscriptionManager.first(); // May be undefined
         if (newSelected) {
             navigate(routes.forSubscription(newSelected));
diff --git a/web/src/components/App.js b/web/src/components/App.js
index e69cfead..772961d4 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -96,10 +96,19 @@ const Layout = () => {
                     if (account.notification.sound) {
                         await prefs.setSound(account.notification.sound);
                     }
+                    if (account.notification.delete_after) {
+                        await prefs.setDeleteAfter(account.notification.delete_after);
+                    }
+                    if (account.notification.min_priority) {
+                        await prefs.setMinPriority(account.notification.min_priority);
+                    }
+                }
+                if (account.subscriptions) {
+                    await subscriptionManager.syncFromRemote(account.subscriptions);
                 }
             }
         })();
-    });
+    }, []);
     return (
         <Box sx={{display: 'flex'}}>
             <CssBaseline/>
diff --git a/web/src/components/Login.js b/web/src/components/Login.js
index 8ae73a0a..7f1469d3 100644
--- a/web/src/components/Login.js
+++ b/web/src/components/Login.js
@@ -28,12 +28,8 @@ const Login = () => {
     const handleSubmit = async (event) => {
         event.preventDefault();
         const data = new FormData(event.currentTarget);
-        console.log({
-            email: data.get('email'),
-            password: data.get('password'),
-        });
         const user = {
-            username: data.get('email'),
+            username: data.get('username'),
             password: data.get('password'),
         }
         const token = await api.login("http://localhost:2586"/*window.location.origin*/, user);
@@ -63,10 +59,9 @@ const Login = () => {
                         margin="normal"
                         required
                         fullWidth
-                        id="email"
-                        label="Email Address"
-                        name="email"
-                        autoComplete="email"
+                        id="username"
+                        label="Username"
+                        name="username"
                         autoFocus
                     />
                     <TextField
diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js
index 612f1a6f..32c7fd78 100644
--- a/web/src/components/Preferences.js
+++ b/web/src/components/Preferences.js
@@ -72,6 +72,13 @@ const Sound = () => {
     const sound = useLiveQuery(async () => prefs.sound());
     const handleChange = async (ev) => {
         await prefs.setSound(ev.target.value);
+        if (session.exists()) {
+            await api.updateUserAccount("http://localhost:2586", session.token(), {
+                notification: {
+                    sound: ev.target.value
+                }
+            });
+        }
     }
     if (!sound) {
         return null; // While loading
@@ -105,6 +112,13 @@ const MinPriority = () => {
     const minPriority = useLiveQuery(async () => prefs.minPriority());
     const handleChange = async (ev) => {
         await prefs.setMinPriority(ev.target.value);
+        if (session.exists()) {
+            await api.updateUserAccount("http://localhost:2586", session.token(), {
+                notification: {
+                    min_priority: ev.target.value
+                }
+            });
+        }
     }
     if (!minPriority) {
         return null; // While loading
@@ -148,6 +162,13 @@ const DeleteAfter = () => {
     const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
     const handleChange = async (ev) => {
         await prefs.setDeleteAfter(ev.target.value);
+        if (session.exists()) {
+            await api.updateUserAccount("http://localhost:2586", session.token(), {
+                notification: {
+                    delete_after: ev.target.value
+                }
+            });
+        }
     }
     if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0"
         return null; // While loading
@@ -445,9 +466,11 @@ const Language = () => {
 
     const handleChange = async (ev) => {
         await i18n.changeLanguage(ev.target.value);
-        await api.updateUserAccount("http://localhost:2586", session.token(), {
-            language: ev.target.value
-        });
+        if (session.exists()) {
+            await api.updateUserAccount("http://localhost:2586", session.token(), {
+                language: ev.target.value
+            });
+        }
     };
 
     // Remember: Flags are not languages. Don't put flags next to the language in the list.
diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js
index 2baecd61..925dec80 100644
--- a/web/src/components/SubscribeDialog.js
+++ b/web/src/components/SubscribeDialog.js
@@ -15,6 +15,7 @@ import subscriptionManager from "../app/SubscriptionManager";
 import poller from "../app/Poller";
 import DialogFooter from "./DialogFooter";
 import {useTranslation} from "react-i18next";
+import session from "../app/Session";
 
 const publicBaseUrl = "https://ntfy.sh";
 
@@ -26,6 +27,13 @@ const SubscribeDialog = (props) => {
     const handleSuccess = async () => {
         const actualBaseUrl = (baseUrl) ? baseUrl : window.location.origin;
         const subscription = await subscriptionManager.add(actualBaseUrl, topic);
+        if (session.exists()) {
+            const remoteSubscription = await api.userSubscriptionAdd("http://localhost:2586", session.token(), {
+                base_url: actualBaseUrl,
+                topic: topic
+            });
+            await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
+        }
         poller.pollInBackground(subscription); // Dangle!
         props.onSuccess(subscription);
     }
diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js
index 3714a9d0..0fa46dbe 100644
--- a/web/src/components/hooks.js
+++ b/web/src/components/hooks.js
@@ -7,6 +7,8 @@ import routes from "./routes";
 import connectionManager from "../app/ConnectionManager";
 import poller from "../app/Poller";
 import pruner from "../app/Pruner";
+import session from "../app/Session";
+import api from "../app/Api";
 
 /**
  * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
@@ -61,6 +63,13 @@ export const useAutoSubscribe = (subscriptions, selected) => {
             console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
             (async () => {
                 const subscription = await subscriptionManager.add(baseUrl, params.topic);
+                if (session.exists()) {
+                    const remoteSubscription = await api.userSubscriptionAdd("http://localhost:2586", session.token(), {
+                        base_url: baseUrl,
+                        topic: params.topic
+                    });
+                    await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
+                }
                 poller.pollInBackground(subscription); // Dangle!
             })();
         }