diff --git a/server/file_cache.go b/server/file_cache.go
index 88de935d..9eae7ea6 100644
--- a/server/file_cache.go
+++ b/server/file_cache.go
@@ -26,7 +26,7 @@ type fileCache struct {
 	mu               sync.Mutex
 }
 
-func newFileCache(dir string, totalSizeLimit int64, fileSizeLimit int64) (*fileCache, error) {
+func newFileCache(dir string, totalSizeLimit int64) (*fileCache, error) {
 	if err := os.MkdirAll(dir, 0700); err != nil {
 		return nil, err
 	}
@@ -38,7 +38,6 @@ func newFileCache(dir string, totalSizeLimit int64, fileSizeLimit int64) (*fileC
 		dir:              dir,
 		totalSizeCurrent: size,
 		totalSizeLimit:   totalSizeLimit,
-		fileSizeLimit:    fileSizeLimit,
 	}, nil
 }
 
@@ -55,7 +54,7 @@ func (c *fileCache) Write(id string, in io.Reader, limiters ...util.Limiter) (in
 		return 0, err
 	}
 	defer f.Close()
-	limiters = append(limiters, util.NewFixedLimiter(c.Remaining()), util.NewFixedLimiter(c.fileSizeLimit))
+	limiters = append(limiters, util.NewFixedLimiter(c.Remaining()))
 	limitWriter := util.NewLimitWriter(f, limiters...)
 	size, err := io.Copy(limitWriter, in)
 	if err != nil {
diff --git a/server/server.go b/server/server.go
index 835d6948..cbe1ca62 100644
--- a/server/server.go
+++ b/server/server.go
@@ -36,15 +36,17 @@ import (
 
 /*
 	TODO
+		use token auth in "SubscribeDialog"
+		upload files based on user limit
 		publishXHR + poll should pick current user, not from userManager
 		expire tokens
 		auto-refresh tokens from UI
 		reserve topics
 		rate limit for signup (2 per 24h)
 		handle invalid session token
-		update disallowed topics
 		purge accounts that were not logged into in X
 		sync subscription display name
+		store users
 		Pages:
 		- Home
 		- Password reset
@@ -103,7 +105,7 @@ 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", "settings"} // If updated, also update in Android app
+	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
@@ -152,7 +154,7 @@ func New(conf *Config) (*Server, error) {
 	}
 	var fileCache *fileCache
 	if conf.AttachmentCacheDir != "" {
-		fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit, conf.AttachmentFileSizeLimit)
+		fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit)
 		if err != nil {
 			return nil, err
 		}
@@ -423,9 +425,13 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
 	w.Header().Set("Content-Type", "text/javascript")
 	_, err := io.WriteString(w, fmt.Sprintf(`// Generated server configuration
 var config = {
+  baseUrl: window.location.origin,
   appRoot: "%s",
-  disallowedTopics: [%s]
-};`, appRoot, disallowedTopicsStr))
+  enableLogin: %t,
+  enableSignup: %t,
+  enableResetPassword: %t,
+  disallowedTopics: [%s], 
+};`, appRoot, s.config.EnableLogin, s.config.EnableSignup, s.config.EnableResetPassword, disallowedTopicsStr))
 	return err
 }
 
@@ -799,7 +805,12 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
 	if m.Message == "" {
 		m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)
 	}
-	m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(stats.AttachmentTotalSizeRemaining))
+	limiters := []util.Limiter{
+		v.BandwidthLimiter(),
+		util.NewFixedLimiter(stats.AttachmentFileSizeLimit),
+		util.NewFixedLimiter(stats.AttachmentTotalSizeRemaining),
+	}
+	m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...)
 	if err == util.ErrLimitReached {
 		return errHTTPEntityTooLargeAttachmentTooLarge
 	} else if err != nil {
diff --git a/web/public/config.js b/web/public/config.js
index 76c02041..c25cddc2 100644
--- a/web/public/config.js
+++ b/web/public/config.js
@@ -1,9 +1,15 @@
-// Configuration injected by the ntfy server.
+// THIS FILE IS JUST AN EXAMPLE
 //
-// This file is just an example. It is removed during the build process.
-// The actual config is dynamically generated server-side.
+// It is removed during the build process. The actual config is dynamically
+// generated server-side and served by the ntfy server.
+//
+// During web development, you may change values here for rapid testing.
 
 var config = {
+    baseUrl: "http://localhost:2586", // window.location.origin FIXME update before merging
     appRoot: "/app",
-    disallowedTopics: ["docs", "static", "file", "app", "settings"]
+    enableLogin: true,
+    enableSignup: true,
+    enableResetPassword: false,
+    disallowedTopics: ["docs", "static", "file", "app", "account", "settings", "pricing", "signup", "login", "reset-password"]
 };
diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json
index 691ad52a..7ca3082a 100644
--- a/web/public/static/langs/en.json
+++ b/web/public/static/langs/en.json
@@ -144,7 +144,9 @@
   "account_type_default": "Default",
   "account_type_unlimited": "Unlimited",
   "account_type_none": "None",
-  "account_type_hobbyist": "Hobbyist",
+  "account_type_pro": "Pro",
+  "account_type_business": "Business",
+  "account_type_business_plus": "Business Plus",
   "prefs_notifications_title": "Notifications",
   "prefs_notifications_sound_title": "Notification sound",
   "prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
diff --git a/web/src/app/Api.js b/web/src/app/Api.js
index 3d753b8f..1f93dc15 100644
--- a/web/src/app/Api.js
+++ b/web/src/app/Api.js
@@ -125,7 +125,9 @@ class Api {
         const response = await fetch(url, {
             headers: maybeWithBasicAuth({}, user)
         });
-        if (response.status !== 200) {
+        if (response.status === 401 || response.status === 403) {
+            return false;
+        } else if (response.status !== 200) {
             throw new Error(`Unexpected server response ${response.status}`);
         }
         const json = await response.json();
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index 66c2b48d..fc2ad85f 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -47,7 +47,7 @@ export const disallowedTopic = (topic) => {
 export const topicDisplayName = (subscription) => {
     if (subscription.displayName) {
         return subscription.displayName;
-    } else if (subscription.baseUrl === window.location.origin) {
+    } else if (subscription.baseUrl === config.baseUrl) {
         return subscription.topic;
     }
     return topicShortUrl(subscription.baseUrl, subscription.topic);
diff --git a/web/src/components/Account.js b/web/src/components/Account.js
index d694a0ec..1294e212 100644
--- a/web/src/components/Account.js
+++ b/web/src/components/Account.js
@@ -147,7 +147,7 @@ const ChangePassword = () => {
     };
     const handleDialogSubmit = async (newPassword) => {
         try {
-            await api.changePassword("http://localhost:2586", session.token(), newPassword);
+            await api.changePassword(config.baseUrl, session.token(), newPassword);
             setDialogOpen(false);
             console.debug(`[Account] Password changed`);
         } catch (e) {
@@ -230,7 +230,7 @@ const DeleteAccount = () => {
     };
     const handleDialogSubmit = async (newPassword) => {
         try {
-            await api.deleteAccount("http://localhost:2586", session.token());
+            await api.deleteAccount(config.baseUrl, session.token());
             setDialogOpen(false);
             console.debug(`[Account] Account deleted`);
             // TODO delete local storage
diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js
index 97025002..c5af9d92 100644
--- a/web/src/components/ActionBar.js
+++ b/web/src/components/ActionBar.js
@@ -118,7 +118,7 @@ const SettingsIcons = (props) => {
         handleClose(event);
         await subscriptionManager.remove(props.subscription.id);
         if (session.exists() && props.subscription.remoteId) {
-            await api.deleteAccountSubscription("http://localhost:2586", session.token(), props.subscription.remoteId);
+            await api.deleteAccountSubscription(config.baseUrl, session.token(), props.subscription.remoteId);
         }
         const newSelected = await subscriptionManager.first(); // May be undefined
         if (newSelected) {
@@ -259,9 +259,8 @@ const ProfileIcon = (props) => {
     const handleClose = () => {
         setAnchorEl(null);
     };
-
     const handleLogout = async () => {
-        await api.logout("http://localhost:2586"/*window.location.origin*/, session.token());
+        await api.logout(config.baseUrl, session.token());
         session.reset();
         window.location.href = routes.app;
     };
@@ -273,11 +272,11 @@ const ProfileIcon = (props) => {
                     <AccountCircleIcon/>
                 </IconButton>
             }
-            {!session.exists() &&
-                <>
-                    <Button color="inherit" variant="outlined" onClick={() => navigate(routes.login)}>Sign in</Button>
-                    <Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)}>Sign up</Button>
-                </>
+            {!session.exists() && config.enableLogin &&
+                <Button color="inherit" variant="text" onClick={() => navigate(routes.login)} sx={{m: 1}}>Sign in</Button>
+            }
+            {!session.exists() && config.enableSignup &&
+                <Button color="inherit" variant="outlined" onClick={() => navigate(routes.signup)}>Sign up</Button>
             }
             <Menu
                 anchorEl={anchorEl}
