diff --git a/server/static/js/app.js b/server/static/js/app.js
index c8f47a5e..d19f8b2c 100644
--- a/server/static/js/app.js
+++ b/server/static/js/app.js
@@ -288,7 +288,7 @@ const formatTitle = (m) => {
     if (m.title) {
         return formatTitleA(m);
     } else {
-        return `${location.host}/${m.topic}`;
+        return `${location.host}/${m.topic}`; // FIXME
     }
 };
 
diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js
index fdf5c99f..9aedf211 100644
--- a/web/src/app/Connection.js
+++ b/web/src/app/Connection.js
@@ -32,13 +32,16 @@ class Connection {
             console.log(`[Connection, ${this.shortUrl}] Message received from server: ${event.data}`);
             try {
                 const data = JSON.parse(event.data);
+                if (data.event === 'open') {
+                    return;
+                }
                 const relevantAndValid =
                     data.event === 'message' &&
                     'id' in data &&
                     'time' in data &&
                     'message' in data;
                 if (!relevantAndValid) {
-                    console.log(`[Connection, ${this.shortUrl}] Message irrelevant or invalid. Ignoring.`);
+                    console.log(`[Connection, ${this.shortUrl}] Unexpected message. Ignoring.`);
                     return;
                 }
                 this.since = data.time + 1; // Sigh. This works because on reconnect, we wait 5+ seconds anyway.
diff --git a/web/src/app/NotificationManager.js b/web/src/app/NotificationManager.js
new file mode 100644
index 00000000..6126add4
--- /dev/null
+++ b/web/src/app/NotificationManager.js
@@ -0,0 +1,33 @@
+import {formatMessage, formatTitle} from "./utils";
+
+class NotificationManager {
+    notify(subscription, notification, onClickFallback) {
+        const message = formatMessage(notification);
+        const title = formatTitle(notification);
+        const n = new Notification(title, {
+            body: message,
+            icon: '/static/img/favicon.png'
+        });
+        if (notification.click) {
+            n.onclick = (e) => window.open(notification.click);
+        } else {
+            n.onclick = onClickFallback;
+        }
+    }
+
+    granted() {
+        return Notification.permission === 'granted';
+    }
+
+    maybeRequestPermission(cb) {
+        if (!this.granted()) {
+            Notification.requestPermission().then((permission) => {
+                const granted = permission === 'granted';
+                cb(granted);
+            });
+        }
+    }
+}
+
+const notificationManager = new NotificationManager();
+export default notificationManager;
diff --git a/web/src/components/App.js b/web/src/components/App.js
index d7aea030..59337344 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -13,6 +13,7 @@ import Subscriptions from "../app/Subscriptions";
 import Navigation from "./Navigation";
 import ActionBar from "./ActionBar";
 import Users from "../app/Users";
+import notificationManager from "../app/NotificationManager";
 
 const App = () => {
     console.log(`[App] Rendering main view`);
@@ -21,9 +22,13 @@ const App = () => {
     const [subscriptions, setSubscriptions] = useState(new Subscriptions());
     const [users, setUsers] = useState(new Users());
     const [selectedSubscription, setSelectedSubscription] = useState(null);
+    const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
     const handleNotification = (subscriptionId, notification) => {
         setSubscriptions(prev => {
             const newSubscription = prev.get(subscriptionId).addNotification(notification);
+            notificationManager.notify(newSubscription, notification, () => {
+                setSelectedSubscription(newSubscription);
+            })
             return prev.update(newSubscription).clone();
         });
     };
@@ -41,6 +46,7 @@ const App = () => {
                     return prev.update(newSubscription).clone();
                 });
             });
+        handleRequestPermission();
     };
     const handleDeleteNotification = (subscriptionId, notificationId) => {
         console.log(`[App] Deleting notification ${notificationId} from ${subscriptionId}`);
@@ -64,6 +70,11 @@ const App = () => {
             return newSubscriptions;
         });
     };
