diff --git a/server/index.html b/server/index.html
index 9ab3b9b9dde459546d52f933e47b6b9337dc6d93..eaa4d5f0f82fe1d1c42e4ff6054f52ae2515c2b9 100644
--- a/server/index.html
+++ b/server/index.html
@@ -8,71 +8,89 @@
 <body>
 <h1>ntfy.sh</h1>
 
-Topics:
-<ul id="topics">
-</ul>
+<p>
+    ntfy.sh is a super simple pub-sub notification service. It allows you to send desktop and (soon) phone notifications
+    via scripts, without signup or cost. It's entirely free and open source.
+</p>
 
-<input type="text" id="topic" size="64" autofocus />
-<button id="topicButton">Add topic</button>
-<button onclick="notifyMe('test'); return false">Notify me!</button>
+<p>
+    <b>Usage:</b> You can subscribe to a topic either in this web UI, or in your own app by subscribing to an SSE/EventSource
+    or JSON feed. Once subscribed, you can publish messages via PUT or POST.
+</p>
 
 <div id="error"></div>
-<script type="text/javascript">
-    window.onload = function() {
-        let topics = {};
 
-        const topicField = document.getElementById("topic");
-        const topicsList = document.getElementById("topics");
-        const topicButton = document.getElementById("topicButton");
-        const errorField = document.getElementById("error");
+<form id="subscribeForm">
+    <input type="text" id="topicField" size="64" autofocus />
+    <input type="submit" id="subscribeButton" value="Subscribe topic" />
+</form>
 
-        const subscribe = function (topic) {
-            let conn = new WebSocket(`ws://${document.location.host}/${topic}/ws`);
-            conn.onclose = function (evt) {
-                errorField.innerHTML = "Connection closed";
-            };
-            conn.onmessage = function (evt) {
-                notify(evt.data)
-            };
-            topics[topic] = conn;
+Topics:
+<ul id="topicsList">
+</ul>
 
-            let topicEntry = document.createElement('li');
-            topicEntry.innerHTML = `${topic} <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
-            topicsList.appendChild(topicEntry);
-        };
+<script type="text/javascript">
+    let topics = {};
 
-        const notify = function (msg) {
-            if (!("Notification" in window)) {
-                alert("This browser does not support desktop notification");
-            } else if (Notification.permission === "granted") {
-                var notification = new Notification(msg);
-            } else if (Notification.permission !== "denied") {
-                Notification.requestPermission().then(function (permission) {
-                    if (permission === "granted") {
-                        var notification = new Notification(msg);
-                    }
-                });
-            }
+    const topicField = document.getElementById("topicField");
+    const topicsList = document.getElementById("topicsList");
+    const subscribeButton = document.getElementById("subscribeButton");
+    const subscribeForm = document.getElementById("subscribeForm");
+    const errorField = document.getElementById("error");
+
+    const subscribe = function (topic) {
+        if (Notification.permission !== "granted") {
+            Notification.requestPermission().then(function (permission) {
+                if (permission === "granted") {
+                    subscribeInternal(topic);
+                }
+            });
+        } else {
+            subscribeInternal(topic);
         }
+    };
 
-        topicButton.onclick = function () {
-            if (!topicField.value) {
-                return false;
-            }
-            subscribe(topicField.value);
-            return false;
+    const subscribeInternal = function (topic) {
+        let eventSource = new EventSource(`${topic}/sse`);
+        eventSource.onerror = function (e) {
+            console.log(e);
+            errorField.innerHTML = "Error " + e;
+        };
+        eventSource.onmessage = function (e) {
+            const event = JSON.parse(e.data);
+            new Notification(event.message);
         };
+        topics[topic] = eventSource;
+
+        let topicEntry = document.createElement('li');
+        topicEntry.id = `topic-${topic}`;
+        topicEntry.innerHTML = `${topic} <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
+        topicsList.appendChild(topicEntry);
+    };
 
-        if (!window["Notification"]) {
-            errorField.innerHTML = "Your browser does not support desktop notifications";
-            topicField.disabled = true;
-            topicButton.disabled = true;
-        } else if (!window["Notification"]) {
-            errorField.innerHTML = "Your browser does not support WebSockets.";
-            topicField.disabled = true;
-            topicButton.disabled = true;
+        const unsubscribe = function(topic) {
+        topics[topic].close();
+        document.getElementById(`topic-${topic}`).remove();
+    };
+
+    subscribeForm.onsubmit = function () {
+        alert("hi")
+        if (!topicField.value) {
+            return false;
         }
+        subscribe(topicField.value);
+        return false;
     };
+
+    if (!window["Notification"] || !window["EventSource"]) {
+        errorField.innerHTML = "Your browser is not compatible to use the web-based desktop notifications.";
+        topicField.disabled = true;
+        subscribeButton.disabled = true;
+    } else if (Notification.permission === "denied") {
+        errorField.innerHTML = "You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications.";
+        topicField.disabled = true;
+        subscribeButton.disabled = true;
+    }
 </script>
 
 </body>
diff --git a/server/server.go b/server/server.go
index 3b839b820c6fa0558098944d337c659fd785c0de..a78019c50dd3bc463224cf2e71f63ee6ebfeee66 100644
--- a/server/server.go
+++ b/server/server.go
@@ -5,6 +5,7 @@ import (
 	_ "embed" // required for go:embed
 	"encoding/json"
 	"errors"
+	"fmt"
 	"github.com/gorilla/websocket"
 	"io"
 	"log"
@@ -31,8 +32,9 @@ const (
 
 var (
 	topicRegex    = regexp.MustCompile(`^/[^/]+$`)
-	wsRegex    = regexp.MustCompile(`^/[^/]+/ws$`)
 	jsonRegex    = regexp.MustCompile(`^/[^/]+/json$`)
+	sseRegex    = regexp.MustCompile(`^/[^/]+/sse$`)
+	wsRegex    = regexp.MustCompile(`^/[^/]+/ws$`)
 	wsUpgrader = websocket.Upgrader{
 		ReadBufferSize:  messageLimit,
 		WriteBufferSize: messageLimit,
@@ -82,7 +84,9 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
 	} else if r.Method == http.MethodGet && wsRegex.MatchString(r.URL.Path) {
 		return s.handleSubscribeWS(w, r)
 	} else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) {
-		return s.handleSubscribeHTTP(w, r)
+		return s.handleSubscribeJSON(w, r)
+	} else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) {
+		return s.handleSubscribeSSE(w, r)
 	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) {
 		return s.handlePublishHTTP(w, r)
 	}
@@ -112,7 +116,7 @@ func (s *Server) handlePublishHTTP(w http.ResponseWriter, r *http.Request) error
 	return t.Publish(msg)
 }
 
-func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request) error {
 	t := s.createTopic(strings.TrimSuffix(r.URL.Path[1:], "/json")) // Hack
 	subscriberID := t.Subscribe(func (msg *message) error {
 		if err := json.NewEncoder(w).Encode(&msg); err != nil {
@@ -131,6 +135,32 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request) err
 	return nil
 }
 
+func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request) error {
+	t := s.createTopic(strings.TrimSuffix(r.URL.Path[1:], "/sse")) // Hack
+	subscriberID := t.Subscribe(func (msg *message) error {
+		var buf bytes.Buffer
+		if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
+			return err
+		}
+		m := fmt.Sprintf("data: %s\n\n", buf.String())
+		if _, err := io.WriteString(w, m); err != nil {
+			return err
+		}
+		if fl, ok := w.(http.Flusher); ok {
+			fl.Flush()
+		}
+		return nil
+	})
+	defer t.Unsubscribe(subscriberID)
+	w.Header().Set("Content-Type", "text/event-stream")
+	w.WriteHeader(http.StatusOK)
+	select {
+	case <-t.ctx.Done():
+	case <-r.Context().Done():
+	}
+	return nil
+}
+
 func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request) error {
 	conn, err := wsUpgrader.Upgrade(w, r, nil)
 	if err != nil {