diff --git a/web/src/components/App.js b/web/src/components/App.js
index eb2fba2f..360f8215 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -87,7 +87,7 @@ const Layout = () => {
     const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0;
     const [selected] = (subscriptions || []).filter(s => {
         return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic)
-            || (window.location.origin === s.baseUrl && params.topic === s.topic)
+            || (config.baseUrl === s.baseUrl && params.topic === s.topic)
     });
 
     useConnectionListeners(subscriptions, users);
@@ -96,7 +96,7 @@ const Layout = () => {
 
     useEffect(() => {
         (async () => {
-            const acc = await api.getAccount("http://localhost:2586", session.token());
+            const acc = await api.getAccount(config.baseUrl, session.token());
             if (acc) {
                 setAccount(acc);
                 if (acc.language) {
diff --git a/web/src/components/AvatarBox.js b/web/src/components/AvatarBox.js
new file mode 100644
index 00000000..3d32997e
--- /dev/null
+++ b/web/src/components/AvatarBox.js
@@ -0,0 +1,29 @@
+import * as React from 'react';
+import {Avatar} from "@mui/material";
+import Box from "@mui/material/Box";
+import logo from "../img/ntfy2.svg";
+
+const AvatarBox = (props) => {
+    return (
+        <Box
+            sx={{
+                display: 'flex',
+                flexGrow: 1,
+                justifyContent: 'center',
+                flexDirection: 'column',
+                alignContent: 'center',
+                alignItems: 'center',
+                height: '100vh'
+            }}
+        >
+            <Avatar
+                sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
+                src={logo}
+                variant="rounded"
+            />
+            {props.children}
+        </Box>
+    );
+}
+
+export default AvatarBox;
diff --git a/web/src/components/Login.js b/web/src/components/Login.js
index e2ab80b5..0a6b6f6a 100644
--- a/web/src/components/Login.js
+++ b/web/src/components/Login.js
@@ -1,17 +1,20 @@
 import * as React from 'react';
-import {Avatar, Checkbox, FormControlLabel, Grid, Link} from "@mui/material";
 import Typography from "@mui/material/Typography";
-import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
+import WarningAmberIcon from '@mui/icons-material/WarningAmber';
 import TextField from "@mui/material/TextField";
 import Button from "@mui/material/Button";
 import Box from "@mui/material/Box";
 import api from "../app/Api";
 import routes from "./routes";
 import session from "../app/Session";
-import logo from "../img/ntfy2.svg";
 import {NavLink} from "react-router-dom";
+import AvatarBox from "./AvatarBox";
+import {useTranslation} from "react-i18next";
+import {useState} from "react";
 
 const Login = () => {
+    const { t } = useTranslation();
+    const [error, setError] = useState("");
     const handleSubmit = async (event) => {
         event.preventDefault();
         const data = new FormData(event.currentTarget);
@@ -19,31 +22,36 @@ const Login = () => {
             username: data.get('username'),
             password: data.get('password'),
         }
-        const token = await api.login("http://localhost:2586"/*window.location.origin*/, user);
-        console.log(`[Api] User auth for user ${user.username} successful, token is ${token}`);
-        session.store(user.username, token);
-        window.location.href = routes.app;
+        try {
+            const token = await api.login(config.baseUrl, user);
+            if (token) {
+                console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`);
+                session.store(user.username, token);
+                window.location.href = routes.app;
+            } else {
+                console.log(`[Login] User auth for user ${user.username} failed, access denied`);
+                setError(t("Login failed: Invalid username or password"));
+            }
+        } catch (e) {
+            console.log(`[Login] User auth for user ${user.username} failed`, e);
+            if (e && e.message) {
+                setError(e.message);
+            } else {
+                setError(t("Unknown error. Check logs for details."))
+            }
+        }
     };
-
+    if (!config.enableLogin) {
+        return (
+            <AvatarBox>
+                <Typography sx={{ typography: 'h6' }}>{t("Login is disabled")}</Typography>
+            </AvatarBox>
+        );
+    }
     return (
-        <Box
-            sx={{
-                display: 'flex',
-                flexGrow: 1,
-                justifyContent: 'center',
-                flexDirection: 'column',
-                alignContent: 'center',
-                alignItems: 'center',
-                height: '100vh'
-            }}
-        >
-            <Avatar
-                sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
-                src={logo}
-                variant="rounded"
-            />
+        <AvatarBox>
             <Typography sx={{ typography: 'h6' }}>
-                Sign in to your ntfy account
+                {t("Sign in to your ntfy account")}
             </Typography>
             <Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}>
                 <TextField
@@ -51,7 +59,7 @@ const Login = () => {
                     required
                     fullWidth
                     id="username"
-                    label="Username"
+                    label={t("Username")}
                     name="username"
                     autoFocus
                 />
@@ -60,7 +68,7 @@ const Login = () => {
                     required
                     fullWidth
                     name="password"
-                    label="Password"
+                    label={t("Password")}
                     type="password"
                     id="password"
                     autoComplete="current-password"
@@ -71,14 +79,25 @@ const Login = () => {
                     variant="contained"
                     sx={{mt: 2, mb: 2}}
                 >
-                    Sign in
+                    {t("Sign in")}
                 </Button>
+                {error &&
+                    <Box sx={{
+                        mb: 1,
+                        display: 'flex',
+                        flexGrow: 1,
+                        justifyContent: 'center',
+                    }}>
+                        <WarningAmberIcon color="error" sx={{mr: 1}}/>
+                        <Typography sx={{color: 'error.main'}}>{error}</Typography>
+                    </Box>
+                }
                 <Box sx={{width: "100%"}}>
-                    <div style={{float: "left"}}><NavLink to={routes.resetPassword} variant="body1">Reset password</NavLink></div>
-                    <div style={{float: "right"}}><NavLink to={routes.signup} variant="body1">Sign up</NavLink></div>
+                    {config.enableResetPassword && <div style={{float: "left"}}><NavLink to={routes.resetPassword} variant="body1">{t("Reset password")}</NavLink></div>}
+                    {config.enableSignup && <div style={{float: "right"}}><NavLink to={routes.signup} variant="body1">{t("Sign up")}</NavLink></div>}
                 </Box>
             </Box>
-        </Box>
+        </AvatarBox>
     );
 }
 
diff --git a/web/src/components/Messaging.js b/web/src/components/Messaging.js
index 4ba1203f..d4d89aa5 100644
--- a/web/src/components/Messaging.js
+++ b/web/src/components/Messaging.js
@@ -38,7 +38,7 @@ const Messaging = (props) => {
             <PublishDialog
                 key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
                 openMode={dialogOpenMode}
-                baseUrl={subscription?.baseUrl ?? window.location.origin}
+                baseUrl={subscription?.baseUrl ?? config.baseUrl}
                 topic={subscription?.topic ?? ""}
                 message={message}
                 onClose={handleDialogClose}
@@ -83,7 +83,7 @@ const MessageBar = (props) => {
                 margin="dense"
                 placeholder={t("message_bar_type_message")}
                 aria-label={t("message_bar_type_message")}
-                role="textbox" 
+                role="textbox"
                 type="text"
                 fullWidth
                 variant="standard"
diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js
index fe09e05f..476063ce 100644
--- a/web/src/components/Preferences.js
+++ b/web/src/components/Preferences.js
@@ -73,7 +73,7 @@ const Sound = () => {
     const handleChange = async (ev) => {
         await prefs.setSound(ev.target.value);
         if (session.exists()) {
-            await api.updateAccountSettings("http://localhost:2586", session.token(), {
+            await api.updateAccountSettings(config.baseUrl, session.token(), {
                 notification: {
                     sound: ev.target.value
                 }
@@ -113,7 +113,7 @@ const MinPriority = () => {
     const handleChange = async (ev) => {
         await prefs.setMinPriority(ev.target.value);
         if (session.exists()) {
-            await api.updateAccountSettings("http://localhost:2586", session.token(), {
+            await api.updateAccountSettings(config.baseUrl, session.token(), {
                 notification: {
                     min_priority: ev.target.value
                 }
@@ -163,7 +163,7 @@ const DeleteAfter = () => {
     const handleChange = async (ev) => {
         await prefs.setDeleteAfter(ev.target.value);
         if (session.exists()) {
-            await api.updateAccountSettings("http://localhost:2586", session.token(), {
+            await api.updateAccountSettings(config.baseUrl, session.token(), {
                 notification: {
                     delete_after: ev.target.value
                 }
@@ -467,7 +467,7 @@ const Language = () => {
     const handleChange = async (ev) => {
         await i18n.changeLanguage(ev.target.value);
         if (session.exists()) {
-            await api.updateAccountSettings("http://localhost:2586", session.token(), {
+            await api.updateAccountSettings(config.baseUrl, session.token(), {
                 language: ev.target.value
             });
         }
diff --git a/web/src/components/ResetPassword.js b/web/src/components/ResetPassword.js
index 9d25e624..bcf635eb 100644
--- a/web/src/components/ResetPassword.js
+++ b/web/src/components/ResetPassword.js
@@ -1,14 +1,11 @@
 import * as React from 'react';
-import {Avatar, Link} from "@mui/material";
 import TextField from "@mui/material/TextField";
 import Button from "@mui/material/Button";
 import Box from "@mui/material/Box";
-import api from "../app/Api";
 import routes from "./routes";
-import session from "../app/Session";
-import logo from "../img/ntfy2.svg";
 import Typography from "@mui/material/Typography";
 import {NavLink} from "react-router-dom";
+import AvatarBox from "./AvatarBox";
 
 const ResetPassword = () => {
     const handleSubmit = async (event) => {
@@ -16,22 +13,7 @@ const ResetPassword = () => {
     };
 
     return (
-        <Box
-            sx={{
-                display: 'flex',
-                flexGrow: 1,
-                justifyContent: 'center',
-                flexDirection: 'column',
-                alignContent: 'center',
-                alignItems: 'center',
-                height: '100vh'
-            }}
-        >
-            <Avatar
-                sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
-                src={logo}
-                variant="rounded"
-            />
+        <AvatarBox>
             <Typography sx={{ typography: 'h6' }}>
                 Reset password
             </Typography>
@@ -59,7 +41,7 @@ const ResetPassword = () => {
                     &lt; Return to sign in
                 </NavLink>
             </Typography>
-        </Box>
+        </AvatarBox>
     );
 }
 
diff --git a/web/src/components/Signup.js b/web/src/components/Signup.js
index 624a35c0..3b10050e 100644
--- a/web/src/components/Signup.js
+++ b/web/src/components/Signup.js
@@ -1,52 +1,41 @@
 import * as React from 'react';
-import {Avatar, Link} from "@mui/material";
 import TextField from "@mui/material/TextField";
 import Button from "@mui/material/Button";
 import Box from "@mui/material/Box";
 import api from "../app/Api";
 import routes from "./routes";
 import session from "../app/Session";
-import logo from "../img/ntfy2.svg";
 import Typography from "@mui/material/Typography";
 import {NavLink} from "react-router-dom";
+import AvatarBox from "./AvatarBox";
+import {useTranslation} from "react-i18next";
 
 const Signup = () => {
+    const { t } = useTranslation();
     const handleSubmit = async (event) => {
         event.preventDefault();
         const data = new FormData(event.currentTarget);
-        const username = data.get('username');
-        const password = data.get('password');
         const user = {
-            username: username,
-            password: password
-        }; // FIXME omg so awful
-
-        await api.createAccount("http://localhost:2586"/*window.location.origin*/, username, password);
-        const token = await api.login("http://localhost:2586"/*window.location.origin*/, user);
+            username: data.get('username'),
+            password: data.get('password')
+        };
+        await api.createAccount(config.baseUrl, user.username, user.password);
+        const token = await api.login(config.baseUrl, user);
         console.log(`[Api] User auth for user ${user.username} successful, token is ${token}`);
         session.store(user.username, token);
         window.location.href = routes.app;
     };
-
+    if (!config.enableSignup) {
+        return (
+            <AvatarBox>
+                <Typography sx={{ typography: 'h6' }}>{t("Signup is disabled")}</Typography>
+            </AvatarBox>
+        );
+    }
     return (
-        <Box
-            sx={{
-                display: 'flex',
-                flexGrow: 1,
-                justifyContent: 'center',
-                flexDirection: 'column',
-                alignContent: 'center',
-                alignItems: 'center',
-                height: '100vh'
-            }}
-        >
-            <Avatar
-                sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }}
-                src={logo}
-                variant="rounded"
-            />
+        <AvatarBox>
             <Typography sx={{ typography: 'h6' }}>
-                Create a ntfy account
+                {t("Create a ntfy account")}
             </Typography>
             <Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}>
                 <TextField
@@ -83,15 +72,17 @@ const Signup = () => {
                     variant="contained"
                     sx={{mt: 2, mb: 2}}
                 >
-                    Sign up
+                    {t("Sign up")}
                 </Button>
             </Box>
-            <Typography sx={{mb: 4}}>
-                <NavLink to={routes.login} variant="body1">
-                    Already have an account? Sign in!
-                </NavLink>
-            </Typography>
-        </Box>
+            {config.enableLogin &&
+                <Typography sx={{mb: 4}}>
+                    <NavLink to={routes.login} variant="body1">
+                        {t("Already have an account? Sign in!")}
+                    </NavLink>
+                </Typography>
+            }
+        </AvatarBox>
     );
 }
 
diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js
index a5e75a4a..948717f5 100644
--- a/web/src/components/SubscribeDialog.js
+++ b/web/src/components/SubscribeDialog.js
@@ -25,10 +25,10 @@ const SubscribeDialog = (props) => {
     const [showLoginPage, setShowLoginPage] = useState(false);
     const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
     const handleSuccess = async () => {
-        const actualBaseUrl = (baseUrl) ? baseUrl : window.location.origin;
+        const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
         const subscription = await subscriptionManager.add(actualBaseUrl, topic);
         if (session.exists()) {
-            const remoteSubscription = await api.addAccountSubscription("http://localhost:2586", session.token(), {
+            const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
                 base_url: actualBaseUrl,
                 topic: topic
             });
@@ -63,11 +63,11 @@ const SubscribePage = (props) => {
     const { t } = useTranslation();
     const [anotherServerVisible, setAnotherServerVisible] = useState(false);
     const [errorText, setErrorText] = useState("");
-    const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin;
+    const baseUrl = (anotherServerVisible) ? props.baseUrl : config.baseUrl;
     const topic = props.topic;
     const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
     const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
-        .filter(s => s !== window.location.origin);
+        .filter(s => s !== config.baseUrl);
     const handleSubscribe = async () => {
         const user = await userManager.get(baseUrl); // May be undefined
         const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
@@ -94,7 +94,7 @@ const SubscribePage = (props) => {
             const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
             return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
         } else {
-            const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(window.location.origin, topic));
+            const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.baseUrl, topic));
             return validTopic(topic) && !isExistingTopicUrl;
         }
     })();
@@ -152,7 +152,7 @@ const SubscribePage = (props) => {
                     renderInput={ (params) =>
                         <TextField
                             {...params}
-                            placeholder={window.location.origin}
+                            placeholder={config.baseUrl}
                             variant="standard"
                             aria-label={t("subscribe_dialog_subscribe_base_url_label")}
                         />
@@ -172,7 +172,7 @@ const LoginPage = (props) => {
     const [username, setUsername] = useState("");
     const [password, setPassword] = useState("");
     const [errorText, setErrorText] = useState("");
-    const baseUrl = (props.baseUrl) ? props.baseUrl : window.location.origin;
+    const baseUrl = (props.baseUrl) ? props.baseUrl : config.baseUrl;
     const topic = props.topic;
     const handleLogin = async () => {
         const user = {baseUrl, username, password};
diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js
index eb526931..f5c7d332 100644
--- a/web/src/components/hooks.js
+++ b/web/src/components/hooks.js
@@ -59,12 +59,12 @@ export const useAutoSubscribe = (subscriptions, selected) => {
         setHasRun(true);
         const eligible = params.topic && !selected && !disallowedTopic(params.topic);
         if (eligible) {
-            const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : window.location.origin;
+            const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : config.baseUrl;
             console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
             (async () => {
                 const subscription = await subscriptionManager.add(baseUrl, params.topic);
                 if (session.exists()) {
-                    const remoteSubscription = await api.addAccountSubscription("http://localhost:2586", session.token(), {
+                    const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
                         base_url: baseUrl,
                         topic: params.topic
                     });
diff --git a/web/src/components/routes.js b/web/src/components/routes.js
index 4802a602..3e07f0fb 100644
--- a/web/src/components/routes.js
+++ b/web/src/components/routes.js
@@ -1,6 +1,8 @@
 import config from "../app/config";
 import {shortUrl} from "../app/utils";
 
+// Remember to also update the "disallowedTopics" list!
+
 const routes = {
     home: "/",
     pricing: "/pricing",
@@ -13,7 +15,7 @@ const routes = {
     subscription: "/:topic",
     subscriptionExternal: "/:baseUrl/:topic",
     forSubscription: (subscription) => {
-        if (subscription.baseUrl !== window.location.origin) {
+        if (subscription.baseUrl !== config.baseUrl) {
             return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`;
         }
         return `/${subscription.topic}`;