diff --git a/enterprise/backend/src/metabase_enterprise/serialization/dump.clj b/enterprise/backend/src/metabase_enterprise/serialization/dump.clj
index 814ff0698eb97a3bdfc7d1885f30b20575fa230c..008caae35c9c8e6317ed0a04cf6d1d5aa5d43d32 100644
--- a/enterprise/backend/src/metabase_enterprise/serialization/dump.clj
+++ b/enterprise/backend/src/metabase_enterprise/serialization/dump.clj
@@ -43,7 +43,7 @@
       (log/warn (str filename " is about to be overwritten."))
       (log/debug (str "With object: " (pr-str entity))))
 
-      (spit-yaml filename (serialize entity))))
+    (spit-yaml filename (serialize entity))))
 
 (defn dump
   "Serialize entities into a directory structure of YAMLs at `path`."
@@ -66,7 +66,7 @@
   "Combine all settings into a map and dump it into YAML at `path`."
   [path]
   (spit-yaml (str path "/settings.yaml")
-             (into {} (for [{:keys [key value]} (setting/admin-writable-settings
+             (into {} (for [{:keys [key value]} (setting/admin-writable-site-wide-settings
                                                  :getter (partial setting/get-value-of-type :string))]
                         [key value]))))
 
diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index b9c775bf01193d4e0fe0810808044c8a82f8161a..1e906e80baf77b75af016a0253ab4a428a999a59 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -10472,6 +10472,21 @@ databaseChangeLog:
               - column:
                   name: timestamp
 
+  - changeSet:
+      id: v43.00-026
+      author: noahmoss
+      comment: >-
+        Added 0.43.0 - adds User.settings column to implement User-local Settings
+      changes:
+        - addColumn:
+            tableName: core_user
+            columns:
+              - column:
+                  name: settings
+                  type: ${text.type}
+                  remarks: "Serialized JSON containing User-local Settings for this User"
+
+
 # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<<
 
 ########################################################################################################################
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index 0d6d48d366edfb76c87083f940dfcf8c988d98df..700c6c3b99ef99c96c86bdc0ae256cc875bac0d4 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -8,6 +8,7 @@
             [compojure.core :refer [DELETE GET POST PUT]]
             [medley.core :as m]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.api.dataset :as dataset-api]
             [metabase.api.timeline :as timeline-api]
             [metabase.async.util :as async.u]
@@ -349,7 +350,7 @@
   [card-before-updates card-updates]
   (when (or (api/column-will-change? :enable_embedding card-before-updates card-updates)
             (api/column-will-change? :embedding_params card-before-updates card-updates))
-    (api/check-embedding-enabled)
+    (validation/check-embedding-enabled)
     (api/check-superuser)))
 
 (defn- publish-card-update!
@@ -732,7 +733,7 @@
   be enabled."
   [card-id]
   (api/check-superuser)
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (api/check-not-archived (api/read-check Card card-id))
   {:uuid (or (db/select-one-field :public_uuid Card :id card-id)
              (u/prog1 (str (UUID/randomUUID))
@@ -744,7 +745,7 @@
   "Delete the publicly-accessible link to this Card."
   [card-id]
   (api/check-superuser)
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (api/check-exists? Card :id card-id, :public_uuid [:not= nil])
   (db/update! Card card-id
     :public_uuid       nil
@@ -755,7 +756,7 @@
   "Fetch a list of Cards with public UUIDs. These cards are publicly-accessible *if* public sharing is enabled."
   []
   (api/check-superuser)
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (db/select [Card :name :id :public_uuid], :public_uuid [:not= nil], :archived false))
 
 (api/defendpoint GET "/embeddable"
@@ -763,7 +764,7 @@
   and a signed JWT."
   []
   (api/check-superuser)
-  (api/check-embedding-enabled)
+  (validation/check-embedding-enabled)
   (db/select [Card :name :id], :enable_embedding true, :archived false))
 
 (api/defendpoint GET "/:id/related"
diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj
index 778ca073ee10c90bb5fb6dc63ad562846a1328e0..c08a68e1a11790136f8fcb567dc77ca0039f1c53 100644
--- a/src/metabase/api/common.clj
+++ b/src/metabase/api/common.clj
@@ -8,7 +8,6 @@
             [metabase.api.common.internal :refer [add-route-param-regexes auto-parse route-dox route-fn-name
                                                   validate-params wrap-response-if-needed]]
             [metabase.models.interface :as mi]
-            [metabase.public-settings :as public-settings]
             [metabase.util :as u]
             [metabase.util.i18n :as ui18n :refer [deferred-tru tru]]
             [metabase.util.schema :as su]
@@ -400,18 +399,6 @@
 
 ;;; ------------------------------------------------ OTHER HELPER FNS ------------------------------------------------
 
-(defn check-public-sharing-enabled
-  "Check that the `public-sharing-enabled` Setting is `true`, or throw a `400`."
-  []
-  (check (public-settings/enable-public-sharing)
-    [400 (tru "Public sharing is not enabled.")]))
-
-(defn check-embedding-enabled
-  "Is embedding of Cards or Objects (secured access via `/api/embed` endpoints with a signed JWT enabled?"
-  []
-  (check (public-settings/enable-embedding)
-    [400 (tru "Embedding is not enabled.")]))
-
 (defn check-not-archived
   "Check that the `object` exists and is not `:archived`, or throw a `404`. Returns `object` as-is if check passes."
   [object]
diff --git a/src/metabase/api/common/validation.clj b/src/metabase/api/common/validation.clj
new file mode 100644
index 0000000000000000000000000000000000000000..2fa6d7d549da0e1fd2b9f21c136f489d017b610b
--- /dev/null
+++ b/src/metabase/api/common/validation.clj
@@ -0,0 +1,19 @@
+(ns metabase.api.common.validation
+  (:require
+   [metabase.api.common :as api]
+   [metabase.public-settings :as public-settings]
+   [metabase.util.i18n :as ui18n :refer [tru]]))
+
+;; TODO: figure out what other functions to move here from metabase.api.common
+
+(defn check-public-sharing-enabled
+  "Check that the `public-sharing-enabled` Setting is `true`, or throw a `400`."
+  []
+  (api/check (public-settings/enable-public-sharing)
+    [400 (tru "Public sharing is not enabled.")]))
+
+(defn check-embedding-enabled
+  "Is embedding of Cards or Objects (secured access via `/api/embed` endpoints with a signed JWT enabled?"
+  []
+  (api/check (public-settings/enable-embedding)
+    [400 (tru "Embedding is not enabled.")]))
diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj
index 10c74b768bd907c9c730b0724e78a4b3b0500533..e7bb34a935ee06877050508e068c28eecc0b326f 100644
--- a/src/metabase/api/dashboard.clj
+++ b/src/metabase/api/dashboard.clj
@@ -7,6 +7,7 @@
             [medley.core :as m]
             [metabase.analytics.snowplow :as snowplow]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.api.dataset :as api.dataset]
             [metabase.automagic-dashboards.populate :as magic.populate]
             [metabase.events :as events]
@@ -271,7 +272,7 @@
   [dash-before-update dash-updates]
   (when (or (api/column-will-change? :enable_embedding dash-before-update dash-updates)
             (api/column-will-change? :embedding_params dash-before-update dash-updates))
-    (api/check-embedding-enabled)
+    (validation/check-embedding-enabled)
     (api/check-superuser)))
 
 (api/defendpoint PUT "/:id"
@@ -513,7 +514,7 @@
   sharing must be enabled."
   [dashboard-id]
   (api/check-superuser)
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (api/check-not-archived (api/read-check Dashboard dashboard-id))
   {:uuid (or (db/select-one-field :public_uuid Dashboard :id dashboard-id)
              (u/prog1 (str (UUID/randomUUID))
@@ -525,7 +526,7 @@
   "Delete the publicly-accessible link to this Dashboard."
   [dashboard-id]
   (api/check-superuser)
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (api/check-exists? Dashboard :id dashboard-id, :public_uuid [:not= nil], :archived false)
   (db/update! Dashboard dashboard-id
     :public_uuid       nil
@@ -537,7 +538,7 @@
   enabled."
   []
   (api/check-superuser)
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (db/select [Dashboard :name :id :public_uuid], :public_uuid [:not= nil], :archived false))
 
 (api/defendpoint GET "/embeddable"
@@ -545,7 +546,7 @@
   endpoints and a signed JWT."
   []
   (api/check-superuser)
-  (api/check-embedding-enabled)
+  (validation/check-embedding-enabled)
   (db/select [Dashboard :name :id], :enable_embedding true, :archived false))
 
 (api/defendpoint GET "/:id/related"
diff --git a/src/metabase/api/embed.clj b/src/metabase/api/embed.clj
index 01e8fd6e9772e68d4c52e5fdb302937672d9c49a..3d2a2c5c7397f88b4053c406cc5237a5afef1fa2 100644
--- a/src/metabase/api/embed.clj
+++ b/src/metabase/api/embed.clj
@@ -20,6 +20,7 @@
             [compojure.core :refer [GET]]
             [medley.core :as m]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.api.dashboard :as dashboard-api]
             [metabase.api.dataset :as dataset-api]
             [metabase.api.public :as public-api]
@@ -283,7 +284,7 @@
    (check-embedding-enabled-for-object (db/select-one [entity :enable_embedding] :id id)))
 
   ([object]
-   (api/check-embedding-enabled)
+   (validation/check-embedding-enabled)
    (api/check-404 object)
    (api/check-not-archived object)
    (api/check (:enable_embedding object)
diff --git a/src/metabase/api/preview_embed.clj b/src/metabase/api/preview_embed.clj
index 4e315dd5154d9e59a4ed8a8252a447cc495eaf33..ddb2a6662ba6e920d2d688908c3f573ac1793457 100644
--- a/src/metabase/api/preview_embed.clj
+++ b/src/metabase/api/preview_embed.clj
@@ -10,13 +10,14 @@
    Refer to the documentation for those endpoints for further details."
   (:require [compojure.core :refer [GET]]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.api.embed :as embed-api]
             [metabase.query-processor.pivot :as qp.pivot]
             [metabase.util.embed :as eu]))
 
 (defn- check-and-unsign [token]
   (api/check-superuser)
-  (api/check-embedding-enabled)
+  (validation/check-embedding-enabled)
   (eu/unsign token))
 
 (api/defendpoint GET "/card/:token"
diff --git a/src/metabase/api/public.clj b/src/metabase/api/public.clj
index f9eb94bfa5880dfc522b7027911623eda3ad3536..a66a7ab237ec9d172226de81cd1a7889f7d22b77 100644
--- a/src/metabase/api/public.clj
+++ b/src/metabase/api/public.clj
@@ -5,6 +5,7 @@
             [compojure.core :refer [GET]]
             [medley.core :as m]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.api.dashboard :as dashboard-api]
             [metabase.api.dataset :as dataset-api]
             [metabase.api.field :as field-api]
@@ -60,7 +61,7 @@
   "Fetch a publicly-accessible Card an return query results as well as `:card` information. Does not require auth
    credentials. Public sharing must be enabled."
   [uuid]
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (card-with-uuid uuid))
 
 (defmulti ^:private transform-results
@@ -131,7 +132,7 @@
   "Run query for a *public* Card with UUID. If public sharing is not enabled, this throws an exception. Returns a
   `StreamingResponse` object that should be returned as the result of an API endpoint."
   [uuid export-format parameters & options]
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [card-id (api/check-404 (db/select-one-id Card :public_uuid uuid, :archived false))]
     (apply run-query-for-card-with-id-async card-id export-format parameters options)))
 
@@ -182,7 +183,7 @@
 (api/defendpoint GET "/dashboard/:uuid"
   "Fetch a publicly-accessible Dashboard. Does not require auth credentials. Public sharing must be enabled."
   [uuid]
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (dashboard-with-uuid uuid))
 
 ;; TODO -- this should probably have a name like `run-query-for-dashcard...` so it matches up with
@@ -221,7 +222,7 @@
    sharing must be enabled."
   [uuid card-id dashcard-id parameters]
   {parameters (s/maybe su/JSONString)}
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [dashboard-id (api/check-404 (db/select-one-id Dashboard :public_uuid uuid, :archived false))]
     (public-dashcard-results-async
      :dashboard-id  dashboard-id
@@ -311,7 +312,7 @@
 (api/defendpoint GET "/card/:uuid/field/:field-id/values"
   "Fetch FieldValues for a Field that is referenced by a public Card."
   [uuid field-id]
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [card-id (db/select-one-id Card :public_uuid uuid, :archived false)]
     (card-and-field-id->values card-id field-id)))
 
@@ -325,7 +326,7 @@
 (api/defendpoint GET "/dashboard/:uuid/field/:field-id/values"
   "Fetch FieldValues for a Field that is referenced by a Card in a public Dashboard."
   [uuid field-id]
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [dashboard-id (api/check-404 (db/select-one-id Dashboard :public_uuid uuid, :archived false))]
     (dashboard-and-field-id->values dashboard-id field-id)))
 
@@ -353,7 +354,7 @@
   [uuid field-id search-field-id value limit]
   {value su/NonBlankString
    limit (s/maybe su/IntStringGreaterThanZero)}
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [card-id (db/select-one-id Card :public_uuid uuid, :archived false)]
     (search-card-fields card-id field-id search-field-id value (when limit (Integer/parseInt limit)))))
 
@@ -362,7 +363,7 @@
   [uuid field-id search-field-id value limit]
   {value su/NonBlankString
    limit (s/maybe su/IntStringGreaterThanZero)}
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [dashboard-id (api/check-404 (db/select-one-id Dashboard :public_uuid uuid, :archived false))]
     (search-dashboard-fields dashboard-id field-id search-field-id value (when limit (Integer/parseInt limit)))))
 
@@ -394,7 +395,7 @@
   Cards."
   [uuid field-id remapped-id value]
   {value su/NonBlankString}
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [card-id (api/check-404 (db/select-one-id Card :public_uuid uuid, :archived false))]
     (card-field-remapped-values card-id field-id remapped-id value)))
 
