diff --git a/web/package.json b/web/package.json
index 735efcda..c1749e0e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -15,7 +15,6 @@
     "@mui/styles": "^5.4.2",
     "react": "latest",
     "react-dom": "latest",
-    "react-router-dom": "^6.2.1",
     "react-scripts": "^3.0.1"
   },
   "browserslist": {
diff --git a/web/public/index.html b/web/public/index.html
index db52435b..c02c0952 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -3,7 +3,7 @@
 <head>
   <meta charset="UTF-8">
 
-  <title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
+  <title>ntfy web</title>
 
   <!-- Mobile view -->
   <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index ea796618..0c04a27e 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -11,8 +11,12 @@ export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/aut
 export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
 export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
 
+export const validUrl = (url) => {
+    return url.match(/^https?:\/\//);
+}
+
 export const validTopic = (topic) => {
-    return topic.match(/^([-_a-zA-Z0-9]{1,64})$/) // Regex must match Go & Android app!
+    return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
 }
 
 // Format emojis (see emoji.js)
diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js
index 132f47e4..6970f4f4 100644
--- a/web/src/components/ActionBar.js
+++ b/web/src/components/ActionBar.js
@@ -6,6 +6,7 @@ import MenuIcon from "@mui/icons-material/Menu";
 import Typography from "@mui/material/Typography";
 import IconSubscribeSettings from "./IconSubscribeSettings";
 import * as React from "react";
+import Box from "@mui/material/Box";
 
 const ActionBar = (props) => {
     const title = (props.selectedSubscription !== null)
@@ -26,7 +27,11 @@ const ActionBar = (props) => {
                 >
                     <MenuIcon />
                 </IconButton>
-                <img src="static/img/ntfy.svg" height="28" style={{ marginRight: '10px' }}/>
+                <Box component="img" src="static/img/ntfy.svg" sx={{
+                    display: { xs: 'none', sm: 'block' },
+                    marginRight: '10px',
+                    height: '28px'
+                }}/>
                 <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
                     {title}
                 </Typography>
diff --git a/web/src/components/App.js b/web/src/components/App.js
index e55bd95e..a95e8d70 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -15,6 +15,7 @@ import ActionBar from "./ActionBar";
 import Users from "../app/Users";
 import notificationManager from "../app/NotificationManager";
 import NoTopics from "./NoTopics";
+import Preferences from "./Preferences";
 
 // TODO subscribe dialog:
 //  - check/use existing user
@@ -26,10 +27,15 @@ const App = () => {
     console.log(`[App] Rendering main view`);
 
     const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
+    const [prefsOpen, setPrefsOpen] = useState(false);
     const [subscriptions, setSubscriptions] = useState(new Subscriptions());
     const [users, setUsers] = useState(new Users());
     const [selectedSubscription, setSelectedSubscription] = useState(null);
     const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
+    const handleSubscriptionClick = (subscriptionId) => {
+        setSelectedSubscription(subscriptions.get(subscriptionId));
+        setPrefsOpen(false);
+    }
     const handleSubscribeSubmit = (subscription, user) => {
         console.log(`[App] New subscription: ${subscription.id}`);
         if (user !== null) {
@@ -67,6 +73,10 @@ const App = () => {
             setNotificationsGranted(granted);
         })
     };
+    const handlePrefsClick = () => {
+        setPrefsOpen(true);
+        setSelectedSubscription(null);
+    };
     const poll = (subscription, user) => {
         const since = subscription.last;
         api.poll(subscription.baseUrl, subscription.topic, since, user)
@@ -138,9 +148,11 @@ const App = () => {
                         selectedSubscription={selectedSubscription}
                         mobileDrawerOpen={mobileDrawerOpen}
                         notificationsGranted={notificationsGranted}
+                        prefsOpen={prefsOpen}
                         onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
-                        onSubscriptionClick={(subscriptionId) => setSelectedSubscription(subscriptions.get(subscriptionId))}
+                        onSubscriptionClick={handleSubscriptionClick}
                         onSubscribeSubmit={handleSubscribeSubmit}
+                        onPrefsClick={handlePrefsClick}
                         onRequestPermissionClick={handleRequestPermission}
                     />
                 </Box>
@@ -155,18 +167,34 @@ const App = () => {
                         height: '100vh',
                         overflow: 'auto',
                         backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
-                }}>
+                    }}
+                >
                     <Toolbar/>
-                    {selectedSubscription !== null &&
-                        <Notifications
-                            subscription={selectedSubscription}
-                            onDeleteNotification={handleDeleteNotification}
-                        />}
-                    {selectedSubscription == null && <NoTopics />}
+                    <MainContent
+                        subscription={selectedSubscription}
+                        prefsOpen={prefsOpen}
+                        onDeleteNotification={handleDeleteNotification}
+                    />
                 </Box>
             </Box>
         </ThemeProvider>
     );
 }
 
+const MainContent = (props) => {
+    if (props.prefsOpen) {
+        return <Preferences/>;
+    }
+    if (props.subscription !== null) {
+        return (
+            <Notifications
+                subscription={props.subscription}
+                onDeleteNotification={props.onDeleteNotification}
+            />
+        );
+    } else {
+        return <NoTopics/>;
+    }
+};
+
 export default App;
diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js
index 940be5d0..26ef22b9 100644
--- a/web/src/components/Navigation.js
+++ b/web/src/components/Navigation.js
@@ -14,6 +14,7 @@ import SubscribeDialog from "./SubscribeDialog";
 import {Alert, AlertTitle, ListSubheader} from "@mui/material";
 import Button from "@mui/material/Button";
 import Typography from "@mui/material/Typography";
+import Preferences from "./Preferences";
 
 const navWidth = 240;
 
@@ -97,11 +98,15 @@ const NavList = (props) => {
                         <SubscriptionList
                             subscriptions={props.subscriptions}
                             selectedSubscription={props.selectedSubscription}
+                            prefsOpen={props.prefsOpen}
                             onSubscriptionClick={props.onSubscriptionClick}
                         />
                         <Divider sx={{my: 1}}/>
                     </>}
-                <ListItemButton>
+                <ListItemButton
+                    onClick={props.onPrefsClick}
+                    selected={props.prefsOpen}
+                >
                     <ListItemIcon>
                         <SettingsIcon/>
                     </ListItemIcon>
@@ -115,7 +120,7 @@ const NavList = (props) => {
                 </ListItemButton>
             </List>
             <SubscribeDialog
-                key={subscribeDialogKey} // Resets dialog when canceled/closed
+                key={`subscribeDialog${subscribeDialogKey}`} // Resets dialog when canceled/closed
                 open={subscribeDialogOpen}
                 subscriptions={props.subscriptions}
                 onCancel={handleSubscribeReset}
@@ -132,7 +137,7 @@ const SubscriptionList = (props) => {
                 <ListItemButton
                     key={id}
                     onClick={() => props.onSubscriptionClick(id)}
-                    selected={props.selectedSubscription && props.selectedSubscription.id === id}
+                    selected={props.selectedSubscription && !props.prefsOpen && props.selectedSubscription.id === id}
                 >
                     <ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
                     <ListItemText primary={subscription.shortUrl()}/>
diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js
new file mode 100644
index 00000000..eb10b74f
--- /dev/null
+++ b/web/src/components/Preferences.js
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import {CardContent} from "@mui/material";
+import Typography from "@mui/material/Typography";
+import Card from "@mui/material/Card";
+
+const Preferences = (props) => {
+    return (
+        <>
+            <Typography variant="h5">
+                Manage users
+            </Typography>
+            <Card sx={{ minWidth: 275 }}>
+                <CardContent>
+                    You may manage users for your protected topics here. Please note that since this is a client
+                    application only, username and password are stored in the browser's local storage.
+                </CardContent>
+            </Card>
+        </>
+    );
+};
+
+export default Preferences;
diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js
index 0b74f725..203aa8b6 100644
--- a/web/src/components/SubscribeDialog.js
+++ b/web/src/components/SubscribeDialog.js
@@ -8,10 +8,10 @@ import DialogContent from '@mui/material/DialogContent';
 import DialogContentText from '@mui/material/DialogContentText';
 import DialogTitle from '@mui/material/DialogTitle';
 import Subscription from "../app/Subscription";
-import {useMediaQuery} from "@mui/material";
+import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
 import theme from "./theme";
 import api from "../app/Api";
-import {topicUrl, validTopic} from "../app/utils";
+import {topicUrl, validTopic, validUrl} from "../app/utils";
 import useStyles from "./styles";
 import User from "../app/User";
 
@@ -19,18 +19,20 @@ const defaultBaseUrl = "http://127.0.0.1"
 //const defaultBaseUrl = "https://ntfy.sh"
 
 const SubscribeDialog = (props) => {
-    const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); // FIXME
+    const [baseUrl, setBaseUrl] = useState("");
     const [topic, setTopic] = useState("");
     const [showLoginPage, setShowLoginPage] = useState(false);
     const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
-    const handleSuccess = (baseUrl, topic, user) => {
-        const subscription = new Subscription(baseUrl, topic);
+    const handleSuccess = (user) => {
+        const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME
+        const subscription = new Subscription(actualBaseUrl, topic);
         props.onSuccess(subscription, user);
     }
     return (
-        <Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
+        <Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
             {!showLoginPage && <SubscribePage
                 baseUrl={baseUrl}
+                setBaseUrl={setBaseUrl}
                 topic={topic}
                 setTopic={setTopic}
                 subscriptions={props.subscriptions}
@@ -49,8 +51,12 @@ const SubscribeDialog = (props) => {
 };
 
 const SubscribePage = (props) => {
-    const baseUrl = props.baseUrl;
+    const [anotherServerVisible, setAnotherServerVisible] = useState(false);
+    const baseUrl = (anotherServerVisible) ? props.baseUrl : defaultBaseUrl;
     const topic = props.topic;
+    const existingTopicUrls = props.subscriptions.map((id, s) => s.url());
+    const existingBaseUrls = Array.from(new Set(["https://ntfy.sh", ...props.subscriptions.map((id, s) => s.baseUrl)]))
+        .filter(s => s !== defaultBaseUrl);
     const handleSubscribe = async () => {
         const success = await api.auth(baseUrl, topic, null);
         if (!success) {
@@ -59,10 +65,21 @@ const SubscribePage = (props) => {
             return;
         }
         console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for anonymous user`);
-        props.onSuccess(baseUrl, topic, null);
+        props.onSuccess(null);
     };
-    const existingTopicUrls = props.subscriptions.map((id, s) => s.url());
-    const subscribeButtonEnabled = validTopic(props.topic) && !existingTopicUrls.includes(topicUrl(baseUrl, topic));
+    const handleUseAnotherChanged = (e) => {
+        props.setBaseUrl("");
+        setAnotherServerVisible(e.target.checked);
+    };
+    const subscribeButtonEnabled = (() => {
+        if (anotherServerVisible) {
+            const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
+            return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
+        } else {
+            const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(defaultBaseUrl, topic)); // FIXME
+            return validTopic(topic) && !isExistingTopicUrl;
+        }
+    })();
     return (
         <>
             <DialogTitle>Subscribe to topic</DialogTitle>
@@ -75,13 +92,27 @@ const SubscribePage = (props) => {
                     autoFocus
                     margin="dense"
                     id="topic"
-                    label="Topic name, e.g. phil_alerts"
+                    placeholder="Topic name, e.g. phil_alerts"
                     value={props.topic}
                     onChange={ev => props.setTopic(ev.target.value)}
                     type="text"
                     fullWidth
                     variant="standard"
                 />
+                <FormControlLabel
+                    sx={{pt: 1}}
+                    control={<Checkbox onChange={handleUseAnotherChanged}/>}
+                    label="Use another server" />
+                {anotherServerVisible && <Autocomplete
+                    freeSolo
+                    options={existingBaseUrls}
+                    sx={{ maxWidth: 400 }}
+                    inputValue={props.baseUrl}
+                    onInputChange={(ev, newVal) => props.setBaseUrl(newVal)}
+                    renderInput={ (params) =>
+                        <TextField {...params} placeholder={defaultBaseUrl} variant="standard"/>
+                    }
+                />}
             </DialogContent>
             <DialogActions>
                 <Button onClick={props.onCancel}>Cancel</Button>
@@ -96,7 +127,7 @@ const LoginPage = (props) => {
     const [username, setUsername] = useState("");
     const [password, setPassword] = useState("");
     const [errorText, setErrorText] = useState("");
-    const baseUrl = props.baseUrl;
+    const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl;
     const topic = props.topic;
     const handleLogin = async () => {
         const user = new User(baseUrl, username, password);
@@ -107,7 +138,7 @@ const LoginPage = (props) => {
             return;
         }
         console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
-        props.onSuccess(baseUrl, topic, user);
+        props.onSuccess(user);
     };
     return (
         <>
diff --git a/web/src/components/theme.js b/web/src/components/theme.js
index 669d5e7d..6e850bfb 100644
--- a/web/src/components/theme.js
+++ b/web/src/components/theme.js
@@ -16,6 +16,15 @@ const theme = createTheme({
       main: '#444',
     }
   },
+  components: {
+    MuiListItemIcon: {
+      styleOverrides: {
+        root: {
+          minWidth: '36px',
+        },
+      },
+    },
+  },
 });
 
 export default theme;