diff --git a/cmd/serve.go b/cmd/serve.go
index 03240330..3b98fc2b 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -58,6 +58,7 @@ var flagsServe = append(
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
+	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
@@ -132,6 +133,7 @@ func execServe(c *cli.Context) error {
 	attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
 	keepaliveInterval := c.Duration("keepalive-interval")
 	managerInterval := c.Duration("manager-interval")
+	disallowedTopics := c.StringSlice("disallowed-topics")
 	webRoot := c.String("web-root")
 	enableSignup := c.Bool("enable-signup")
 	enableLogin := c.Bool("enable-login")
@@ -251,6 +253,9 @@ func execServe(c *cli.Context) error {
 		stripe.Key = stripeSecretKey
 	}
 
+	// Add default forbidden topics
+	disallowedTopics = append(disallowedTopics, server.DefaultDisallowedTopics...)
+
 	// Run server
 	conf := server.NewConfig()
 	conf.File = config
@@ -276,6 +281,7 @@ func execServe(c *cli.Context) error {
 	conf.AttachmentExpiryDuration = attachmentExpiryDuration
 	conf.KeepaliveInterval = keepaliveInterval
 	conf.ManagerInterval = managerInterval
+	conf.DisallowedTopics = disallowedTopics
 	conf.WebRootIsApp = webRootIsApp
 	conf.UpstreamBaseURL = upstreamBaseURL
 	conf.SMTPSenderAddr = smtpSenderAddr
diff --git a/server/config.go b/server/config.go
index 0ea32162..2ee29498 100644
--- a/server/config.go
+++ b/server/config.go
@@ -58,6 +58,10 @@ const (
 var (
 	// DefaultVisitorStatsResetTime defines the time at which visitor stats are reset (wall clock only)
 	DefaultVisitorStatsResetTime = time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)
+
+	// DefaultDisallowedTopics defines the topics that are forbidden, because they are used elsewhere. This array can be
+	// extended using the server.yml config. If updated, also update in Android and web app.
+	DefaultDisallowedTopics = []string{"docs", "static", "file", "app", "account", "settings", "signup", "login"}
 )
 
 // Config is the main config struct for the application. Use New to instantiate a default config struct.
@@ -87,6 +91,7 @@ type Config struct {
 	AttachmentExpiryDuration             time.Duration
 	KeepaliveInterval                    time.Duration
 	ManagerInterval                      time.Duration
+	DisallowedTopics                     []string
 	WebRootIsApp                         bool
 	DelayedSenderInterval                time.Duration
 	FirebaseKeepaliveInterval            time.Duration
diff --git a/server/errors.go b/server/errors.go
index 5fd6d625..ddc34629 100644
--- a/server/errors.go
+++ b/server/errors.go
@@ -52,7 +52,7 @@ var (
 	errHTTPBadRequestPriorityInvalid                 = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority"}
 	errHTTPBadRequestSinceInvalid                    = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages"}
 	errHTTPBadRequestTopicInvalid                    = &errHTTP{40009, http.StatusBadRequest, "invalid request: topic invalid", ""}
-	errHTTPBadRequestTopicDisallowed                 = &errHTTP{40010, http.StatusBadRequest, "invalid request: topic name is disallowed", ""}
+	errHTTPBadRequestTopicDisallowed                 = &errHTTP{40010, http.StatusBadRequest, "invalid request: topic name is not allowed", ""}
 	errHTTPBadRequestMessageNotUTF8                  = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""}
 	errHTTPBadRequestAttachmentURLInvalid            = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments"}
 	errHTTPBadRequestAttachmentsDisallowed           = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments"}
diff --git a/server/server.go b/server/server.go
index a8b3a9f9..c32dd977 100644
--- a/server/server.go
+++ b/server/server.go
@@ -39,7 +39,8 @@ import (
   - api
 - HIGH Self-review
 - MEDIUM: Test for expiring messages after reservation removal
-- MEDIUM: disallowed-topics
+- MEDIUM: uploading attachments leads to 404 -- race
+- MEDIUM: Do not call tiers endoint when payments is not enabled
 - MEDIUM: Test new token endpoints & never-expiring token
 - LOW: UI: Flickering upgrade banner when logging in
 
@@ -103,7 +104,6 @@ var (
 	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", "account", "settings", "pricing", "signup", "login", "reset-password"} // If updated, also update in Android and web app
 	urlRegex                                             = regexp.MustCompile(`^https?://`)
 
 	//go:embed site
@@ -496,7 +496,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
 		EnableSignup:       s.config.EnableSignup,
 		EnablePayments:     s.config.StripeSecretKey != "",
 		EnableReservations: s.config.EnableReservations,
-		DisallowedTopics:   disallowedTopics,
+		DisallowedTopics:   s.config.DisallowedTopics,
 	}
 	b, err := json.MarshalIndent(response, "", "  ")
 	if err != nil {
@@ -1260,7 +1260,7 @@ func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
 	defer s.mu.Unlock()
 	topics := make([]*topic, 0)
 	for _, id := range ids {
-		if util.Contains(disallowedTopics, id) {
+		if util.Contains(s.config.DisallowedTopics, id) {
 			return nil, errHTTPBadRequestTopicDisallowed
 		}
 		if _, ok := s.topics[id]; !ok {
diff --git a/server/server.yml b/server/server.yml
index 21ed1fa2..330a24e7 100644
--- a/server/server.yml
+++ b/server/server.yml
@@ -155,6 +155,17 @@
 #
 # manager-interval: "1m"
 
+# Defines topic names that are not allowed, because they are otherwise used. There are a few default topics
+# that cannot be used (e.g. app, account, settings, ...). To extend the default list, define them here.
+#
+# Example:
+#   disallowed-topics:
+#     - about
+#     - pricing
+#     - contact
+#
+# disallowed-topics:
+
 # Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
 # web app. If you self-host, you don't want to change this.
 # Can be "app" (default), "home" or "disable" to disable the web app entirely.
diff --git a/server/server_test.go b/server/server_test.go
index 40a5bcbb..f96fe117 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -158,6 +158,19 @@ func TestServer_PublishAndSubscribe(t *testing.T) {
 	require.Equal(t, []string{"tag1", "tag 2", "tag3"}, messages[2].Tags)
 }
 
+func TestServer_Publish_Disallowed_Topic(t *testing.T) {
+	c := newTestConfig(t)
+	c.DisallowedTopics = []string{"about", "time", "this", "got", "added"}
+	s := newTestServer(t, c)
+
+	rr := request(t, s, "PUT", "/mytopic", "my first message", nil)
+	require.Equal(t, 200, rr.Code)
+
+	rr = request(t, s, "PUT", "/about", "another message", nil)
+	require.Equal(t, 400, rr.Code)
+	require.Equal(t, 40010, toHTTPError(t, rr.Body.String()).Code)
+}
+
 func TestServer_StaticSites(t *testing.T) {
 	s := newTestServer(t, newTestConfig(t))
 
diff --git a/web/src/components/routes.js b/web/src/components/routes.js
index 5e0a54a2..6ab3e66a 100644
--- a/web/src/components/routes.js
+++ b/web/src/components/routes.js
@@ -6,7 +6,6 @@ import {shortUrl} from "../app/utils";
 const routes = {
     login: "/login",
     signup: "/signup",
-    resetPassword: "/reset-password", // Not used (yet)
     app: config.app_root,
     account: "/account",
     settings: "/settings",