@@ -403,7 +404,7 @@
   Dashboards."
   [uuid field-id remapped-id value]
   {value su/NonBlankString}
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [dashboard-id (db/select-one-id Dashboard :public_uuid uuid, :archived false)]
     (dashboard-field-remapped-values dashboard-id field-id remapped-id value)))
 
@@ -438,7 +439,7 @@
    sharing must be enabled."
   [uuid card-id dashcard-id parameters]
   {parameters (s/maybe su/JSONString)}
-  (api/check-public-sharing-enabled)
+  (validation/check-public-sharing-enabled)
   (let [dashboard-id (api/check-404 (db/select-one-id Dashboard :public_uuid uuid, :archived false))]
     (public-dashcard-results-async
      :dashboard-id  dashboard-id
@@ -450,7 +451,7 @@
 ;;; ----------------------------------------- Route Definitions & Complaints -----------------------------------------
 
 ;; TODO - why don't we just make these routes have a bit of middleware that includes the
-;; `api/check-public-sharing-enabled` check in each of them? That way we don't need to remember to include the line in
+;; `validation/check-public-sharing-enabled` check in each of them? That way we don't need to remember to include the line in
 ;; every single endpoint definition here? Wouldn't that be 100x better?!
 ;;
 ;; TODO - also a smart person would probably just parse the UUIDs automatically in middleware as appropriate for
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index a4c797fe36e2effe2e598436034117da6e24f486..c68a9239e00cad5ac72018ec7b476d0574d6dc3a 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -8,6 +8,7 @@
   [[metabase.driver.sql-jdbc]] for more details."
   (:require [clojure.string :as str]
             [clojure.tools.logging :as log]
+            [java-time :as t]
             [metabase.driver.impl :as impl]
             [metabase.models.setting :as setting :refer [defsetting]]
             [metabase.plugins.classloader :as classloader]
@@ -33,6 +34,15 @@
       (catch Throwable e
         (log/error e (trs "Failed to notify {0} Database {1} updated" driver id))))))
 
