diff --git a/server/server.go b/server/server.go
index 64891313..c8d94400 100644
--- a/server/server.go
+++ b/server/server.go
@@ -44,6 +44,8 @@ import (
 		- delete subscription when account deleted
 		- remove tier.paid
 		- add tier.visible
+		- fix tier selection boxes
+		- account sync after switching tiers
 
 		Limits & rate limiting:
 			users without tier: should the stats be persisted? are they meaningful?
diff --git a/server/server_account.go b/server/server_account.go
index fe7d4c11..66250db1 100644
--- a/server/server_account.go
+++ b/server/server_account.go
@@ -97,6 +97,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
 				Subscription: v.user.Billing.StripeSubscriptionID != "",
 				Status:       string(v.user.Billing.StripeSubscriptionStatus),
 				PaidUntil:    v.user.Billing.StripeSubscriptionPaidUntil.Unix(),
+				CancelAt:     v.user.Billing.StripeSubscriptionCancelAt.Unix(),
 			}
 		}
 		reservations, err := s.userManager.Reservations(v.user.Name)
diff --git a/server/server_payments.go b/server/server_payments.go
index 81e52217..b78a94a7 100644
--- a/server/server_payments.go
+++ b/server/server_payments.go
@@ -62,6 +62,7 @@ func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r
 	v.user.Billing.StripeSubscriptionID = ""
 	v.user.Billing.StripeSubscriptionStatus = ""
 	v.user.Billing.StripeSubscriptionPaidUntil = time.Unix(0, 0)
+	v.user.Billing.StripeSubscriptionCancelAt = time.Unix(0, 0)
 	if err := s.userManager.ChangeBilling(v.user); err != nil {
 		return err
 	}
