diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 92816e6b..e5e989b2 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -13,7 +13,7 @@ jobs:
         name: Install node
         uses: actions/setup-node@v2
         with:
-          node-version: '17'
+          node-version: '18'
       -
         name: Checkout code
         uses: actions/checkout@v2
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index be13b96c..7341addd 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -16,7 +16,7 @@ jobs:
         name: Install node
         uses: actions/setup-node@v2
         with:
-          node-version: '17'
+          node-version: '18'
       -
         name: Checkout code
         uses: actions/checkout@v2
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 544857c1..372a87ce 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -13,7 +13,7 @@ jobs:
         name: Install node
         uses: actions/setup-node@v2
         with:
-          node-version: '17'
+          node-version: '18'
       -
         name: Checkout code
         uses: actions/checkout@v2
diff --git a/client/client.yml b/client/client.yml
index d3ba2722..1b81b80d 100644
--- a/client/client.yml
+++ b/client/client.yml
@@ -5,10 +5,12 @@
 #
 # default-host: https://ntfy.sh
 
-# Default username and password will be used with "ntfy publish" if no credentials are provided on command line
-# Default username and password will be used with "ntfy subscribe" if no credentials are provided in subscription below
-# For an empty password, use empty double-quotes ("")
-#
+# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
+# You can set a default token to use or a default user:password combination, but not both. For an empty password,
+# use empty double-quotes ("")
+
+# default-token:
+
 # default-user:
 # default-password:
 
@@ -30,6 +32,8 @@
 #         command: 'notify-send "$m"'
 #         user: phill
 #         password: mypass
+#       - topic: token_topic
+#         token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
 #
 # Variables:
 #     Variable        Aliases               Description
diff --git a/client/config.go b/client/config.go
index b2efc1d0..d4337d47 100644
--- a/client/config.go
+++ b/client/config.go
@@ -12,17 +12,22 @@ const (
 
 // Config is the config struct for a Client
 type Config struct {
-	DefaultHost     string  `yaml:"default-host"`
-	DefaultUser     string  `yaml:"default-user"`
-	DefaultPassword *string `yaml:"default-password"`
-	DefaultCommand  string  `yaml:"default-command"`
-	Subscribe       []struct {
-		Topic    string            `yaml:"topic"`
-		User     string            `yaml:"user"`
-		Password *string           `yaml:"password"`
-		Command  string            `yaml:"command"`
-		If       map[string]string `yaml:"if"`
-	} `yaml:"subscribe"`
+	DefaultHost     string      `yaml:"default-host"`
+	DefaultUser     string      `yaml:"default-user"`
+	DefaultPassword *string     `yaml:"default-password"`
+	DefaultToken    string      `yaml:"default-token"`
+	DefaultCommand  string      `yaml:"default-command"`
+	Subscribe       []Subscribe `yaml:"subscribe"`
+}
+
+// Subscribe is the struct for a Subscription within Config
+type Subscribe struct {
+	Topic    string            `yaml:"topic"`
+	User     string            `yaml:"user"`
+	Password *string           `yaml:"password"`
+	Token    string            `yaml:"token"`
+	Command  string            `yaml:"command"`
+	If       map[string]string `yaml:"if"`
 }
 
 // NewConfig creates a new Config struct for a Client
@@ -31,6 +36,7 @@ func NewConfig() *Config {
 		DefaultHost:     DefaultBaseURL,
 		DefaultUser:     "",
 		DefaultPassword: nil,
+		DefaultToken:    "",
 		DefaultCommand:  "",
 		Subscribe:       nil,
 	}
diff --git a/client/config_test.go b/client/config_test.go
index 0a71c3bb..f22e6b20 100644
--- a/client/config_test.go
+++ b/client/config_test.go
@@ -116,3 +116,25 @@ subscribe:
 	require.Equal(t, "phil", conf.Subscribe[0].User)
 	require.Nil(t, conf.Subscribe[0].Password)
 }