+(defn- short-timezone-name [timezone-id]
+  (let [^java.time.ZoneId zone (if (seq timezone-id)
+                                 (t/zone-id timezone-id)
+                                 (t/zone-id))]
+    (.getDisplayName
+     zone
+     java.time.format.TextStyle/SHORT
+     (java.util.Locale/getDefault))))
+
 (defsetting report-timezone
   (deferred-tru "Connection timezone to use when executing queries. Defaults to system timezone.")
   :setter
@@ -40,6 +50,12 @@
     (setting/set-value-of-type! :string :report-timezone new-value)
     (notify-all-databases-updated)))
 
+(defsetting report-timezone-short
+  "Current report timezone abbreviation"
+  :visibility :public
+  :setter     :none
+  :getter     (fn [] (short-timezone-name (report-timezone))))
+
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                                 Current Driver                                                 |
diff --git a/src/metabase/driver/util.clj b/src/metabase/driver/util.clj
index 786e982ae75a8e9c370a2ff037f8376d63ea97d2..9a19307c62c03384ebdd776ff0102f7f2d2eb877 100644
--- a/src/metabase/driver/util.clj
+++ b/src/metabase/driver/util.clj
@@ -334,6 +334,12 @@
                       :driver-name    (driver/display-name driver)
                       :superseded-by  (driver/superseded-by driver)}])))
 
