diff --git a/web/public/static/sounds/beep.mp3 b/web/public/static/sounds/beep.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..d02e2106f61a86bca222cbe80e587ab403f5c7b8
Binary files /dev/null and b/web/public/static/sounds/beep.mp3 differ
diff --git a/web/public/static/sounds/juntos.mp3 b/web/public/static/sounds/juntos.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..aeadbb821f4301df111158bacb1286accaa6cc74
Binary files /dev/null and b/web/public/static/sounds/juntos.mp3 differ
diff --git a/web/public/static/sounds/mixkit-correct-answer-tone.mp3 b/web/public/static/sounds/mixkit-correct-answer-tone.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..cdfd445fe3cb1867805193c77f24b2ee1819d1c1
Binary files /dev/null and b/web/public/static/sounds/mixkit-correct-answer-tone.mp3 differ
diff --git a/web/public/static/sounds/mixkit-long-pop.mp3 b/web/public/static/sounds/mixkit-long-pop.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..e650bb270ae67e62e2fc7ca9f1b2bcfdd8700242
Binary files /dev/null and b/web/public/static/sounds/mixkit-long-pop.mp3 differ
diff --git a/web/public/static/sounds/mixkit-message-pop-alert.mp3 b/web/public/static/sounds/mixkit-message-pop-alert.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..d8a83b70a9cd0b76d231cab281a7d0044401e4f8
Binary files /dev/null and b/web/public/static/sounds/mixkit-message-pop-alert.mp3 differ
diff --git a/web/public/static/sounds/mixkit-software-interface-start.mp3 b/web/public/static/sounds/mixkit-software-interface-start.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..759057b77dd0d14199afe9f7972d3cee71236659
Binary files /dev/null and b/web/public/static/sounds/mixkit-software-interface-start.mp3 differ
diff --git a/web/public/static/sounds/pristine.mp3 b/web/public/static/sounds/pristine.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..ed3e3083a06169399e2df43bf1b8ae57026831a7
Binary files /dev/null and b/web/public/static/sounds/pristine.mp3 differ
diff --git a/web/src/app/NotificationManager.js b/web/src/app/Notifier.js
similarity index 69%
rename from web/src/app/NotificationManager.js
rename to web/src/app/Notifier.js
index 8834d2e3e151a9743988f56241e07bc2b21b3642..5db6f4871033c9c1d9eec18c5030f6784d6b8a48 100644
--- a/web/src/app/NotificationManager.js
+++ b/web/src/app/Notifier.js
@@ -1,8 +1,8 @@
-import {formatMessage, formatTitleWithDefault, openUrl, topicShortUrl} from "./utils";
+import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicShortUrl} from "./utils";
 import prefs from "./Prefs";
 import subscriptionManager from "./SubscriptionManager";
 