@@ -170,6 +171,7 @@ func (s *Server) handleAccountCheckoutSessionSuccessGet(w http.ResponseWriter, r
 	u.Billing.StripeSubscriptionID = sub.ID
 	u.Billing.StripeSubscriptionStatus = sub.Status
 	u.Billing.StripeSubscriptionPaidUntil = time.Unix(sub.CurrentPeriodEnd, 0)
+	u.Billing.StripeSubscriptionCancelAt = time.Unix(sub.CancelAt, 0)
 	if err := s.userManager.ChangeBilling(u); err != nil {
 		return err
 	}
@@ -240,8 +242,9 @@ func (s *Server) handleAccountBillingWebhook(w http.ResponseWriter, r *http.Requ
 func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(stripeCustomerID string, event json.RawMessage) error {
 	status := gjson.GetBytes(event, "status")
 	currentPeriodEnd := gjson.GetBytes(event, "current_period_end")
+	cancelAt := gjson.GetBytes(event, "cancel_at")
 	priceID := gjson.GetBytes(event, "items.data.0.price.id")
-	if !status.Exists() || !currentPeriodEnd.Exists() || !priceID.Exists() {
+	if !status.Exists() || !currentPeriodEnd.Exists() || !cancelAt.Exists() || !priceID.Exists() {
 		return errHTTPBadRequestInvalidStripeRequest
 	}
 	log.Info("Stripe: customer %s: subscription updated to %s, with price %s", stripeCustomerID, status, priceID)
@@ -258,6 +261,7 @@ func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(stripeCustomerID
 	}
 	u.Billing.StripeSubscriptionStatus = stripe.SubscriptionStatus(status.String())
 	u.Billing.StripeSubscriptionPaidUntil = time.Unix(currentPeriodEnd.Int(), 0)
+	u.Billing.StripeSubscriptionCancelAt = time.Unix(cancelAt.Int(), 0)
 	if err := s.userManager.ChangeBilling(u); err != nil {
 		return err
 	}
@@ -280,6 +284,7 @@ func (s *Server) handleAccountBillingWebhookSubscriptionDeleted(stripeCustomerID
 	u.Billing.StripeSubscriptionID = ""
 	u.Billing.StripeSubscriptionStatus = ""
 	u.Billing.StripeSubscriptionPaidUntil = time.Unix(0, 0)
+	u.Billing.StripeSubscriptionCancelAt = time.Unix(0, 0)
 	if err := s.userManager.ChangeBilling(u); err != nil {
 		return err
 	}
diff --git a/server/types.go b/server/types.go
index cee114dc..fc81a2a6 100644
--- a/server/types.go
+++ b/server/types.go
@@ -273,6 +273,7 @@ type apiAccountBilling struct {
 	Subscription bool   `json:"subscription"`
 	Status       string `json:"status,omitempty"`
 	PaidUntil    int64  `json:"paid_until,omitempty"`
+	CancelAt     int64  `json:"cancel_at,omitempty"`
 }
 
 type apiAccountResponse struct {
diff --git a/user/manager.go b/user/manager.go
index 7e50b4a1..7b37b8f8 100644
--- a/user/manager.go
+++ b/user/manager.go
@@ -63,7 +63,8 @@ const (
 			stripe_customer_id TEXT,
 			stripe_subscription_id TEXT,
 			stripe_subscription_status TEXT,
-			stripe_subscription_paid_until INT,			
+			stripe_subscription_paid_until INT,
+			stripe_subscription_cancel_at INT,
 			created_by TEXT NOT NULL,
 			created_at INT NOT NULL,
 			last_seen INT NOT NULL,
@@ -103,20 +104,20 @@ const (
 	`
 
 	selectUserByNameQuery = `
-		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id
+		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id
 		FROM user u
 		LEFT JOIN tier p on p.id = u.tier_id
 		WHERE user = ?		
 	`
 	selectUserByTokenQuery = `
-		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id
+		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at , p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id
 		FROM user u
 		JOIN user_token t on u.id = t.user_id
 		LEFT JOIN tier p on p.id = u.tier_id
 		WHERE t.token = ? AND t.expires >= ?
 	`
 	selectUserByStripeCustomerIDQuery = `
-		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id
+		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at , p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id
 		FROM user u
 		LEFT JOIN tier p on p.id = u.tier_id
 		WHERE u.stripe_customer_id = ?
@@ -236,7 +237,7 @@ const (
 
 	updateBillingQuery = `
 		UPDATE user 
-		SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_subscription_status = ?, stripe_subscription_paid_until = ?
+		SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_subscription_status = ?, stripe_subscription_paid_until = ?, stripe_subscription_cancel_at = ?
 		WHERE user = ?
 	`
 )
@@ -607,11 +608,11 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 	var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripePriceID, tierCode, tierName sql.NullString
 	var paid sql.NullBool
 	var messages, emails int64
-	var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, stripeSubscriptionPaidUntil sql.NullInt64
+	var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt sql.NullInt64
 	if !rows.Next() {
 		return nil, ErrUserNotFound
 	}
-	if err := rows.Scan(&username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionPaidUntil, &tierCode, &tierName, &paid, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &stripePriceID); err != nil {
+	if err := rows.Scan(&username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &tierCode, &tierName, &paid, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &stripePriceID); err != nil {
 		return nil, err
 	} else if err := rows.Err(); err != nil {
 		return nil, err
@@ -631,6 +632,7 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 			StripeSubscriptionID:        stripeSubscriptionID.String,                                // May be empty
 			StripeSubscriptionStatus:    stripe.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty
 			StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0),            // May be zero
+			StripeSubscriptionCancelAt:  time.Unix(stripeSubscriptionCancelAt.Int64, 0),             // May be zero
 		},
 	}
 	if err := json.Unmarshal([]byte(prefs), user.Prefs); err != nil {
@@ -875,7 +877,7 @@ func (a *Manager) CreateTier(tier *Tier) error {
 }
 
 func (a *Manager) ChangeBilling(user *User) error {
-	if _, err := a.db.Exec(updateBillingQuery, nullString(user.Billing.StripeCustomerID), nullString(user.Billing.StripeSubscriptionID), nullString(string(user.Billing.StripeSubscriptionStatus)), nullInt64(user.Billing.StripeSubscriptionPaidUntil.Unix()), user.Name); err != nil {
+	if _, err := a.db.Exec(updateBillingQuery, nullString(user.Billing.StripeCustomerID), nullString(user.Billing.StripeSubscriptionID), nullString(string(user.Billing.StripeSubscriptionStatus)), nullInt64(user.Billing.StripeSubscriptionPaidUntil.Unix()), nullInt64(user.Billing.StripeSubscriptionCancelAt.Unix()), user.Name); err != nil {
 		return err
 	}
 	return nil
diff --git a/user/types.go b/user/types.go
index e9a689fa..2aca5652 100644
--- a/user/types.go
+++ b/user/types.go
@@ -90,6 +90,7 @@ type Billing struct {
 	StripeSubscriptionID        string
 	StripeSubscriptionStatus    stripe.SubscriptionStatus
 	StripeSubscriptionPaidUntil time.Time
+	StripeSubscriptionCancelAt  time.Time
 }
 
 // Grant is a struct that represents an access control entry to a topic by a user
diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json
index f18aedfb..56e6366b 100644
--- a/web/public/static/langs/en.json
+++ b/web/public/static/langs/en.json
@@ -183,7 +183,9 @@
   "account_usage_tier_none": "Basic",
   "account_usage_tier_upgrade_button": "Upgrade to Pro",
   "account_usage_tier_change_button": "Change",
+  "account_usage_tier_paid_until": "Subscription paid until {{date}}, and will auto-renew",
   "account_usage_tier_payment_overdue": "Your payment is overdue. Please update your payment method, or your account will be downgraded soon.",
+  "account_usage_tier_canceled_subscription": "Your subscription was canceled and will be downgraded to a free account on {{date}}.",
   "account_usage_manage_billing_button": "Manage billing",
   "account_usage_messages_title": "Published messages",
   "account_usage_emails_title": "Emails sent",
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index 8603ec55..b34af7e3 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -184,6 +184,11 @@ export const formatShortDateTime = (timestamp) => {
         .format(new Date(timestamp * 1000));
 }
 
+export const formatShortDate = (timestamp) => {
+    return new Intl.DateTimeFormat('default', {dateStyle: 'short'})
+        .format(new Date(timestamp * 1000));
+}
+
 export const formatBytes = (bytes, decimals = 2) => {
     if (bytes === 0) return '0 bytes';
     const k = 1024;
diff --git a/web/src/components/Account.js b/web/src/components/Account.js
index 8744dbfc..622ca42d 100644
--- a/web/src/components/Account.js
+++ b/web/src/components/Account.js
@@ -18,7 +18,7 @@ import TextField from "@mui/material/TextField";
 import DialogActions from "@mui/material/DialogActions";
 import routes from "./routes";
 import IconButton from "@mui/material/IconButton";
-import {formatBytes, formatShortDateTime} from "../app/utils";
+import {formatBytes, formatShortDate, formatShortDateTime} from "../app/utils";
 import accountApi, {UnauthorizedError} from "../app/AccountApi";
 import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
 import {Pref, PrefGroup} from "./Pref";
@@ -201,7 +201,7 @@ const Stats = () => {
             </Typography>
             <PrefGroup>
                 <Pref
-                    alignTop={account.billing?.status === "past_due"}
+                    alignTop={account.billing?.status === "past_due" || account.billing?.cancel_at > 0}
                     title={t("account_usage_tier_title")}
                 >
                     <div>
@@ -213,6 +213,11 @@ const Stats = () => {
                         }
                         {account.role === "user" && account.tier && account.tier.name}
                         {account.role === "user" && !account.tier && t("account_usage_tier_none")}
+                        {account.billing?.paid_until &&
+                            <Tooltip title={t("account_usage_tier_paid_until", { date: formatShortDate(account.billing?.paid_until) })}>
+                                <span><InfoIcon/></span>
+                            </Tooltip>
+                        }
                         {config.enable_payments && account.role === "user" && (!account.tier || !account.tier.paid) &&
                             <Button
                                 variant="outlined"
@@ -246,6 +251,9 @@ const Stats = () => {
                     {account.billing?.status === "past_due" &&
                         <Alert severity="error" sx={{mt: 1}}>{t("account_usage_tier_payment_overdue")}</Alert>
                     }
+                    {account.billing?.cancel_at > 0 &&
+                        <Alert severity="info" sx={{mt: 1}}>{t("account_usage_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}</Alert>
+                    }
                 </Pref>
                 {account.role !== "admin" &&
                     <Pref title={t("account_usage_reservations_title")}>
@@ -331,7 +339,7 @@ const Stats = () => {
 const InfoIcon = () => {
     return (
         <InfoOutlinedIcon sx={{
-            verticalAlign: "bottom",
+            verticalAlign: "middle",
             width: "18px",
             marginLeft: "4px",
             color: "gray"