+(defsetting engines
+  "Available database engines"
+  :visibility :public
+  :setter     :none
+  :getter     available-drivers-info)
+
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                             TLS Helpers                                                        |
 ;;; +----------------------------------------------------------------------------------------------------------------+
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 411d4eb78188886ef4bd21553281c8ce2baf01ab..7194562985d13ce8149d568fdaabcbf440ffbbeb 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -32,39 +32,45 @@
   [[admin-writable-settings]] and [[user-readable-values-map]] can be used to fetch *all* Admin-writable and
   User-readable Settings, respectively. See their docstrings for more information.
 
-  ### Database-Local Settings
+  ### User-local and Database-local Settings
 
   Starting in 0.42.0, some Settings are allowed to have Database-specific values that override the normal site-wide
-  value. These are similar in concept to buffer-local variables in Emacs Lisp.
+  value. Similarly, starting in 0.43.0, some Settings are allowed to have User-specific values. These are similar in
+  concept to buffer-local variables in Emacs Lisp.
 
-  When a Setting is allowed to be Database-local, any values in [[*database-local-values*]] for that Setting will be
-  returned preferentially to site-wide values of that Setting. [[*database-local-values*]] comes from the
-  `Database.settings` column in the application DB. `nil` values in [[*database-local-values*]] are ignored, i.e. you
-  cannot 'unset' a site-wide value with a Database-local one.
+  When a Setting is allowed to be User or Database local, any values in [[*user-local-values*]] or
+  [[*database-local-values*]] for that Setting will be returned preferentially to site-wide values of that Setting.
+  [[*user-local-values*]] comes from the `User.settings` column in the application DB, and [[*database-local-values*]]
+  comes from the `Database.settings` column. `nil` values in [[*user-local-values*]] and [[*database-local-values*]]
+  are ignored, i.e. you cannot 'unset' a site-wide value with a User- or Database-local one.
 
-  Whether or not a Setting can be Database-local is controlled by the `:database-local` option passed
-  to [[defsetting]]. There are three valid values of this option:
+  Whether or not a Setting can be User- or Database-local is controlled by the `:user-local` and `:database-local`
+  options passed to [[defsetting]]. A Setting can only be User-local *or* Database-local, not both; this is enforced
+  when the Setting is defined. There are three valid values of these options:
 
-  * `:only` means this Setting can *only* have a Database-local value and cannot have a 'normal' site-wide value. It
-  cannot be set via env var. Default values are still allowed for Database-local-only Settings. Database-local-only
-  Settings are never returned by [[admin-writable-settings]] or [[user-readable-values-map]] regardless of
-  their [[Visibility]].
+  * `:only` means this Setting can *only* have a User- or Database-local value and cannot have a 'normal' site-wide
+  value. It cannot be set via env var. Default values are still allowed for User- and Database-local-only Settings.
+  User- and Database-local-only Settings are never returned by [[admin-writable-settings]] or
+  [[user-readable-values-map]] regardless of their [[Visibility]].
 
-  * `:allowed` means this Setting can be Database-local and can also have a normal site-wide value; if both are
-  specified, the Database-specific value will be returned preferentially when we are in the context of a specific
-  Database (i.e., [[*database-local-values*]] is bound).
+  * `:allowed` means this Setting can be User- or Database-local and can also have a normal site-wide value; if both
+  are specified, the User- or Database-specific value will be returned preferentially when we are in the context of a
+  specific User or Database (i.e., [[*user-local-values*]] or [[*database-local-values*]] is bound).
 
-  * `:never` means Database-specific values cannot be set for this Setting. Values in [[*database-local-values*]]
-  will be ignored.
+  * `:never` means User- or Database-specific values cannot be set for this Setting. Values in [[*user-local-values*]]
+  and [[*database-local-values*]] will be ignored.
 
-  `:never` is the default value of `:database-local`; to allow Database-local values, the Setting definition must
-  explicitly specify `:database-local` `:only` or `:allowed`.
+  `:never` is the default value of both `:user-local` and `:database-local`; to allow User- or Database-local values,
+  the Setting definition must explicitly specify `:only` or `:allowed` for the appropriate option.
 