-class NotificationManager {
+class Notifier {
     async notify(subscriptionId, notification, onClickFallback) {
         const subscription = await subscriptionManager.get(subscriptionId);
         const shouldNotify = await this.shouldNotify(subscription, notification);
@@ -13,7 +13,8 @@ class NotificationManager {
         const message = formatMessage(notification);
         const title = formatTitleWithDefault(notification, shortUrl);
 
-        console.log(`[NotificationManager, ${shortUrl}] Displaying notification ${notification.id}: ${message}`);
+        // Show notification
+        console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`);
         const n = new Notification(title, {
             body: message,
             icon: '/static/img/favicon.png'
@@ -23,6 +24,17 @@ class NotificationManager {
         } else {
             n.onclick = onClickFallback;
         }
+
+        // Play sound
+        const sound = await prefs.sound();
+        if (sound && sound !== "none") {
+            try {
+                await playSound(sound);
+            } catch (e) {
+                console.log(`[Notifier, ${shortUrl}] Error playing audio`, e);
+                // FIXME show no sound allowed popup
+            }
+        }
     }
 
     granted() {
@@ -48,5 +60,5 @@ class NotificationManager {
     }
 }
 
-const notificationManager = new NotificationManager();
-export default notificationManager;
+const notifier = new Notifier();
+export default notifier;
diff --git a/web/src/app/Prefs.js b/web/src/app/Prefs.js
index 359fbf6f24baa29a296d165cd5ced0b8a46d63f4..6acc8f96fd63c8ed529814f6134a94b98614bd60 100644
--- a/web/src/app/Prefs.js
+++ b/web/src/app/Prefs.js
@@ -1,13 +1,13 @@
 import db from "./db";
 
 class Prefs {
-    async setSelectedSubscriptionId(selectedSubscriptionId) {
-        db.prefs.put({key: 'selectedSubscriptionId', value: selectedSubscriptionId});
+    async setSound(sound) {
+        db.prefs.put({key: 'sound', value: sound.toString()});
     }
 
-    async selectedSubscriptionId() {
-        const selectedSubscriptionId = await db.prefs.get('selectedSubscriptionId');
-        return (selectedSubscriptionId) ? selectedSubscriptionId.value : "";
+    async sound() {
+        const sound = await db.prefs.get('sound');
+        return (sound) ? sound.value : "mixkit-correct-answer-tone";
     }
 
     async setMinPriority(minPriority) {
diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js
index 17edce40cad2f962c1fae802a7277b795482d122..a64c3bddea33b16894ea2d77ee46809190800b98 100644
--- a/web/src/app/SubscriptionManager.js
+++ b/web/src/app/SubscriptionManager.js
@@ -41,7 +41,7 @@ class SubscriptionManager {
         if (exists) {
             return false;
         }
-        await db.notifications.add({ ...notification, subscriptionId });
+        await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab
         await db.subscriptions.update(subscriptionId, {
             last: notification.id
         });
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index c047ff7e50c43fbad2f8b345ef35cc9c2fe6ce35..c9b5b4c5c51b2d0cab4f9a31571e5455901d2e52 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -121,6 +121,11 @@ export const subscriptionRoute = (subscription) => {
     return `/${subscription.topic}`;
 }
 
+export const playSound = async (sound) => {
+    const audio = new Audio(`/static/sounds/${sound}.mp3`);
+    return audio.play();
+};
+
 // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
 export async function* fetchLinesIterator(fileURL, headers) {
     const utf8Decoder = new TextDecoder('utf-8');
diff --git a/web/src/components/App.js b/web/src/components/App.js
index 182df34973480d7f65b260e2749e99e7c68594f3..6db04aa5d52131a83cefed2c031138975f16ea2a 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -9,7 +9,7 @@ import theme from "./theme";
 import connectionManager from "../app/ConnectionManager";
 import Navigation from "./Navigation";
 import ActionBar from "./ActionBar";
-import notificationManager from "../app/NotificationManager";
+import notifier from "../app/Notifier";
 import NoTopics from "./NoTopics";
 import Preferences from "./Preferences";
 import {useLiveQuery} from "dexie-react-hooks";
@@ -26,6 +26,11 @@ import {subscriptionRoute} from "../app/utils";
 // TODO sound
 // TODO "copy url" toast
 // TODO "copy link url" button
+// TODO races when two tabs are open
+// TODO sound mentions
+//  https://notificationsounds.com/message-tones/pristine-609
+//  https://notificationsounds.com/message-tones/juntos-607
+//  https://notificationsounds.com/notification-sounds/beep-472
 
 const App = () => {
     return (
@@ -40,7 +45,7 @@ const App = () => {
 
 const Root = () => {
     const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
-    const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
+    const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted());
     const navigate = useNavigate();
     const location = useLocation();
     const users = useLiveQuery(() => userManager.all());
@@ -54,7 +59,7 @@ const Root = () => {
     };
 
     const handleRequestPermission = () => {
-        notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted));
+        notifier.maybeRequestPermission(granted => setNotificationsGranted(granted));
     };
 
     useEffect(() => {
@@ -68,7 +73,7 @@ const Root = () => {
                 const added = await subscriptionManager.addNotification(subscriptionId, notification);
                 if (added) {
                     const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); // FIXME
-                    await notificationManager.notify(subscriptionId, notification, defaultClickAction)
+                    await notifier.notify(subscriptionId, notification, defaultClickAction)
                 }
             } catch (e) {
                 console.error(`[App] Error handling notification`, e);
diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js
index 6f5ac0ebec8e14e6565769cdcc9e5321d65bed68..a9b4e874a957e102890a709f92eeebec4adcfd65 100644
--- a/web/src/components/Preferences.js
+++ b/web/src/components/Preferences.js
@@ -19,6 +19,7 @@ import {Paragraph} from "./styles";
 import EditIcon from '@mui/icons-material/Edit';
 import CloseIcon from "@mui/icons-material/Close";
 import IconButton from "@mui/material/IconButton";
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
 import Container from "@mui/material/Container";
 import TextField from "@mui/material/TextField";
 import MenuItem from "@mui/material/MenuItem";
@@ -31,6 +32,7 @@ import DialogTitle from "@mui/material/DialogTitle";
 import DialogContent from "@mui/material/DialogContent";
 import DialogActions from "@mui/material/DialogActions";
 import userManager from "../app/UserManager";
+import {playSound} from "../app/utils";
 
 const Preferences = () => {
     return (
@@ -50,6 +52,7 @@ const Notifications = () => {
                 Notifications
             </Typography>
             <PrefGroup>
+                <Sound/>
                 <MinPriority/>
                 <DeleteAfter/>
             </PrefGroup>
@@ -57,8 +60,40 @@ const Notifications = () => {
     );
 };
 
+
+const Sound = () => {
+    const sound = useLiveQuery(async () => prefs.sound());
+    const handleChange = async (ev) => {
+        await prefs.setSound(ev.target.value);
+    }
+    if (!sound) {
+        return null; // While loading
+    }
+    return (
+        <Pref title="Notification sound">
+            <div style={{ display: 'flex', width: '100%' }}>
+                <FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
+                    <Select value={sound} onChange={handleChange}>
+                        <MenuItem value={"none"}>No sound</MenuItem>
+                        <MenuItem value={"mixkit-correct-answer-tone"}>Ding</MenuItem>
+                        <MenuItem value={"juntos"}>Juntos</MenuItem>
+                        <MenuItem value={"pristine"}>Pristine</MenuItem>
+                        <MenuItem value={"mixkit-software-interface-start"}>Dadum</MenuItem>
+                        <MenuItem value={"mixkit-message-pop-alert"}>Pop</MenuItem>
+                        <MenuItem value={"mixkit-long-pop"}>Pop swoosh</MenuItem>
+                        <MenuItem value={"beep"}>Beep</MenuItem>
+                    </Select>
+                </FormControl>
+                <IconButton onClick={() => playSound(sound)} disabled={sound === "none"}>
+                    <PlayArrowIcon />
+                </IconButton>
+            </div>
+        </Pref>
+    )
+};
+
 const MinPriority = () => {
-    const minPriority = useLiveQuery(() => prefs.minPriority());
+    const minPriority = useLiveQuery(async () => prefs.minPriority());
     const handleChange = async (ev) => {
         await prefs.setMinPriority(ev.target.value);
     }