+
+func TestConfig_DefaultToken(t *testing.T) {
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(`
+default-host: http://localhost
+default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+subscribe:
+  - topic: mytopic
+`), 0600))
+
+	conf, err := client.LoadConfig(filename)
+	require.Nil(t, err)
+	require.Equal(t, "http://localhost", conf.DefaultHost)
+	require.Equal(t, "", conf.DefaultUser)
+	require.Nil(t, conf.DefaultPassword)
+	require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
+	require.Equal(t, 1, len(conf.Subscribe))
+	require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
+	require.Equal(t, "", conf.Subscribe[0].User)
+	require.Nil(t, conf.Subscribe[0].Password)
+	require.Equal(t, "", conf.Subscribe[0].Token)
+}
diff --git a/cmd/publish.go b/cmd/publish.go
index 21578d34..0179f9fa 100644
--- a/cmd/publish.go
+++ b/cmd/publish.go
@@ -154,8 +154,7 @@ func execPublish(c *cli.Context) error {
 	}
 	if token != "" {
 		options = append(options, client.WithBearerAuth(token))
-	}
-	if user != "" {
+	} else if user != "" {
 		var pass string
 		parts := strings.SplitN(user, ":", 2)
 		if len(parts) == 2 {
@@ -171,7 +170,9 @@ func execPublish(c *cli.Context) error {
 			fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
 		}
 		options = append(options, client.WithBasicAuth(user, pass))
-	} else if token == "" && conf.DefaultUser != "" && conf.DefaultPassword != nil {
+	} else if conf.DefaultToken != "" {
+		options = append(options, client.WithBearerAuth(conf.DefaultToken))
+	} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
 		options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
 	}
 	if pid > 0 {
diff --git a/cmd/publish_test.go b/cmd/publish_test.go
index 6fe2d000..a254f47d 100644
--- a/cmd/publish_test.go
+++ b/cmd/publish_test.go
@@ -5,8 +5,11 @@ import (
 	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/test"
 	"heckel.io/ntfy/util"
+	"net/http"
+	"net/http/httptest"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"testing"
@@ -130,7 +133,7 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
 	require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
 
 	// Tests with NTFY_TOPIC set ////
-	require.Nil(t, os.Setenv("NTFY_TOPIC", topic))
+	t.Setenv("NTFY_TOPIC", topic)
 
 	// Test: Successful command with NTFY_TOPIC
 	app, _, stdout, _ = newTestApp()
@@ -147,3 +150,151 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
 	m = toMessage(t, stdout.String())
 	require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
 }
+
+func TestCLI_Publish_Default_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-user: philipp
+default-password: mypass
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
+	m := toMessage(t, stdout.String())
+	require.Equal(t, "triggered", m.Message)
+}
+
+func TestCLI_Publish_Default_Token(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
+	m := toMessage(t, stdout.String())
+	require.Equal(t, "triggered", m.Message)
+}
+
+func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-user: philipp
+default-password: mypass
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
+	m := toMessage(t, stdout.String())
+	require.Equal(t, "triggered", m.Message)
+}
+
+func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
+	m := toMessage(t, stdout.String())
+	require.Equal(t, "triggered", m.Message)
+}
+
+func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_FAKETOKEN01234567890FAKETOKEN
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
+	m := toMessage(t, stdout.String())
+	require.Equal(t, "triggered", m.Message)
+}
+
+func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-user: philipp
+default-password: fakepass
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
+	m := toMessage(t, stdout.String())
+	require.Equal(t, "triggered", m.Message)
+}
+
+func TestCLI_Publish_Token_And_UserPass(t *testing.T) {
+	app, _, _, _ := newTestApp()
+	err := app.Run([]string{"ntfy", "publish", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
+	require.Error(t, err)
+	require.Equal(t, "cannot set both --user and --token", err.Error())
+}
diff --git a/cmd/subscribe.go b/cmd/subscribe.go
index bbc6fb33..3b4b4477 100644
--- a/cmd/subscribe.go
+++ b/cmd/subscribe.go
@@ -30,6 +30,7 @@ var flagsSubscribe = append(
 	&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
 	&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
 	&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
+	&cli.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token used to auth against the server"},
 	&cli.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
 	&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
 	&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
@@ -97,11 +98,18 @@ func execSubscribe(c *cli.Context) error {
 	cl := client.New(conf)
 	since := c.String("since")
 	user := c.String("user")
+	token := c.String("token")
 	poll := c.Bool("poll")
 	scheduled := c.Bool("scheduled")
 	fromConfig := c.Bool("from-config")
 	topic := c.Args().Get(0)
 	command := c.Args().Get(1)
+
+	// Checks
+	if user != "" && token != "" {
+		return errors.New("cannot set both --user and --token")
+	}
+
 	if !fromConfig {
 		conf.Subscribe = nil // wipe if --from-config not passed
 	}
@@ -109,6 +117,9 @@ func execSubscribe(c *cli.Context) error {
 	if since != "" {
 		options = append(options, client.WithSince(since))
 	}
+	if token != "" {
+		options = append(options, client.WithBearerAuth(token))
+	}
 	if user != "" {
 		var pass string
 		parts := strings.SplitN(user, ":", 2)
@@ -126,9 +137,6 @@ func execSubscribe(c *cli.Context) error {
 		}
 		options = append(options, client.WithBasicAuth(user, pass))
 	}
-	if poll {
-		options = append(options, client.WithPoll())
-	}
 	if scheduled {
 		options = append(options, client.WithScheduled())
 	}
@@ -145,6 +153,9 @@ func execSubscribe(c *cli.Context) error {
 
 func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
 	for _, s := range conf.Subscribe { // may be nil
+		if auth := maybeAddAuthHeader(s, conf); auth != nil {
+			options = append(options, auth)
+		}
 		if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
 			return err
 		}
@@ -175,21 +186,11 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
 		for filter, value := range s.If {
 			topicOptions = append(topicOptions, client.WithFilter(filter, value))
 		}
-		var user string
-		var password *string
-		if s.User != "" {
-			user = s.User
-		} else if conf.DefaultUser != "" {
-			user = conf.DefaultUser
-		}
-		if s.Password != nil {
-			password = s.Password
-		} else if conf.DefaultPassword != nil {
-			password = conf.DefaultPassword
-		}
-		if user != "" && password != nil {
-			topicOptions = append(topicOptions, client.WithBasicAuth(user, *password))
+
+		if auth := maybeAddAuthHeader(s, conf); auth != nil {
+			topicOptions = append(topicOptions, auth)
 		}
+
 		subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
 		if s.Command != "" {
 			cmds[subscriptionID] = s.Command
@@ -214,6 +215,25 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
 	return nil
 }
 
+func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
+	// check for subscription token then subscription user:pass
+	if s.Token != "" {
+		return client.WithBearerAuth(s.Token)
+	}
+	if s.User != "" && s.Password != nil {
+		return client.WithBasicAuth(s.User, *s.Password)
+	}
+
+	// if no subscription token nor subscription user:pass, check for default token then default user:pass
+	if conf.DefaultToken != "" {
+		return client.WithBearerAuth(conf.DefaultToken)
+	}
+	if conf.DefaultUser != "" && conf.DefaultPassword != nil {
+		return client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)
+	}
+	return nil
+}
+
 func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
 	if command != "" {
 		runCommand(c, command, m)
diff --git a/cmd/subscribe_test.go b/cmd/subscribe_test.go
new file mode 100644
index 00000000..a22b0c97
--- /dev/null
+++ b/cmd/subscribe_test.go
@@ -0,0 +1,312 @@
+package cmd
+
+import (
+	"fmt"
+	"github.com/stretchr/testify/require"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestCLI_Subscribe_Default_UserPass_Subscription_Token(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-user: philipp
+default-password: mypass
+subscribe:
+  - topic: mytopic
+    token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Token_Subscription_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+subscribe:
+  - topic: mytopic
+    user: philipp
+    password: mypass
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Token_Subscription_Token(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_FAKETOKEN01234567890FAKETOKEN
+subscribe:
+  - topic: mytopic
+    token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_UserPass_Subscription_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-user: fake
+default-password: password
+subscribe:
+  - topic: mytopic
+    user: philipp
+    password: mypass
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Token_Subscription_Empty(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+subscribe:
+  - topic: mytopic
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_UserPass_Subscription_Empty(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-user: philipp
+default-password: mypass
+subscribe:
+  - topic: mytopic
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Empty_Subscription_Token(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+subscribe:
+  - topic: mytopic
+    token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Empty_Subscription_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+subscribe:
+  - topic: mytopic
+    user: philipp
+    password: mypass
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Token_CLI_Token(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_FAKETOKEN0123456789FAKETOKEN
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic"}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Token_CLI_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass", "mytopic"}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Default_Token_Subscription_Token_CLI_UserPass(t *testing.T) {
+	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/mytopic/json", r.URL.Path)
+		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(message))
+	}))
+	defer server.Close()
+
+	filename := filepath.Join(t.TempDir(), "client.yml")
+	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
+default-host: %s
+default-token: tk_FAKETOKEN01234567890FAKETOKEN
+subscribe:
+  - topic: mytopic
+    token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
+`, server.URL)), 0600))
+
+	app, _, stdout, _ := newTestApp()
+
+	require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass"}))
+
+	require.Equal(t, message, strings.TrimSpace(stdout.String()))
+}
+
+func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
+	app, _, _, _ := newTestApp()
+	err := app.Run([]string{"ntfy", "subscribe", "--poll", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
+	require.Error(t, err)
+	require.Equal(t, "cannot set both --user and --token", err.Error())
+}
diff --git a/docs/hooks.py b/docs/hooks.py
new file mode 100644
index 00000000..cdb31a52
--- /dev/null
+++ b/docs/hooks.py
@@ -0,0 +1,6 @@
+import os
+import shutil
+
+def copy_fonts(config, **kwargs):
+    site_dir = config['site_dir']
+    shutil.copytree('docs/static/fonts', os.path.join(site_dir, 'get'))
diff --git a/docs/integrations.md b/docs/integrations.md
index 498a2ff4..1640570c 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -16,6 +16,7 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!**
 | [ntfy.jae.fi](https://ntfy.jae.fi/)               | 🇫🇮 Finland       |
 | [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany       |
 | [ntfy.envs.net](https://ntfy.envs.net)            | 🇩🇪 Germany       |
+| [ntfy.mzte.de](https://ntfy.mzte.de/)             | 🇩🇪 Germany       |
 
 Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
 and uptime of third party servers, so use of each server is **at your own discretion**.
@@ -75,6 +76,7 @@ and uptime of third party servers, so use of each server is **at your own discre
 
 - [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
 - [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go)
+- [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js)
 - [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
 - [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
 - [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
@@ -117,6 +119,7 @@ and uptime of third party servers, so use of each server is **at your own discre
 
 ## Blog + forum posts
 
+- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023 
 - [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023 
 - [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023 
 - [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023 
diff --git a/docs/publish.md b/docs/publish.md
index 231336d1..8561ef90 100644
--- a/docs/publish.md
+++ b/docs/publish.md
@@ -3177,10 +3177,11 @@ These limits can be changed on a per-user basis using [tiers](config.md#tiers).
 a higher tier. ntfy.sh offers multiple paid tiers, which allows for much hier limits than the ones listed above. 
 
 ## List of all parameters
-The following is a list of all parameters that can be passed when publishing a message. Parameter names are **case-insensitive**,
-and can be passed as **HTTP headers** or **query parameters in the URL**. They are listed in the table in their canonical form.
+The following is a list of all parameters that can be passed when publishing a message. Parameter names are **case-insensitive**
+when used in **HTTP headers**, and must be **lowercase** when used as **query parameters in the URL**. They are listed in the 
+table in their canonical form.
 
-| Parameter       | Aliases (case-insensitive)                 | Description                                                                                   |
+| Parameter       | Aliases                                    | Description                                                                                   |
 |-----------------|--------------------------------------------|-----------------------------------------------------------------------------------------------|
 | `X-Message`     | `Message`, `m`                             | Main body of the message as shown in the notification                                         |
 | `X-Title`       | `Title`, `t`                               | [Message title](#message-title)                                                               |
diff --git a/docs/releases.md b/docs/releases.md
index 612ad77f..507e6e35 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -2,6 +2,38 @@
 Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
 and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
 
+## ntfy server v2.2.0 (UNRELEASED)
+
+**Features:**
+
+* You can now use tokens in `client.yml` for publishing and subscribing ([#653](https://github.com/binwiederhier/ntfy/issues/653), thanks to [@wunter8](https://github.com/wunter8))
+
+**Bug fixes + maintenance:**
+
+* `ntfy sub --poll --from-config` will now include authentication headers from client.yml (if applicable) ([#658](https://github.com/binwiederhier/ntfy/issues/658), thanks to [@wunter8](https://github.com/wunter8))
+* Docs: Removed dependency on Google Fonts in docs ([#554](https://github.com/binwiederhier/ntfy/issues/554), thanks to [@bt90](https://github.com/bt90) for reporting, and [@ozskywalker](https://github.com/ozskywalker) for implementing)
+* Increase allowed auth failure attempts per IP address to 30 (no ticket)
+* Web app: Increase maximum incremental backoff retry interval to 2 minutes (no ticket)
+
+**Documentation:**
+
+* Make query parameter description more clear ([#630](https://github.com/binwiederhier/ntfy/issues/630), thanks to [@bbaa-bbaa](https://github.com/bbaa-bbaa) for reporting, and to [@wunter8](https://github.com/wunter8) for a fix)
+
+## ntfy Android app v1.16.1 (UNRELEASED)
+
+**Features:**
+
+* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
+
+**Bug fixes + maintenance:**
+
+* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
+* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
+
+**Additional languages:**
+
+* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
+
 ## ntfy server v2.1.2
 Released March 4, 2023
 
diff --git a/docs/static/css/extra.css b/docs/static/css/extra.css
index 0329b352..3104da16 100644
--- a/docs/static/css/extra.css
+++ b/docs/static/css/extra.css
@@ -3,6 +3,8 @@
     --md-primary-fg-color--light: #338574;
     --md-primary-fg-color--dark:  #338574;
     --md-footer-bg-color:         #353744;
+    --md-text-font:               "Roboto";
+    --md-code-font:               "Roboto Mono";
 }
 
 .md-header__button.md-logo :is(img, svg) {
@@ -147,3 +149,57 @@ figure video {
 .lightbox .close-lightbox:hover::before {
     background-color: #fff;
 }
+
+/* roboto-300 - latin */
+@font-face {
+    font-display: swap;
+    font-family: 'Roboto';
+    font-style: normal;
+    font-weight: 300;
+    src: url('../fonts/roboto-v30-latin-300.woff2') format('woff2');
+}
+
+/* roboto-regular - latin */
+@font-face {
+    font-display: swap;
+    font-family: 'Roboto';
+    font-style: normal;
+    font-weight: 400;
+    src: url('../fonts/roboto-v30-latin-regular.woff2') format('woff2');
+}
+
+/* roboto-italic - latin */
+@font-face {
+    font-display: swap;
+    font-family: 'Roboto';
+    font-style: italic;
+    font-weight: 400;
+    src: url('../fonts/roboto-v30-latin-italic.woff2') format('woff2');
+}
+
+/* roboto-500 - latin */
+@font-face {
+    font-display: swap;
+    font-family: 'Roboto';
+    font-style: normal;
+    font-weight: 500;
+    src: url('../fonts/roboto-v30-latin-500.woff2') format('woff2');
+}
+
+/* roboto-700 - latin */
+@font-face {
+    font-display: swap;
+    font-family: 'Roboto';
+    font-style: normal;
+    font-weight: 700;
+    src: url('../fonts/roboto-v30-latin-700.woff2') format('woff2');
+}
+
+/* roboto-mono - latin */
+@font-face {
+    font-display: swap;
+    font-family: 'Roboto Mono';
+    font-style: normal;
+    font-weight: 400;
+    src: url('../fonts/roboto-mono-v22-latin-regular.woff2') format('woff2');
+}
diff --git a/docs/static/fonts/roboto-mono-v22-latin-regular.woff2 b/docs/static/fonts/roboto-mono-v22-latin-regular.woff2
new file mode 100644
index 00000000..f8894bab
Binary files /dev/null and b/docs/static/fonts/roboto-mono-v22-latin-regular.woff2 differ
diff --git a/docs/static/fonts/roboto-v30-latin-300.woff2 b/docs/static/fonts/roboto-v30-latin-300.woff2
new file mode 100644
index 00000000..60681387
Binary files /dev/null and b/docs/static/fonts/roboto-v30-latin-300.woff2 differ
diff --git a/docs/static/fonts/roboto-v30-latin-500.woff2 b/docs/static/fonts/roboto-v30-latin-500.woff2
new file mode 100644
index 00000000..29342a8d
Binary files /dev/null and b/docs/static/fonts/roboto-v30-latin-500.woff2 differ
diff --git a/docs/static/fonts/roboto-v30-latin-700.woff2 b/docs/static/fonts/roboto-v30-latin-700.woff2
new file mode 100644
index 00000000..771fbecc
Binary files /dev/null and b/docs/static/fonts/roboto-v30-latin-700.woff2 differ
diff --git a/docs/static/fonts/roboto-v30-latin-italic.woff2 b/docs/static/fonts/roboto-v30-latin-italic.woff2
new file mode 100644
index 00000000..e1b7a79f
Binary files /dev/null and b/docs/static/fonts/roboto-v30-latin-italic.woff2 differ
diff --git a/docs/static/fonts/roboto-v30-latin-regular.woff2 b/docs/static/fonts/roboto-v30-latin-regular.woff2
new file mode 100644
index 00000000..020729ef
Binary files /dev/null and b/docs/static/fonts/roboto-v30-latin-regular.woff2 differ
diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md
index f1f9e760..59cfc8e7 100644
--- a/docs/subscribe/cli.md
+++ b/docs/subscribe/cli.md
@@ -254,13 +254,13 @@ I hope this shows how powerful this command is. Here's a short video that demons
   <figcaption>Execute all the things</figcaption>
 </figure>
 
-If most (or all) of your subscription usernames, passwords, and commands are the same, you can specify a `default-user`, `default-password`, and `default-command` at the top of the
-`client.yml`. If a subscription does not specify a username/password to use or does not have a command, the defaults will be used, otherwise, the subscription settings will
-override the defaults.
+If most (or all) of your subscriptions use the same credentials, you can set defaults in `client.yml`. Use `default-user` and `default-password` or `default-token` (but not both).
+You can also specify a `default-command` that will run when a message is received. If a subscription does not include credentials to use or does not have a command, the defaults
+will be used, otherwise, the subscription settings will override the defaults.
 
 !!! warning
-    Because the `default-user` and `default-password` will be sent for each topic that does not have its own username/password (even if the topic does not require authentication),
-    be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
+    Because the `default-user`, `default-password`, and `default-token` will be sent for each topic that does not have its own username/password (even if the topic does not
+    require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
 
 ### Using the systemd service
 You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
diff --git a/mkdocs.yml b/mkdocs.yml
index e3a0d507..66fc4c84 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,6 +9,7 @@ edit_uri: blob/main/docs/
 
 theme:
   name: material
+  font: false
   language: en
   custom_dir: docs/_overrides
   logo: static/img/ntfy.png
@@ -70,6 +71,9 @@ plugins:
   - search
   - minify:
       minify_html: true
+  - mkdocs-simple-hooks:
+      hooks:
+        on_post_build: "docs.hooks:copy_fonts"
 
 nav:
 - "Getting started": index.md
diff --git a/requirements.txt b/requirements.txt
index 9c2212a8..17b0fc1a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 # The documentation uses 'mkdocs', which is written in Python
 mkdocs-material
 mkdocs-minify-plugin
+mkdocs-simple-hooks
diff --git a/server/config.go b/server/config.go
index 75290223..dd161e4c 100644
--- a/server/config.go
+++ b/server/config.go
@@ -49,7 +49,7 @@ const (
 	DefaultVisitorEmailLimitReplenish           = time.Hour
 	DefaultVisitorAccountCreationLimitBurst     = 3
 	DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
-	DefaultVisitorAuthFailureLimitBurst         = 10
+	DefaultVisitorAuthFailureLimitBurst         = 30
 	DefaultVisitorAuthFailureLimitReplenish     = time.Minute
 	DefaultVisitorAttachmentTotalSizeLimit      = 100 * 1024 * 1024 // 100 MB
 	DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024 // 500 MB
diff --git a/server/server.go b/server/server.go
index 389601b1..8454869c 100644
--- a/server/server.go
+++ b/server/server.go
@@ -1642,6 +1642,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc
 // maybeAuthenticate reads the "Authorization" header and will try to authenticate the user
 // if it is set.
 //
+//   - If auth-file is not configured, immediately return an IP-based visitor
 //   - If the header is not set or not supported (anything non-Basic and non-Bearer),
 //     an IP-based visitor is returned
 //   - If the header is set, authenticate will be called to check the username/password (Basic auth),
@@ -1653,13 +1654,14 @@ func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) {
 	// Read "Authorization" header value, and exit out early if it's not set
 	ip := extractIPAddress(r, s.config.BehindProxy)
 	vip := s.visitor(ip, nil)
+	if s.userManager == nil {
+		return vip, nil
+	}
 	header, err := readAuthHeader(r)
 	if err != nil {
 		return vip, err
 	} else if !supportedAuthHeader(header) {
 		return vip, nil
-	} else if s.userManager == nil {
-		return vip, errHTTPUnauthorized
 	}
 	// If we're trying to auth, check the rate limiter first
 	if !vip.AuthAllowed() {
diff --git a/server/server_test.go b/server/server_test.go
index 032ec6ff..fdda5d96 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -796,6 +796,7 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
 
 func TestServer_Auth_Fail_Rate_Limiting(t *testing.T) {
 	c := newTestConfigWithAuthFile(t)
+	c.VisitorAuthFailureLimitBurst = 10
 	s := newTestServer(t, c)
 
 	for i := 0; i < 10; i++ {
diff --git a/user/manager_test.go b/user/manager_test.go
index f242af71..cd2e1032 100644
--- a/user/manager_test.go
+++ b/user/manager_test.go
@@ -133,29 +133,6 @@ func TestManager_AddUser_And_Query(t *testing.T) {
 	require.Equal(t, u.ID, u3.ID)
 }
 
-func TestManager_Authenticate_Timing(t *testing.T) {
-	a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
-	require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
-
-	// Timing a correct attempt
-	start := time.Now().UnixMilli()
-	_, err := a.Authenticate("user", "pass")
-	require.Nil(t, err)
-	require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
-
-	// Timing an incorrect attempt
-	start = time.Now().UnixMilli()
-	_, err = a.Authenticate("user", "INCORRECT")
-	require.Equal(t, ErrUnauthenticated, err)
-	require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
-
-	// Timing a non-existing user attempt
-	start = time.Now().UnixMilli()
-	_, err = a.Authenticate("DOES-NOT-EXIST", "hithere")
-	require.Equal(t, ErrUnauthenticated, err)
-	require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
-}
-
 func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
 
diff --git a/web/public/static/css/fonts.css b/web/public/static/css/fonts.css
index d14bad03..4245d0f5 100644
--- a/web/public/static/css/fonts.css
+++ b/web/public/static/css/fonts.css
@@ -6,8 +6,7 @@
     font-style: normal;
     font-weight: 300;
     src: local(''),
-    url('../fonts/roboto-v29-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
-    url('../fonts/roboto-v29-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+    url('../fonts/roboto-v29-latin-300.woff2') format('woff2');
 }
 
 /* roboto-regular - latin */
@@ -16,8 +15,7 @@
     font-style: normal;
     font-weight: 400;
     src: local(''),
-    url('../fonts/roboto-v29-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
-    url('../fonts/roboto-v29-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+    url('../fonts/roboto-v29-latin-regular.woff2') format('woff2');
 }
 
 /* roboto-500 - latin */
@@ -26,8 +24,7 @@
     font-style: normal;
     font-weight: 500;
     src: local(''),
-    url('../fonts/roboto-v29-latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
-    url('../fonts/roboto-v29-latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+    url('../fonts/roboto-v29-latin-500.woff2') format('woff2');
 }
 
 /* roboto-700 - latin */
@@ -36,6 +33,5 @@
     font-style: normal;
     font-weight: 700;
     src: local(''),
-    url('../fonts/roboto-v29-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
-    url('../fonts/roboto-v29-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+    url('../fonts/roboto-v29-latin-700.woff2') format('woff2');
 }
diff --git a/web/public/static/fonts/roboto-v29-latin-300.woff b/web/public/static/fonts/roboto-v29-latin-300.woff
deleted file mode 100644
index 5565042e..00000000
Binary files a/web/public/static/fonts/roboto-v29-latin-300.woff and /dev/null differ
diff --git a/web/public/static/fonts/roboto-v29-latin-500.woff b/web/public/static/fonts/roboto-v29-latin-500.woff
deleted file mode 100644
index c9eb5cab..00000000
Binary files a/web/public/static/fonts/roboto-v29-latin-500.woff and /dev/null differ
diff --git a/web/public/static/fonts/roboto-v29-latin-700.woff b/web/public/static/fonts/roboto-v29-latin-700.woff
deleted file mode 100644
index a5d98fc6..00000000
Binary files a/web/public/static/fonts/roboto-v29-latin-700.woff and /dev/null differ
diff --git a/web/public/static/fonts/roboto-v29-latin-regular.woff b/web/public/static/fonts/roboto-v29-latin-regular.woff
deleted file mode 100644
index 86b38637..00000000
Binary files a/web/public/static/fonts/roboto-v29-latin-regular.woff and /dev/null differ
diff --git a/web/public/static/langs/ar.json b/web/public/static/langs/ar.json
index 83663794..d6634681 100644
--- a/web/public/static/langs/ar.json
+++ b/web/public/static/langs/ar.json
@@ -56,12 +56,12 @@
     "publish_dialog_title_topic": "أنشُر إلى {{topic}}",
     "publish_dialog_title_no_topic": "انشُر الإشعار",
     "publish_dialog_emoji_picker_show": "اختر رمزًا تعبيريًا",
-    "publish_dialog_priority_min": "الحد الأدنى للأولوية",
+    "publish_dialog_priority_min": "أولوية دنيا",
     "publish_dialog_priority_low": "أولوية منخفضة",
     "publish_dialog_priority_default": "الأولوية الافتراضية",
     "publish_dialog_priority_high": "أولوية عالية",
     "publish_dialog_base_url_label": "الرابط التشعبي للخدمة",
-    "publish_dialog_priority_max": "الأولوية القصوى",
+    "publish_dialog_priority_max": "أولوية قصوى",
     "publish_dialog_topic_placeholder": "اسم الموضوع، على سبيل المثال phil_alerts",
     "publish_dialog_title_label": "العنوان",
     "publish_dialog_title_placeholder": "عنوان الإشعار، على سبيل المثال تنبيه مساحة القرص",
@@ -154,7 +154,7 @@
     "subscribe_dialog_subscribe_button_cancel": "إلغاء",
     "subscribe_dialog_login_button_back": "العودة",
     "prefs_notifications_sound_play": "تشغيل الصوت المحدد",
-    "prefs_notifications_min_priority_title": "الحد الأدنى للأولوية",
+    "prefs_notifications_min_priority_title": "أولوية دنيا",
     "prefs_notifications_min_priority_max_only": "الأولوية القصوى فقط",
     "notifications_no_subscriptions_description": "انقر فوق الرابط \"{{linktext}}\" لإنشاء موضوع أو الاشتراك فيه. بعد ذلك، يمكنك إرسال رسائل عبر PUT أو POST وستتلقى إشعارات هنا.",
     "publish_dialog_click_label": "الرابط التشعبي URL للنقر",
@@ -296,5 +296,38 @@
     "prefs_users_description_no_sync": "لا تتم مزامنة المستخدمين وكلمات المرور مع حسابك.",
     "reservation_delete_dialog_action_delete_description": "سيتم حذف الرسائل والمرفقات المخزنة مؤقتا نهائيا. لا يمكن التراجع عن هذا الإجراء.",
     "notifications_actions_http_request_title": "إرسال طلب HTTP {{method}} إلى {{url}}",
-    "notifications_none_for_any_description": "لإرسال إشعارات إلى موضوع ما، ما عليك سوى إرسال طلب PUT أو POST إلى الرابط التشعبي URL للموضوع. إليك مثال باستخدام أحد مواضيعك."
+    "notifications_none_for_any_description": "لإرسال إشعارات إلى موضوع ما، ما عليك سوى إرسال طلب PUT أو POST إلى الرابط التشعبي URL للموضوع. إليك مثال باستخدام أحد مواضيعك.",
+    "error_boundary_description": "من الواضح أن هذا لا ينبغي أن يحدث. آسف جدًا بشأن هذا. <br/> إن كان لديك دقيقة، يرجى <githubLink> الإبلاغ عن ذلك على GitHub </githubLink> ، أو إعلامنا عبر <discordLink> Discord </discordLink> أو <matrixLink> Matrix </matrixLink>.",
+    "nav_button_muted": "الإشعارات المكتومة",
+    "priority_min": "دنيا",
+    "signup_error_username_taken": "تم حجز اسم المستخدم {{username}} مِن قَبلُ",
+    "action_bar_reservation_limit_reached": "بلغت الحد الأقصى",
+    "prefs_reservations_delete_button": "إعادة تعيين الوصول إلى الموضوع",
+    "prefs_reservations_edit_button": "تعديل الوصول إلى موضوع",
+    "prefs_reservations_limit_reached": "لقد بلغت الحد الأقصى من المواضيع المحجوزة.",
+    "reservation_delete_dialog_action_keep_description": "ستصبح الرسائل والمرفقات المخزنة مؤقتًا على الخادم مرئية للعموم وللأشخاص الذين لديهم معرفة باسم الموضوع.",
+    "reservation_delete_dialog_description": "تؤدي إزالة الحجز إلى التخلي عن ملكية الموضوع، مما يسمح للآخرين بحجزه. يمكنك الاحتفاظ بالرسائل والمرفقات الموجودة أو حذفها.",
+    "prefs_reservations_dialog_description": "يمنحك حجز موضوع ما ملكية الموضوع، ويسمح لك بتحديد تصريحات وصول المستخدمين الآخرين إليه.",
+    "account_upgrade_dialog_interval_yearly_discount_save_up_to": "توفير ما يصل إلى {{discount}}٪",
+    "account_upgrade_dialog_interval_monthly": "شهريا",
+    "account_upgrade_dialog_tier_features_attachment_total_size": "إجمالي مساحة التخزين {{totalsize}}",
+    "publish_dialog_progress_uploading_detail": "تحميل {{loaded}}/{{total}} ({{percent}}٪) …",
+    "account_basics_tier_interval_monthly": "شهريا",
+    "account_basics_tier_interval_yearly": "سنويا",
+    "account_upgrade_dialog_tier_features_reservations": "{{reservations}} مواضيع محجوزة",
+    "account_upgrade_dialog_billing_contact_website": "للأسئلة المتعلقة بالفوترة، يرجى الرجوع إلى <Link>موقعنا على الويب</Link>.",
+    "prefs_notifications_min_priority_description_x_or_higher": "إظهار الإشعارات إذا كانت الأولوية {{number}} ({{name}}) أو أعلى",
+    "account_upgrade_dialog_billing_contact_email": "للأسئلة المتعلقة بالفوترة، الرجاء <Link>الاتصال بنا</Link> مباشرة.",
+    "account_upgrade_dialog_tier_selected_label": "المحدد",
+    "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} لكل ملف",
+    "account_upgrade_dialog_interval_yearly": "سنويا",
+    "account_upgrade_dialog_tier_features_no_reservations": "لا توجد مواضيع محجوزة",
+    "account_upgrade_dialog_interval_yearly_discount_save": "وفر {{discount}}٪",
+    "publish_dialog_click_reset": "إزالة الرابط التشعبي URL للنقر",
+    "prefs_notifications_min_priority_description_max": "إظهار الإشعارات إذا كانت الأولوية 5 (كحد أقصى)",
+    "publish_dialog_attachment_limits_file_reached": "يتجاوز الحد الأقصى للملف {{fileSizeLimit}}",
+    "publish_dialog_attachment_limits_quota_reached": "يتجاوز الحصة، {{remainingBytes}} متبقية",
+    "account_basics_tier_paid_until": "تم دفع مبلغ الاشتراك إلى غاية {{date}}، وسيتم تجديده تِلْقائيًا",
+    "account_basics_tier_canceled_subscription": "تم إلغاء اشتراكك وسيتم إعادته إلى مستوى حساب مجاني بداية مِن {{date}}.",
+    "account_delete_dialog_billing_warning": "إلغاء حسابك أيضاً يلغي اشتراكك في الفوترة فوراً ولن تتمكن من الوصول إلى لوح الفوترة بعد الآن."
 }
diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json
index 11987f86..5f417357 100644
--- a/web/public/static/langs/bg.json
+++ b/web/public/static/langs/bg.json
@@ -228,5 +228,25 @@
     "account_basics_username_description": "Хей, това сте вие ❤",
     "account_basics_username_admin_tooltip": "Вие сте администратор",
     "account_basics_password_title": "Парола",
-    "account_delete_dialog_label": "Парола"
+    "account_delete_dialog_label": "Парола",
+    "account_basics_password_dialog_title": "Смяна на парола",
+    "account_basics_password_dialog_current_password_label": "Текуща парола",
+    "account_basics_password_dialog_new_password_label": "Нова парола",
+    "account_basics_password_dialog_confirm_password_label": "Парола отново",
+    "account_basics_password_dialog_button_submit": "Смяна на парола",
+    "account_usage_title": "Употреба",
+    "account_usage_of_limit": "от {{limit}}",
+    "account_usage_unlimited": "Неограничено",
+    "account_usage_limits_reset_daily": "Ограниченията се нулират всеки ден в полунощ (UTC)",
+    "account_basics_tier_interval_monthly": "месечно",
+    "account_basics_tier_interval_yearly": "годишно",
+    "account_basics_password_description": "Промяна на паролата на профила",
+    "account_basics_tier_title": "Вид на профила",
+    "account_basics_tier_admin": "Администратор",
+    "account_basics_tier_admin_suffix_with_tier": "(с {{tier}} ниво)",
+    "account_basics_tier_admin_suffix_no_tier": "(без ниво)",
+    "account_basics_tier_free": "безплатен",
+    "account_basics_tier_basic": "базов",
+    "account_basics_tier_change_button": "Променяне",
+    "account_basics_tier_paid_until": "Абонаментът е платен до {{date}} и автоматично ще се поднови"
 }
diff --git a/web/public/static/langs/cs.json b/web/public/static/langs/cs.json
index 93e9abb6..032e8b7a 100644
--- a/web/public/static/langs/cs.json
+++ b/web/public/static/langs/cs.json
@@ -285,7 +285,7 @@
     "account_delete_dialog_button_submit": "Trvale odstranit účet",
     "account_delete_dialog_billing_warning": "Odstraněním účtu se také okamžitě zruší vaše předplatné. Nebudete již mít přístup k fakturačnímu panelu.",
     "account_upgrade_dialog_title": "Změna úrovně účtu",
-    "account_upgrade_dialog_proration_info": "<strong>Prohlášení</strong>: Při přechodu mezi placenými úrovněmi bude rozdíl v ceně účtován nebo vrácen v následující faktuře. Další fakturu obdržíte až na konci dalšího zúčtovacího období.",
+    "account_upgrade_dialog_proration_info": "<strong>Prohlášení</strong>: Při přechodu mezi placenými úrovněmi bude rozdíl v ceně <strong>zaúčtován okamžitě</strong>. Při přechodu na nižší úroveň se zůstatek použije na platbu za budoucí zúčtovací období.",
     "account_upgrade_dialog_reservations_warning_one": "Vybraná úroveň umožňuje méně rezervovaných témat než vaše aktuální úroveň. Než změníte svou úroveň, <strong>odstraňte alespoň jednu rezervaci</strong>. Rezervace můžete odstranit v <Link>Nastavení</Link>.",
     "account_upgrade_dialog_tier_features_reservations": "{{reservations}} rezervovaných témat",
     "account_upgrade_dialog_tier_features_messages": "{{messages}} denních zpráv",
@@ -340,5 +340,17 @@
     "reservation_delete_dialog_action_keep_description": "Zprávy a přílohy, které jsou uloženy v mezipaměti serveru, se stanou veřejně viditelnými pro osoby, které znají název tématu.",
     "reservation_delete_dialog_action_delete_title": "Odstranění zpráv a příloh uložených v mezipaměti",
     "reservation_delete_dialog_action_delete_description": "Zprávy a přílohy uložené v mezipaměti budou trvale odstraněny. Tuto akci nelze vrátit zpět.",
-    "reservation_delete_dialog_submit_button": "Odstranit rezervaci"
+    "reservation_delete_dialog_submit_button": "Odstranit rezervaci",
+    "account_basics_tier_interval_yearly": "roční",
+    "account_upgrade_dialog_interval_yearly_discount_save": "ušetříte {{discount}}%",
+    "account_upgrade_dialog_tier_price_per_month": "měsíc",
+    "account_upgrade_dialog_tier_features_no_reservations": "Žádná rezervovaná témata",
+    "account_upgrade_dialog_interval_yearly_discount_save_up_to": "ušetříte až {{discount}}%",
+    "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} účtováno ročně. Ušetříte {{save}}.",
+    "account_basics_tier_interval_monthly": "měsíční",
+    "account_upgrade_dialog_interval_monthly": "Měsíční",
+    "account_upgrade_dialog_interval_yearly": "Roční",
+    "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} za rok. Účtuje se měsíčně.",
+    "account_upgrade_dialog_billing_contact_email": "V případě dotazů týkajících se fakturace nás prosím <Link>kontaktujte</Link> přímo.",
+    "account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich <Link>webových stránkách</Link>."
 }
diff --git a/web/public/static/langs/da.json b/web/public/static/langs/da.json
index 2f871f53..276e442f 100644
--- a/web/public/static/langs/da.json
+++ b/web/public/static/langs/da.json
@@ -221,5 +221,63 @@
     "account_tokens_delete_dialog_submit_button": "Slet token permanent",
     "prefs_notifications_delete_after_one_month": "Efter en måned",
     "prefs_notifications_delete_after_one_week": "Efter en uge",
-    "prefs_users_dialog_username_label": "Brugernavn, f.eks. phil"
+    "prefs_users_dialog_username_label": "Brugernavn, f.eks. phil",
+    "prefs_notifications_delete_after_one_day_description": "Notifikationer slettes automatisk efter en dag",
+    "notifications_none_for_topic_description": "For at sende en notifikation til dette emne, skal du blot sende en PUT eller POST til emne-URL'en.",
+    "notifications_none_for_any_description": "For at sende en notifikation til et emne, skal du blot sende en PUT eller POST til emne-URL'en. Her er et eksempel med et af dine emner.",
+    "notifications_no_subscriptions_title": "Det ser ud til, at du ikke har nogen abonnementer endnu.",
+    "notifications_more_details": "For mere information, se <websiteLink>webstedet</websiteLink> eller <docsLink>dokumentationen</docsLink>.",
+    "display_name_dialog_description": "Angiv et alternativt navn for et emne, der vises på abonnementslisten. Dette gør det nemmere at identificere emner med komplicerede navne.",
+    "reserve_dialog_checkbox_label": "Reserver emne og konfigurer adgang",
+    "publish_dialog_attachment_limits_file_reached": "overskrider {{fileSizeLimit}} filgrænse",
+    "publish_dialog_attachment_limits_quota_reached": "overskrider kvote, {{remainingBytes}} tilbage",
+    "publish_dialog_topic_label": "Emnenavn",
+    "publish_dialog_topic_placeholder": "Emnenavn, f.eks. phil_alerts",
+    "publish_dialog_topic_reset": "Nulstil emne",
+    "publish_dialog_click_reset": "Fjern klik-URL",
+    "publish_dialog_delay_placeholder": "Forsink levering, f.eks. {{unixTimestamp}}, {{relativeTime}} eller \"{{naturalLanguage}}\" (kun på engelsk)",
+    "publish_dialog_other_features": "Andre funktioner:",
+    "publish_dialog_chip_attach_url_label": "Vedhæft fil via URL",
+    "publish_dialog_chip_attach_file_label": "Vedhæft lokal fil",
+    "publish_dialog_details_examples_description": "For eksempler og en detaljeret beskrivelse af alle afsendelsesfunktioner henvises til <docsLink>dokumentationen</docsLink>.",
+    "publish_dialog_button_cancel_sending": "Annuller afsendelse",
+    "publish_dialog_attached_file_title": "Vedhæftet fil:",
+    "emoji_picker_search_placeholder": "Søg emoji",
+    "emoji_picker_search_clear": "Ryd søgning",
+    "subscribe_dialog_subscribe_title": "Abonner på emne",
+    "subscribe_dialog_subscribe_topic_placeholder": "Emnenavn, f.eks. phil_alerts",
+    "subscribe_dialog_subscribe_button_generate_topic_name": "Generer navn",
+    "subscribe_dialog_login_title": "Login påkrævet",
+    "subscribe_dialog_login_description": "Dette emne er adgangskodebeskyttet. Indtast venligst brugernavn og adgangskode for at abonnere.",
+    "subscribe_dialog_error_user_not_authorized": "Brugeren {{username}} er ikke autoriseret",
+    "account_basics_password_description": "Skift adgangskoden til din konto",
+    "account_usage_limits_reset_daily": "Brugsgrænser nulstilles dagligt ved midnat (UTC)",
+    "account_basics_tier_paid_until": "Abonnementet er betalt indtil {{date}} og fornys automatisk",
+    "account_basics_tier_payment_overdue": "Din betaling er forfalden. Opdater venligst din betalingsmetode, ellers bliver din konto snart nedgraderet.",
+    "account_basics_tier_canceled_subscription": "Dit abonnement blev annulleret og vil blive nedgraderet til en gratis konto den {{date}}.",
+    "account_usage_cannot_create_portal_session": "Kan ikke åbne faktureringsportalen",
+    "account_delete_description": "Slet din konto permanent",
+    "account_delete_dialog_description": "Dette vil slette din konto permanent, inklusive alle data, der er gemt på serveren. Efter sletning vil dit brugernavn være utilgængeligt i 7 dage. Hvis du virkelig ønsker at fortsætte, bedes du bekræfte med dit kodeord i feltet nedenfor.",
+    "account_upgrade_dialog_button_pay_now": "Betal nu og abonner",
+    "account_tokens_table_last_origin_tooltip": "Fra IP-adresse {{ip}}, klik for at slå op",
+    "account_tokens_dialog_label": "Label, f.eks. radarmeddelelser",
+    "account_tokens_dialog_expires_label": "Adgangstoken udløber om",
+    "account_tokens_dialog_expires_unchanged": "Lad udløbsdatoen forblive uændret",
+    "account_tokens_dialog_expires_x_hours": "Token udløber om {{hours}} timer",
+    "account_tokens_dialog_expires_x_days": "Token udløber om {{days}} dage",
+    "prefs_notifications_sound_description_none": "Notifikationer afspiller ingen lyd, når de ankommer",
+    "prefs_notifications_sound_description_some": "Notifikationer afspiller {{sound}}-lyden, når de ankommer",
+    "prefs_notifications_min_priority_low_and_higher": "Lav prioritet og højere",
+    "prefs_notifications_min_priority_default_and_higher": "Standardprioritet og højere",
+    "prefs_notifications_min_priority_high_and_higher": "Høj prioritet og højere",
+    "prefs_notifications_delete_after_never_description": "Notifikationer slettes aldrig automatisk",
+    "prefs_notifications_delete_after_three_hours_description": "Notifikationer slettes automatisk efter tre timer",
+    "prefs_notifications_delete_after_one_week_description": "Notifikationer slettes automatisk efter en uge",
+    "prefs_notifications_delete_after_one_month_description": "Notifikationer slettes automatisk efter en måned",
+    "prefs_reservations_limit_reached": "Du har nået din grænse for reserverede emner.",
+    "prefs_reservations_table_click_to_subscribe": "Klik for at abonnere",
+    "reservation_delete_dialog_action_keep_title": "Behold cachelagrede meddelelser og vedhæftede filer",
+    "reservation_delete_dialog_action_delete_title": "Slet cachelagrede meddelelser og vedhæftede filer",
+    "error_boundary_title": "Oh nej, ntfy brød sammen",
+    "error_boundary_description": "Dette bør naturligvis ikke ske. Det beklager vi meget.<br/>Hvis du har et øjeblik, bedes du <githubLink>rapportere dette på GitHub</githubLink>, eller give os besked via <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>."
 }
diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json
index cf7e23a1..f6a73618 100644
--- a/web/public/static/langs/de.json
+++ b/web/public/static/langs/de.json
@@ -82,7 +82,7 @@
     "publish_dialog_attach_placeholder": "Datei von URL anhängen, z.B. https://f-droid.org/F-Droid.apk",
     "publish_dialog_filename_placeholder": "Dateiname des Anhangs",
     "publish_dialog_delay_label": "Verzögerung",
-    "publish_dialog_email_placeholder": "E-Mail-Adresse, an die die Benachrichtigung gesendet werden soll, z. B. phil@example.com",
+    "publish_dialog_email_placeholder": "E-Mail-Adresse, an welche die Benachrichtigung gesendet werden soll, z. B. phil@example.com",
     "publish_dialog_chip_click_label": "Klick-URL",
     "publish_dialog_button_cancel_sending": "Senden abbrechen",
     "publish_dialog_drop_file_here": "Datei hierher ziehen",
@@ -261,7 +261,7 @@
     "account_usage_basis_ip_description": "Nutzungsstatistiken und Limits für diesen Account basieren auf Deiner IP-Adresse, können also mit anderen Usern geteilt sein. Die oben gezeigten Limits sind Schätzungen basierend auf den bestehenden Limits.",
     "account_delete_dialog_billing_warning": "Das Löschen Deines Kontos storniert auch sofort Deine Zahlung. Du wirst dann keinen Zugang zum Abrechnungs-Dashboard haben.",
     "account_upgrade_dialog_title": "Konto-Level ändern",
-    "account_upgrade_dialog_proration_info": "<strong>Anrechnung</strong>: Wenn Du zwischen kostenpflichtigen Leveln wechselst wir die Differenz bei der nächsten Abrechnung nachberechnet oder erstattet. Du erhältst bis zum Ende der Abrechnungsperiode keine neue Rechnung.",
+    "account_upgrade_dialog_proration_info": "<strong>Anrechnung</strong>: Wenn Du auf einen höheren kostenpflichtigen Level wechselst wird die Differenz <strong>sofort berechnet</strong>. Beim Wechsel auf ein kleineres Level verwenden wir Dein Guthaben für zukünftige Abrechnungsperioden.",
     "account_upgrade_dialog_reservations_warning_one": "Das gewählte Level erlaubt weniger reservierte Themen als Dein aktueller Level. <strong>Bitte löschen vor dem Wechsel Deines Levels mindestens eine Reservierung</strong>. Du kannst Reservierungen in den <Link>Einstellungen</Link> löschen.",
     "account_upgrade_dialog_reservations_warning_other": "Das gewählte Level erlaubt weniger reservierte Themen als Dein aktueller Level. <strong>Bitte löschen vor dem Wechsel Deines Levels mindestens {{count}} Reservierungen</strong>. Du kannst Reservierungen in den <Link>Einstellungen</Link> löschen.",
     "account_upgrade_dialog_tier_features_reservations": "{{reservations}} reservierte Themen",
@@ -340,5 +340,17 @@
     "nav_upgrade_banner_label": "Upgrade auf ntfy Pro",
     "alert_not_supported_context_description": "Benachrichtigungen werden nur über HTTPS unterstützt. Das ist eine Einschränkung der <mdnLink>Notifications API</mdnLink>.",
     "display_name_dialog_description": "Lege einen alternativen Namen für ein Thema fest, der in der Abo-Liste angezeigt wird. So kannst Du Themen mit komplizierten Namen leichter finden.",
-    "account_basics_username_admin_tooltip": "Du bist Admin"
+    "account_basics_username_admin_tooltip": "Du bist Admin",
+    "account_upgrade_dialog_interval_yearly_discount_save": "spare {{discount}}%",
+    "account_upgrade_dialog_interval_yearly_discount_save_up_to": "spare bis zu {{discount}}%",
+    "account_upgrade_dialog_tier_price_per_month": "Monat",
+    "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} pro Jahr. Spare {{save}}.",
+    "account_upgrade_dialog_billing_contact_email": "Bei Fragen zur Abrechnung, <Link>kontaktiere uns</Link> bitte direkt.",
+    "account_upgrade_dialog_billing_contact_website": "Bei Fragen zur Abrechnung sieh bitte auf unserer <Link>Webseite</Link> nach.",
+    "account_upgrade_dialog_tier_features_no_reservations": "Keine reservierten Themen",
+    "account_basics_tier_interval_yearly": "jährlich",
+    "account_basics_tier_interval_monthly": "monatlich",
+    "account_upgrade_dialog_interval_monthly": "Monatlich",
+    "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pro Jahr. Monatlich abgerechnet.",
+    "account_upgrade_dialog_interval_yearly": "Jährlich"
 }
diff --git a/web/public/static/langs/es.json b/web/public/static/langs/es.json
index bb1014fe..b19df52b 100644
--- a/web/public/static/langs/es.json
+++ b/web/public/static/langs/es.json
@@ -293,7 +293,7 @@
     "account_delete_dialog_button_submit": "Eliminar permanentemente la cuenta",
     "account_upgrade_dialog_tier_features_reservations": "{{reservations}} tópicos reservados",
     "account_upgrade_dialog_cancel_warning": "Esto <strong>cancelará su suscripción</strong> y degradará su cuenta en {{date}}. En esa fecha, sus tópicos reservados y sus mensajes almacenados en caché en el servidor <strong>serán eliminados</strong>.",
-    "account_upgrade_dialog_proration_info": "<strong>Prorrateo</strong>: Al cambiar entre planes de pago, la diferencia de precio se cargará o reembolsará en la siguiente factura. No recibirá otra factura hasta el final del siguiente periodo de facturación.",
+    "account_upgrade_dialog_proration_info": "<strong>Prorrateo</strong>: al actualizar entre planes pagos, la diferencia de precio se <strong>cobrará de inmediato</strong>. Al cambiar a un nivel inferior, el saldo se utilizará para pagar futuros períodos de facturación.",
     "account_upgrade_dialog_reservations_warning_other": "El nivel seleccionado permite menos tópicos reservados que su nivel actual. Antes de cambiar de nivel, <strong>por favor elimine al menos {{count}} reservaciones</strong>. Puede eliminar reservaciones en <Link>Configuración</Link>.",
     "account_upgrade_dialog_tier_features_messages": "{{messages}} mensajes diarios",
     "account_upgrade_dialog_tier_features_emails": "{{emails}} correos diarios",
@@ -340,5 +340,17 @@
     "prefs_reservations_dialog_topic_label": "Tópico",
     "reservation_delete_dialog_description": "Al eliminar una reserva se renuncia a la propiedad sobre el tópico y se permite que otros lo reserven. Puede conservar o eliminar los mensajes y archivos adjuntos existentes.",
     "reservation_delete_dialog_action_delete_title": "Eliminar mensajes y archivos adjuntos en caché",
-    "reservation_delete_dialog_submit_button": "Eliminar reserva"
+    "reservation_delete_dialog_submit_button": "Eliminar reserva",
+    "account_basics_tier_interval_monthly": "mensualmente",
+    "account_basics_tier_interval_yearly": "anualmente",
+    "account_upgrade_dialog_interval_monthly": "Mensualmente",
+    "account_upgrade_dialog_interval_yearly": "Anualmente",
+    "account_upgrade_dialog_interval_yearly_discount_save": "ahorrar {{discount}}%",
+    "account_upgrade_dialog_interval_yearly_discount_save_up_to": "ahorra hasta un {{discount}}%",
+    "account_upgrade_dialog_tier_features_no_reservations": "Ningún tema reservado",
+    "account_upgrade_dialog_tier_price_per_month": "mes",
+    "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} facturado anualmente. Guardar {{save}}.",
+    "account_upgrade_dialog_billing_contact_website": "Si tiene preguntas sobre facturación, consulte nuestra <Link>página web</Link>.",
+    "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} al año. Facturación mensual.",
+    "account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor <Link>contáctenos</Link> directamente."
 }
diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json
index c3b79b0c..6be8c9f8 100644
--- a/web/public/static/langs/id.json
+++ b/web/public/static/langs/id.json
@@ -256,7 +256,7 @@
     "account_usage_cannot_create_portal_session": "Tidak dapat membuka portal tagihan",
     "account_delete_dialog_billing_warning": "Menghapus akun Anda juga membatalkan tagihan langganan dengan segera. Anda tidak akan memiliki akses lagi ke dasbor tagihan.",
     "account_upgrade_dialog_title": "Ubah peringkat akun",
-    "account_upgrade_dialog_proration_info": "<strong>Prorasi</strong>: Ketika mengubah rencana berbayar, perubahan harga akan ditagih atau dikembalikan di faktur berikutnya. Anda tidak akan menerima faktur lain sampai akhir periode tagihan.",
+    "account_upgrade_dialog_proration_info": "<strong>Prorasi</strong>: Saat melakukan upgrade antar paket berbayar, selisih harga akan <strong>langsung dibebankan ke</strong>. Saat menurunkan ke tingkat yang lebih rendah, saldo akan digunakan untuk membayar periode penagihan di masa mendatang.",
     "account_upgrade_dialog_reservations_warning_other": "Peringkat yang dipilih memperbolehkan lebih sedikit reservasi topik daripada peringkat Anda saat ini. Sebelum mengubah peringkat Anda, <strong>silakan menghapus setidaknya {{count}} reservasi</strong>. Anda dapat menghapus reservasi di <Link>Pengaturan</Link>.",
     "account_upgrade_dialog_tier_features_reservations": "{{reservations}} topik yang telah direservasi",
     "account_upgrade_dialog_tier_features_messages": "{{messages}} pesan harian",
@@ -340,5 +340,17 @@
     "prefs_reservations_dialog_description": "Mereservasikan sebuah topik memberikan Anda kemilikan pada topik, dan memungkinkan Anda untuk mendefinisikan perizinan akses untuk pengguna lain melalui topik.",
     "prefs_reservations_dialog_topic_label": "Topik",
     "prefs_reservations_dialog_access_label": "Akses",
-    "reservation_delete_dialog_description": "Menghapus sebuah reservasi menghapus kemilikan pada topik, dan memperbolehkan orang-orang lain untuk mereservasinya."
+    "reservation_delete_dialog_description": "Menghapus sebuah reservasi menghapus kemilikan pada topik, dan memperbolehkan orang-orang lain untuk mereservasinya.",
+    "account_upgrade_dialog_interval_yearly": "Setiap tahun",
+    "account_upgrade_dialog_tier_price_billed_yearly": "Ditagih {{price}} setiap tahun. Hemat {{save}}.",
+    "account_upgrade_dialog_interval_yearly_discount_save": "hemat {{discount}}%",
+    "account_upgrade_dialog_interval_monthly": "Setiap bulan",
+    "account_basics_tier_interval_monthly": "setiap bulan",
+    "account_basics_tier_interval_yearly": "setiap tahun",
+    "account_upgrade_dialog_interval_yearly_discount_save_up_to": "hemat sampai {{discount}}%",
+    "account_upgrade_dialog_tier_features_no_reservations": "Tidak ada topik yang direservasi",
+    "account_upgrade_dialog_tier_price_per_month": "bulan",
+    "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per bulan. Ditagih setiap bulan.",
+    "account_upgrade_dialog_billing_contact_email": "Untuk pertanyaan penagihan, silakan <Link>hubungi kami</Link> secara langsung.",
+    "account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke <Link>situs web</Link> kami."
 }
diff --git a/web/public/static/langs/pl.json b/web/public/static/langs/pl.json
index 36ce8690..deccad95 100644
--- a/web/public/static/langs/pl.json
+++ b/web/public/static/langs/pl.json
@@ -235,5 +235,78 @@
     "account_usage_title": "Użycie",
     "account_usage_of_limit": "z {{limit}}",
     "account_usage_unlimited": "Bez limitu",
-    "account_usage_limits_reset_daily": "Limity są resetowane codziennie o północy (UTC)"
+    "account_usage_limits_reset_daily": "Limity są resetowane codziennie o północy (UTC)",
+    "account_delete_dialog_button_submit": "Nieodwracalnie usuń konto",
+    "account_upgrade_dialog_tier_features_no_reservations": "Brak rezerwacji tematów",
+    "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} na plik",
+    "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} pamięci łącznie",
+    "account_upgrade_dialog_tier_price_per_month": "miesiąc",
+    "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} na rok. Płatne miesięcznie.",
+    "account_upgrade_dialog_billing_contact_email": "W razie pytań dotyczących rozliczeń <Link>skontaktuj się z nami</Link> bezpośrednio.",
+    "account_upgrade_dialog_billing_contact_website": "W razie pytań dotyczących rozliczeń sprawdź naszą <Link>stronę</Link>.",
+    "account_upgrade_dialog_button_cancel_subscription": "Anuluj subskrypcję",
+    "account_upgrade_dialog_button_update_subscription": "Zmień subskrypcję",
+    "account_tokens_title": "Tokeny dostępowe",
+    "account_tokens_table_token_header": "Token",
+    "account_tokens_table_label_header": "Etykieta",
+    "account_tokens_table_last_access_header": "Ostatnie użycie",
+    "account_tokens_table_expires_header": "Termin ważności",
+    "account_tokens_table_never_expires": "Bezterminowy",
+    "account_tokens_table_current_session": "Aktualna sesja przeglądarki",
+    "account_tokens_table_copy_to_clipboard": "Kopiuj do schowka",
+    "account_tokens_table_copied_to_clipboard": "Token został skopiowany",
+    "account_tokens_table_cannot_delete_or_edit": "Nie można edytować ani usunąć tokenu aktualnej sesji",
+    "account_tokens_table_create_token_button": "Utwórz token dostępowy",
+    "account_tokens_dialog_label": "Etykieta, np. Powiadomienia Radarr",
+    "account_tokens_dialog_button_update": "Zmień token",
+    "account_basics_tier_interval_monthly": "miesięcznie",
+    "account_basics_tier_interval_yearly": "rocznie",
+    "account_upgrade_dialog_interval_monthly": "Miesięcznie",
+    "account_upgrade_dialog_title": "Zmień plan konta",
+    "account_delete_dialog_description": "Konto, wraz ze wszystkimi związanymi z nim danymi przechowywanymi na serwerze, będzie nieodwracalnie usunięte. Po usunięciu Twoja nazwa użytkownika będzie niedostępna jeszcze przez 7 dni. Jeśli chcesz kontynuować, potwierdź wpisując swoje hasło w polu poniżej.",
+    "account_delete_dialog_billing_warning": "Usunięcie konta powoduje natychmiastowe anulowanie subskrypcji. Nie będziesz już mieć dostępu do strony z rachunkami.",
+    "account_upgrade_dialog_interval_yearly": "Rocznie",
+    "account_upgrade_dialog_interval_yearly_discount_save": "taniej o {{discount}}%",
+    "account_upgrade_dialog_interval_yearly_discount_save_up_to": "nawet {{discount}}% taniej",
+    "account_upgrade_dialog_button_cancel": "Anuluj",
+    "account_tokens_description": "Używaj tokenów do publikowania wiadomości i subskrybowania tematów przez API ntfy, żeby uniknąć konieczności podawania danych do logowania. Szczegóły znajdziesz w <Link>dokumentacji</Link>.",
+    "account_tokens_dialog_title_create": "Utwórz token dostępowy",
+    "account_tokens_table_last_origin_tooltip": "Z adresu IP {{ip}}, kliknij żeby sprawdzić",
+    "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} płatne jednorazowo. Oszczędzasz {{save}}.",
+    "account_tokens_dialog_title_edit": "Edytuj token dostępowy",
+    "account_tokens_dialog_title_delete": "Usuń token dostępowy",
+    "account_tokens_dialog_button_create": "Utwórz token",
+    "nav_upgrade_banner_label": "Przejdź na ntfy Pro",
+    "nav_upgrade_banner_description": "Rezerwuj tematy, więcej powiadomień i maili oraz większe załączniki",
+    "alert_not_supported_context_description": "Powiadomienia działają tylko przez HTTPS. To jest ograniczenie <mdnLink>Notifications API</mdnLink>.",
+    "account_basics_tier_canceled_subscription": "Twoja subskrypcja została anulowana i konto zostanie ograniczone do wersji darmowej w dniu {{date}}.",
+    "account_basics_tier_manage_billing_button": "Zarządzaj rachunkami",
+    "account_usage_messages_title": "Wysłane wiadomości",
+    "account_usage_emails_title": "Wysłane maile",
+    "account_basics_tier_title": "Rodzaj konta",
+    "account_basics_tier_description": "Mocarność Twojego konta",
+    "account_basics_tier_admin": "Administrator",
+    "account_basics_tier_admin_suffix_with_tier": "(plan {{tier}})",
+    "account_basics_tier_admin_suffix_no_tier": "(brak planu)",
+    "account_basics_tier_basic": "Podstawowe",
+    "account_basics_tier_free": "Darmowe",
+    "account_basics_tier_upgrade_button": "Przejdź na Pro",
+    "account_basics_tier_change_button": "Zmień",
+    "account_basics_tier_paid_until": "Subskrypcja opłacona do {{date}} i będzie odnowiona automatycznie",
+    "account_basics_tier_payment_overdue": "Minął termin płatności. Zaktualizuj metodę płatności, w przeciwnym razie Twoje konto wkrótce zostanie ograniczone.",
+    "account_usage_reservations_title": "Zarezerwowane tematy",
+    "account_usage_reservations_none": "Brak zarezerwowanych tematów na tym koncie",
+    "account_usage_attachment_storage_title": "Miejsce na załączniki",
+    "account_usage_attachment_storage_description": "{{filesize}} na każdy plik, przechowywane przez {{expiry}}",
+    "account_usage_basis_ip_description": "Statystyki i limity dla tego konta bazują na Twoim adresie IP, więc mogą być współdzielone z innymi użytkownikami. Limity pokazane powyżej to wartości przybliżone bazujące na rzeczywistych limitach.",
+    "account_usage_cannot_create_portal_session": "Nie można otworzyć portalu z rachunkami",
+    "account_delete_title": "Usuń konto",
+    "account_delete_description": "Usuń swoje konto nieodwracalnie",
+    "account_delete_dialog_label": "Hasło",
+    "account_delete_dialog_button_cancel": "Anuluj",
+    "account_upgrade_dialog_button_redirect_signup": "Załóż konto",
+    "account_upgrade_dialog_button_pay_now": "Zapłać i aktywuj subskrypcję",
+    "account_tokens_dialog_button_cancel": "Anuluj",
+    "account_tokens_dialog_expires_label": "Token dostępowy wygasa po",
+    "account_tokens_dialog_expires_unchanged": "Pozostaw termin ważności bez zmian"
 }
diff --git a/web/public/static/langs/tr.json b/web/public/static/langs/tr.json
index 4a74866f..66136cf7 100644
--- a/web/public/static/langs/tr.json
+++ b/web/public/static/langs/tr.json
@@ -251,7 +251,7 @@
     "account_delete_dialog_button_submit": "Hesabı kalıcı olarak sil",
     "account_delete_dialog_billing_warning": "Hesabınızı silmek, faturalandırma aboneliğinizi de anında iptal eder. Artık faturalandırma sayfasına erişiminiz olmayacak.",
     "account_upgrade_dialog_title": "Hesap seviyesini değiştir",
-    "account_upgrade_dialog_proration_info": "<strong>Ödeme oranı</strong>: Ücretli planlar arasında geçiş yaparken, fiyat farkı bir sonraki faturada tahsil edilecek veya iade edilecektir. Bir sonraki fatura döneminin sonuna kadar başka bir fatura almayacaksınız.",
+    "account_upgrade_dialog_proration_info": "<strong>Fiyatlandırma</strong>: Ücretli planlar arasında yükseltme yaparken, fiyat farkı <strong>hemen tahsil edilecektir</strong>. Daha düşük bir seviyeye inildiğinde, bakiye gelecek faturalandırma dönemleri için ödeme yapmak üzere kullanılacaktır.",
     "account_upgrade_dialog_reservations_warning_other": "Seçilen seviye, geçerli seviyenizden daha az konu ayırtmaya izin veriyor. Seviyenizi değiştirmeden önce <strong>lütfen en az {{count}} ayırtmayı silin</strong>. Ayırtmaları <Link>Ayarlar</Link> sayfasından kaldırabilirsiniz.",
     "account_upgrade_dialog_tier_features_reservations": "{{reservations}} konu ayırtıldı",
     "account_upgrade_dialog_tier_features_messages": "{{messages}} günlük mesaj",
@@ -340,5 +340,17 @@
     "prefs_reservations_table_everyone_read_only": "Ben yayınlayabilir ve abone olabilirim, herkes abone olabilir",
     "prefs_reservations_table_not_subscribed": "Abone olunmadı",
     "prefs_reservations_table_everyone_read_write": "Herkes yayınlayabilir ve abone olabilir",
-    "reservation_delete_dialog_description": "Ayırtmanın kaldırılması, konu üzerindeki sahiplikten vazgeçer ve başkalarının onu ayırtmasına izin verir. Mevcut mesajları ve ekleri saklayabilir veya silebilirsiniz."
+    "reservation_delete_dialog_description": "Ayırtmanın kaldırılması, konu üzerindeki sahiplikten vazgeçer ve başkalarının onu ayırtmasına izin verir. Mevcut mesajları ve ekleri saklayabilir veya silebilirsiniz.",
+    "account_basics_tier_interval_yearly": "yıllık",
+    "account_upgrade_dialog_tier_features_no_reservations": "Ayırtılan konu yok",
+    "account_upgrade_dialog_tier_price_billed_monthly": "Yıllık {{price}}. Aylık faturalandırılır.",
+    "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} yıllık olarak faturalandırılır. {{save}} tasarruf edin.",
+    "account_upgrade_dialog_interval_yearly": "Yıllık",
+    "account_upgrade_dialog_interval_yearly_discount_save": "%{{discount}} tasarruf edin",
+    "account_upgrade_dialog_tier_price_per_month": "ay",
+    "account_upgrade_dialog_billing_contact_email": "Faturalama ile ilgili sorularınız için lütfen doğrudan <Link>bizimle iletişime geçin</Link>.",
+    "account_upgrade_dialog_interval_yearly_discount_save_up_to": "%{{discount}} kadar tasarruf edin",
+    "account_upgrade_dialog_interval_monthly": "Aylık",
+    "account_basics_tier_interval_monthly": "aylık",
+    "account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen <Link>web sitemizi ziyaret edin</Link>."
 }
diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js
index 8b795377..e86af78a 100644
--- a/web/src/app/Connection.js
+++ b/web/src/app/Connection.js
@@ -1,6 +1,6 @@
 import {basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils";
 
-const retryBackoffSeconds = [5, 10, 15, 20, 30];
+const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
 
 /**
  * A connection contains a single WebSocket connection for one topic. It handles its connection