-  Setting setter functions do not affect Database-local values; they always set the site-wide value. (At the time of
-  this writing, there is not yet a FE-client-friendly way to set Database-local values. Just set them manually in the
-  application DB until we figure that out.)
+  If a User-local setting is written in the context of an API request (i.e., when [[metabase.api.common/*current-user*]]
+  is bound), the value will be local to the current user. If it is written outside of an API request, a site-wide
+  value will be written. (At the time of this writing, there is not yet a FE-client-friendly way to set Database-local
+  values. Just set them manually in the application DB until we figure that out.)
 
-  See #19399 for more information about and motivation behind Database-local Settings."
+  Custom setter functions do not affect User- or Database-local values; they always set the site-wide value.
+
+  See #14055 and #19399 for more information about and motivation behind User- and Database-local Settings."
   (:refer-clojure :exclude [get])
   (:require [cheshire.core :as json]
             [clojure.core :as core]
@@ -74,6 +80,7 @@
             [clojure.tools.logging :as log]
             [environ.core :as env]
             [medley.core :as m]
+            [metabase.api.common :as api]
             [metabase.models.setting.cache :as cache]
             [metabase.util :as u]
             [metabase.util.date-2 :as u.date]
@@ -98,6 +105,15 @@
   TODO -- we should probably also bind this in sync contexts e.g. functions in [[metabase.sync]]."
   nil)
 
+(def ^:dynamic *user-local-values*
+  "User-local Settings values (as a map of Setting name -> already-deserialized value). This comes from the value of
+  `User.settings` in the application DB. When bound, any Setting that *can* be User-local will have a value from this
+  map returned preferentially to the site-wide value.
+
+  This is normally bound automatically by session middleware, in
+  [[metabase.server.middleware.session/do-with-current-user]]."
+  (atom nil))
+
 (def ^:private retired-setting-names
   "A set of setting names which existed in previous versions of Metabase, but are no longer used. New settings may not use
   these names to avoid unintended side-effects if an application database still stores values for these settings."
@@ -189,8 +205,9 @@
    :cache?      s/Bool           ; should the getter always fetch this value "fresh" from the DB? (default: false)
    :deprecated  (s/maybe s/Str)            ; if non-nil, contains the Metabase version in which this setting was deprecated
 
-   ;; whether this Setting can be Database-local. See [[metabase.models.setting]] docstring for more info.
+   ;; whether this Setting can be Database-local or User-local. See [[metabase.models.setting]] docstring for more info.
    :database-local LocalOption
+   :user-local     LocalOption
 
    ;; called whenever setting value changes, whether from update-setting! or a cache refresh. used to handle cases
    ;; where a change to the cache necessitates a change to some value outside the cache, like when a change the
@@ -251,17 +268,42 @@
   (setting-name [this]
     (name this)))
 
+(defn- allows-site-wide-values? [setting]
+  (and
+   (not= (:database-local (resolve-setting setting)) :only)
+   (not= (:user-local (resolve-setting setting)) :only)))
+
 (defn- allows-database-local-values? [setting]
   (#{:only :allowed} (:database-local (resolve-setting setting))))
 
-(defn- allows-site-wide-values? [setting]
-  (not= (:database-local (resolve-setting setting)) :only))
-
 (defn- database-local-value [setting-definition-or-name]
   (let [{setting-name :name, :as setting} (resolve-setting setting-definition-or-name)]
     (when (allows-database-local-values? setting)
       (core/get *database-local-values* setting-name))))
 
+(defn- allows-user-local-values? [setting]
+  (#{:only :allowed} (:user-local (resolve-setting setting))))
+
+(defn- user-local-value [setting-definition-or-name]
+  (let [{setting-name :name, :as setting} (resolve-setting setting-definition-or-name)]
+    (when (allows-user-local-values? setting)
+      (core/get @*user-local-values* setting-name))))
+
+(defn- should-set-user-local-value? [setting-definition-or-name]
+  (let [setting (resolve-setting setting-definition-or-name)]
+    (and (allows-user-local-values? setting)
+         @*user-local-values*)))
+
+(defn- set-user-local-value! [setting-definition-or-name value]
+  (let [{setting-name :name} (resolve-setting setting-definition-or-name)]
+    ;; Update the atom in *user-local-values* with the new value before writing to the DB. This ensures that
+    ;; subsequent setting updates within the same API request will not overwrite this value.
+    (swap! *user-local-values*
+           (fn [old-settings] (if value
+                                (assoc old-settings setting-name value)
+                                (dissoc old-settings setting-name))))
+    (db/update! 'User api/*current-user-id* {:settings (json/generate-string @*user-local-values*)})))
+
 (defn- munge-setting-name
   "Munge names so that they are legal for bash. Only allows for alphanumeric characters,  underscores, and hyphens."
   [setting-nm]
@@ -313,10 +355,11 @@
   "Get the raw value of a Setting from wherever it may be specified. Value is fetched by trying the following sources in
   order:
 
-  1. From [[*database-local-values*]] if this Setting is allowed to have a Database-local value
-  2. From the corresponding env var (excluding empty string values)
-  3. From the application database (i.e., set via the admin panel) (excluding empty string values)
-  4. The default value, if one was specified
+  1. From [[*user-local-values*]] if this Setting is allowed to have User-local values
+  2. From [[*database-local-values*]] if this Setting is allowed to have Database-local values
+  3. From the corresponding env var (excluding empty string values)
+  4. From the application database (i.e., set via the admin panel) (excluding empty string values)
+  5. The default value, if one was specified
 
   !!!!!!!!!! The value returned MAY OR MAY NOT be a String depending on the source !!!!!!!!!!
 
@@ -328,7 +371,8 @@
   conditions values can be returned directly (`pred`) -- see [[get-value-of-type]] for `:boolean` for example usage."
   ([setting-definition-or-name]
    (let [setting    (resolve-setting setting-definition-or-name)
-         source-fns [database-local-value
+         source-fns [user-local-value
+                     database-local-value
                      env-var-value
                      db-or-cache-value
                      default-value]]
@@ -481,10 +525,7 @@
          :as setting}                     (resolve-setting setting-definition-or-name)
         obfuscated?                       (and sensitive? (obfuscated-value? new-value))
         setting-name                      (setting-name setting)]
-    ;; make sure we're not trying to set the value of a Database-local-only Setting or something like that
-    (when-not (allows-site-wide-values? setting)
-      (throw (ex-info (tru "Site-wide values are not allowed for Setting {0}" (:name setting))
-                      {:setting (:name setting)})))
+    ;; if someone attempts to set a sensitive setting to an obfuscated value (probably via a misuse of the `set-many!`
     ;; function, setting values that have not changed), ignore the change. Log a message that we are ignoring it.
     (if obfuscated?
       (log/info (trs "Attempted to set Setting {0} to obfuscated value. Ignoring change." setting-name))
@@ -493,27 +534,36 @@
           (log/warn (trs "Setting {0} is deprecated as of Metabase {1} and may be removed in a future version."
                          setting-name
                          deprecated)))
-        ;; always update the cache entirely when updating a Setting.
-        (cache/restore-cache!)
-        ;; write to DB
-        (cond
-          (nil? new-value)
-          (db/simple-delete! Setting :key setting-name)
-
-          ;; if there's a value in the cache then the row already exists in the DB; update that
-          (contains? (cache/cache) setting-name)
-          (update-setting! setting-name new-value)
-
-          ;; if there's nothing in the cache then the row doesn't exist, insert a new one
-          :else
-          (set-new-setting! setting-name new-value))
-        ;; update cached value
-        (cache/update-cache! setting-name new-value)
-        ;; Record the fact that a Setting has been updated so eventaully other instances (if applicable) find out
-        ;; about it (For Settings that don't use the Cache, don't update the `last-updated` value, because it will
-        ;; cause other instances to do needless reloading of the cache from the DB)
-        (when-not *disable-cache*
-          (cache/update-settings-last-updated!))
+        (if (should-set-user-local-value? setting)
+          ;; If this is user-local and this is being set in the context of an API call, we don't want to update the
+          ;; site-wide value or write or read from the cache
+          (set-user-local-value! setting-name new-value)
+          (do
+            ;; make sure we're not trying to set the value of a Database-local-only Setting
+            (when-not (allows-site-wide-values? setting)
+              (throw (ex-info (tru "Site-wide values are not allowed for Setting {0}" (:name setting))
+                              {:setting (:name setting)})))
+            ;; always update the cache entirely when updating a Setting.
+            (cache/restore-cache!)
+            ;; write to DB
+            (cond
+              (nil? new-value)
+              (db/simple-delete! Setting :key setting-name)
+
+              ;; if there's a value in the cache then the row already exists in the DB; update that
+              (contains? (cache/cache) setting-name)
+              (update-setting! setting-name new-value)
+
+              ;; if there's nothing in the cache then the row doesn't exist, insert a new one
+              :else
+              (set-new-setting! setting-name new-value))
+            ;; update cached value
+            (cache/update-cache! setting-name new-value)
+            ;; Record the fact that a Setting has been updated so eventaully other instances (if applicable) find out
+            ;; about it (For Settings that don't use the Cache, don't update the `last-updated` value, because it will
+            ;; cause other instances to do needless reloading of the cache from the DB)
+            (when-not *disable-cache*
+              (cache/update-settings-last-updated!))))
         ;; Now return the `new-value`.
         new-value))))
 
@@ -630,6 +680,7 @@
                  :sensitive?     false
                  :cache?         true
                  :database-local :never
+                 :user-local     :never
                  :deprecated     nil}
                 (dissoc setting :name :type :default)))
       (s/validate SettingDefinition <>)
@@ -650,6 +701,10 @@
         (throw (ex-info (tru "Setting name ''{0}'' is retired; use a different name instead" (name setting-name))
                         {:retired-setting-name (name setting-name)
                          :new-setting          (dissoc <> :on-change :getter :setter)})))
+      (when (and (allows-user-local-values? setting) (allows-database-local-values? setting))
+        (throw (ex-info (tru "Setting {0} allows both user-local and database-local values; this is not supported"
+                             setting-name)
+                        {:setting setting})))
       (swap! registered-settings assoc setting-name <>))))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -905,12 +960,10 @@
   `options` are passed to [[user-facing-value]].
 
   This is currently used by `GET /api/setting` ([[metabase.api.setting/GET_]]; admin-only; powers the Admin Settings
-  page) so all admin-visible Settings should be included. Also used
-  by [[metabase-enterprise.serialization.dump/dump-settings]] which should also have access to everything not
-  `:internal`. In either case we *do not* want to return env var values -- we don't want to serialize them regardless
-  of whether the value should be readable or not, and admins should not be allowed to modify them."
+  page) so all admin-visible Settings should be included. We *do not* want to return env var values, since admins
+  are not allowed to modify them."
   [& {:as options}]
-  ;; ignore Database-local values even if this is bound for some reason
+  ;; ignore User-local and Database-local values
   (binding [*database-local-values* nil]
     (into
      []
@@ -920,6 +973,16 @@
            (map #(m/mapply user-facing-info % options)))
      (sort-by :name (vals @registered-settings)))))
 
+(defn admin-writable-site-wide-settings
+  "Returns a sequence of site-wide Settings maps, similar to [[admin-writable-settings]]. However, this function
+  excludes User-local Settings in addition to Database-local Settings. Settings that are optionally user-local will
+  be included with their site-wide value, if a site-wide value is set.
+
+  This is used in [[metabase-enterprise.serialization.dump/dump-settings]] to serialize site-wide Settings."
+  [& options]
+  (binding [*user-local-values* (atom nil)]
+    (apply admin-writable-settings options)))
+
 (defn user-readable-values-map
   "Returns Settings as a map of setting name -> site-wide value for a given [[Visibility]] e.g. `:public`.
 
@@ -930,7 +993,7 @@
   the frontend client. For that reason, these Settings *should* include values that come back from environment
   variables, *unless* they are marked `:sensitive?`."
   [visibility]
-  ;; ignore Database-local values even if this is bound for some reason
+  ;; ignore Database-local values, but not User-local values
   (binding [*database-local-values* nil]
     (into
      {}
diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj
index 9c81b7b178dab9b171c8af796993acac9b21eb31..09f475aad7adbda0ae95796b01fdb175af349bc1 100644
--- a/src/metabase/models/user.clj
+++ b/src/metabase/models/user.clj
@@ -134,7 +134,8 @@
           :post-insert    post-insert
           :pre-update     pre-update
           :post-select    post-select
-          :types          (constantly {:login_attributes :json-no-keywordization})}))
+          :types          (constantly {:login_attributes :json-no-keywordization
+                                       :settings         :encrypted-json})}))
 
 (defn group-ids
   "Fetch set of IDs of PermissionsGroup a User belongs to."
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index 628c3837ed0b3c8b495f305dcae311fd09cd4afb..404ceae1c917f7ebbeeefc8359d3f86570599e78 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -6,8 +6,6 @@
             [clojure.tools.logging :as log]
             [java-time :as t]
             [metabase.config :as config]
-            [metabase.driver :as driver]
-            [metabase.driver.util :as driver.u]
             [metabase.models.setting :as setting :refer [defsetting]]
             [metabase.plugins.classloader :as classloader]
             [metabase.public-settings.premium-features :as premium-features]
@@ -354,15 +352,6 @@
     (assoc object :public_uuid nil)
     object))
 
-(defn- short-timezone-name [timezone-id]
-  (let [^java.time.ZoneId zone (if (seq timezone-id)
-                                 (t/zone-id timezone-id)
-                                 (t/zone-id))]
-    (.getDisplayName
-     zone
-     java.time.format.TextStyle/SHORT
-     (java.util.Locale/getDefault))))
-
 (defsetting available-locales
   "Available i18n locales"
   :visibility :public
@@ -375,12 +364,6 @@
   :setter     :none
   :getter     (comp sort t/available-zone-ids))
 
-(defsetting engines
-  "Available database engines"
-  :visibility :public
-  :setter     :none
-  :getter     driver.u/available-drivers-info)
-
 (defsetting has-sample-database?
   "Whether this instance has a Sample Database database"
   :visibility :authenticated
@@ -399,12 +382,6 @@
   :visibility :public
   :default    nil)
 
-(defsetting report-timezone-short
-  "Current report timezone abbreviation"
-  :visibility :public
-  :setter     :none
-  :getter     (fn [] (short-timezone-name (driver/report-timezone))))
-
 (defsetting version
   "Metabase's version info"
   :visibility :public
diff --git a/src/metabase/server/middleware/session.clj b/src/metabase/server/middleware/session.clj
index 03d580fbe07bb4c51846d70d8e527b79be07b5dd..320234b1a71c0569a30a45ee09ade921c9de93dd 100644
--- a/src/metabase/server/middleware/session.clj
+++ b/src/metabase/server/middleware/session.clj
@@ -1,6 +1,7 @@
 (ns metabase.server.middleware.session
   "Ring middleware related to session (binding current user and permissions)."
-  (:require [clojure.java.jdbc :as jdbc]
+  (:require
+            [clojure.java.jdbc :as jdbc]
             [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
             [metabase.api.common :refer [*current-user* *current-user-id* *current-user-permissions-set* *is-superuser?*]]
@@ -9,6 +10,7 @@
             [metabase.db :as mdb]
             [metabase.driver.sql.query-processor :as sql.qp]
             [metabase.models.session :refer [Session]]
+            [metabase.models.setting :refer [*user-local-values*]]
             [metabase.models.user :as user :refer [User]]
             [metabase.public-settings :as public-settings]
             [metabase.server.request.util :as request.u]
@@ -234,12 +236,13 @@
 
 (defn do-with-current-user
   "Impl for `with-current-user`."
-  [{:keys [metabase-user-id is-superuser? user-locale]} thunk]
+  [{:keys [metabase-user-id is-superuser? user-locale settings]} thunk]
   (binding [*current-user-id*              metabase-user-id
             i18n/*user-locale*             user-locale
             *is-superuser?*                (boolean is-superuser?)
             *current-user*                 (delay (find-user metabase-user-id))
-            *current-user-permissions-set* (delay (some-> metabase-user-id user/permissions-set))]
+            *current-user-permissions-set* (delay (some-> metabase-user-id user/permissions-set))
+            *user-local-values*            (atom (or settings {}))]
     (thunk)))
 
 (defmacro ^:private with-current-user-for-request
@@ -247,15 +250,16 @@
   `(do-with-current-user ~request (fn [] ~@body)))
 
 (defn bind-current-user
-  "Middleware that binds `metabase.api.common/*current-user*`, `*current-user-id*`, `*is-superuser?*`, and
-  `*current-user-permissions-set*`.
+  "Middleware that binds [[metabase.api.common/*current-user*]], [[*current-user-id*]], [[*is-superuser?*]],
+  [[*current-user-permissions-set*]], and [[metabase.models.setting/*user-local-values*]].
 
   *  `*current-user-id*`                int ID or nil of user associated with request
   *  `*current-user*`                   delay that returns current user (or nil) from DB
   *  `metabase.util.i18n/*user-locale*` ISO locale code e.g `en` or `en-US` to use for the current User.
                                         Overrides `site-locale` if set.
   *  `*is-superuser?*`                  Boolean stating whether current user is a superuser.
-  *  `current-user-permissions-set*`    delay that returns the set of permissions granted to the current user from DB"
+  *  `current-user-permissions-set*`    delay that returns the set of permissions granted to the current user from DB
+  *  `*user-local-values*`              atom containing a map of user-local settings and values for the current user"
   [handler]
   (fn [request respond raise]
     (with-current-user-for-request request
@@ -265,7 +269,7 @@
   "Part of the impl for `with-current-user` -- don't use this directly."
   [current-user-id]
   (when current-user-id
-    (db/select-one [User [:id :metabase-user-id] [:is_superuser :is-superuser?] [:locale :user-locale]]
+    (db/select-one [User [:id :metabase-user-id] [:is_superuser :is-superuser?] [:locale :user-locale] :settings]
       :id current-user-id)))
 
 (defmacro with-current-user
diff --git a/test/metabase/models/setting_test.clj b/test/metabase/models/setting_test.clj
index ada4090ee31d46c8936c7ad67698b83279ec3918..4d4db4b0cf4a65aa6ed7c1694140a28b43e05988 100644
--- a/test/metabase/models/setting_test.clj
+++ b/test/metabase/models/setting_test.clj
@@ -708,7 +708,6 @@
                                                (db/delete! Setting :key (name setting-name))
                                                (cache/restore-cache!)))))
                                        (fn [thunk]
-                                         ()
                                          (tu/do-with-temp-env-var-value
                                           (keyword (str "mb-" (name setting-name)))
                                           site-wide-value
@@ -824,3 +823,74 @@
         (catch Exception e
           (is (= "Setting name 'retired-setting' is retired; use a different name instead"
                  (ex-message e))))))))
+
+(defsetting ^:private test-user-local-only-setting
+  "test Setting"
+  :visibility :internal
+  :type       :integer
+  :user-local :only)
+
+(defsetting ^:private test-user-local-allowed-setting
+  (deferred-tru "test Setting")
+  :visibility :authenticated
+  :type       :integer
+  :user-local :allowed)
+
+(defsetting ^:private test-user-local-never-setting
+  "test Setting"
+  :visibility :internal
+  :type       :integer) ; `:never` should be the default
+
+(deftest user-local-settings-test
+  (testing "Reading and writing a user-local-only setting in the context of a user uses the user-local value"
+    (mt/with-current-user (mt/user->id :rasta)
+      (test-user-local-only-setting 1)
+      (is (= 1 (test-user-local-only-setting))))
+    (mt/with-current-user (mt/user->id :crowberto)
+      (test-user-local-only-setting 2)
+      (is (= 2 (test-user-local-only-setting))))
+    (mt/with-current-user (mt/user->id :rasta)
+      (is (= 1 (test-user-local-only-setting)))))
+
+  (testing "A user-local-only setting cannot have a site-wide value"
+    (is (thrown-with-msg? Throwable #"Site-wide values are not allowed" (test-user-local-only-setting 1))))
+
+  (testing "Reading and writing a user-local-allowed setting in the context of a user uses the user-local value"
+    ;; TODO: mt/with-temporary-setting-values only affects site-wide value, we should figure out whether it should also
+    ;; affect user-local settings.
+    (mt/with-temporary-setting-values [test-user-local-allowed-setting nil]
+      (mt/with-current-user (mt/user->id :rasta)
+        (test-user-local-allowed-setting 1)
+        (is (= 1 (test-user-local-allowed-setting))))
+      (mt/with-current-user (mt/user->id :crowberto)
+        (test-user-local-allowed-setting 2)
+        (is (= 2 (test-user-local-allowed-setting))))
+      (mt/with-current-user (mt/user->id :rasta)
+        (is (= 1 (test-user-local-allowed-setting))))
+      ;; Calling the setter when not in the context of a user should set the site-wide value
+      (is (nil? (test-user-local-allowed-setting)))
+      (test-user-local-allowed-setting 3)
+      (mt/with-current-user (mt/user->id :crowberto)
+        (is (= 2 (test-user-local-allowed-setting))))
+      (mt/with-current-user (mt/user->id :rasta)
+        (is (= 1 (test-user-local-allowed-setting))))))
+
+  (testing "Reading and writing a user-local-never setting in the context of a user uses the site-wide value"
+    (mt/with-current-user (mt/user->id :rasta)
+      (test-user-local-never-setting 1)
+      (is (= 1 (test-user-local-never-setting))))
+    (mt/with-current-user (mt/user->id :crowberto)
+      (test-user-local-never-setting 2)
+      (is (= 2 (test-user-local-never-setting))))
+    (mt/with-current-user (mt/user->id :rasta)
+      (is (= 2 (test-user-local-never-setting))))
+    (is (= 2 (test-user-local-never-setting))))
+
+  (testing "A setting cannot be defined to allow both user-local and database-local values"
+    (is (thrown-with-msg?
+         Throwable
+         #"Setting .* allows both user-local and database-local values; this is not supported"
+         (defsetting test-user-local-and-db-local-setting
+           (deferred-tru "test Setting")
+           :user-local     :allowed
+           :database-local :allowed)))))
diff --git a/test/metabase/server/middleware/session_test.clj b/test/metabase/server/middleware/session_test.clj
index 15686abeed8894696f12dcb0b396271917ae4273..d2854d20e2de54db326371a73342dc2d96297197 100644
--- a/test/metabase/server/middleware/session_test.clj
+++ b/test/metabase/server/middleware/session_test.clj
@@ -166,7 +166,7 @@
 (deftest no-session-id-in-request-test
   (testing "no session-id in the request"
     (is (= nil
-           (-> (wrapped-handler (mock/request :get "/anyurl") )
+           (-> (wrapped-handler (mock/request :get "/anyurl"))
                :metabase-session-id)))))
 
 (deftest header-test