+    const handleRequestPermission = () => {
+        notificationManager.maybeRequestPermission((granted) => {
+            setNotificationsGranted(granted);
+        })
+    };
     useEffect(() => {
         setSubscriptions(repository.loadSubscriptions());
         setUsers(repository.loadUsers());
@@ -90,9 +101,11 @@ const App = () => {
                         subscriptions={subscriptions}
                         selectedSubscription={selectedSubscription}
                         mobileDrawerOpen={mobileDrawerOpen}
+                        notificationsGranted={notificationsGranted}
                         onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
                         onSubscriptionClick={(subscriptionId) => setSelectedSubscription(subscriptions.get(subscriptionId))}
                         onSubscribeSubmit={handleSubscribeSubmit}
+                        onRequestPermissionClick={handleRequestPermission}
                     />
                 </Box>
                 <Box
diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js
index 2fc29d23..df56bb8d 100644
--- a/web/src/components/Navigation.js
+++ b/web/src/components/Navigation.js
@@ -1,27 +1,24 @@
 import Drawer from "@mui/material/Drawer";
 import * as React from "react";
+import {useState} from "react";
 import ListItemButton from "@mui/material/ListItemButton";
 import ListItemIcon from "@mui/material/ListItemIcon";
 import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
 import ListItemText from "@mui/material/ListItemText";
-import {useState} from "react";
 import Toolbar from "@mui/material/Toolbar";
 import Divider from "@mui/material/Divider";
 import List from "@mui/material/List";
 import SettingsIcon from "@mui/icons-material/Settings";
 import AddIcon from "@mui/icons-material/Add";
 import SubscribeDialog from "./SubscribeDialog";
+import {Alert, AlertTitle} from "@mui/material";
+import Button from "@mui/material/Button";
+import Typography from "@mui/material/Typography";
 
 const navWidth = 240;
 
 const Navigation = (props) => {
-    const navigationList =
-        <NavList
-            subscriptions={props.subscriptions}
-            selectedSubscription={props.selectedSubscription}
-            onSubscriptionClick={props.onSubscriptionClick}
-            onSubscribeSubmit={props.onSubscribeSubmit}
-        />;
+    const navigationList = <NavList {...props}/>;
     return (
         <>
             {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */}
@@ -64,29 +61,51 @@ const NavList = (props) => {
         handleSubscribeReset();
         props.onSubscribeSubmit(subscription, user);
     }
+    const showSubscriptionsList = props.subscriptions.size() > 0;
+    const showGrantPermissionsBox = props.subscriptions.size() > 0 && !props.notificationsGranted;
     return (
         <>
-            <Toolbar />
-            {props.subscriptions.size() > 0 &&
-                <Divider />}
-            <List component="nav">
-                <NavSubscriptionList
-                    subscriptions={props.subscriptions}
-                    selectedSubscription={props.selectedSubscription}
-                    onSubscriptionClick={props.onSubscriptionClick}
-                />
-                <Divider sx={{ my: 1 }} />
+            <Toolbar/>
+            <List component="nav" sx={{paddingTop: 0}}>
+                {showGrantPermissionsBox &&
+                    <>
+                        <Divider/>
+                        <Alert severity="warning" sx={{paddingTop: 2}}>
+                            <AlertTitle>Notifications are disabled</AlertTitle>
+                            <Typography gutterBottom>
+                                Grant your browser permission to display desktop notifications.
+                            </Typography>
+                            <Button
+                                sx={{float: 'right'}}
+                                color="inherit"
+                                size="small"
+                                onClick={props.onRequestPermissionClick}
+                            >
+                                Grant now
+                            </Button>
+                        </Alert>
+                    </>}
+                {showSubscriptionsList &&
+                    <>
+                        <Divider/>
+                        <SubscriptionList
+                            subscriptions={props.subscriptions}
+                            selectedSubscription={props.selectedSubscription}
+                            onSubscriptionClick={props.onSubscriptionClick}
+                        />
+                    </>}
+                <Divider sx={{my: 1}}/>
                 <ListItemButton>
                     <ListItemIcon>
-                        <SettingsIcon />
+                        <SettingsIcon/>
                     </ListItemIcon>
-                    <ListItemText primary="Settings" />
+                    <ListItemText primary="Settings"/>
                 </ListItemButton>
                 <ListItemButton onClick={() => setSubscribeDialogOpen(true)}>
                     <ListItemIcon>
-                        <AddIcon />
+                        <AddIcon/>
                     </ListItemIcon>
-                    <ListItemText primary="Add subscription" />
+                    <ListItemText primary="Add subscription"/>
                 </ListItemButton>
             </List>
             <SubscribeDialog
@@ -99,30 +118,21 @@ const NavList = (props) => {
     );
 };
 
-const NavSubscriptionList = (props) => {
-    const subscriptions = props.subscriptions;
+const SubscriptionList = (props) => {
     return (
         <>
-            {subscriptions.map((id, subscription) =>
-                <NavSubscriptionItem
+            {props.subscriptions.map((id, subscription) =>
+                <ListItemButton
                     key={id}
-                    subscription={subscription}
-                    selected={props.selectedSubscription && props.selectedSubscription.id === id}
                     onClick={() => props.onSubscriptionClick(id)}
-                />)
-            }
+                    selected={props.selectedSubscription && props.selectedSubscription.id === id}
+                >
+                    <ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
+                    <ListItemText primary={subscription.shortUrl()}/>
+                </ListItemButton>
+            )}
         </>
     );
 }
 
-const NavSubscriptionItem = (props) => {
-    const subscription = props.subscription;
-    return (
-        <ListItemButton onClick={props.onClick} selected={props.selected}>
-            <ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
-            <ListItemText primary={subscription.shortUrl()}/>
-        </ListItemButton>
-    );
-}
-
 export default Navigation;