diff --git a/config/config.exs b/config/config.exs
index 9be1c721f..2c154eb45 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -112,7 +112,6 @@ config :pleroma, :emoji,
   shortcode_globs: ["/emoji/custom/**/*.png"],
   pack_extensions: [".png", ".gif"],
   groups: [
-    # Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
     Custom: ["/emoji/*.png", "/emoji/**/*.png"]
   ],
   default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
@@ -265,7 +264,6 @@ config :pleroma, :instance,
   remote_post_retention_days: 90,
   skip_thread_containment: true,
   limit_to_local_content: :unauthenticated,
-  dynamic_configuration: false,
   user_bio_length: 5000,
   user_name_length: 100,
   max_account_fields: 10,
@@ -620,6 +618,8 @@ config :pleroma, :web_cache_ttl,
 
 config :pleroma, :modules, runtime_dir: "instance/modules"
 
+config :pleroma, configurable_from_database: false
+
 config :swarm, node_blacklist: [~r/myhtml_.*$/]
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
diff --git a/config/description.exs b/config/description.exs
index 1089fd86c..f941349d5 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -23,17 +23,17 @@ config :pleroma, :config_description, [
         key: :uploader,
         type: :module,
         description: "Module which will be used for uploads",
-        suggestions: [
-          Generator.uploaders_list()
-        ]
+        suggestions: [Pleroma.Uploaders.Local, Pleroma.Uploaders.MDII, Pleroma.Uploaders.S3]
       },
       %{
         key: :filters,
         type: {:list, :module},
         description: "List of filter modules for uploads",
-        suggestions: [
-          Generator.filters_list()
-        ]
+        suggestions:
+          Generator.list_modules_in_dir(
+            "lib/pleroma/upload/filter",
+            "Elixir.Pleroma.Upload.Filter."
+          )
       },
       %{
         key: :link_name,
@@ -58,7 +58,66 @@ config :pleroma, :config_description, [
       %{
         key: :proxy_opts,
         type: :keyword,
-        description: "Proxy options, see `Pleroma.ReverseProxy` documentation"
+        description: "Options for Pleroma.ReverseProxy",
+        suggestions: [
+          redirect_on_failure: false,
+          max_body_length: 25 * 1_048_576,
+          http: [
+            follow_redirect: true,
+            pool: :media
+          ]
+        ],
+        children: [
+          %{
+            key: :redirect_on_failure,
+            type: :boolean,
+            description:
+              "Redirects the client to the real remote URL if there's any HTTP errors. " <>
+                "Any error during body processing will not be redirected as the response is chunked"
+          },
+          %{
+            key: :max_body_length,
+            type: :integer,
+            description:
+              "limits the content length to be approximately the " <>
+                "specified length. It is validated with the `content-length` header and also verified when proxying"
+          },
+          %{
+            key: :http,
+            type: :keyword,
+            description: "HTTP options",
+            children: [
+              %{
+                key: :adapter,
+                type: :keyword,
+                description: "Adapter specific options",
+                children: [
+                  %{
+                    key: :ssl_options,
+                    type: :keyword,
+                    label: "SSL Options",
+                    description: "SSL options for HTTP adapter",
+                    children: [
+                      %{
+                        key: :versions,
+                        type: {:list, :atom},
+                        description: "List of TLS version to use",
+                        suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
+                      }
+                    ]
+                  }
+                ]
+              },
+              %{
+                key: :proxy_url,
+                label: "Proxy URL",
+                type: [:string, :tuple],
+                description: "Proxy URL",
+                suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}]
+              }
+            ]
+          }
+        ]
       }
     ]
   },
@@ -131,9 +190,8 @@ config :pleroma, :config_description, [
         description: "List of actions for the mogrify command",
         suggestions: [
           "strip",
-          ["strip", "auto-orient"],
-          [{"implode", "1"}],
-          ["strip", "auto-orient", {"implode", "1"}]
+          "auto-orient",
+          {"implode", "1"}
         ]
       }
     ]
@@ -151,8 +209,7 @@ config :pleroma, :config_description, [
           "Text to replace filenames in links. If no setting, {random}.extension will be used. You can get the original" <>
             " filename extension by using {extension}, for example custom-file-name.{extension}",
         suggestions: [
-          "custom-file-name.{extension}",
-          nil
+          "custom-file-name.{extension}"
         ]
       }
     ]
@@ -181,7 +238,8 @@ config :pleroma, :config_description, [
           Swoosh.Adapters.AmazonSES,
           Swoosh.Adapters.Dyn,
           Swoosh.Adapters.SocketLabs,
-          Swoosh.Adapters.Gmail
+          Swoosh.Adapters.Gmail,
+          Swoosh.Adapters.Local
         ]
       },
       %{
@@ -213,12 +271,14 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.SMTP},
         key: :ssl,
+        label: "SSL",
         type: :boolean,
         description: "`Swoosh.Adapters.SMTP` adapter specific setting"
       },
       %{
         group: {:subgroup, Swoosh.Adapters.SMTP},
         key: :tls,
+        label: "TLS",
         type: :atom,
         description: "`Swoosh.Adapters.SMTP` adapter specific setting",
         suggestions: [:always, :never, :if_available]
@@ -247,12 +307,14 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.SMTP},
         key: :no_mx_lookups,
+        label: "No MX lookups",
         type: :boolean,
         description: "`Swoosh.Adapters.SMTP` adapter specific setting"
       },
       %{
         group: {:subgroup, Swoosh.Adapters.Sendgrid},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.Sendgrid` adapter specific setting",
         suggestions: ["my-api-key"]
@@ -280,6 +342,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.Mandrill},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.Mandrill` adapter specific setting",
         suggestions: ["my-api-key"]
@@ -287,6 +350,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.Mailgun},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.Mailgun` adapter specific setting",
         suggestions: ["my-api-key"]
@@ -301,6 +365,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.Mailjet},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.Mailjet` adapter specific setting",
         suggestions: ["my-api-key"]
@@ -315,6 +380,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.Postmark},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.Postmark` adapter specific setting",
         suggestions: ["my-api-key"]
@@ -322,6 +388,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.SparkPost},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.SparkPost` adapter specific setting",
         suggestions: ["my-api-key"]
@@ -336,7 +403,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.AmazonSES},
         key: :region,
-        type: {:string},
+        type: :string,
         description: "`Swoosh.Adapters.AmazonSES` adapter specific setting",
         suggestions: ["us-east-1", "us-east-2"]
       },
@@ -357,6 +424,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.Dyn},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.Dyn` adapter specific setting",
         suggestions: ["my-api-key"]
@@ -370,6 +438,7 @@ config :pleroma, :config_description, [
       %{
         group: {:subgroup, Swoosh.Adapters.SocketLabs},
         key: :api_key,
+        label: "API key",
         type: :string,
         description: "`Swoosh.Adapters.SocketLabs` adapter specific setting"
       },
@@ -381,6 +450,26 @@ config :pleroma, :config_description, [
       }
     ]
   },
+  %{
+    group: :swoosh,
+    type: :group,
+    description: "`Swoosh.Adapters.Local` adapter specific settings",
+    children: [
+      %{
+        group: {:subgroup, Swoosh.Adapters.Local},
+        key: :serve_mailbox,
+        type: :boolean,
+        description: "Run the preview server together as part of your app"
+      },
+      %{
+        group: {:subgroup, Swoosh.Adapters.Local},
+        key: :preview_port,
+        type: :integer,
+        description: "The preview server port",
+        suggestions: [4001]
+      }
+    ]
+  },
   %{
     group: :pleroma,
     key: :uri_schemes,
@@ -392,22 +481,20 @@ config :pleroma, :config_description, [
         type: {:list, :string},
         description: "List of the scheme part that is considered valid to be an URL",
         suggestions: [
-          [
-            "https",
-            "http",
-            "dat",
-            "dweb",
-            "gopher",
-            "ipfs",
-            "ipns",
-            "irc",
-            "ircs",
-            "magnet",
-            "mailto",
-            "mumble",
-            "ssb",
-            "xmpp"
-          ]
+          "https",
+          "http",
+          "dat",
+          "dweb",
+          "gopher",
+          "ipfs",
+          "ipns",
+          "irc",
+          "ircs",
+          "magnet",
+          "mailto",
+          "mumble",
+          "ssb",
+          "xmpp"
         ]
       }
     ]
@@ -458,6 +545,14 @@ config :pleroma, :config_description, [
           5_000
         ]
       },
+      %{
+        key: :chat_limit,
+        type: :integer,
+        description: "Character limit of the instance chat messages",
+        suggestions: [
+          5_000
+        ]
+      },
       %{
         key: :remote_limit,
         type: :integer,
@@ -559,6 +654,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :federation_incoming_replies_max_depth,
+        label: "Fed. incoming replies max depth",
         type: :integer,
         description:
           "Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while" <>
@@ -569,6 +665,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :federation_reachability_timeout_days,
+        label: "Fed. reachability timeout days",
         type: :integer,
         description:
           "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it",
@@ -578,7 +675,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :federation_publisher_modules,
-        type: [:list, :module],
+        type: {:list, :module},
         description: "List of modules for federation publishing",
         suggestions: [
           Pleroma.Web.ActivityPub.Publisher
@@ -591,12 +688,13 @@ config :pleroma, :config_description, [
       },
       %{
         key: :rewrite_policy,
-        type: {:list, :module},
+        type: [:module, {:list, :module}],
         description: "A list of MRF policies enabled",
-        suggestions: [
-          Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
-          Generator.mrf_list()
-        ]
+        suggestions:
+          Generator.list_modules_in_dir(
+            "lib/pleroma/web/activity_pub/mrf",
+            "Elixir.Pleroma.Web.ActivityPub.MRF."
+          )
       },
       %{
         key: :public,
@@ -634,27 +732,27 @@ config :pleroma, :config_description, [
         type: {:list, :string},
         description: "MIME-type list of formats allowed to be posted (transformed into HTML)",
         suggestions: [
-          [
-            "text/plain",
-            "text/html",
-            "text/markdown",
-            "text/bbcode"
-          ]
+          "text/plain",
+          "text/html",
+          "text/markdown",
+          "text/bbcode"
         ]
       },
       %{
         key: :mrf_transparency,
+        label: "MRF transparency",
         type: :boolean,
         description:
           "Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
       },
       %{
         key: :mrf_transparency_exclusions,
+        label: "MRF transparency exclusions",
         type: {:list, :string},
         description:
           "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value",
         suggestions: [
-          ["exclusion.com"]
+          "exclusion.com"
         ]
       },
       %{
@@ -698,8 +796,7 @@ config :pleroma, :config_description, [
         description:
           "A message that will be send to a newly registered users as a direct message",
         suggestions: [
-          "Hi, @username! Welcome to the board!",
-          nil
+          "Hi, @username! Welcome on board!"
         ]
       },
       %{
@@ -707,8 +804,7 @@ config :pleroma, :config_description, [
         type: :string,
         description: "The nickname of the local user that sends the welcome message",
         suggestions: [
-          "lain",
-          nil
+          "lain"
         ]
       },
       %{
@@ -773,12 +869,6 @@ config :pleroma, :config_description, [
           false
         ]
       },
-      %{
-        key: :dynamic_configuration,
-        type: :boolean,
-        description:
-          "Allow transferring configuration to DB with the subsequent customization from Admin api. Defaults to `false`"
-      },
       %{
         key: :max_account_fields,
         type: :integer,
@@ -829,7 +919,7 @@ config :pleroma, :config_description, [
         type: [:atom, :tuple, :module],
         description:
           "Where logs will be send, :console - send logs to stdout, {ExSyslogger, :ex_syslogger} - to syslog, Quack.Logger - to Slack.",
-        suggestions: [[:console, {ExSyslogger, :ex_syslogger}, Quack.Logger]]
+        suggestions: [:console, {ExSyslogger, :ex_syslogger}, Quack.Logger]
       }
     ]
   },
@@ -861,7 +951,7 @@ config :pleroma, :config_description, [
       %{
         key: :metadata,
         type: {:list, :atom},
-        suggestions: [[:request_id]]
+        suggestions: [:request_id]
       }
     ]
   },
@@ -886,7 +976,7 @@ config :pleroma, :config_description, [
       %{
         key: :metadata,
         type: {:list, :atom},
-        suggestions: [[:request_id]]
+        suggestions: [:request_id]
       }
     ]
   },
@@ -931,10 +1021,14 @@ config :pleroma, :config_description, [
     group: :pleroma,
     key: :frontend_configurations,
     type: :group,
-    description: "A keyword list that keeps the configuration data for any kind of frontend",
+    description:
+      "This form can be used to configure a keyword list that keeps the configuration data for any " <>
+        "kind of frontend. By default, settings for pleroma_fe and masto_fe are configured. If you want to " <>
+        "add your own configuration your settings need to be complete as they will override the defaults.",
     children: [
       %{
         key: :pleroma_fe,
+        label: "Pleroma FE",
         type: :map,
         description: "Settings for Pleroma FE",
         suggestions: [
@@ -977,6 +1071,7 @@ config :pleroma, :config_description, [
           },
           %{
             key: :redirectRootNoLogin,
+            label: "Redirect root no login",
             type: :string,
             description:
               "relative URL which indicates where to redirect when a user isn't logged in",
@@ -984,6 +1079,7 @@ config :pleroma, :config_description, [
           },
           %{
             key: :redirectRootLogin,
+            label: "Redirect root login",
             type: :string,
             description:
               "relative URL which indicates where to redirect when a user is logged in",
@@ -991,44 +1087,52 @@ config :pleroma, :config_description, [
           },
           %{
             key: :showInstanceSpecificPanel,
+            label: "Show instance specific panel",
             type: :boolean,
             description: "Whenether to show the instance's specific panel"
           },
           %{
             key: :scopeOptionsEnabled,
+            label: "Scope options enabled",
             type: :boolean,
             description: "Enable setting an notice visibility and subject/CW when posting"
           },
           %{
             key: :formattingOptionsEnabled,
+            label: "Formatting options enabled",
             type: :boolean,
             description:
               "Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to :instance, allowed_post_formats"
           },
           %{
             key: :collapseMessageWithSubject,
+            label: "Collapse message with subject",
             type: :boolean,
             description:
               "When a message has a subject(aka Content Warning), collapse it by default"
           },
           %{
             key: :hidePostStats,
+            label: "Hide post stats",
             type: :boolean,
             description: "Hide notices statistics(repeats, favorites, ...)"
           },
           %{
             key: :hideUserStats,
+            label: "Hide user stats",
             type: :boolean,
             description:
               "Hide profile statistics(posts, posts per day, followers, followings, ...)"
           },
           %{
             key: :scopeCopy,
+            label: "Scope copy",
             type: :boolean,
             description: "Copy the scope (private/unlisted/public) in replies to posts by default"
           },
           %{
             key: :subjectLineBehavior,
+            label: "Subject line behavior",
             type: :string,
             description: "Allows changing the default behaviour of subject lines in replies.
           `email`: Copy and preprend re:, as in email,
@@ -1038,6 +1142,7 @@ config :pleroma, :config_description, [
           },
           %{
             key: :alwaysShowSubjectInput,
+            label: "Always show subject input",
             type: :boolean,
             description: "When set to false, auto-hide the subject field when it's empty"
           }
@@ -1045,6 +1150,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :masto_fe,
+        label: "Masto FE",
         type: :map,
         description: "Settings for Masto FE",
         suggestions: [
@@ -1055,6 +1161,7 @@ config :pleroma, :config_description, [
         children: [
           %{
             key: :showInstanceSpecificPanel,
+            label: "Show instance specific panel",
             type: :boolean,
             description: "Whenether to show the instance's specific panel"
           }
@@ -1071,20 +1178,18 @@ config :pleroma, :config_description, [
     children: [
       %{
         key: :mascots,
-        type: :keyword,
+        type: {:keyword, :map},
         description:
           "Keyword of mascots, each element MUST contain both a url and a mime_type key",
         suggestions: [
-          [
-            pleroma_fox_tan: %{
-              url: "/images/pleroma-fox-tan-smol.png",
-              mime_type: "image/png"
-            },
-            pleroma_fox_tan_shy: %{
-              url: "/images/pleroma-fox-tan-shy.png",
-              mime_type: "image/png"
-            }
-          ]
+          pleroma_fox_tan: %{
+            url: "/images/pleroma-fox-tan-smol.png",
+            mime_type: "image/png"
+          },
+          pleroma_fox_tan_shy: %{
+            url: "/images/pleroma-fox-tan-shy.png",
+            mime_type: "image/png"
+          }
         ]
       },
       %{
@@ -1140,6 +1245,7 @@ config :pleroma, :config_description, [
   %{
     group: :pleroma,
     key: :mrf_simple,
+    label: "MRF simple",
     type: :group,
     description: "Message Rewrite Facility",
     children: [
@@ -1151,6 +1257,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :media_nsfw,
+        label: "Media NSFW",
         type: {:list, :string},
         description: "List of instances to put medias as NSFW(sensitive) from",
         suggestions: ["example.com", "*.example.com"]
@@ -1197,6 +1304,7 @@ config :pleroma, :config_description, [
   %{
     group: :pleroma,
     key: :mrf_subchain,
+    label: "MRF subchain",
     type: :group,
     description:
       "This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
@@ -1217,10 +1325,14 @@ config :pleroma, :config_description, [
   %{
     group: :pleroma,
     key: :mrf_rejectnonpublic,
+    description:
+      "MRF RejectNonPublic settings. RejectNonPublic drops posts with non-public visibility settings.",
+    label: "MRF reject non public",
     type: :group,
     children: [
       %{
         key: :allow_followersonly,
+        label: "Allow followers-only",
         type: :boolean,
         description: "whether to allow followers-only posts"
       },
@@ -1234,6 +1346,7 @@ config :pleroma, :config_description, [
   %{
     group: :pleroma,
     key: :mrf_hellthread,
+    label: "MRF hellthread",
     type: :group,
     description: "Block messages with too much mentions",
     children: [
@@ -1257,6 +1370,7 @@ config :pleroma, :config_description, [
   %{
     group: :pleroma,
     key: :mrf_keyword,
+    label: "MRF keyword",
     type: :group,
     description: "Reject or Word-Replace messages with a keyword or regex",
     children: [
@@ -1276,9 +1390,9 @@ config :pleroma, :config_description, [
       },
       %{
         key: :replace,
-        type: [{:string, :string}, {:regex, :string}],
+        type: [{:tuple, :string, :string}, {:tuple, :regex, :string}],
         description:
-          "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression",
+          "A list of tuples containing {pattern, replacement}, pattern can be a string or a regular expression.",
         suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}]
       }
     ]
@@ -1286,6 +1400,7 @@ config :pleroma, :config_description, [
   %{
     group: :pleroma,
     key: :mrf_mention,
+    label: "MRF mention",
     type: :group,
     description: "Block messages which mention a user",
     children: [
@@ -1293,13 +1408,14 @@ config :pleroma, :config_description, [
         key: :actors,
         type: {:list, :string},
         description: "A list of actors, for which to drop any posts mentioning",
-        suggestions: [["actor1", "actor2"]]
+        suggestions: ["actor1", "actor2"]
       }
     ]
   },
   %{
     group: :pleroma,
     key: :mrf_vocabulary,
+    label: "MRF vocabulary",
     type: :group,
     description: "Filter messages which belong to certain activity vocabularies",
     children: [
@@ -1308,14 +1424,14 @@ config :pleroma, :config_description, [
         type: {:list, :string},
         description:
           "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted",
-        suggestions: [["Create", "Follow", "Mention", "Announce", "Like"]]
+        suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
       },
       %{
         key: :reject,
         type: {:list, :string},
         description:
           "A list of ActivityStreams terms to reject. If empty, no messages are rejected",
-        suggestions: [["Create", "Follow", "Mention", "Announce", "Like"]]
+        suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
       }
     ]
   },
@@ -1355,7 +1471,65 @@ config :pleroma, :config_description, [
         key: :proxy_opts,
         type: :keyword,
         description: "Options for Pleroma.ReverseProxy",
-        suggestions: [[max_body_length: 25 * 1_048_576, redirect_on_failure: false]]
+        suggestions: [
+          redirect_on_failure: false,
+          max_body_length: 25 * 1_048_576,
+          http: [
+            follow_redirect: true,
+            pool: :media
+          ]
+        ],
+        children: [
+          %{
+            key: :redirect_on_failure,
+            type: :boolean,
+            description:
+              "Redirects the client to the real remote URL if there's any HTTP errors. " <>
+                "Any error during body processing will not be redirected as the response is chunked"
+          },
+          %{
+            key: :max_body_length,
+            type: :integer,
+            description:
+              "limits the content length to be approximately the " <>
+                "specified length. It is validated with the `content-length` header and also verified when proxying"
+          },
+          %{
+            key: :http,
+            type: :keyword,
+            description: "HTTP options",
+            children: [
+              %{
+                key: :adapter,
+                type: :keyword,
+                description: "Adapter specific options",
+                children: [
+                  %{
+                    key: :ssl_options,
+                    type: :keyword,
+                    label: "SSL Options",
+                    description: "SSL options for HTTP adapter",
+                    children: [
+                      %{
+                        key: :versions,
+                        type: {:list, :atom},
+                        description: "List of TLS version to use",
+                        suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
+                      }
+                    ]
+                  }
+                ]
+              },
+              %{
+                key: :proxy_url,
+                label: "Proxy URL",
+                type: [:string, :tuple],
+                description: "Proxy URL",
+                suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}]
+              }
+            ]
+          }
+        ]
       },
       %{
         key: :whitelist,
@@ -1404,10 +1578,12 @@ config :pleroma, :config_description, [
     children: [
       %{
         key: :http,
-        type: :keyword,
+        label: "HTTP",
+        type: {:keyword, :integer, :tuple},
         description: "http protocol configuration",
         suggestions: [
-          [port: 8080, ip: {127, 0, 0, 1}]
+          port: 8080,
+          ip: {127, 0, 0, 1}
         ],
         children: [
           %{
@@ -1415,21 +1591,20 @@ config :pleroma, :config_description, [
             type: {:list, :tuple},
             description: "dispatch settings",
             suggestions: [
-              [
-                {:_,
-                 [
-                   {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-                   {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
-                    {Phoenix.Transports.WebSocket,
-                     {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
-                   {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-                 ]}
-                # end copied from config.exs
-              ]
+              {:_,
+               [
+                 {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
+                 {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
+                  {Phoenix.Transports.WebSocket,
+                   {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
+                 {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
+               ]}
+              # end copied from config.exs
             ]
           },
           %{
             key: :ip,
+            label: "IP",
             type: :tuple,
             description: "ip",
             suggestions: [
@@ -1448,10 +1623,13 @@ config :pleroma, :config_description, [
       },
       %{
         key: :url,
-        type: :keyword,
+        label: "URL",
+        type: {:keyword, :string, :integer},
         description: "configuration for generating urls",
         suggestions: [
-          [host: "example.com", port: 2020, scheme: "https"]
+          host: "example.com",
+          port: 2020,
+          scheme: "https"
         ],
         children: [
           %{
@@ -1504,7 +1682,7 @@ config :pleroma, :config_description, [
       %{
         key: :render_errors,
         type: :keyword,
-        suggestions: [[view: Pleroma.Web.ErrorView, accepts: ~w(json)]],
+        suggestions: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
         children: [
           %{
             key: :view,
@@ -1521,7 +1699,7 @@ config :pleroma, :config_description, [
       %{
         key: :pubsub,
         type: :keyword,
-        suggestions: [[name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]],
+        suggestions: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
         children: [
           %{
             key: :name,
@@ -1588,17 +1766,20 @@ config :pleroma, :config_description, [
       },
       %{
         key: :sts,
+        label: "STS",
         type: :boolean,
         description: "Whether to additionally send a Strict-Transport-Security header"
       },
       %{
         key: :sts_max_age,
+        label: "STS max age",
         type: :integer,
         description: "The maximum age for the Strict-Transport-Security header if sent",
         suggestions: [31_536_000]
       },
       %{
         key: :ct_max_age,
+        label: "CT max age",
         type: :integer,
         description: "The maximum age for the Expect-CT header if sent",
         suggestions: [2_592_000]
@@ -1611,6 +1792,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :report_uri,
+        label: "Report URI",
         type: :string,
         description: "Adds the specified url to report-uri and report-to group in CSP header",
         suggestions: ["https://example.com/report-uri"]
@@ -1642,7 +1824,7 @@ config :pleroma, :config_description, [
       %{
         key: :private_key,
         type: :string,
-        description: "VAPID private keyn",
+        description: "VAPID private key",
         suggestions: ["Private key"]
       }
     ]
@@ -1662,7 +1844,7 @@ config :pleroma, :config_description, [
         key: :method,
         type: :module,
         description: "The method/service to use for captcha",
-        suggestions: [Pleroma.Captcha.Kocaptcha]
+        suggestions: [Pleroma.Captcha.Kocaptcha, Pleroma.Captcha.Native]
       },
       %{
         key: :seconds_valid,
@@ -1754,20 +1936,18 @@ config :pleroma, :config_description, [
       },
       %{
         key: :queues,
-        type: :keyword,
+        type: {:keyword, :integer},
         description:
           "Background jobs queues (keys: queues, values: max numbers of concurrent jobs)",
         suggestions: [
-          [
-            activity_expiration: 10,
-            background: 5,
-            federator_incoming: 50,
-            federator_outgoing: 50,
-            mailer: 10,
-            scheduled_activities: 10,
-            transmogrifier: 20,
-            web_push: 50
-          ]
+          activity_expiration: 10,
+          background: 5,
+          federator_incoming: 50,
+          federator_outgoing: 50,
+          mailer: 10,
+          scheduled_activities: 10,
+          transmogrifier: 20,
+          web_push: 50
         ],
         children: [
           %{
@@ -1830,13 +2010,11 @@ config :pleroma, :config_description, [
     children: [
       %{
         key: :retries,
-        type: :keyword,
+        type: {:keyword, :integer},
         description: "Max retry attempts for failed jobs, per `Oban` queue",
         suggestions: [
-          [
-            federator_incoming: 5,
-            federator_outgoing: 5
-          ]
+          federator_incoming: 5,
+          federator_outgoing: 5
         ]
       }
     ]
@@ -1845,22 +2023,22 @@ config :pleroma, :config_description, [
     group: :pleroma,
     key: Pleroma.Web.Metadata,
     type: :group,
-    decsription: "Metadata-related settings",
+    description: "Metadata-related settings",
     children: [
       %{
         key: :providers,
         type: {:list, :module},
         description: "List of metadata providers to enable",
         suggestions: [
-          [
-            Pleroma.Web.Metadata.Providers.OpenGraph,
-            Pleroma.Web.Metadata.Providers.TwitterCard,
-            Pleroma.Web.Metadata.Providers.RelMe
-          ]
+          Pleroma.Web.Metadata.Providers.OpenGraph,
+          Pleroma.Web.Metadata.Providers.TwitterCard,
+          Pleroma.Web.Metadata.Providers.RelMe,
+          Pleroma.Web.Metadata.Providers.Feed
         ]
       },
       %{
         key: :unfurl_nsfw,
+        label: "Unfurl NSFW",
         type: :boolean,
         description: "If set to true nsfw attachments will be shown in previews"
       }
@@ -1870,39 +2048,45 @@ config :pleroma, :config_description, [
     group: :pleroma,
     key: :rich_media,
     type: :group,
+    description:
+      "If enabled the instance will parse metadata from attached links to generate link previews.",
     children: [
       %{
         key: :enabled,
         type: :boolean,
-        description:
-          "if enabled the instance will parse metadata from attached links to generate link previews"
+        description: "Enables/disables RichMedia."
       },
       %{
         key: :ignore_hosts,
         type: {:list, :string},
-        description: "list of hosts which will be ignored by the metadata parser",
-        suggestions: [["accounts.google.com", "xss.website"]]
+        description: "List of hosts which will be ignored by the metadata parser.",
+        suggestions: ["accounts.google.com", "xss.website"]
       },
       %{
         key: :ignore_tld,
+        label: "Ignore TLD",
         type: {:list, :string},
-        description: "list TLDs (top-level domains) which will ignore for parse metadata",
-        suggestions: [["local", "localdomain", "lan"]]
+        description: "List TLDs (top-level domains) which will ignore for parse metadata.",
+        suggestions: ["local", "localdomain", "lan"]
       },
       %{
         key: :parsers,
         type: {:list, :module},
-        description: "list of Rich Media parsers",
+        description: "List of Rich Media parsers.",
         suggestions: [
-          Generator.richmedia_parsers()
+          Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
+          Pleroma.Web.RichMedia.Parsers.OEmbed,
+          Pleroma.Web.RichMedia.Parsers.OGP,
+          Pleroma.Web.RichMedia.Parsers.TwitterCard
         ]
       },
       %{
         key: :ttl_setters,
+        label: "TTL setters",
         type: {:list, :module},
-        description: "list of rich media ttl setters",
+        description: "List of rich media ttl setters.",
         suggestions: [
-          [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
+          Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl
         ]
       }
     ]
@@ -2055,23 +2239,57 @@ config :pleroma, :config_description, [
       },
       %{
         key: :ssl,
+        label: "SSL",
         type: :boolean,
         description: "true to use SSL, usually implies the port 636"
       },
       %{
         key: :sslopts,
+        label: "SSL options",
         type: :keyword,
-        description: "additional SSL options"
+        description: "additional SSL options",
+        suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer],
+        children: [
+          %{
+            key: :cacertfile,
+            type: :string,
+            description: "Path to file with PEM encoded cacerts",
+            suggestions: ["path/to/file/with/PEM/cacerts"]
+          },
+          %{
+            key: :verify,
+            type: :atom,
+            description: "Type of cert verification",
+            suggestions: [:verify_peer]
+          }
+        ]
       },
       %{
         key: :tls,
+        label: "TLS",
         type: :boolean,
         description: "true to start TLS, usually implies the port 389"
       },
       %{
         key: :tlsopts,
+        label: "TLS options",
         type: :keyword,
-        description: "additional TLS options"
+        description: "additional TLS options",
+        suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer],
+        children: [
+          %{
+            key: :cacertfile,
+            type: :string,
+            description: "Path to file with PEM encoded cacerts",
+            suggestions: ["path/to/file/with/PEM/cacerts"]
+          },
+          %{
+            key: :verify,
+            type: :atom,
+            description: "Type of cert verification",
+            suggestions: [:verify_peer]
+          }
+        ]
       },
       %{
         key: :base,
@@ -2120,7 +2338,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :oauth_consumer_strategies,
-        type: :string,
+        type: {:list, :string},
         description:
           "the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <>
             " Each entry in this space-delimited string should be of format <strategy> or <strategy>:<dependency>" <>
@@ -2163,7 +2381,7 @@ config :pleroma, :config_description, [
           },
           %{
             key: :interval,
-            type: :ininteger,
+            type: :integer,
             description: "Minimum interval between digest emails to one user",
             suggestions: [7]
           },
@@ -2185,9 +2403,9 @@ config :pleroma, :config_description, [
     children: [
       %{
         key: :logo,
-        type: [:string, nil],
+        type: :string,
         description: "a path to a custom logo. Set it to nil to use the default Pleroma logo",
-        suggestions: ["some/path/logo.png", nil]
+        suggestions: ["some/path/logo.png"]
       },
       %{
         key: :styling,
@@ -2279,26 +2497,23 @@ config :pleroma, :config_description, [
         key: :shortcode_globs,
         type: {:list, :string},
         description: "Location of custom emoji files. * can be used as a wildcard",
-        suggestions: [["/emoji/custom/**/*.png"]]
+        suggestions: ["/emoji/custom/**/*.png"]
       },
       %{
         key: :pack_extensions,
         type: {:list, :string},
         description:
           "A list of file extensions for emojis, when no emoji.txt for a pack is present",
-        suggestions: [[".png", ".gif"]]
+        suggestions: [".png", ".gif"]
       },
       %{
         key: :groups,
-        type: :keyword,
+        type: {:keyword, :string, {:list, :string}},
         description:
           "Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname" <>
             " and the value the location or array of locations. * can be used as a wildcard",
         suggestions: [
-          [
-            # Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
-            Custom: ["/emoji/*.png", "/emoji/**/*.png"]
-          ]
+          Custom: ["/emoji/*.png", "/emoji/**/*.png"]
         ]
       },
       %{
@@ -2311,6 +2526,7 @@ config :pleroma, :config_description, [
       },
       %{
         key: :shared_pack_cache_seconds_per_file,
+        label: "Shared pack cache s/file",
         type: :integer,
         descpiption:
           "When an emoji pack is shared, the archive is created and cached in memory" <>
@@ -2389,7 +2605,8 @@ config :pleroma, :config_description, [
     group: :esshd,
     type: :group,
     description:
-      "To enable simple command line interface accessible over ssh, add a setting like this to your configuration file",
+      "Before enabling this you must add :esshd to mix.exs as one of the extra_applications " <>
+        "and generate host keys in your priv dir with ssh-keygen -m PEM -N \"\" -b 2048 -t rsa -f ssh_host_rsa_key",
     children: [
       %{
         key: :enabled,
@@ -2443,27 +2660,27 @@ config :pleroma, :config_description, [
           %{
             key: "application/xml",
             type: {:list, :string},
-            suggestions: [["xml"]]
+            suggestions: ["xml"]
           },
           %{
             key: "application/xrd+xml",
             type: {:list, :string},
-            suggestions: [["xrd+xml"]]
+            suggestions: ["xrd+xml"]
           },
           %{
             key: "application/jrd+json",
             type: {:list, :string},
-            suggestions: [["jrd+json"]]
+            suggestions: ["jrd+json"]
           },
           %{
             key: "application/activity+json",
             type: {:list, :string},
-            suggestions: [["activity+json"]]
+            suggestions: ["activity+json"]
           },
           %{
             key: "application/ld+json",
             type: {:list, :string},
-            suggestions: [["activity+json"]]
+            suggestions: ["activity+json"]
           }
         ]
       }
@@ -2565,25 +2782,42 @@ config :pleroma, :config_description, [
     children: [
       %{
         key: :proxy_url,
-        type: [:string, :atom, nil],
-        suggestions: ["localhost:9020", {:socks5, :localhost, 3090}, nil]
+        label: "Proxy URL",
+        type: [:string, :tuple],
+        description: "Proxy URL",
+        suggestions: ["localhost:9020", {:socks5, :localhost, 3090}]
       },
       %{
         key: :send_user_agent,
         type: :boolean
       },
+      %{
+        key: :user_agent,
+        type: [:string, :atom],
+        description:
+          "What user agent to use. Must be a string or an atom `:default`. Default value is `:default`",
+        suggestions: ["Pleroma", :default]
+      },
       %{
         key: :adapter,
         type: :keyword,
-        suggestions: [
-          [
-            ssl_options: [
-              # Workaround for remote server certificate chain issues
-              partial_chain: &:hackney_connect.partial_chain/1,
-              # We don't support TLS v1.3 yet
-              versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
+        description: "Adapter specific options",
+        suggestions: [],
+        children: [
+          %{
+            key: :ssl_options,
+            type: :keyword,
+            label: "SSL Options",
+            description: "SSL options for HTTP adapter",
+            children: [
+              %{
+                key: :versions,
+                type: {:list, :atom},
+                description: "List of TLS version to use",
+                suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
+              }
             ]
-          ]
+          }
         ]
       }
     ]
@@ -2612,7 +2846,7 @@ config :pleroma, :config_description, [
       %{
         key: :scrub_policy,
         type: {:list, :module},
-        suggestions: [[Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]]
+        suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]
       }
     ]
   },
@@ -2630,6 +2864,8 @@ config :pleroma, :config_description, [
   %{
     group: :pleroma,
     key: :mrf_normalize_markup,
+    label: "MRF normalize markup",
+    description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
     type: :group,
     children: [
       %{
@@ -2648,38 +2884,36 @@ config :pleroma, :config_description, [
         key: :restricted_nicknames,
         type: {:list, :string},
         suggestions: [
-          [
-            ".well-known",
-            "~",
-            "about",
-            "activities",
-            "api",
-            "auth",
-            "check_password",
-            "dev",
-            "friend-requests",
-            "inbox",
-            "internal",
-            "main",
-            "media",
-            "nodeinfo",
-            "notice",
-            "oauth",
-            "objects",
-            "ostatus_subscribe",
-            "pleroma",
-            "proxy",
-            "push",
-            "registration",
-            "relay",
-            "settings",
-            "status",
-            "tag",
-            "user-search",
-            "user_exists",
-            "users",
-            "web"
-          ]
+          ".well-known",
+          "~",
+          "about",
+          "activities",
+          "api",
+          "auth",
+          "check_password",
+          "dev",
+          "friend-requests",
+          "inbox",
+          "internal",
+          "main",
+          "media",
+          "nodeinfo",
+          "notice",
+          "oauth",
+          "objects",
+          "ostatus_subscribe",
+          "pleroma",
+          "proxy",
+          "push",
+          "registration",
+          "relay",
+          "settings",
+          "status",
+          "tag",
+          "user-search",
+          "user_exists",
+          "users",
+          "web"
         ]
       }
     ]
@@ -2696,20 +2930,18 @@ config :pleroma, :config_description, [
       %{
         key: :methods,
         type: {:list, :string},
-        suggestions: [["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]]
+        suggestions: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]
       },
       %{
         key: :expose,
-        type: :string,
+        type: {:list, :string},
         suggestions: [
-          [
-            "Link",
-            "X-RateLimit-Reset",
-            "X-RateLimit-Limit",
-            "X-RateLimit-Remaining",
-            "X-Request-Id",
-            "Idempotency-Key"
-          ]
+          "Link",
+          "X-RateLimit-Reset",
+          "X-RateLimit-Limit",
+          "X-RateLimit-Remaining",
+          "X-Request-Id",
+          "Idempotency-Key"
         ]
       },
       %{
@@ -2719,7 +2951,7 @@ config :pleroma, :config_description, [
       %{
         key: :headers,
         type: {:list, :string},
-        suggestions: [["Authorization", "Content-Type", "Idempotency-Key"]]
+        suggestions: ["Authorization", "Content-Type", "Idempotency-Key"]
       }
     ]
   },
@@ -2728,16 +2960,14 @@ config :pleroma, :config_description, [
     key: Pleroma.Plugs.RemoteIp,
     type: :group,
     description: """
-    **If your instance is not behind at least one reverse proxy, you should not enable this plug.**
-
     `Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+    **If your instance is not behind at least one reverse proxy, you should not enable this plug.**
     """,
     children: [
       %{
         key: :enabled,
         type: :boolean,
-        description: "Enable/disable the plug. Defaults to `false`.",
-        suggestions: [true, false]
+        description: "Enable/disable the plug. Defaults to `false`."
       },
       %{
         key: :headers,
@@ -2771,7 +3001,7 @@ config :pleroma, :config_description, [
         type: :integer,
         description:
           "activity pub routes (except question activities). Defaults to `nil` (no expiration).",
-        suggestions: [30_000, nil]
+        suggestions: [30_000]
       },
       %{
         key: :activity_pub_question,
@@ -2781,5 +3011,95 @@ config :pleroma, :config_description, [
         suggestions: [30_000]
       }
     ]
+  },
+  %{
+    group: :pleroma,
+    key: :static_fe,
+    type: :group,
+    description:
+      "Render profiles and posts using server-generated HTML that is viewable without using JavaScript.",
+    children: [
+      %{
+        key: :enabled,
+        type: :boolean,
+        description: "Enables the rendering of static HTML. Defaults to `false`."
+      }
+    ]
+  },
+  %{
+    group: :pleroma,
+    key: :feed,
+    type: :group,
+    description: "Configure feed rendering.",
+    children: [
+      %{
+        key: :post_title,
+        type: :map,
+        description: "Configure title rendering.",
+        children: [
+          %{
+            key: :max_length,
+            type: :integer,
+            description: "Maximum number of characters before truncating title.",
+            suggestions: [100]
+          },
+          %{
+            key: :omission,
+            type: :string,
+            description: "Replacement which will be used after truncating string.",
+            suggestions: ["..."]
+          }
+        ]
+      }
+    ]
+  },
+  %{
+    group: :pleroma,
+    key: :mrf_object_age,
+    type: :group,
+    description: "Rejects or delists posts based on their age when received.",
+    children: [
+      %{
+        key: :threshold,
+        type: :integer,
+        description: "Required age (in seconds) of a post before actions are taken.",
+        suggestions: [172_800]
+      },
+      %{
+        key: :actions,
+        type: {:list, :atom},
+        description:
+          "A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
+            "`:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines; " <>
+            "`:reject` rejects the message entirely",
+        suggestions: [:delist, :strip_followers, :reject]
+      }
+    ]
+  },
+  %{
+    group: :pleroma,
+    key: :modules,
+    type: :group,
+    description: "Custom Runtime Modules.",
+    children: [
+      %{
+        key: :runtime_dir,
+        type: :string,
+        description: "A path to custom Elixir modules (such as MRF policies)."
+      }
+    ]
+  },
+  %{
+    group: :pleroma,
+    type: :group,
+    description: "Allow instance configuration from database.",
+    children: [
+      %{
+        key: :configurable_from_database,
+        type: :boolean,
+        description:
+          "Allow transferring configuration to DB with the subsequent customization from Admin api. Defaults to `false`"
+      }
+    ]
   }
 ]
diff --git a/config/releases.exs b/config/releases.exs
index b224960db..19636765f 100644
--- a/config/releases.exs
+++ b/config/releases.exs
@@ -6,6 +6,8 @@ config :pleroma, :modules, runtime_dir: "/var/lib/pleroma/modules"
 
 config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
 
+config :pleroma, release: true, config_path: config_path
+
 if File.exists?(config_path) do
   import_config config_path
 else
@@ -18,3 +20,12 @@ else
 
   IO.puts(warning)
 end
+
+exported_config =
+  config_path
+  |> Path.dirname()
+  |> Path.join("prod.exported_from_db.secret.exs")
+
+if File.exists?(exported_config) do
+  import_config exported_config
+end
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index d98a78af0..07aa7ec3f 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -3,7 +3,7 @@
 Authentication is required and the user must be an admin.
 
 Configuration options:
- 
+
 * `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle.
     If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes).
     If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions.
@@ -665,27 +665,16 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
     - 404 Not Found `"Not found"`
   - On success: 200 OK `{}`
 
-## `GET /api/pleroma/admin/config/migrate_to_db`
-
-### Run mix task pleroma.config migrate_to_db
-
-Copy settings on key `:pleroma` to DB.
-
-- Params: none
-- Response:
-
-```json
-{}
-```
-
 ## `GET /api/pleroma/admin/config/migrate_from_db`
 
 ### Run mix task pleroma.config migrate_from_db
 
-Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
+Copies all settings from database to `config/{env}.exported_from_db.secret.exs` with deletion from the table. Where `{env}` is the environment in which `pleroma` is running.
 
 - Params: none
 - Response:
+  - On failure:
+    - 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
 
 ```json
 {}
@@ -693,20 +682,24 @@ Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with dele
 
 ## `GET /api/pleroma/admin/config`
 
-### List config settings
+### Get list of merged default settings with saved in database.
 
-List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
+**Only works when configuration from database is enabled.**
 
-- Params: none
+- Params:
+  - `only_db`: true (*optional*, get only saved in database settings)
 - Response:
+  - On failure:
+    - 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
+    - 400 Bad Request `"To use configuration from database migrate your settings to database."`
 
 ```json
 {
   configs: [
     {
-      "group": string,
-      "key": string or string with leading `:` for atoms,
-      "value": string or {} or [] or {"tuple": []}
+      "group": ":pleroma",
+      "key": "Pleroma.Upload",
+      "value": []
      }
   ]
 }
@@ -716,44 +709,107 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur
 
 ### Update config settings
 
-Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
-Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`.
-Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`.
-Tuples can be passed as `{"tuple": ["first_val", Pleroma.Module, []]}`.
-`{"tuple": ["some_string", "Pleroma.Some.Module", []]}` will be converted to `{"some_string", Pleroma.Some.Module, []}`.
-Keywords can be passed as lists with 2 child tuples, e.g.
-`[{"tuple": ["first_val", Pleroma.Module]}, {"tuple": ["second_val", true]}]`.
+**Only works when configuration from database is enabled.**
 
-If value contains list of settings `[subkey: val1, subkey2: val2, subkey3: val3]`, it's possible to remove only subkeys instead of all settings passing `subkeys` parameter. E.g.:
-{"group": "pleroma", "key": "some_key", "delete": "true", "subkeys": [":subkey", ":subkey3"]}.
+Some modifications are necessary to save the config settings correctly:
 
-Compile time settings (need instance reboot):
-- all settings by this keys:
+- strings which start with `Pleroma.`, `Phoenix.`, `Tesla.` or strings like `Oban`, `Ueberauth` will be converted to modules;
+```
+"Pleroma.Upload" -> Pleroma.Upload
+"Oban" -> Oban
+```
+- strings starting with `:` will be converted to atoms;
+```
+":pleroma" -> :pleroma
+```
+- objects with `tuple` key and array value will be converted to tuples;
+```
+{"tuple": ["string", "Pleroma.Upload", []]} -> {"string", Pleroma.Upload, []}
+```
+- arrays with *tuple objects* will be converted to keywords;
+```
+[{"tuple": [":key1", "value"]}, {"tuple": [":key2", "value"]}] -> [key1: "value", key2: "value"]
+```
+
+Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as:
+- all settings inside these keys:
   - `:hackney_pools`
   - `:chat`
-  - `Pleroma.Web.Endpoint`
-  - `Pleroma.Repo`
-- part settings:
-  - `Pleroma.Captcha` -> `:seconds_valid`
-  - `Pleroma.Upload` -> `:proxy_remote`
-  - `:instance` -> `:upload_limit`
+- partially settings inside these keys:
+  - `:seconds_valid` in `Pleroma.Captcha`
+  - `:proxy_remote` in `Pleroma.Upload`
+  - `:upload_limit` in `:instance`
 
 - Params:
-  - `configs` => [
-    - `group` (string)
-    - `key` (string or string with leading `:` for atoms)
-    - `value` (string, [], {} or {"tuple": []})
-    - `delete` = true (optional, if parameter must be deleted)
-    - `subkeys` [(string with leading `:` for atoms)] (optional, works only if `delete=true` parameter is passed, otherwise will be ignored)
-  ]
+  - `configs` - array of config objects
+  - config object params:
+    - `group` - string (**required**)
+    - `key` - string (**required**)
+    - `value` - string, [], {} or {"tuple": []} (**required**)
+    - `delete` - true (*optional*, if setting must be deleted)
+    - `subkeys` - array of strings (*optional*, only works when `delete=true` parameter is passed, otherwise will be ignored)
 
-- Request (example):
+*When a value have several nested settings, you can delete only some nested settings by passing a parameter `subkeys`, without deleting all settings by key.*
+```
+[subkey: val1, subkey2: val2, subkey3: val3] \\ initial value
+{"group": ":pleroma", "key": "some_key", "delete": true, "subkeys": [":subkey", ":subkey3"]} \\ passing json for deletion
+[subkey2: val2] \\ value after deletion
+```
+
+*Most of the settings can be partially updated through merge old values with new values, except settings value of which is list or is not keyword.*
+
+Example of setting without keyword in value:
+```elixir
+config :tesla, :adapter, Tesla.Adapter.Hackney
+```
+
+List of settings which support only full update by key:
+```elixir
+@full_key_update [
+    {:pleroma, :ecto_repos},
+    {:quack, :meta},
+    {:mime, :types},
+    {:cors_plug, [:max_age, :methods, :expose, :headers]},
+    {:auto_linker, :opts},
+    {:swarm, :node_blacklist},
+    {:logger, :backends}
+  ]
+```
+
+List of settings which support only full update by subkey:
+```elixir
+@full_subkey_update [
+    {:pleroma, :assets, :mascots},
+    {:pleroma, :emoji, :groups},
+    {:pleroma, :workers, :retries},
+    {:pleroma, :mrf_subchain, :match_actor},
+    {:pleroma, :mrf_keyword, :replace}
+  ]
+```
+
+*Settings without explicit key must be sended in separate config object params.*
+```elixir
+config :quack,
+  level: :debug,
+  meta: [:all],
+  ...
+```
+```json
+{
+  configs: [
+    {"group": ":quack", "key": ":level", "value": ":debug"},
+    {"group": ":quack", "key": ":meta", "value": [":all"]},
+    ...
+  ]
+}
+```
+- Request:
 
 ```json
 {
   configs: [
     {
-      "group": "pleroma",
+      "group": ":pleroma",
       "key": "Pleroma.Upload",
       "value": [
         {"tuple": [":uploader", "Pleroma.Uploaders.Local"]},
@@ -763,7 +819,7 @@ Compile time settings (need instance reboot):
         {"tuple": [":proxy_opts", [
           {"tuple": [":redirect_on_failure", false]},
           {"tuple": [":max_body_length", 1048576]},
-          {"tuple": [":http": [
+          {"tuple": [":http", [
             {"tuple": [":follow_redirect", true]},
             {"tuple": [":pool", ":upload"]},
           ]]}
@@ -779,19 +835,53 @@ Compile time settings (need instance reboot):
 ```
 
 - Response:
-
+  - On failure:
+    - 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
 ```json
 {
   configs: [
     {
-      "group": string,
-      "key": string or string with leading `:` for atoms,
-      "value": string or {} or [] or {"tuple": []}
+      "group": ":pleroma",
+      "key": "Pleroma.Upload",
+      "value": [...]
      }
   ]
 }
 ```
 
+## ` GET /api/pleroma/admin/config/descriptions`
+
+### Get JSON with config descriptions.
+Loads json generated from `config/descriptions.exs`.
+
+- Params: none
+- Response:
+
+```json
+[{
+    "group": ":pleroma", // string
+    "key": "ModuleName", // string
+    "type": "group", // string or list with possible values,
+    "description": "Upload general settings", // string
+    "children": [
+      {
+        "key": ":uploader", // string or module name `Pleroma.Upload`
+        "type": "module",
+        "description": "Module which will be used for uploads",
+        "suggestions": ["module1", "module2"]
+      },
+      {
+        "key": ":filters",
+        "type": ["list", "module"],
+        "description": "List of filter modules for uploads",
+        "suggestions": [
+          "module1", "module2", "module3"
+        ]
+      }
+    ]
+}]
+```
+
 ## `GET /api/pleroma/admin/moderation_log`
 
 ### Get moderation log
diff --git a/docs/admin/config.md b/docs/admin/config.md
new file mode 100644
index 000000000..35e43b6a9
--- /dev/null
+++ b/docs/admin/config.md
@@ -0,0 +1,79 @@
+# Configuring instance
+You can configure your instance from admin interface. You need account with admin rights and little change in config file, which will allow settings configuration from database.
+
+```elixir
+config :pleroma, configurable_from_database: true
+```
+
+## How it works
+Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot. These settings are needed in `compile time`, that's why settings are duplicated to the file.
+
+File with duplicated settings is located in `config/{env}.exported_from_db.exs` if pleroma is runned from source. For prod env it will be `config/prod.exported_from_db.exs`.
+
+For releases: `/etc/pleroma/prod.exported_from_db.secret.exs` or `PLEROMA_CONFIG_PATH/prod.exported_from_db.exs`.
+
+## How to set it up
+You need to migrate your existing settings to the database. This task will migrate only added by user settings.
+For example you add settings to `prod.secret.exs` file, only these settings will be migrated to database. For release it will be `/etc/pleroma/config.exs` or `PLEROMA_CONFIG_PATH`.
+You can do this with mix task (all config files will remain untouched):
+
+```sh tab="OTP"
+ ./bin/pleroma_ctl config migrate_to_db
+```
+
+```sh tab="From Source"
+mix pleroma.config migrate_to_db
+```
+
+Now you can change settings in admin interface. After each save, settings from database are duplicated to the `config/{env}.exported_from_db.exs` file.
+
+<span style="color:red">**ATTENTION**</span>
+
+**<span style="color:red">Be careful while changing the settings. Every inaccurate configuration change can break the federation or the instance load.</span>**
+
+*Compile time settings, which require instance reboot and can break instance loading:*
+- all settings inside these keys:
+  - `:hackney_pools`
+  - `:chat`
+- partially settings inside these keys:
+  - `:seconds_valid` in `Pleroma.Captcha`
+  - `:proxy_remote` in `Pleroma.Upload`
+  - `:upload_limit` in `:instance`
+
+## How to dump settings from database to file
+
+*Adding `-d` flag will delete migrated settings from database table.*
+
+```sh tab="OTP"
+ ./bin/pleroma_ctl config migrate_from_db [-d]
+```
+
+```sh tab="From Source"
+mix pleroma.config migrate_from_db [-d]
+```
+
+
+## How to completely remove it
+
+1. Truncate or delete all values from `config` table
+```sql
+TRUNCATE TABLE config;
+```
+2. Delete `config/{env}.exported_from_db.exs`.
+
+For `prod` env:
+```bash
+cd /opt/pleroma
+cp config/prod.exported_from_db.exs config/exported_from_db.back
+rm -rf config/prod.exported_from_db.exs
+```
+*If you don't want to backup settings, you can skip step with `cp` command.*
+
+3. Set configurable_from_database to `false`.
+```elixir
+config :pleroma, configurable_from_database: false
+```
+4. Restart pleroma instance
+```bash
+sudo service pleroma restart
+```
diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md
index e9d44b9a4..2af51c247 100644
--- a/docs/administration/CLI_tasks/config.md
+++ b/docs/administration/CLI_tasks/config.md
@@ -18,11 +18,11 @@ mix pleroma.config migrate_to_db
 
 ## Transfer config from DB to `config/env.exported_from_db.secret.exs`
 
+To delete transfered settings from database optional flag `-d` can be used. <env> is `prod` by default.
 ```sh tab="OTP"
- ./bin/pleroma_ctl config migrate_from_db <env>
+ ./bin/pleroma_ctl config migrate_from_db [--env=<env>] [-d]
 ```
 
 ```sh tab="From Source"
-mix pleroma.config migrate_from_db <env>
+mix pleroma.config migrate_from_db [--env=<env>] [-d]
 ```
-
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index cad3af68d..30d673eba 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -70,11 +70,6 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic
 * `account_field_value_length`: An account field value maximum length (default: `2048`).
 * `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
 
-!!! danger
-    This is a Work In Progress, not usable just yet
-
-* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
-
 ## Federation
 ### MRF policies
 
@@ -355,7 +350,7 @@ Available caches:
 
 * `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
 * `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
-* `user_agent`: what user agent should  we use? (default: `:default`), must be string or `:default`
+* `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
 * `adapter`: array of hackney options
 
 
@@ -841,3 +836,7 @@ config :auto_linker,
 ## Custom Runtime Modules (`:modules`)
 
 * `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
+
+
+## :configurable_from_database
+Enable/disable configuration from database.
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 590c7a914..861832451 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -4,71 +4,144 @@
 
 defmodule Mix.Tasks.Pleroma.Config do
   use Mix.Task
+
   import Mix.Pleroma
+
+  alias Pleroma.ConfigDB
   alias Pleroma.Repo
-  alias Pleroma.Web.AdminAPI.Config
+
   @shortdoc "Manages the location of the config"
   @moduledoc File.read!("docs/administration/CLI_tasks/config.md")
+
   def run(["migrate_to_db"]) do
     start_pleroma()
-
-    if Pleroma.Config.get([:instance, :dynamic_configuration]) do
-      Application.get_all_env(:pleroma)
-      |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end)
-      |> Enum.each(fn {k, v} ->
-        key = to_string(k) |> String.replace("Elixir.", "")
-
-        key =
-          if String.starts_with?(key, "Pleroma.") do
-            key
-          else
-            ":" <> key
-          end
-
-        {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v})
-        Mix.shell().info("#{key} is migrated.")
-      end)
-
-      Mix.shell().info("Settings migrated.")
-    else
-      Mix.shell().info(
-        "Migration is not allowed by config. You can change this behavior in instance settings."
-      )
-    end
+    migrate_to_db()
   end
 
-  def run(["migrate_from_db", env, delete?]) do
+  def run(["migrate_from_db" | options]) do
     start_pleroma()
 
-    delete? = if delete? == "true", do: true, else: false
-
-    if Pleroma.Config.get([:instance, :dynamic_configuration]) do
-      config_path = "config/#{env}.exported_from_db.secret.exs"
-
-      {:ok, file} = File.open(config_path, [:write, :utf8])
-      IO.write(file, "use Mix.Config\r\n")
-
-      Repo.all(Config)
-      |> Enum.each(fn config ->
-        IO.write(
-          file,
-          "config :#{config.group}, #{config.key}, #{
-            inspect(Config.from_binary(config.value), limit: :infinity)
-          }\r\n\r\n"
-        )
-
-        if delete? do
-          {:ok, _} = Repo.delete(config)
-          Mix.shell().info("#{config.key} deleted from DB.")
-        end
-      end)
-
-      File.close(file)
-      System.cmd("mix", ["format", config_path])
-    else
-      Mix.shell().info(
-        "Migration is not allowed by config. You can change this behavior in instance settings."
+    {opts, _} =
+      OptionParser.parse!(options,
+        strict: [env: :string, delete: :boolean],
+        aliases: [d: :delete]
       )
+
+    migrate_from_db(opts)
+  end
+
+  @spec migrate_to_db(Path.t() | nil) :: any()
+  def migrate_to_db(file_path \\ nil) do
+    if Pleroma.Config.get([:configurable_from_database]) do
+      config_file =
+        if file_path do
+          file_path
+        else
+          if Pleroma.Config.get(:release) do
+            Pleroma.Config.get(:config_path)
+          else
+            "config/#{Pleroma.Config.get(:env)}.secret.exs"
+          end
+        end
+
+      do_migrate_to_db(config_file)
+    else
+      migration_error()
     end
   end
+
+  defp do_migrate_to_db(config_file) do
+    if File.exists?(config_file) do
+      custom_config =
+        config_file
+        |> read_file()
+        |> elem(0)
+
+      custom_config
+      |> Keyword.keys()
+      |> Enum.each(&create(&1, custom_config))
+    else
+      shell_info("To migrate settings, you must define custom settings in #{config_file}.")
+    end
+  end
+
+  defp create(group, settings) do
+    group
+    |> Pleroma.Config.Loader.filter_group(settings)
+    |> Enum.each(fn {key, value} ->
+      key = inspect(key)
+      {:ok, _} = ConfigDB.update_or_create(%{group: inspect(group), key: key, value: value})
+
+      shell_info("Settings for key #{key} migrated.")
+    end)
+
+    shell_info("Settings for group :#{group} migrated.")
+  end
+
+  defp migrate_from_db(opts) do
+    if Pleroma.Config.get([:configurable_from_database]) do
+      env = opts[:env] || "prod"
+
+      config_path =
+        if Pleroma.Config.get(:release) do
+          :config_path
+          |> Pleroma.Config.get()
+          |> Path.dirname()
+        else
+          "config"
+        end
+        |> Path.join("#{env}.exported_from_db.secret.exs")
+
+      file = File.open!(config_path, [:write, :utf8])
+
+      IO.write(file, config_header())
+
+      ConfigDB
+      |> Repo.all()
+      |> Enum.each(&write_and_delete(&1, file, opts[:delete]))
+
+      :ok = File.close(file)
+      System.cmd("mix", ["format", config_path])
+    else
+      migration_error()
+    end
+  end
+
+  defp migration_error do
+    shell_error(
+      "Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true."
+    )
+  end
+
+  if Code.ensure_loaded?(Config.Reader) do
+    defp config_header, do: "import Config\r\n\r\n"
+    defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
+  else
+    defp config_header, do: "use Mix.Config\r\n\r\n"
+    defp read_file(config_file), do: Mix.Config.eval!(config_file)
+  end
+
+  defp write_and_delete(config, file, delete?) do
+    config
+    |> write(file)
+    |> delete(delete?)
+  end
+
+  defp write(config, file) do
+    value =
+      config.value
+      |> ConfigDB.from_binary()
+      |> inspect(limit: :infinity)
+
+    IO.write(file, "config #{config.group}, #{config.key}, #{value}\r\n\r\n")
+
+    config
+  end
+
+  defp delete(config, true) do
+    {:ok, _} = Repo.delete(config)
+    shell_info("#{config.key} deleted from DB.")
+  end
+
+  defp delete(_config, _), do: :ok
 end
diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex
index 0d2663648..3c870f876 100644
--- a/lib/mix/tasks/pleroma/docs.ex
+++ b/lib/mix/tasks/pleroma/docs.ex
@@ -28,7 +28,7 @@ defmodule Mix.Tasks.Pleroma.Docs do
   defp do_run(implementation) do
     start_pleroma()
 
-    with {descriptions, _paths} <- Mix.Config.eval!("config/description.exs"),
+    with descriptions <- Pleroma.Config.Loader.load("config/description.exs"),
          {:ok, file_path} <-
            Pleroma.Docs.Generator.process(
              implementation,
diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex
new file mode 100644
index 000000000..91a1aa0cc
--- /dev/null
+++ b/lib/pleroma/config/config_db.ex
@@ -0,0 +1,422 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ConfigDB do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+  import Ecto.Query
+  import Pleroma.Web.Gettext
+
+  alias __MODULE__
+  alias Pleroma.Repo
+
+  @type t :: %__MODULE__{}
+
+  @full_key_update [
+    {:pleroma, :ecto_repos},
+    {:quack, :meta},
+    {:mime, :types},
+    {:cors_plug, [:max_age, :methods, :expose, :headers]},
+    {:auto_linker, :opts},
+    {:swarm, :node_blacklist},
+    {:logger, :backends}
+  ]
+
+  @full_subkey_update [
+    {:pleroma, :assets, :mascots},
+    {:pleroma, :emoji, :groups},
+    {:pleroma, :workers, :retries},
+    {:pleroma, :mrf_subchain, :match_actor},
+    {:pleroma, :mrf_keyword, :replace}
+  ]
+
+  @regex ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
+
+  @delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
+
+  schema "config" do
+    field(:key, :string)
+    field(:group, :string)
+    field(:value, :binary)
+    field(:db, {:array, :string}, virtual: true, default: [])
+
+    timestamps()
+  end
+
+  @spec get_all_as_keyword() :: keyword()
+  def get_all_as_keyword do
+    ConfigDB
+    |> select([c], {c.group, c.key, c.value})
+    |> Repo.all()
+    |> Enum.reduce([], fn {group, key, value}, acc ->
+      group = ConfigDB.from_string(group)
+      key = ConfigDB.from_string(key)
+      value = from_binary(value)
+
+      Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
+    end)
+  end
+
+  @spec get_by_params(map()) :: ConfigDB.t() | nil
+  def get_by_params(params), do: Repo.get_by(ConfigDB, params)
+
+  @spec changeset(ConfigDB.t(), map()) :: Changeset.t()
+  def changeset(config, params \\ %{}) do
+    params = Map.put(params, :value, transform(params[:value]))
+
+    config
+    |> cast(params, [:key, :group, :value])
+    |> validate_required([:key, :group, :value])
+    |> unique_constraint(:key, name: :config_group_key_index)
+  end
+
+  @spec create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+  def create(params) do
+    %ConfigDB{}
+    |> changeset(params)
+    |> Repo.insert()
+  end
+
+  @spec update(ConfigDB.t(), map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+  def update(%ConfigDB{} = config, %{value: value}) do
+    config
+    |> changeset(%{value: value})
+    |> Repo.update()
+  end
+
+  @spec get_db_keys(ConfigDB.t()) :: [String.t()]
+  def get_db_keys(%ConfigDB{} = config) do
+    config.value
+    |> ConfigDB.from_binary()
+    |> get_db_keys(config.key)
+  end
+
+  @spec get_db_keys(keyword(), any()) :: [String.t()]
+  def get_db_keys(value, key) do
+    if Keyword.keyword?(value) do
+      value |> Keyword.keys() |> Enum.map(&convert(&1))
+    else
+      [convert(key)]
+    end
+  end
+
+  @spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
+  def merge_group(group, key, old_value, new_value) do
+    new_keys = to_map_set(new_value)
+
+    intersect_keys =
+      old_value |> to_map_set() |> MapSet.intersection(new_keys) |> MapSet.to_list()
+
+    merged_value = ConfigDB.merge(old_value, new_value)
+
+    @full_subkey_update
+    |> Enum.map(fn
+      {g, k, subkey} when g == group and k == key ->
+        if subkey in intersect_keys, do: subkey, else: []
+
+      _ ->
+        []
+    end)
+    |> List.flatten()
+    |> Enum.reduce(merged_value, fn subkey, acc ->
+      Keyword.put(acc, subkey, new_value[subkey])
+    end)
+  end
+
+  defp to_map_set(keyword) do
+    keyword
+    |> Keyword.keys()
+    |> MapSet.new()
+  end
+
+  @spec sub_key_full_update?(atom(), atom(), [Keyword.key()]) :: boolean()
+  def sub_key_full_update?(group, key, subkeys) do
+    Enum.any?(@full_subkey_update, fn {g, k, subkey} ->
+      g == group and k == key and subkey in subkeys
+    end)
+  end
+
+  @spec merge(keyword(), keyword()) :: keyword()
+  def merge(config1, config2) when is_list(config1) and is_list(config2) do
+    Keyword.merge(config1, config2, fn _, app1, app2 ->
+      if Keyword.keyword?(app1) and Keyword.keyword?(app2) do
+        Keyword.merge(app1, app2, &deep_merge/3)
+      else
+        app2
+      end
+    end)
+  end
+
+  defp deep_merge(_key, value1, value2) do
+    if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
+      Keyword.merge(value1, value2, &deep_merge/3)
+    else
+      value2
+    end
+  end
+
+  @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+  def update_or_create(params) do
+    search_opts = Map.take(params, [:group, :key])
+
+    with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
+         {:partial_update, true, config} <-
+           {:partial_update, can_be_partially_updated?(config), config},
+         old_value <- from_binary(config.value),
+         transformed_value <- do_transform(params[:value]),
+         {:can_be_merged, true, config} <- {:can_be_merged, is_list(transformed_value), config},
+         new_value <-
+           merge_group(
+             ConfigDB.from_string(config.group),
+             ConfigDB.from_string(config.key),
+             old_value,
+             transformed_value
+           ) do
+      ConfigDB.update(config, %{value: new_value})
+    else
+      {reason, false, config} when reason in [:partial_update, :can_be_merged] ->
+        ConfigDB.update(config, params)
+
+      nil ->
+        ConfigDB.create(params)
+    end
+  end
+
+  defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
+
+  defp only_full_update?(%ConfigDB{} = config) do
+    config_group = ConfigDB.from_string(config.group)
+    config_key = ConfigDB.from_string(config.key)
+
+    Enum.any?(@full_key_update, fn
+      {group, key} when is_list(key) ->
+        config_group == group and config_key in key
+
+      {group, key} ->
+        config_group == group and config_key == key
+    end)
+  end
+
+  @spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+  def delete(params) do
+    search_opts = Map.delete(params, :subkeys)
+
+    with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
+         {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
+         old_value <- from_binary(config.value),
+         keys <- Enum.map(sub_keys, &do_transform_string(&1)),
+         {:partial_remove, config, new_value} when new_value != [] <-
+           {:partial_remove, config, Keyword.drop(old_value, keys)} do
+      ConfigDB.update(config, %{value: new_value})
+    else
+      {:partial_remove, config, []} ->
+        Repo.delete(config)
+
+      {config, nil} ->
+        Repo.delete(config)
+
+      nil ->
+        err =
+          dgettext("errors", "Config with params %{params} not found", params: inspect(params))
+
+        {:error, err}
+    end
+  end
+
+  @spec from_binary(binary()) :: term()
+  def from_binary(binary), do: :erlang.binary_to_term(binary)
+
+  @spec from_binary_with_convert(binary()) :: any()
+  def from_binary_with_convert(binary) do
+    binary
+    |> from_binary()
+    |> do_convert()
+  end
+
+  @spec from_string(String.t()) :: atom() | no_return()
+  def from_string(":" <> entity), do: String.to_existing_atom(entity)
+
+  def from_string(entity) when is_binary(entity) do
+    if is_module_name?(entity) do
+      String.to_existing_atom("Elixir.#{entity}")
+    else
+      entity
+    end
+  end
+
+  @spec convert(any()) :: any()
+  def convert(entity), do: do_convert(entity)
+
+  defp do_convert(entity) when is_list(entity) do
+    for v <- entity, into: [], do: do_convert(v)
+  end
+
+  defp do_convert(%Regex{} = entity), do: inspect(entity)
+
+  defp do_convert(entity) when is_map(entity) do
+    for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
+  end
+
+  defp do_convert({:proxy_url, {type, :localhost, port}}) do
+    %{"tuple" => [":proxy_url", %{"tuple" => [do_convert(type), "localhost", port]}]}
+  end
+
+  defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do
+    ip =
+      host
+      |> :inet_parse.ntoa()
+      |> to_string()
+
+    %{
+      "tuple" => [
+        ":proxy_url",
+        %{"tuple" => [do_convert(type), ip, port]}
+      ]
+    }
+  end
+
+  defp do_convert({:proxy_url, {type, host, port}}) do
+    %{
+      "tuple" => [
+        ":proxy_url",
+        %{"tuple" => [do_convert(type), to_string(host), port]}
+      ]
+    }
+  end
+
+  defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
+
+  defp do_convert(entity) when is_tuple(entity) do
+    value =
+      entity
+      |> Tuple.to_list()
+      |> do_convert()
+
+    %{"tuple" => value}
+  end
+
+  defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
+    entity
+  end
+
+  defp do_convert(entity)
+       when is_atom(entity) and entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
+    ":#{entity}"
+  end
+
+  defp do_convert(entity) when is_atom(entity), do: inspect(entity)
+
+  defp do_convert(entity) when is_binary(entity), do: entity
+
+  @spec transform(any()) :: binary() | no_return()
+  def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
+    entity
+    |> do_transform()
+    |> to_binary()
+  end
+
+  def transform(entity), do: to_binary(entity)
+
+  @spec transform_with_out_binary(any()) :: any()
+  def transform_with_out_binary(entity), do: do_transform(entity)
+
+  @spec to_binary(any()) :: binary()
+  def to_binary(entity), do: :erlang.term_to_binary(entity)
+
+  defp do_transform(%Regex{} = entity), do: entity
+
+  defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
+    {:proxy_url, {do_transform_string(type), parse_host(host), port}}
+  end
+
+  defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
+    {partial_chain, []} =
+      entity
+      |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
+      |> Code.eval_string()
+
+    {:partial_chain, partial_chain}
+  end
+
+  defp do_transform(%{"tuple" => entity}) do
+    Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
+  end
+
+  defp do_transform(entity) when is_map(entity) do
+    for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
+  end
+
+  defp do_transform(entity) when is_list(entity) do
+    for v <- entity, into: [], do: do_transform(v)
+  end
+
+  defp do_transform(entity) when is_binary(entity) do
+    entity
+    |> String.trim()
+    |> do_transform_string()
+  end
+
+  defp do_transform(entity), do: entity
+
+  defp parse_host("localhost"), do: :localhost
+
+  defp parse_host(host) do
+    charlist = to_charlist(host)
+
+    case :inet.parse_address(charlist) do
+      {:error, :einval} ->
+        charlist
+
+      {:ok, ip} ->
+        ip
+    end
+  end
+
+  defp find_valid_delimiter([], _string, _) do
+    raise(ArgumentError, message: "valid delimiter for Regex expression not found")
+  end
+
+  defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter)
+       when is_tuple(delimiter) do
+    if String.contains?(pattern, closing) do
+      find_valid_delimiter(others, pattern, regex_delimiter)
+    else
+      {:ok, {leading, closing}}
+    end
+  end
+
+  defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
+    if String.contains?(pattern, delimiter) do
+      find_valid_delimiter(others, pattern, regex_delimiter)
+    else
+      {:ok, {delimiter, delimiter}}
+    end
+  end
+
+  defp do_transform_string("~r" <> _pattern = regex) do
+    with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
+           Regex.named_captures(@regex, regex),
+         {:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter),
+         {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
+      result
+    end
+  end
+
+  defp do_transform_string(":" <> atom), do: String.to_atom(atom)
+
+  defp do_transform_string(value) do
+    if is_module_name?(value) do
+      String.to_existing_atom("Elixir." <> value)
+    else
+      value
+    end
+  end
+
+  @spec is_module_name?(String.t()) :: boolean()
+  def is_module_name?(string) do
+    Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth)\./, string) or
+      string in ["Oban", "Ueberauth", "ExSyslogger"]
+  end
+end
diff --git a/lib/pleroma/config/holder.ex b/lib/pleroma/config/holder.ex
new file mode 100644
index 000000000..d4fe892af
--- /dev/null
+++ b/lib/pleroma/config/holder.ex
@@ -0,0 +1,16 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Holder do
+  @config Pleroma.Config.Loader.load_and_merge()
+
+  @spec config() :: keyword()
+  def config, do: @config
+
+  @spec config(atom()) :: any()
+  def config(group), do: @config[group]
+
+  @spec config(atom(), atom()) :: any()
+  def config(group, key), do: @config[group][key]
+end
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex
new file mode 100644
index 000000000..68b247381
--- /dev/null
+++ b/lib/pleroma/config/loader.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Loader do
+  @paths ["config/config.exs", "config/#{Mix.env()}.exs"]
+
+  @reject_keys [
+    Pleroma.Repo,
+    Pleroma.Web.Endpoint,
+    :env,
+    :configurable_from_database,
+    :database,
+    :swarm
+  ]
+
+  if Code.ensure_loaded?(Config.Reader) do
+    @spec load(Path.t()) :: keyword()
+    def load(path), do: Config.Reader.read!(path)
+
+    defp do_merge(conf1, conf2), do: Config.Reader.merge(conf1, conf2)
+  else
+    # support for Elixir less than 1.9
+    @spec load(Path.t()) :: keyword()
+    def load(path) do
+      path
+      |> Mix.Config.eval!()
+      |> elem(0)
+    end
+
+    defp do_merge(conf1, conf2), do: Mix.Config.merge(conf1, conf2)
+  end
+
+  @spec load_and_merge() :: keyword()
+  def load_and_merge do
+    all_paths =
+      if Pleroma.Config.get(:release),
+        do: @paths ++ ["config/releases.exs"],
+        else: @paths
+
+    all_paths
+    |> Enum.map(&load(&1))
+    |> Enum.reduce([], &do_merge(&2, &1))
+    |> filter()
+  end
+
+  defp filter(configs) do
+    configs
+    |> Keyword.keys()
+    |> Enum.reduce([], &Keyword.put(&2, &1, filter_group(&1, configs)))
+  end
+
+  @spec filter_group(atom(), keyword()) :: keyword()
+  def filter_group(group, configs) do
+    Enum.reject(configs[group], fn {key, _v} ->
+      key in @reject_keys or (group == :phoenix and key == :serve_endpoints)
+    end)
+  end
+end
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index 3214c9951..d54f38ee4 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -4,56 +4,111 @@
 
 defmodule Pleroma.Config.TransferTask do
   use Task
-  alias Pleroma.Web.AdminAPI.Config
+
+  alias Pleroma.ConfigDB
+  alias Pleroma.Repo
+
+  require Logger
 
   def start_link(_) do
     load_and_update_env()
-    if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo)
+    if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
     :ignore
   end
 
-  def load_and_update_env do
-    if Pleroma.Config.get([:instance, :dynamic_configuration]) and
-         Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do
-      for_restart =
-        Pleroma.Repo.all(Config)
-        |> Enum.map(&update_env(&1))
-
+  @spec load_and_update_env([ConfigDB.t()]) :: :ok | false
+  def load_and_update_env(deleted \\ []) do
+    with true <- Pleroma.Config.get(:configurable_from_database),
+         true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"),
+         started_applications <- Application.started_applications() do
       # We need to restart applications for loaded settings take effect
-      for_restart
-      |> Enum.reject(&(&1 in [:pleroma, :ok]))
-      |> Enum.each(fn app ->
-        Application.stop(app)
-        :ok = Application.start(app)
-      end)
+      in_db = Repo.all(ConfigDB)
+
+      with_deleted = in_db ++ deleted
+
+      with_deleted
+      |> Enum.map(&merge_and_update(&1))
+      |> Enum.uniq()
+      # TODO: some problem with prometheus after restart!
+      |> Enum.reject(&(&1 in [:pleroma, nil, :prometheus]))
+      |> Enum.each(&restart(started_applications, &1))
+
+      :ok
     end
   end
 
-  defp update_env(setting) do
+  defp merge_and_update(setting) do
     try do
-      key =
-        if String.starts_with?(setting.key, "Pleroma.") do
-          "Elixir." <> setting.key
+      key = ConfigDB.from_string(setting.key)
+      group = ConfigDB.from_string(setting.group)
+
+      default = Pleroma.Config.Holder.config(group, key)
+      merged_value = merge_value(setting, default, group, key)
+
+      :ok = update_env(group, key, merged_value)
+
+      if group != :logger do
+        group
+      else
+        # change logger configuration in runtime, without restart
+        if Keyword.keyword?(merged_value) and
+             key not in [:compile_time_application, :backends, :compile_time_purge_matching] do
+          Logger.configure_backend(key, merged_value)
         else
-          String.trim_leading(setting.key, ":")
+          Logger.configure([{key, merged_value}])
         end
 
-      group = String.to_existing_atom(setting.group)
-
-      Application.put_env(
-        group,
-        String.to_existing_atom(key),
-        Config.from_binary(setting.value)
-      )
-
-      group
+        nil
+      end
     rescue
-      e ->
-        require Logger
+      error ->
+        error_msg =
+          "updating env causes error, group: " <>
+            inspect(setting.group) <>
+            " key: " <>
+            inspect(setting.key) <>
+            " value: " <>
+            inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error)
 
-        Logger.warn(
-          "updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}"
-        )
+        Logger.warn(error_msg)
+
+        nil
     end
   end
+
+  defp merge_value(%{__meta__: %{state: :deleted}}, default, _group, _key), do: default
+
+  defp merge_value(setting, default, group, key) do
+    value = ConfigDB.from_binary(setting.value)
+
+    if can_be_merged?(default, value) do
+      ConfigDB.merge_group(group, key, default, value)
+    else
+      value
+    end
+  end
+
+  defp update_env(group, key, nil), do: Application.delete_env(group, key)
+  defp update_env(group, key, value), do: Application.put_env(group, key, value)
+
+  defp restart(started_applications, app) do
+    with {^app, _, _} <- List.keyfind(started_applications, app, 0),
+         :ok <- Application.stop(app) do
+      :ok = Application.start(app)
+    else
+      nil ->
+        Logger.warn("#{app} is not started.")
+
+      error ->
+        error
+        |> inspect()
+        |> Logger.warn()
+    end
+  end
+
+  defp can_be_merged?(val1, val2) when is_list(val1) and is_list(val2) do
+    Keyword.keyword?(val1) and Keyword.keyword?(val2)
+  end
+
+  defp can_be_merged?(_val1, _val2), do: false
 end
diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex
index aa578eee2..6b12dcdd9 100644
--- a/lib/pleroma/docs/generator.ex
+++ b/lib/pleroma/docs/generator.ex
@@ -6,68 +6,116 @@ defmodule Pleroma.Docs.Generator do
     implementation.process(descriptions)
   end
 
-  @spec uploaders_list() :: [module()]
-  def uploaders_list do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
-
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
-
-      List.starts_with?(name_as_list, ["Pleroma", "Uploaders"]) and
-        List.last(name_as_list) != "Uploader"
-    end)
+  @spec list_modules_in_dir(String.t(), String.t()) :: [module()]
+  def list_modules_in_dir(dir, start) do
+    with {:ok, files} <- File.ls(dir) do
+      files
+      |> Enum.filter(&String.ends_with?(&1, ".ex"))
+      |> Enum.map(fn filename ->
+        module = filename |> String.trim_trailing(".ex") |> Macro.camelize()
+        String.to_existing_atom(start <> module)
+      end)
+    end
   end
 
-  @spec filters_list() :: [module()]
-  def filters_list do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
-
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
-
-      List.starts_with?(name_as_list, ["Pleroma", "Upload", "Filter"])
-    end)
+  @doc """
+  Converts:
+  - atoms to strings with leading `:`
+  - module names to strings, without leading `Elixir.`
+  - add humanized labels to `keys` if label is not defined, e.g. `:instance` -> `Instance`
+  """
+  @spec convert_to_strings([map()]) :: [map()]
+  def convert_to_strings(descriptions) do
+    Enum.map(descriptions, &format_entity(&1))
   end
 
-  @spec mrf_list() :: [module()]
-  def mrf_list do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
-
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
-
-      List.starts_with?(name_as_list, ["Pleroma", "Web", "ActivityPub", "MRF"]) and
-        length(name_as_list) > 4
-    end)
+  defp format_entity(entity) do
+    entity
+    |> format_key()
+    |> Map.put(:group, atom_to_string(entity[:group]))
+    |> format_children()
   end
 
-  @spec richmedia_parsers() :: [module()]
-  def richmedia_parsers do
-    {:ok, modules} = :application.get_key(:pleroma, :modules)
-
-    Enum.filter(modules, fn module ->
-      name_as_list = Module.split(module)
-
-      List.starts_with?(name_as_list, ["Pleroma", "Web", "RichMedia", "Parsers"]) and
-        length(name_as_list) == 5
-    end)
+  defp format_key(%{key: key} = entity) do
+    entity
+    |> Map.put(:key, atom_to_string(key))
+    |> Map.put(:label, entity[:label] || humanize(key))
   end
+
+  defp format_key(%{group: group} = entity) do
+    Map.put(entity, :label, entity[:label] || humanize(group))
+  end
+
+  defp format_key(entity), do: entity
+
+  defp format_children(%{children: children} = entity) do
+    Map.put(entity, :children, Enum.map(children, &format_child(&1)))
+  end
+
+  defp format_children(entity), do: entity
+
+  defp format_child(%{suggestions: suggestions} = entity) do
+    entity
+    |> Map.put(:suggestions, format_suggestions(suggestions))
+    |> format_key()
+    |> format_group()
+    |> format_children()
+  end
+
+  defp format_child(entity) do
+    entity
+    |> format_key()
+    |> format_group()
+    |> format_children()
+  end
+
+  defp format_group(%{group: group} = entity) do
+    Map.put(entity, :group, format_suggestion(group))
+  end
+
+  defp format_group(entity), do: entity
+
+  defp atom_to_string(entity) when is_binary(entity), do: entity
+
+  defp atom_to_string(entity) when is_atom(entity), do: inspect(entity)
+
+  defp humanize(entity) do
+    string = inspect(entity)
+
+    if String.starts_with?(string, ":"),
+      do: Phoenix.Naming.humanize(entity),
+      else: string
+  end
+
+  defp format_suggestions([]), do: []
+
+  defp format_suggestions([suggestion | tail]) do
+    [format_suggestion(suggestion) | format_suggestions(tail)]
+  end
+
+  defp format_suggestion(entity) when is_atom(entity) do
+    atom_to_string(entity)
+  end
+
+  defp format_suggestion([head | tail] = entity) when is_list(entity) do
+    [format_suggestion(head) | format_suggestions(tail)]
+  end
+
+  defp format_suggestion(entity) when is_tuple(entity) do
+    format_suggestions(Tuple.to_list(entity)) |> List.to_tuple()
+  end
+
+  defp format_suggestion(entity), do: entity
 end
 
 defimpl Jason.Encoder, for: Tuple do
-  def encode(tuple, opts) do
-    Jason.Encode.list(Tuple.to_list(tuple), opts)
-  end
+  def encode(tuple, opts), do: Jason.Encode.list(Tuple.to_list(tuple), opts)
 end
 
 defimpl Jason.Encoder, for: [Regex, Function] do
-  def encode(term, opts) do
-    Jason.Encode.string(inspect(term), opts)
-  end
+  def encode(term, opts), do: Jason.Encode.string(inspect(term), opts)
 end
 
 defimpl String.Chars, for: Regex do
-  def to_string(term) do
-    inspect(term)
-  end
+  def to_string(term), do: inspect(term)
 end
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index f2a56d845..6508a7bdb 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -3,18 +3,22 @@ defmodule Pleroma.Docs.JSON do
 
   @spec process(keyword()) :: {:ok, String.t()}
   def process(descriptions) do
-    config_path = "docs/generate_config.json"
-
-    with {:ok, file} <- File.open(config_path, [:write, :utf8]),
-         json <- generate_json(descriptions),
+    with path <- "docs/generated_config.json",
+         {:ok, file} <- File.open(path, [:write, :utf8]),
+         formatted_descriptions <-
+           Pleroma.Docs.Generator.convert_to_strings(descriptions),
+         json <- Jason.encode!(formatted_descriptions),
          :ok <- IO.write(file, json),
          :ok <- File.close(file) do
-      {:ok, config_path}
+      {:ok, path}
     end
   end
 
-  @spec generate_json([keyword()]) :: String.t()
-  def generate_json(descriptions) do
-    Jason.encode!(descriptions)
+  def compile do
+    with config <- Pleroma.Config.Loader.load("config/description.exs") do
+      config[:pleroma][:config_description]
+      |> Pleroma.Docs.Generator.convert_to_strings()
+      |> Jason.encode!()
+    end
   end
 end
diff --git a/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
similarity index 100%
rename from lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex
rename to lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
diff --git a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
similarity index 100%
rename from lib/pleroma/web/activity_pub/mrf/noop_policy.ex
rename to lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
similarity index 100%
rename from lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex
rename to lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 7118faf94..2314d3274 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -4,7 +4,11 @@
 
 defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   use Pleroma.Web, :controller
+
+  import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
   alias Pleroma.Activity
+  alias Pleroma.ConfigDB
   alias Pleroma.ModerationLog
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.ReportNote
@@ -14,7 +18,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   alias Pleroma.Web.ActivityPub.Relay
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.AdminAPI.AccountView
-  alias Pleroma.Web.AdminAPI.Config
   alias Pleroma.Web.AdminAPI.ConfigView
   alias Pleroma.Web.AdminAPI.ModerationLogView
   alias Pleroma.Web.AdminAPI.Report
@@ -25,10 +28,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   alias Pleroma.Web.MastodonAPI.StatusView
   alias Pleroma.Web.Router
 
-  import Pleroma.Web.ControllerHelper, only: [json_response: 3]
-
   require Logger
 
+  @descriptions_json Pleroma.Docs.JSON.compile()
+  @users_page_size 50
+
   plug(
     OAuthScopesPlug,
     %{scopes: ["read:accounts"], admin: true}
@@ -93,7 +97,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   plug(
     OAuthScopesPlug,
     %{scopes: ["read"], admin: true}
-    when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
+    when action in [:config_show, :migrate_from_db, :list_log]
   )
 
   plug(
@@ -102,8 +106,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     when action == :config_update
   )
 
-  @users_page_size 50
-
   action_fallback(:errors)
 
   def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
@@ -785,49 +787,132 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     |> render("index.json", %{log: log})
   end
 
-  def migrate_to_db(conn, _params) do
-    Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
-    json(conn, %{})
+  def config_descriptions(conn, _params) do
+    conn
+    |> Plug.Conn.put_resp_content_type("application/json")
+    |> Plug.Conn.send_resp(200, @descriptions_json)
   end
 
   def migrate_from_db(conn, _params) do
-    Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
-    json(conn, %{})
+    with :ok <- configurable_from_database(conn) do
+      Mix.Tasks.Pleroma.Config.run([
+        "migrate_from_db",
+        "--env",
+        to_string(Pleroma.Config.get(:env)),
+        "-d"
+      ])
+
+      json(conn, %{})
+    end
+  end
+
+  def config_show(conn, %{"only_db" => true}) do
+    with :ok <- configurable_from_database(conn) do
+      configs = Pleroma.Repo.all(ConfigDB)
+
+      if configs == [] do
+        errors(
+          conn,
+          {:error, "To use configuration from database migrate your settings to database."}
+        )
+      else
+        conn
+        |> put_view(ConfigView)
+        |> render("index.json", %{configs: configs})
+      end
+    end
   end
 
   def config_show(conn, _params) do
-    configs = Pleroma.Repo.all(Config)
+    with :ok <- configurable_from_database(conn) do
+      configs = ConfigDB.get_all_as_keyword()
 
-    conn
-    |> put_view(ConfigView)
-    |> render("index.json", %{configs: configs})
+      if configs == [] do
+        errors(
+          conn,
+          {:error, "To use configuration from database migrate your settings to database."}
+        )
+      else
+        merged =
+          Pleroma.Config.Holder.config()
+          |> ConfigDB.merge(configs)
+          |> Enum.map(fn {group, values} ->
+            Enum.map(values, fn {key, value} ->
+              db =
+                if configs[group][key] do
+                  ConfigDB.get_db_keys(configs[group][key], key)
+                end
+
+              db_value = configs[group][key]
+
+              merged_value =
+                if !is_nil(db_value) and Keyword.keyword?(db_value) and
+                     ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
+                  ConfigDB.merge_group(group, key, value, db_value)
+                else
+                  value
+                end
+
+              setting = %{
+                group: ConfigDB.convert(group),
+                key: ConfigDB.convert(key),
+                value: ConfigDB.convert(merged_value)
+              }
+
+              if db, do: Map.put(setting, :db, db), else: setting
+            end)
+          end)
+          |> List.flatten()
+
+        json(conn, %{configs: merged})
+      end
+    end
   end
 
   def config_update(conn, %{"configs" => configs}) do
-    updated =
-      if Pleroma.Config.get([:instance, :dynamic_configuration]) do
-        updated =
-          Enum.map(configs, fn
-            %{"group" => group, "key" => key, "delete" => "true"} = params ->
-              {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
-              config
+    with :ok <- configurable_from_database(conn) do
+      {_errors, results} =
+        Enum.map(configs, fn
+          %{"group" => group, "key" => key, "delete" => true} = params ->
+            ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
 
-            %{"group" => group, "key" => key, "value" => value} ->
-              {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
-              config
-          end)
-          |> Enum.reject(&is_nil(&1))
+          %{"group" => group, "key" => key, "value" => value} ->
+            ConfigDB.update_or_create(%{group: group, key: key, value: value})
+        end)
+        |> Enum.split_with(fn result -> elem(result, 0) == :error end)
 
-        Pleroma.Config.TransferTask.load_and_update_env()
-        Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
-        updated
-      else
-        []
-      end
+      {deleted, updated} =
+        results
+        |> Enum.map(fn {:ok, config} ->
+          Map.put(config, :db, ConfigDB.get_db_keys(config))
+        end)
+        |> Enum.split_with(fn config ->
+          Ecto.get_meta(config, :state) == :deleted
+        end)
 
-    conn
-    |> put_view(ConfigView)
-    |> render("index.json", %{configs: updated})
+      Pleroma.Config.TransferTask.load_and_update_env(deleted)
+
+      Mix.Tasks.Pleroma.Config.run([
+        "migrate_from_db",
+        "--env",
+        to_string(Pleroma.Config.get(:env))
+      ])
+
+      conn
+      |> put_view(ConfigView)
+      |> render("index.json", %{configs: updated})
+    end
+  end
+
+  defp configurable_from_database(conn) do
+    if Pleroma.Config.get(:configurable_from_database) do
+      :ok
+    else
+      errors(
+        conn,
+        {:error, "To use this endpoint you need to enable configuration from database."}
+      )
+    end
   end
 
   def reload_emoji(conn, _params) do
diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex
deleted file mode 100644
index 1917a5580..000000000
--- a/lib/pleroma/web/admin_api/config.ex
+++ /dev/null
@@ -1,182 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.AdminAPI.Config do
-  use Ecto.Schema
-  import Ecto.Changeset
-  import Pleroma.Web.Gettext
-  alias __MODULE__
-  alias Pleroma.Repo
-
-  @type t :: %__MODULE__{}
-
-  schema "config" do
-    field(:key, :string)
-    field(:group, :string)
-    field(:value, :binary)
-
-    timestamps()
-  end
-
-  @spec get_by_params(map()) :: Config.t() | nil
-  def get_by_params(params), do: Repo.get_by(Config, params)
-
-  @spec changeset(Config.t(), map()) :: Changeset.t()
-  def changeset(config, params \\ %{}) do
-    config
-    |> cast(params, [:key, :group, :value])
-    |> validate_required([:key, :group, :value])
-    |> unique_constraint(:key, name: :config_group_key_index)
-  end
-
-  @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
-  def create(params) do
-    %Config{}
-    |> changeset(Map.put(params, :value, transform(params[:value])))
-    |> Repo.insert()
-  end
-
-  @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()}
-  def update(%Config{} = config, %{value: value}) do
-    config
-    |> change(value: transform(value))
-    |> Repo.update()
-  end
-
-  @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
-  def update_or_create(params) do
-    with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do
-      Config.update(config, params)
-    else
-      nil -> Config.create(params)
-    end
-  end
-
-  @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
-  def delete(params) do
-    with %Config{} = config <- Config.get_by_params(Map.delete(params, :subkeys)) do
-      if params[:subkeys] do
-        updated_value =
-          Keyword.drop(
-            :erlang.binary_to_term(config.value),
-            Enum.map(params[:subkeys], &do_transform_string(&1))
-          )
-
-        Config.update(config, %{value: updated_value})
-      else
-        Repo.delete(config)
-        {:ok, nil}
-      end
-    else
-      nil ->
-        err =
-          dgettext("errors", "Config with params %{params} not found", params: inspect(params))
-
-        {:error, err}
-    end
-  end
-
-  @spec from_binary(binary()) :: term()
-  def from_binary(binary), do: :erlang.binary_to_term(binary)
-
-  @spec from_binary_with_convert(binary()) :: any()
-  def from_binary_with_convert(binary) do
-    from_binary(binary)
-    |> do_convert()
-  end
-
-  defp do_convert(entity) when is_list(entity) do
-    for v <- entity, into: [], do: do_convert(v)
-  end
-
-  defp do_convert(%Regex{} = entity), do: inspect(entity)
-
-  defp do_convert(entity) when is_map(entity) do
-    for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
-  end
-
-  defp do_convert({:dispatch, [entity]}), do: %{"tuple" => [":dispatch", [inspect(entity)]]}
-  defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
-
-  defp do_convert(entity) when is_tuple(entity),
-    do: %{"tuple" => do_convert(Tuple.to_list(entity))}
-
-  defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity),
-    do: entity
-
-  defp do_convert(entity) when is_atom(entity) do
-    string = to_string(entity)
-
-    if String.starts_with?(string, "Elixir."),
-      do: do_convert(string),
-      else: ":" <> string
-  end
-
-  defp do_convert("Elixir." <> module_name), do: module_name
-
-  defp do_convert(entity) when is_binary(entity), do: entity
-
-  @spec transform(any()) :: binary()
-  def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
-    :erlang.term_to_binary(do_transform(entity))
-  end
-
-  def transform(entity), do: :erlang.term_to_binary(entity)
-
-  defp do_transform(%Regex{} = entity), do: entity
-
-  defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do
-    {dispatch_settings, []} = do_eval(entity)
-    {:dispatch, [dispatch_settings]}
-  end
-
-  defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
-    {partial_chain, []} = do_eval(entity)
-    {:partial_chain, partial_chain}
-  end
-
-  defp do_transform(%{"tuple" => entity}) do
-    Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
-  end
-
-  defp do_transform(entity) when is_map(entity) do
-    for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
-  end
-
-  defp do_transform(entity) when is_list(entity) do
-    for v <- entity, into: [], do: do_transform(v)
-  end
-
-  defp do_transform(entity) when is_binary(entity) do
-    String.trim(entity)
-    |> do_transform_string()
-  end
-
-  defp do_transform(entity), do: entity
-
-  defp do_transform_string("~r/" <> pattern) do
-    modificator = String.split(pattern, "/") |> List.last()
-    pattern = String.trim_trailing(pattern, "/" <> modificator)
-
-    case modificator do
-      "" -> ~r/#{pattern}/
-      "i" -> ~r/#{pattern}/i
-      "u" -> ~r/#{pattern}/u
-      "s" -> ~r/#{pattern}/s
-    end
-  end
-
-  defp do_transform_string(":" <> atom), do: String.to_atom(atom)
-
-  defp do_transform_string(value) do
-    if String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix"),
-      do: String.to_existing_atom("Elixir." <> value),
-      else: value
-  end
-
-  defp do_eval(entity) do
-    cleaned_string = String.replace(entity, ~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
-    Code.eval_string(cleaned_string, [], requires: [], macros: [])
-  end
-end
diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex
index 49add0b6e..23d97e847 100644
--- a/lib/pleroma/web/admin_api/views/config_view.ex
+++ b/lib/pleroma/web/admin_api/views/config_view.ex
@@ -12,10 +12,16 @@ defmodule Pleroma.Web.AdminAPI.ConfigView do
   end
 
   def render("show.json", %{config: config}) do
-    %{
+    map = %{
       key: config.key,
       group: config.group,
-      value: Pleroma.Web.AdminAPI.Config.from_binary_with_convert(config.value)
+      value: Pleroma.ConfigDB.from_binary_with_convert(config.value)
     }
+
+    if config.db != [] do
+      Map.put(map, :db, config.db)
+    else
+      map
+    end
   end
 end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 9654ab8a3..ef6e5a565 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -195,7 +195,7 @@ defmodule Pleroma.Web.Router do
 
     get("/config", AdminAPIController, :config_show)
     post("/config", AdminAPIController, :config_update)
-    get("/config/migrate_to_db", AdminAPIController, :migrate_to_db)
+    get("/config/descriptions", AdminAPIController, :config_descriptions)
     get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
 
     get("/moderation_log", AdminAPIController, :list_log)
diff --git a/priv/templates/sample_config.eex b/priv/templates/sample_config.eex
index dc75d4008..bc7e37375 100644
--- a/priv/templates/sample_config.eex
+++ b/priv/templates/sample_config.eex
@@ -20,8 +20,7 @@ config :pleroma, :instance,
   email: "<%= email %>",
   notify_email: "<%= notify_email %>",
   limit: 5000,
-  registrations_open: true,
-  dynamic_configuration: <%= db_configurable? %>
+  registrations_open: true
 
 config :pleroma, :media_proxy,
   enabled: false,
@@ -70,3 +69,5 @@ config :pleroma, Pleroma.Uploaders.Local, uploads: "<%= uploads_dir %>"
 #   host: "s3.wasabisys.com"
 
 config :joken, default_signer: "<%= jwt_secret %>"
+
+config :pleroma, configurable_from_database: <%= db_configurable? %>
diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs
new file mode 100644
index 000000000..61a0b1d5d
--- /dev/null
+++ b/test/config/config_db_test.exs
@@ -0,0 +1,704 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ConfigDBTest do
+  use Pleroma.DataCase, async: true
+  import Pleroma.Factory
+  alias Pleroma.ConfigDB
+
+  test "get_by_key/1" do
+    config = insert(:config)
+    insert(:config)
+
+    assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key})
+  end
+
+  test "create/1" do
+    {:ok, config} = ConfigDB.create(%{group: ":pleroma", key: ":some_key", value: "some_value"})
+    assert config == ConfigDB.get_by_params(%{group: ":pleroma", key: ":some_key"})
+  end
+
+  test "update/1" do
+    config = insert(:config)
+    {:ok, updated} = ConfigDB.update(config, %{value: "some_value"})
+    loaded = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+    assert loaded == updated
+  end
+
+  test "get_all_as_keyword/0" do
+    saved = insert(:config)
+    insert(:config, group: ":quack", key: ":level", value: ConfigDB.to_binary(:info))
+    insert(:config, group: ":quack", key: ":meta", value: ConfigDB.to_binary([:none]))
+
+    insert(:config,
+      group: ":quack",
+      key: ":webhook_url",
+      value: ConfigDB.to_binary("https://hooks.slack.com/services/KEY/some_val")
+    )
+
+    config = ConfigDB.get_all_as_keyword()
+
+    assert config[:pleroma] == [
+             {ConfigDB.from_string(saved.key), ConfigDB.from_binary(saved.value)}
+           ]
+
+    assert config[:quack] == [
+             level: :info,
+             meta: [:none],
+             webhook_url: "https://hooks.slack.com/services/KEY/some_val"
+           ]
+  end
+
+  describe "update_or_create/1" do
+    test "common" do
+      config = insert(:config)
+      key2 = "another_key"
+
+      params = [
+        %{group: "pleroma", key: key2, value: "another_value"},
+        %{group: config.group, key: config.key, value: "new_value"}
+      ]
+
+      assert Repo.all(ConfigDB) |> length() == 1
+
+      Enum.each(params, &ConfigDB.update_or_create(&1))
+
+      assert Repo.all(ConfigDB) |> length() == 2
+
+      config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+      config2 = ConfigDB.get_by_params(%{group: "pleroma", key: key2})
+
+      assert config1.value == ConfigDB.transform("new_value")
+      assert config2.value == ConfigDB.transform("another_value")
+    end
+
+    test "partial update" do
+      config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: :val2))
+
+      {:ok, _config} =
+        ConfigDB.update_or_create(%{
+          group: config.group,
+          key: config.key,
+          value: [key1: :val1, key3: :val3]
+        })
+
+      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+      value = ConfigDB.from_binary(updated.value)
+      assert length(value) == 3
+      assert value[:key1] == :val1
+      assert value[:key2] == :val2
+      assert value[:key3] == :val3
+    end
+
+    test "deep merge" do
+      config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: [k1: :v1, k2: "v2"]))
+
+      {:ok, config} =
+        ConfigDB.update_or_create(%{
+          group: config.group,
+          key: config.key,
+          value: [key1: :val1, key2: [k2: :v2, k3: :v3], key3: :val3]
+        })
+
+      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+      assert config.value == updated.value
+
+      value = ConfigDB.from_binary(updated.value)
+      assert value[:key1] == :val1
+      assert value[:key2] == [k1: :v1, k2: :v2, k3: :v3]
+      assert value[:key3] == :val3
+    end
+
+    test "only full update for some keys" do
+      config1 = insert(:config, key: ":ecto_repos", value: ConfigDB.to_binary(repo: Pleroma.Repo))
+
+      config2 =
+        insert(:config, group: ":cors_plug", key: ":max_age", value: ConfigDB.to_binary(18))
+
+      {:ok, _config} =
+        ConfigDB.update_or_create(%{
+          group: config1.group,
+          key: config1.key,
+          value: [another_repo: [Pleroma.Repo]]
+        })
+
+      {:ok, _config} =
+        ConfigDB.update_or_create(%{
+          group: config2.group,
+          key: config2.key,
+          value: 777
+        })
+
+      updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
+      updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})
+
+      assert ConfigDB.from_binary(updated1.value) == [another_repo: [Pleroma.Repo]]
+      assert ConfigDB.from_binary(updated2.value) == 777
+    end
+
+    test "full update if value is not keyword" do
+      config =
+        insert(:config,
+          group: ":tesla",
+          key: ":adapter",
+          value: ConfigDB.to_binary(Tesla.Adapter.Hackney)
+        )
+
+      {:ok, _config} =
+        ConfigDB.update_or_create(%{
+          group: config.group,
+          key: config.key,
+          value: Tesla.Adapter.Httpc
+        })
+
+      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+      assert ConfigDB.from_binary(updated.value) == Tesla.Adapter.Httpc
+    end
+
+    test "only full update for some subkeys" do
+      config1 =
+        insert(:config,
+          key: ":emoji",
+          value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
+        )
+
+      config2 =
+        insert(:config,
+          key: ":assets",
+          value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
+        )
+
+      {:ok, _config} =
+        ConfigDB.update_or_create(%{
+          group: config1.group,
+          key: config1.key,
+          value: [groups: [c: 3, d: 4], key: [b: 2]]
+        })
+
+      {:ok, _config} =
+        ConfigDB.update_or_create(%{
+          group: config2.group,
+          key: config2.key,
+          value: [mascots: [c: 3, d: 4], key: [b: 2]]
+        })
+
+      updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
+      updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})
+
+      assert ConfigDB.from_binary(updated1.value) == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
+      assert ConfigDB.from_binary(updated2.value) == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
+    end
+  end
+
+  describe "delete/1" do
+    test "error on deleting non existing setting" do
+      {:error, error} = ConfigDB.delete(%{group: ":pleroma", key: ":key"})
+      assert error =~ "Config with params %{group: \":pleroma\", key: \":key\"} not found"
+    end
+
+    test "full delete" do
+      config = insert(:config)
+      {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key})
+      assert Ecto.get_meta(deleted, :state) == :deleted
+      refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
+    end
+
+    test "partial subkeys delete" do
+      config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]))
+
+      {:ok, deleted} =
+        ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
+
+      assert Ecto.get_meta(deleted, :state) == :loaded
+
+      assert deleted.value == ConfigDB.to_binary(key: [a: 1])
+
+      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+
+      assert updated.value == deleted.value
+    end
+
+    test "full delete if remaining value after subkeys deletion is empty list" do
+      config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2]))
+
+      {:ok, deleted} =
+        ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
+
+      assert Ecto.get_meta(deleted, :state) == :deleted
+
+      refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
+    end
+  end
+
+  describe "transform/1" do
+    test "string" do
+      binary = ConfigDB.transform("value as string")
+      assert binary == :erlang.term_to_binary("value as string")
+      assert ConfigDB.from_binary(binary) == "value as string"
+    end
+
+    test "boolean" do
+      binary = ConfigDB.transform(false)
+      assert binary == :erlang.term_to_binary(false)
+      assert ConfigDB.from_binary(binary) == false
+    end
+
+    test "nil" do
+      binary = ConfigDB.transform(nil)
+      assert binary == :erlang.term_to_binary(nil)
+      assert ConfigDB.from_binary(binary) == nil
+    end
+
+    test "integer" do
+      binary = ConfigDB.transform(150)
+      assert binary == :erlang.term_to_binary(150)
+      assert ConfigDB.from_binary(binary) == 150
+    end
+
+    test "atom" do
+      binary = ConfigDB.transform(":atom")
+      assert binary == :erlang.term_to_binary(:atom)
+      assert ConfigDB.from_binary(binary) == :atom
+    end
+
+    test "ssl options" do
+      binary = ConfigDB.transform([":tlsv1", ":tlsv1.1", ":tlsv1.2"])
+      assert binary == :erlang.term_to_binary([:tlsv1, :"tlsv1.1", :"tlsv1.2"])
+      assert ConfigDB.from_binary(binary) == [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
+    end
+
+    test "pleroma module" do
+      binary = ConfigDB.transform("Pleroma.Bookmark")
+      assert binary == :erlang.term_to_binary(Pleroma.Bookmark)
+      assert ConfigDB.from_binary(binary) == Pleroma.Bookmark
+    end
+
+    test "pleroma string" do
+      binary = ConfigDB.transform("Pleroma")
+      assert binary == :erlang.term_to_binary("Pleroma")
+      assert ConfigDB.from_binary(binary) == "Pleroma"
+    end
+
+    test "phoenix module" do
+      binary = ConfigDB.transform("Phoenix.Socket.V1.JSONSerializer")
+      assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer)
+      assert ConfigDB.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer
+    end
+
+    test "tesla module" do
+      binary = ConfigDB.transform("Tesla.Adapter.Hackney")
+      assert binary == :erlang.term_to_binary(Tesla.Adapter.Hackney)
+      assert ConfigDB.from_binary(binary) == Tesla.Adapter.Hackney
+    end
+
+    test "ExSyslogger module" do
+      binary = ConfigDB.transform("ExSyslogger")
+      assert binary == :erlang.term_to_binary(ExSyslogger)
+      assert ConfigDB.from_binary(binary) == ExSyslogger
+    end
+
+    test "Quack.Logger module" do
+      binary = ConfigDB.transform("Quack.Logger")
+      assert binary == :erlang.term_to_binary(Quack.Logger)
+      assert ConfigDB.from_binary(binary) == Quack.Logger
+    end
+
+    test "sigil" do
+      binary = ConfigDB.transform("~r[comp[lL][aA][iI][nN]er]")
+      assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/)
+      assert ConfigDB.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/
+    end
+
+    test "link sigil" do
+      binary = ConfigDB.transform("~r/https:\/\/example.com/")
+      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/)
+      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/
+    end
+
+    test "link sigil with um modifiers" do
+      binary = ConfigDB.transform("~r/https:\/\/example.com/um")
+      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/um)
+      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/um
+    end
+
+    test "link sigil with i modifier" do
+      binary = ConfigDB.transform("~r/https:\/\/example.com/i")
+      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/i)
+      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/i
+    end
+
+    test "link sigil with s modifier" do
+      binary = ConfigDB.transform("~r/https:\/\/example.com/s")
+      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/s)
+      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/s
+    end
+
+    test "raise if valid delimiter not found" do
+      assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn ->
+        ConfigDB.transform("~r/https://[]{}<>\"'()|example.com/s")
+      end
+    end
+
+    test "2 child tuple" do
+      binary = ConfigDB.transform(%{"tuple" => ["v1", ":v2"]})
+      assert binary == :erlang.term_to_binary({"v1", :v2})
+      assert ConfigDB.from_binary(binary) == {"v1", :v2}
+    end
+
+    test "proxy tuple with localhost" do
+      binary =
+        ConfigDB.transform(%{
+          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
+        })
+
+      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, :localhost, 1234}})
+      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, :localhost, 1234}}
+    end
+
+    test "proxy tuple with domain" do
+      binary =
+        ConfigDB.transform(%{
+          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
+        })
+
+      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, 'domain.com', 1234}})
+      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, 'domain.com', 1234}}
+    end
+
+    test "proxy tuple with ip" do
+      binary =
+        ConfigDB.transform(%{
+          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]
+        })
+
+      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}})
+      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
+    end
+
+    test "tuple with n childs" do
+      binary =
+        ConfigDB.transform(%{
+          "tuple" => [
+            "v1",
+            ":v2",
+            "Pleroma.Bookmark",
+            150,
+            false,
+            "Phoenix.Socket.V1.JSONSerializer"
+          ]
+        })
+
+      assert binary ==
+               :erlang.term_to_binary(
+                 {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
+               )
+
+      assert ConfigDB.from_binary(binary) ==
+               {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
+    end
+
+    test "map with string key" do
+      binary = ConfigDB.transform(%{"key" => "value"})
+      assert binary == :erlang.term_to_binary(%{"key" => "value"})
+      assert ConfigDB.from_binary(binary) == %{"key" => "value"}
+    end
+
+    test "map with atom key" do
+      binary = ConfigDB.transform(%{":key" => "value"})
+      assert binary == :erlang.term_to_binary(%{key: "value"})
+      assert ConfigDB.from_binary(binary) == %{key: "value"}
+    end
+
+    test "list of strings" do
+      binary = ConfigDB.transform(["v1", "v2", "v3"])
+      assert binary == :erlang.term_to_binary(["v1", "v2", "v3"])
+      assert ConfigDB.from_binary(binary) == ["v1", "v2", "v3"]
+    end
+
+    test "list of modules" do
+      binary = ConfigDB.transform(["Pleroma.Repo", "Pleroma.Activity"])
+      assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity])
+      assert ConfigDB.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity]
+    end
+
+    test "list of atoms" do
+      binary = ConfigDB.transform([":v1", ":v2", ":v3"])
+      assert binary == :erlang.term_to_binary([:v1, :v2, :v3])
+      assert ConfigDB.from_binary(binary) == [:v1, :v2, :v3]
+    end
+
+    test "list of mixed values" do
+      binary =
+        ConfigDB.transform([
+          "v1",
+          ":v2",
+          "Pleroma.Repo",
+          "Phoenix.Socket.V1.JSONSerializer",
+          15,
+          false
+        ])
+
+      assert binary ==
+               :erlang.term_to_binary([
+                 "v1",
+                 :v2,
+                 Pleroma.Repo,
+                 Phoenix.Socket.V1.JSONSerializer,
+                 15,
+                 false
+               ])
+
+      assert ConfigDB.from_binary(binary) == [
+               "v1",
+               :v2,
+               Pleroma.Repo,
+               Phoenix.Socket.V1.JSONSerializer,
+               15,
+               false
+             ]
+    end
+
+    test "simple keyword" do
+      binary = ConfigDB.transform([%{"tuple" => [":key", "value"]}])
+      assert binary == :erlang.term_to_binary([{:key, "value"}])
+      assert ConfigDB.from_binary(binary) == [{:key, "value"}]
+      assert ConfigDB.from_binary(binary) == [key: "value"]
+    end
+
+    test "keyword with partial_chain key" do
+      binary =
+        ConfigDB.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}])
+
+      assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1)
+      assert ConfigDB.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1]
+    end
+
+    test "keyword" do
+      binary =
+        ConfigDB.transform([
+          %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
+          %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
+          %{"tuple" => [":migration_lock", nil]},
+          %{"tuple" => [":key1", 150]},
+          %{"tuple" => [":key2", "string"]}
+        ])
+
+      assert binary ==
+               :erlang.term_to_binary(
+                 types: Pleroma.PostgresTypes,
+                 telemetry_event: [Pleroma.Repo.Instrumenter],
+                 migration_lock: nil,
+                 key1: 150,
+                 key2: "string"
+               )
+
+      assert ConfigDB.from_binary(binary) == [
+               types: Pleroma.PostgresTypes,
+               telemetry_event: [Pleroma.Repo.Instrumenter],
+               migration_lock: nil,
+               key1: 150,
+               key2: "string"
+             ]
+    end
+
+    test "complex keyword with nested mixed childs" do
+      binary =
+        ConfigDB.transform([
+          %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
+          %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
+          %{"tuple" => [":link_name", true]},
+          %{"tuple" => [":proxy_remote", false]},
+          %{"tuple" => [":common_map", %{":key" => "value"}]},
+          %{
+            "tuple" => [
+              ":proxy_opts",
+              [
+                %{"tuple" => [":redirect_on_failure", false]},
+                %{"tuple" => [":max_body_length", 1_048_576]},
+                %{
+                  "tuple" => [
+                    ":http",
+                    [%{"tuple" => [":follow_redirect", true]}, %{"tuple" => [":pool", ":upload"]}]
+                  ]
+                }
+              ]
+            ]
+          }
+        ])
+
+      assert binary ==
+               :erlang.term_to_binary(
+                 uploader: Pleroma.Uploaders.Local,
+                 filters: [Pleroma.Upload.Filter.Dedupe],
+                 link_name: true,
+                 proxy_remote: false,
+                 common_map: %{key: "value"},
+                 proxy_opts: [
+                   redirect_on_failure: false,
+                   max_body_length: 1_048_576,
+                   http: [
+                     follow_redirect: true,
+                     pool: :upload
+                   ]
+                 ]
+               )
+
+      assert ConfigDB.from_binary(binary) ==
+               [
+                 uploader: Pleroma.Uploaders.Local,
+                 filters: [Pleroma.Upload.Filter.Dedupe],
+                 link_name: true,
+                 proxy_remote: false,
+                 common_map: %{key: "value"},
+                 proxy_opts: [
+                   redirect_on_failure: false,
+                   max_body_length: 1_048_576,
+                   http: [
+                     follow_redirect: true,
+                     pool: :upload
+                   ]
+                 ]
+               ]
+    end
+
+    test "common keyword" do
+      binary =
+        ConfigDB.transform([
+          %{"tuple" => [":level", ":warn"]},
+          %{"tuple" => [":meta", [":all"]]},
+          %{"tuple" => [":path", ""]},
+          %{"tuple" => [":val", nil]},
+          %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
+        ])
+
+      assert binary ==
+               :erlang.term_to_binary(
+                 level: :warn,
+                 meta: [:all],
+                 path: "",
+                 val: nil,
+                 webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
+               )
+
+      assert ConfigDB.from_binary(binary) == [
+               level: :warn,
+               meta: [:all],
+               path: "",
+               val: nil,
+               webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
+             ]
+    end
+
+    test "complex keyword with sigil" do
+      binary =
+        ConfigDB.transform([
+          %{"tuple" => [":federated_timeline_removal", []]},
+          %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
+          %{"tuple" => [":replace", []]}
+        ])
+
+      assert binary ==
+               :erlang.term_to_binary(
+                 federated_timeline_removal: [],
+                 reject: [~r/comp[lL][aA][iI][nN]er/],
+                 replace: []
+               )
+
+      assert ConfigDB.from_binary(binary) ==
+               [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []]
+    end
+
+    test "complex keyword with tuples with more than 2 values" do
+      binary =
+        ConfigDB.transform([
+          %{
+            "tuple" => [
+              ":http",
+              [
+                %{
+                  "tuple" => [
+                    ":key1",
+                    [
+                      %{
+                        "tuple" => [
+                          ":_",
+                          [
+                            %{
+                              "tuple" => [
+                                "/api/v1/streaming",
+                                "Pleroma.Web.MastodonAPI.WebsocketHandler",
+                                []
+                              ]
+                            },
+                            %{
+                              "tuple" => [
+                                "/websocket",
+                                "Phoenix.Endpoint.CowboyWebSocket",
+                                %{
+                                  "tuple" => [
+                                    "Phoenix.Transports.WebSocket",
+                                    %{
+                                      "tuple" => [
+                                        "Pleroma.Web.Endpoint",
+                                        "Pleroma.Web.UserSocket",
+                                        []
+                                      ]
+                                    }
+                                  ]
+                                }
+                              ]
+                            },
+                            %{
+                              "tuple" => [
+                                ":_",
+                                "Phoenix.Endpoint.Cowboy2Handler",
+                                %{"tuple" => ["Pleroma.Web.Endpoint", []]}
+                              ]
+                            }
+                          ]
+                        ]
+                      }
+                    ]
+                  ]
+                }
+              ]
+            ]
+          }
+        ])
+
+      assert binary ==
+               :erlang.term_to_binary(
+                 http: [
+                   key1: [
+                     _: [
+                       {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
+                       {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
+                        {Phoenix.Transports.WebSocket,
+                         {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
+                       {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
+                     ]
+                   ]
+                 ]
+               )
+
+      assert ConfigDB.from_binary(binary) == [
+               http: [
+                 key1: [
+                   {:_,
+                    [
+                      {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
+                      {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
+                       {Phoenix.Transports.WebSocket,
+                        {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
+                      {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
+                    ]}
+                 ]
+               ]
+             ]
+    end
+  end
+end
diff --git a/test/config/holder_test.exs b/test/config/holder_test.exs
new file mode 100644
index 000000000..0c1882d0f
--- /dev/null
+++ b/test/config/holder_test.exs
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.HolderTest do
+  use ExUnit.Case, async: true
+
+  alias Pleroma.Config.Holder
+
+  test "config/0" do
+    config = Holder.config()
+    assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+    assert config[:tesla][:adapter] == Tesla.Mock
+
+    refute config[:pleroma][Pleroma.Repo]
+    refute config[:pleroma][Pleroma.Web.Endpoint]
+    refute config[:pleroma][:env]
+    refute config[:pleroma][:configurable_from_database]
+    refute config[:pleroma][:database]
+    refute config[:phoenix][:serve_endpoints]
+  end
+
+  test "config/1" do
+    pleroma_config = Holder.config(:pleroma)
+    assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+    tesla_config = Holder.config(:tesla)
+    assert tesla_config[:adapter] == Tesla.Mock
+  end
+
+  test "config/2" do
+    assert Holder.config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"]
+    assert Holder.config(:tesla, :adapter) == Tesla.Mock
+  end
+end
diff --git a/test/config/loader_test.exs b/test/config/loader_test.exs
new file mode 100644
index 000000000..0dd4c60bb
--- /dev/null
+++ b/test/config/loader_test.exs
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.LoaderTest do
+  use ExUnit.Case, async: true
+
+  alias Pleroma.Config.Loader
+
+  test "load/1" do
+    config = Loader.load("test/fixtures/config/temp.secret.exs")
+    assert config[:pleroma][:first_setting][:key] == "value"
+    assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo]
+    assert config[:quack][:level] == :info
+  end
+
+  test "load_and_merge/0" do
+    config = Loader.load_and_merge()
+
+    refute config[:pleroma][Pleroma.Repo]
+    refute config[:pleroma][Pleroma.Web.Endpoint]
+    refute config[:pleroma][:env]
+    refute config[:pleroma][:configurable_from_database]
+    refute config[:pleroma][:database]
+    refute config[:phoenix][:serve_endpoints]
+
+    assert config[:pleroma][:ecto_repos] == [Pleroma.Repo]
+    assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads"
+    assert config[:tesla][:adapter] == Tesla.Mock
+  end
+
+  test "filter_group/2" do
+    assert Loader.filter_group(:pleroma,
+             pleroma: [
+               {Pleroma.Repo, [a: 1, b: 2]},
+               {Pleroma.Upload, [a: 1, b: 2]},
+               {Pleroma.Web.Endpoint, []},
+               env: :test,
+               configurable_from_database: true,
+               database: []
+             ]
+           ) == [{Pleroma.Upload, [a: 1, b: 2]}]
+  end
+end
diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs
index 9074f3b97..b9072e0fc 100644
--- a/test/config/transfer_task_test.exs
+++ b/test/config/transfer_task_test.exs
@@ -5,47 +5,117 @@
 defmodule Pleroma.Config.TransferTaskTest do
   use Pleroma.DataCase
 
-  clear_config([:instance, :dynamic_configuration]) do
-    Pleroma.Config.put([:instance, :dynamic_configuration], true)
+  alias Pleroma.Config.TransferTask
+  alias Pleroma.ConfigDB
+
+  clear_config(:configurable_from_database) do
+    Pleroma.Config.put(:configurable_from_database, true)
   end
 
   test "transfer config values from db to env" do
     refute Application.get_env(:pleroma, :test_key)
     refute Application.get_env(:idna, :test_key)
+    refute Application.get_env(:quack, :test_key)
 
-    Pleroma.Web.AdminAPI.Config.create(%{
-      group: "pleroma",
-      key: "test_key",
+    ConfigDB.create(%{
+      group: ":pleroma",
+      key: ":test_key",
       value: [live: 2, com: 3]
     })
 
-    Pleroma.Web.AdminAPI.Config.create(%{
-      group: "idna",
-      key: "test_key",
+    ConfigDB.create(%{
+      group: ":idna",
+      key: ":test_key",
       value: [live: 15, com: 35]
     })
 
-    Pleroma.Config.TransferTask.start_link([])
+    ConfigDB.create(%{
+      group: ":quack",
+      key: ":test_key",
+      value: [:test_value1, :test_value2]
+    })
+
+    TransferTask.start_link([])
 
     assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
     assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
+    assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2]
 
     on_exit(fn ->
       Application.delete_env(:pleroma, :test_key)
       Application.delete_env(:idna, :test_key)
+      Application.delete_env(:quack, :test_key)
+    end)
+  end
+
+  test "transfer config values for 1 group and some keys" do
+    level = Application.get_env(:quack, :level)
+    meta = Application.get_env(:quack, :meta)
+
+    ConfigDB.create(%{
+      group: ":quack",
+      key: ":level",
+      value: :info
+    })
+
+    ConfigDB.create(%{
+      group: ":quack",
+      key: ":meta",
+      value: [:none]
+    })
+
+    TransferTask.start_link([])
+
+    assert Application.get_env(:quack, :level) == :info
+    assert Application.get_env(:quack, :meta) == [:none]
+    default = Pleroma.Config.Holder.config(:quack, :webhook_url)
+    assert Application.get_env(:quack, :webhook_url) == default
+
+    on_exit(fn ->
+      Application.put_env(:quack, :level, level)
+      Application.put_env(:quack, :meta, meta)
+    end)
+  end
+
+  test "transfer config values with full subkey update" do
+    emoji = Application.get_env(:pleroma, :emoji)
+    assets = Application.get_env(:pleroma, :assets)
+
+    ConfigDB.create(%{
+      group: ":pleroma",
+      key: ":emoji",
+      value: [groups: [a: 1, b: 2]]
+    })
+
+    ConfigDB.create(%{
+      group: ":pleroma",
+      key: ":assets",
+      value: [mascots: [a: 1, b: 2]]
+    })
+
+    TransferTask.start_link([])
+
+    emoji_env = Application.get_env(:pleroma, :emoji)
+    assert emoji_env[:groups] == [a: 1, b: 2]
+    assets_env = Application.get_env(:pleroma, :assets)
+    assert assets_env[:mascots] == [a: 1, b: 2]
+
+    on_exit(fn ->
+      Application.put_env(:pleroma, :emoji, emoji)
+      Application.put_env(:pleroma, :assets, assets)
     end)
   end
 
   test "non existing atom" do
-    Pleroma.Web.AdminAPI.Config.create(%{
-      group: "pleroma",
-      key: "undefined_atom_key",
+    ConfigDB.create(%{
+      group: ":pleroma",
+      key: ":undefined_atom_key",
       value: [live: 2, com: 3]
     })
 
     assert ExUnit.CaptureLog.capture_log(fn ->
-             Pleroma.Config.TransferTask.start_link([])
+             TransferTask.start_link([])
            end) =~
-             "updating env causes error, key: \"undefined_atom_key\", error: %ArgumentError{message: \"argument error\"}"
+             "updating env causes error, group: \":pleroma\" key: \":undefined_atom_key\" value: [live: 2, com: 3] error: %ArgumentError{message: \"argument error\"}"
   end
 end
diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs
new file mode 100644
index 000000000..9c9f4357b
--- /dev/null
+++ b/test/docs/generator_test.exs
@@ -0,0 +1,230 @@
+defmodule Pleroma.Docs.GeneratorTest do
+  use ExUnit.Case, async: true
+  alias Pleroma.Docs.Generator
+
+  @descriptions [
+    %{
+      group: :pleroma,
+      key: Pleroma.Upload,
+      type: :group,
+      description: "",
+      children: [
+        %{
+          key: :uploader,
+          type: :module,
+          description: "",
+          suggestions:
+            Generator.list_modules_in_dir(
+              "lib/pleroma/upload/filter",
+              "Elixir.Pleroma.Upload.Filter."
+            )
+        },
+        %{
+          key: :filters,
+          type: {:list, :module},
+          description: "",
+          suggestions:
+            Generator.list_modules_in_dir(
+              "lib/pleroma/web/activity_pub/mrf",
+              "Elixir.Pleroma.Web.ActivityPub.MRF."
+            )
+        },
+        %{
+          key: Pleroma.Upload,
+          type: :string,
+          description: "",
+          suggestions: [""]
+        },
+        %{
+          key: :some_key,
+          type: :keyword,
+          description: "",
+          suggestions: [],
+          children: [
+            %{
+              key: :another_key,
+              type: :integer,
+              description: "",
+              suggestions: [5]
+            },
+            %{
+              key: :another_key_with_label,
+              label: "Another label",
+              type: :integer,
+              description: "",
+              suggestions: [7]
+            }
+          ]
+        },
+        %{
+          key: :key1,
+          type: :atom,
+          description: "",
+          suggestions: [
+            :atom,
+            Pleroma.Upload,
+            {:tuple, "string", 8080},
+            [:atom, Pleroma.Upload, {:atom, Pleroma.Upload}]
+          ]
+        },
+        %{
+          key: Pleroma.Upload,
+          label: "Special Label",
+          type: :string,
+          description: "",
+          suggestions: [""]
+        },
+        %{
+          group: {:subgroup, Swoosh.Adapters.SMTP},
+          key: :auth,
+          type: :atom,
+          description: "`Swoosh.Adapters.SMTP` adapter specific setting",
+          suggestions: [:always, :never, :if_available]
+        },
+        %{
+          key: "application/xml",
+          type: {:list, :string},
+          suggestions: ["xml"]
+        },
+        %{
+          key: :versions,
+          type: {:list, :atom},
+          description: "List of TLS version to use",
+          suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
+        }
+      ]
+    },
+    %{
+      group: :tesla,
+      key: :adapter,
+      type: :group,
+      description: ""
+    },
+    %{
+      group: :cors_plug,
+      type: :group,
+      children: [%{key: :key1, type: :string, suggestions: [""]}]
+    },
+    %{group: "Some string group", key: "Some string key", type: :group}
+  ]
+
+  describe "convert_to_strings/1" do
+    test "group, key, label" do
+      [desc1, desc2 | _] = Generator.convert_to_strings(@descriptions)
+
+      assert desc1[:group] == ":pleroma"
+      assert desc1[:key] == "Pleroma.Upload"
+      assert desc1[:label] == "Pleroma.Upload"
+
+      assert desc2[:group] == ":tesla"
+      assert desc2[:key] == ":adapter"
+      assert desc2[:label] == "Adapter"
+    end
+
+    test "group without key" do
+      descriptions = Generator.convert_to_strings(@descriptions)
+      desc = Enum.at(descriptions, 2)
+
+      assert desc[:group] == ":cors_plug"
+      refute desc[:key]
+      assert desc[:label] == "Cors plug"
+    end
+
+    test "children key, label, type" do
+      [%{children: [child1, child2, child3, child4 | _]} | _] =
+        Generator.convert_to_strings(@descriptions)
+
+      assert child1[:key] == ":uploader"
+      assert child1[:label] == "Uploader"
+      assert child1[:type] == :module
+
+      assert child2[:key] == ":filters"
+      assert child2[:label] == "Filters"
+      assert child2[:type] == {:list, :module}
+
+      assert child3[:key] == "Pleroma.Upload"
+      assert child3[:label] == "Pleroma.Upload"
+      assert child3[:type] == :string
+
+      assert child4[:key] == ":some_key"
+      assert child4[:label] == "Some key"
+      assert child4[:type] == :keyword
+    end
+
+    test "child with predefined label" do
+      [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+      child = Enum.at(children, 5)
+      assert child[:key] == "Pleroma.Upload"
+      assert child[:label] == "Special Label"
+    end
+
+    test "subchild" do
+      [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+      child = Enum.at(children, 3)
+      %{children: [subchild | _]} = child
+
+      assert subchild[:key] == ":another_key"
+      assert subchild[:label] == "Another key"
+      assert subchild[:type] == :integer
+    end
+
+    test "subchild with predefined label" do
+      [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+      child = Enum.at(children, 3)
+      %{children: subchildren} = child
+      subchild = Enum.at(subchildren, 1)
+
+      assert subchild[:key] == ":another_key_with_label"
+      assert subchild[:label] == "Another label"
+    end
+
+    test "module suggestions" do
+      [%{children: [%{suggestions: suggestions} | _]} | _] =
+        Generator.convert_to_strings(@descriptions)
+
+      Enum.each(suggestions, fn suggestion ->
+        assert String.starts_with?(suggestion, "Pleroma.")
+      end)
+    end
+
+    test "atoms in suggestions with leading `:`" do
+      [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+      %{suggestions: suggestions} = Enum.at(children, 4)
+      assert Enum.at(suggestions, 0) == ":atom"
+      assert Enum.at(suggestions, 1) == "Pleroma.Upload"
+      assert Enum.at(suggestions, 2) == {":tuple", "string", 8080}
+      assert Enum.at(suggestions, 3) == [":atom", "Pleroma.Upload", {":atom", "Pleroma.Upload"}]
+
+      %{suggestions: suggestions} = Enum.at(children, 6)
+      assert Enum.at(suggestions, 0) == ":always"
+      assert Enum.at(suggestions, 1) == ":never"
+      assert Enum.at(suggestions, 2) == ":if_available"
+    end
+
+    test "group, key as string in main desc" do
+      descriptions = Generator.convert_to_strings(@descriptions)
+      desc = Enum.at(descriptions, 3)
+      assert desc[:group] == "Some string group"
+      assert desc[:key] == "Some string key"
+    end
+
+    test "key as string subchild" do
+      [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+      child = Enum.at(children, 7)
+      assert child[:key] == "application/xml"
+    end
+
+    test "suggestion for tls versions" do
+      [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+      child = Enum.at(children, 8)
+      assert child[:suggestions] == [":tlsv1", ":tlsv1.1", ":tlsv1.2"]
+    end
+
+    test "subgroup with module name" do
+      [%{children: children} | _] = Generator.convert_to_strings(@descriptions)
+
+      %{group: subgroup} = Enum.at(children, 6)
+      assert subgroup == {":subgroup", "Swoosh.Adapters.SMTP"}
+    end
+  end
+end
diff --git a/test/fixtures/config/temp.secret.exs b/test/fixtures/config/temp.secret.exs
new file mode 100644
index 000000000..f4686c101
--- /dev/null
+++ b/test/fixtures/config/temp.secret.exs
@@ -0,0 +1,9 @@
+use Mix.Config
+
+config :pleroma, :first_setting, key: "value", key2: [Pleroma.Repo]
+
+config :pleroma, :second_setting, key: "value2", key2: ["Activity"]
+
+config :quack, level: :info
+
+config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 100864055..780235cb9 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -394,9 +394,15 @@ defmodule Pleroma.Factory do
   end
 
   def config_factory do
-    %Pleroma.Web.AdminAPI.Config{
-      key: sequence(:key, &"some_key_#{&1}"),
-      group: "pleroma",
+    %Pleroma.ConfigDB{
+      key:
+        sequence(:key, fn key ->
+          # Atom dynamic registration hack in tests
+          "some_key_#{key}"
+          |> String.to_atom()
+          |> inspect()
+        end),
+      group: ":pleroma",
       value:
         sequence(
           :value,
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index af2b2eddf..9f817622d 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Tests.Helpers do
   @moduledoc """
   Helpers for use in tests.
   """
+  alias Pleroma.Config
 
   defmacro clear_config(config_path) do
     quote do
@@ -17,9 +18,9 @@ defmodule Pleroma.Tests.Helpers do
   defmacro clear_config(config_path, do: yield) do
     quote do
       setup do
-        initial_setting = Pleroma.Config.get(unquote(config_path))
+        initial_setting = Config.get(unquote(config_path))
         unquote(yield)
-        on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
+        on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
         :ok
       end
     end
@@ -35,9 +36,9 @@ defmodule Pleroma.Tests.Helpers do
   defmacro clear_config_all(config_path, do: yield) do
     quote do
       setup_all do
-        initial_setting = Pleroma.Config.get(unquote(config_path))
+        initial_setting = Config.get(unquote(config_path))
         unquote(yield)
-        on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
+        on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
         :ok
       end
     end
@@ -94,10 +95,10 @@ defmodule Pleroma.Tests.Helpers do
 
       defmacro guards_config(config_path) do
         quote do
-          initial_setting = Pleroma.Config.get(config_path)
+          initial_setting = Config.get(config_path)
 
-          Pleroma.Config.put(config_path, true)
-          on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+          Config.put(config_path, true)
+          on_exit(fn -> Config.put(config_path, initial_setting) end)
         end
       end
     end
diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs
index fab9d6e9a..2e56e6cfe 100644
--- a/test/tasks/config_test.exs
+++ b/test/tasks/config_test.exs
@@ -4,143 +4,173 @@
 
 defmodule Mix.Tasks.Pleroma.ConfigTest do
   use Pleroma.DataCase
+
+  alias Pleroma.ConfigDB
   alias Pleroma.Repo
-  alias Pleroma.Web.AdminAPI.Config
 
   setup_all do
     Mix.shell(Mix.Shell.Process)
-    temp_file = "config/temp.exported_from_db.secret.exs"
 
     on_exit(fn ->
       Mix.shell(Mix.Shell.IO)
       Application.delete_env(:pleroma, :first_setting)
       Application.delete_env(:pleroma, :second_setting)
-      :ok = File.rm(temp_file)
     end)
 
-    {:ok, temp_file: temp_file}
+    :ok
   end
 
-  clear_config_all([:instance, :dynamic_configuration]) do
-    Pleroma.Config.put([:instance, :dynamic_configuration], true)
+  clear_config_all(:configurable_from_database) do
+    Pleroma.Config.put(:configurable_from_database, true)
+  end
+
+  test "error if file with custom settings doesn't exist" do
+    Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
+
+    assert_receive {:mix_shell, :info,
+                    [
+                      "To migrate settings, you must define custom settings in config/test.secret.exs."
+                    ]},
+                   15
   end
 
   test "settings are migrated to db" do
-    assert Repo.all(Config) == []
+    initial = Application.get_env(:quack, :level)
+    on_exit(fn -> Application.put_env(:quack, :level, initial) end)
+    assert Repo.all(ConfigDB) == []
 
-    Application.put_env(:pleroma, :first_setting, key: "value", key2: [Pleroma.Repo])
-    Application.put_env(:pleroma, :second_setting, key: "value2", key2: [Pleroma.Activity])
+    Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
 
-    Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
+    config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
+    config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"})
+    config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"})
+    refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"})
 
-    first_db = Config.get_by_params(%{group: "pleroma", key: ":first_setting"})
-    second_db = Config.get_by_params(%{group: "pleroma", key: ":second_setting"})
-    refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"})
-
-    assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]]
-    assert Config.from_binary(second_db.value) == [key: "value2", key2: [Pleroma.Activity]]
+    assert ConfigDB.from_binary(config1.value) == [key: "value", key2: [Repo]]
+    assert ConfigDB.from_binary(config2.value) == [key: "value2", key2: ["Activity"]]
+    assert ConfigDB.from_binary(config3.value) == :info
   end
 
-  test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
-    Config.create(%{
-      group: "pleroma",
-      key: ":setting_first",
-      value: [key: "value", key2: [Pleroma.Activity]]
-    })
+  describe "with deletion temp file" do
+    setup do
+      temp_file = "config/temp.exported_from_db.secret.exs"
 
-    Config.create(%{
-      group: "pleroma",
-      key: ":setting_second",
-      value: [key: "valu2", key2: [Pleroma.Repo]]
-    })
+      on_exit(fn ->
+        :ok = File.rm(temp_file)
+      end)
 
-    Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"])
+      {:ok, temp_file: temp_file}
+    end
 
-    assert Repo.all(Config) == []
-    assert File.exists?(temp_file)
-    {:ok, file} = File.read(temp_file)
+    test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
+      ConfigDB.create(%{
+        group: ":pleroma",
+        key: ":setting_first",
+        value: [key: "value", key2: ["Activity"]]
+      })
 
-    assert file =~ "config :pleroma, :setting_first,"
-    assert file =~ "config :pleroma, :setting_second,"
-  end
+      ConfigDB.create(%{
+        group: ":pleroma",
+        key: ":setting_second",
+        value: [key: "value2", key2: [Repo]]
+      })
 
-  test "load a settings with large values and pass to file", %{temp_file: temp_file} do
-    Config.create(%{
-      group: "pleroma",
-      key: ":instance",
-      value: [
-        name: "Pleroma",
-        email: "example@example.com",
-        notify_email: "noreply@example.com",
-        description: "A Pleroma instance, an alternative fediverse server",
-        limit: 5_000,
-        chat_limit: 5_000,
-        remote_limit: 100_000,
-        upload_limit: 16_000_000,
-        avatar_upload_limit: 2_000_000,
-        background_upload_limit: 4_000_000,
-        banner_upload_limit: 4_000_000,
-        poll_limits: %{
-          max_options: 20,
-          max_option_chars: 200,
-          min_expiration: 0,
-          max_expiration: 365 * 24 * 60 * 60
-        },
-        registrations_open: true,
-        federating: true,
-        federation_incoming_replies_max_depth: 100,
-        federation_reachability_timeout_days: 7,
-        federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],
-        allow_relay: true,
-        rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
-        public: true,
-        quarantined_instances: [],
-        managed_config: true,
-        static_dir: "instance/static/",
-        allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
-        mrf_transparency: true,
-        mrf_transparency_exclusions: [],
-        autofollowed_nicknames: [],
-        max_pinned_statuses: 1,
-        no_attachment_links: true,
-        welcome_user_nickname: nil,
-        welcome_message: nil,
-        max_report_comment_size: 1000,
-        safe_dm_mentions: false,
-        healthcheck: false,
-        remote_post_retention_days: 90,
-        skip_thread_containment: true,
-        limit_to_local_content: :unauthenticated,
-        dynamic_configuration: false,
-        user_bio_length: 5000,
-        user_name_length: 100,
-        max_account_fields: 10,
-        max_remote_account_fields: 20,
-        account_field_name_length: 512,
-        account_field_value_length: 2048,
-        external_user_synchronization: true,
-        extended_nickname_format: true,
-        multi_factor_authentication: [
-          totp: [
-            # digits 6 or 8
-            digits: 6,
-            period: 30
-          ],
-          backup_codes: [
-            number: 2,
-            length: 6
+      ConfigDB.create(%{group: ":quack", key: ":level", value: :info})
+
+      Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
+
+      assert Repo.all(ConfigDB) == []
+
+      file = File.read!(temp_file)
+      assert file =~ "config :pleroma, :setting_first,"
+      assert file =~ "config :pleroma, :setting_second,"
+      assert file =~ "config :quack, :level, :info"
+    end
+
+    test "load a settings with large values and pass to file", %{temp_file: temp_file} do
+      ConfigDB.create(%{
+        group: ":pleroma",
+        key: ":instance",
+        value: [
+          name: "Pleroma",
+          email: "example@example.com",
+          notify_email: "noreply@example.com",
+          description: "A Pleroma instance, an alternative fediverse server",
+          limit: 5_000,
+          chat_limit: 5_000,
+          remote_limit: 100_000,
+          upload_limit: 16_000_000,
+          avatar_upload_limit: 2_000_000,
+          background_upload_limit: 4_000_000,
+          banner_upload_limit: 4_000_000,
+          poll_limits: %{
+            max_options: 20,
+            max_option_chars: 200,
+            min_expiration: 0,
+            max_expiration: 365 * 24 * 60 * 60
+          },
+          registrations_open: true,
+          federating: true,
+          federation_incoming_replies_max_depth: 100,
+          federation_reachability_timeout_days: 7,
+          federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],
+          allow_relay: true,
+          rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
+          public: true,
+          quarantined_instances: [],
+          managed_config: true,
+          static_dir: "instance/static/",
+          allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
+          mrf_transparency: true,
+          mrf_transparency_exclusions: [],
+          autofollowed_nicknames: [],
+          max_pinned_statuses: 1,
+          no_attachment_links: true,
+          welcome_user_nickname: nil,
+          welcome_message: nil,
+          max_report_comment_size: 1000,
+          safe_dm_mentions: false,
+          healthcheck: false,
+          remote_post_retention_days: 90,
+          skip_thread_containment: true,
+          limit_to_local_content: :unauthenticated,
+          user_bio_length: 5000,
+          user_name_length: 100,
+          max_account_fields: 10,
+          max_remote_account_fields: 20,
+          account_field_name_length: 512,
+          account_field_value_length: 2048,
+          external_user_synchronization: true,
+          extended_nickname_format: true,
+          multi_factor_authentication: [
+            totp: [
+              # digits 6 or 8
+              digits: 6,
+              period: 30
+            ],
+            backup_codes: [
+              number: 2,
+              length: 6
+            ]
           ]
         ]
-      ]
-    })
+      })
 
-    Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"])
+      Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
 
-    assert Repo.all(Config) == []
-    assert File.exists?(temp_file)
-    {:ok, file} = File.read(temp_file)
+      assert Repo.all(ConfigDB) == []
+      assert File.exists?(temp_file)
+      {:ok, file} = File.read(temp_file)
 
-    assert file ==
-             "use Mix.Config\n\nconfig :pleroma, :instance,\n  name: \"Pleroma\",\n  email: \"example@example.com\",\n  notify_email: \"noreply@example.com\",\n  description: \"A Pleroma instance, an alternative fediverse server\",\n  limit: 5000,\n  chat_limit: 5000,\n  remote_limit: 100_000,\n  upload_limit: 16_000_000,\n  avatar_upload_limit: 2_000_000,\n  background_upload_limit: 4_000_000,\n  banner_upload_limit: 4_000_000,\n  poll_limits: %{\n    max_expiration: 31_536_000,\n    max_option_chars: 200,\n    max_options: 20,\n    min_expiration: 0\n  },\n  registrations_open: true,\n  federating: true,\n  federation_incoming_replies_max_depth: 100,\n  federation_reachability_timeout_days: 7,\n  federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n  allow_relay: true,\n  rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n  public: true,\n  quarantined_instances: [],\n  managed_config: true,\n  static_dir: \"instance/static/\",\n  allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n  mrf_transparency: true,\n  mrf_transparency_exclusions: [],\n  autofollowed_nicknames: [],\n  max_pinned_statuses: 1,\n  no_attachment_links: true,\n  welcome_user_nickname: nil,\n  welcome_message: nil,\n  max_report_comment_size: 1000,\n  safe_dm_mentions: false,\n  healthcheck: false,\n  remote_post_retention_days: 90,\n  skip_thread_containment: true,\n  limit_to_local_content: :unauthenticated,\n  dynamic_configuration: false,\n  user_bio_length: 5000,\n  user_name_length: 100,\n  max_account_fields: 10,\n  max_remote_account_fields: 20,\n  account_field_name_length: 512,\n  account_field_value_length: 2048,\n  external_user_synchronization: true,\n  extended_nickname_format: true,\n  multi_factor_authentication: [\n    totp: [digits: 6, period: 30],\n    backup_codes: [number: 2, length: 6]\n  ]\n"
+      header =
+        if Code.ensure_loaded?(Config.Reader) do
+          "import Config"
+        else
+          "use Mix.Config"
+        end
+
+      assert file ==
+               "#{header}\n\nconfig :pleroma, :instance,\n  name: \"Pleroma\",\n  email: \"example@example.com\",\n  notify_email: \"noreply@example.com\",\n  description: \"A Pleroma instance, an alternative fediverse server\",\n  limit: 5000,\n  chat_limit: 5000,\n  remote_limit: 100_000,\n  upload_limit: 16_000_000,\n  avatar_upload_limit: 2_000_000,\n  background_upload_limit: 4_000_000,\n  banner_upload_limit: 4_000_000,\n  poll_limits: %{\n    max_expiration: 31_536_000,\n    max_option_chars: 200,\n    max_options: 20,\n    min_expiration: 0\n  },\n  registrations_open: true,\n  federating: true,\n  federation_incoming_replies_max_depth: 100,\n  federation_reachability_timeout_days: 7,\n  federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n  allow_relay: true,\n  rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n  public: true,\n  quarantined_instances: [],\n  managed_config: true,\n  static_dir: \"instance/static/\",\n  allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n  mrf_transparency: true,\n  mrf_transparency_exclusions: [],\n  autofollowed_nicknames: [],\n  max_pinned_statuses: 1,\n  no_attachment_links: true,\n  welcome_user_nickname: nil,\n  welcome_message: nil,\n  max_report_comment_size: 1000,\n  safe_dm_mentions: false,\n  healthcheck: false,\n  remote_post_retention_days: 90,\n  skip_thread_containment: true,\n  limit_to_local_content: :unauthenticated,\n  user_bio_length: 5000,\n  user_name_length: 100,\n  max_account_fields: 10,\n  max_remote_account_fields: 20,\n  account_field_name_length: 512,\n  account_field_value_length: 2048,\n  external_user_synchronization: true,\n  extended_nickname_format: true,\n  multi_factor_authentication: [\n    totp: [digits: 6, period: 30],\n    backup_codes: [number: 2, length: 6]\n  ]\n"
+    end
   end
 end
diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs
index 6d7eed4c1..d69275726 100644
--- a/test/tasks/instance_test.exs
+++ b/test/tasks/instance_test.exs
@@ -78,7 +78,7 @@ defmodule Pleroma.InstanceTest do
     assert generated_config =~ "database: \"dbname\""
     assert generated_config =~ "username: \"dbuser\""
     assert generated_config =~ "password: \"dbpass\""
-    assert generated_config =~ "dynamic_configuration: true"
+    assert generated_config =~ "configurable_from_database: true"
     assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]"
     assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql()
   end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index c8f8ba310..5c767219a 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
   use Oban.Testing, repo: Pleroma.Repo
 
   alias Pleroma.Activity
+  alias Pleroma.ConfigDB
   alias Pleroma.HTML
   alias Pleroma.ModerationLog
   alias Pleroma.Repo
@@ -597,7 +598,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
 
       assert json_response(conn, :no_content)
 
-      token_record = List.last(Pleroma.Repo.all(Pleroma.UserInviteToken))
+      token_record = List.last(Repo.all(Pleroma.UserInviteToken))
       assert token_record
       refute token_record.used
 
@@ -1884,25 +1885,42 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
   end
 
   describe "GET /api/pleroma/admin/config" do
+    clear_config(:configurable_from_database) do
+      Pleroma.Config.put(:configurable_from_database, true)
+    end
+
+    test "when configuration from database is off", %{conn: conn} do
+      initial = Pleroma.Config.get(:configurable_from_database)
+      Pleroma.Config.put(:configurable_from_database, false)
+      on_exit(fn -> Pleroma.Config.put(:configurable_from_database, initial) end)
+      conn = get(conn, "/api/pleroma/admin/config")
+
+      assert json_response(conn, 400) ==
+               "To use this endpoint you need to enable configuration from database."
+    end
+
     test "without any settings in db", %{conn: conn} do
       conn = get(conn, "/api/pleroma/admin/config")
 
-      assert json_response(conn, 200) == %{"configs" => []}
+      assert json_response(conn, 400) ==
+               "To use configuration from database migrate your settings to database."
     end
 
-    test "with settings in db", %{conn: conn} do
+    test "with settings only in db", %{conn: conn} do
       config1 = insert(:config)
       config2 = insert(:config)
 
-      conn = get(conn, "/api/pleroma/admin/config")
+      conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true})
 
       %{
         "configs" => [
           %{
+            "group" => ":pleroma",
             "key" => key1,
             "value" => _
           },
           %{
+            "group" => ":pleroma",
             "key" => key2,
             "value" => _
           }
@@ -1912,11 +1930,107 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert key1 == config1.key
       assert key2 == config2.key
     end
+
+    test "db is added to settings that are in db", %{conn: conn} do
+      _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name"))
+
+      %{"configs" => configs} =
+        conn
+        |> get("/api/pleroma/admin/config")
+        |> json_response(200)
+
+      [instance_config] =
+        Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+          group == ":pleroma" and key == ":instance"
+        end)
+
+      assert instance_config["db"] == [":name"]
+    end
+
+    test "merged default setting with db settings", %{conn: conn} do
+      config1 = insert(:config)
+      config2 = insert(:config)
+
+      config3 =
+        insert(:config,
+          value: ConfigDB.to_binary(k1: :v1, k2: :v2)
+        )
+
+      %{"configs" => configs} =
+        conn
+        |> get("/api/pleroma/admin/config")
+        |> json_response(200)
+
+      assert length(configs) > 3
+
+      received_configs =
+        Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+          group == ":pleroma" and key in [config1.key, config2.key, config3.key]
+        end)
+
+      assert length(received_configs) == 3
+
+      db_keys =
+        config3.value
+        |> ConfigDB.from_binary()
+        |> Keyword.keys()
+        |> ConfigDB.convert()
+
+      Enum.each(received_configs, fn %{"value" => value, "db" => db} ->
+        assert db in [[config1.key], [config2.key], db_keys]
+
+        assert value in [
+                 ConfigDB.from_binary_with_convert(config1.value),
+                 ConfigDB.from_binary_with_convert(config2.value),
+                 ConfigDB.from_binary_with_convert(config3.value)
+               ]
+      end)
+    end
+
+    test "subkeys with full update right merge", %{conn: conn} do
+      config1 =
+        insert(:config,
+          key: ":emoji",
+          value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
+        )
+
+      config2 =
+        insert(:config,
+          key: ":assets",
+          value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
+        )
+
+      %{"configs" => configs} =
+        conn
+        |> get("/api/pleroma/admin/config")
+        |> json_response(200)
+
+      vals =
+        Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+          group == ":pleroma" and key in [config1.key, config2.key]
+        end)
+
+      emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end)
+      assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end)
+
+      emoji_val = ConfigDB.transform_with_out_binary(emoji["value"])
+      assets_val = ConfigDB.transform_with_out_binary(assets["value"])
+
+      assert emoji_val[:groups] == [a: 1, b: 2]
+      assert assets_val[:mascots] == [a: 1, b: 2]
+    end
+  end
+
+  test "POST /api/pleroma/admin/config error", %{conn: conn} do
+    conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []})
+
+    assert json_response(conn, 400) ==
+             "To use this endpoint you need to enable configuration from database."
   end
 
   describe "POST /api/pleroma/admin/config" do
     setup do
-      temp_file = "config/test.exported_from_db.secret.exs"
+      http = Application.get_env(:pleroma, :http)
 
       on_exit(fn ->
         Application.delete_env(:pleroma, :key1)
@@ -1927,28 +2041,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         Application.delete_env(:pleroma, :keyaa2)
         Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal)
         Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
-        :ok = File.rm(temp_file)
+        Application.put_env(:pleroma, :http, http)
+        Application.put_env(:tesla, :adapter, Tesla.Mock)
+        :ok = File.rm("config/test.exported_from_db.secret.exs")
       end)
     end
 
-    clear_config([:instance, :dynamic_configuration]) do
-      Pleroma.Config.put([:instance, :dynamic_configuration], true)
+    clear_config(:configurable_from_database) do
+      Pleroma.Config.put(:configurable_from_database, true)
     end
 
     @tag capture_log: true
     test "create new config setting in db", %{conn: conn} do
+      ueberauth = Application.get_env(:ueberauth, Ueberauth)
+      on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
+
       conn =
         post(conn, "/api/pleroma/admin/config", %{
           configs: [
-            %{group: "pleroma", key: "key1", value: "value1"},
+            %{group: ":pleroma", key: ":key1", value: "value1"},
             %{
-              group: "ueberauth",
-              key: "Ueberauth.Strategy.Twitter.OAuth",
+              group: ":ueberauth",
+              key: "Ueberauth",
               value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
             },
             %{
-              group: "pleroma",
-              key: "key2",
+              group: ":pleroma",
+              key: ":key2",
               value: %{
                 ":nested_1" => "nested_value1",
                 ":nested_2" => [
@@ -1958,21 +2077,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
               }
             },
             %{
-              group: "pleroma",
-              key: "key3",
+              group: ":pleroma",
+              key: ":key3",
               value: [
                 %{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
                 %{"nested_4" => true}
               ]
             },
             %{
-              group: "pleroma",
-              key: "key4",
+              group: ":pleroma",
+              key: ":key4",
               value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
             },
             %{
-              group: "idna",
-              key: "key5",
+              group: ":idna",
+              key: ":key5",
               value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
             }
           ]
@@ -1981,43 +2100,49 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert json_response(conn, 200) == %{
                "configs" => [
                  %{
-                   "group" => "pleroma",
-                   "key" => "key1",
-                   "value" => "value1"
+                   "group" => ":pleroma",
+                   "key" => ":key1",
+                   "value" => "value1",
+                   "db" => [":key1"]
                  },
                  %{
-                   "group" => "ueberauth",
-                   "key" => "Ueberauth.Strategy.Twitter.OAuth",
-                   "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}]
+                   "group" => ":ueberauth",
+                   "key" => "Ueberauth",
+                   "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
+                   "db" => [":consumer_secret"]
                  },
                  %{
-                   "group" => "pleroma",
-                   "key" => "key2",
+                   "group" => ":pleroma",
+                   "key" => ":key2",
                    "value" => %{
                      ":nested_1" => "nested_value1",
                      ":nested_2" => [
                        %{":nested_22" => "nested_value222"},
                        %{":nested_33" => %{":nested_44" => "nested_444"}}
                      ]
-                   }
+                   },
+                   "db" => [":key2"]
                  },
                  %{
-                   "group" => "pleroma",
-                   "key" => "key3",
+                   "group" => ":pleroma",
+                   "key" => ":key3",
                    "value" => [
                      %{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
                      %{"nested_4" => true}
-                   ]
+                   ],
+                   "db" => [":key3"]
                  },
                  %{
-                   "group" => "pleroma",
-                   "key" => "key4",
-                   "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}
+                   "group" => ":pleroma",
+                   "key" => ":key4",
+                   "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"},
+                   "db" => [":key4"]
                  },
                  %{
-                   "group" => "idna",
-                   "key" => "key5",
-                   "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
+                   "group" => ":idna",
+                   "key" => ":key5",
+                   "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
+                   "db" => [":key5"]
                  }
                ]
              }
@@ -2045,25 +2170,34 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
     end
 
-    test "update config setting & delete", %{conn: conn} do
-      config1 = insert(:config, key: "keyaa1")
-      config2 = insert(:config, key: "keyaa2")
+    test "save config setting without key", %{conn: conn} do
+      level = Application.get_env(:quack, :level)
+      meta = Application.get_env(:quack, :meta)
+      webhook_url = Application.get_env(:quack, :webhook_url)
 
-      insert(:config,
-        group: "ueberauth",
-        key: "Ueberauth.Strategy.Microsoft.OAuth",
-        value: :erlang.term_to_binary([])
-      )
+      on_exit(fn ->
+        Application.put_env(:quack, :level, level)
+        Application.put_env(:quack, :meta, meta)
+        Application.put_env(:quack, :webhook_url, webhook_url)
+      end)
 
       conn =
         post(conn, "/api/pleroma/admin/config", %{
           configs: [
-            %{group: config1.group, key: config1.key, value: "another_value"},
-            %{group: config2.group, key: config2.key, delete: "true"},
             %{
-              group: "ueberauth",
-              key: "Ueberauth.Strategy.Microsoft.OAuth",
-              delete: "true"
+              group: ":quack",
+              key: ":level",
+              value: ":info"
+            },
+            %{
+              group: ":quack",
+              key: ":meta",
+              value: [":none"]
+            },
+            %{
+              group: ":quack",
+              key: ":webhook_url",
+              value: "https://hooks.slack.com/services/KEY"
             }
           ]
         })
@@ -2071,23 +2205,300 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert json_response(conn, 200) == %{
                "configs" => [
                  %{
-                   "group" => "pleroma",
+                   "group" => ":quack",
+                   "key" => ":level",
+                   "value" => ":info",
+                   "db" => [":level"]
+                 },
+                 %{
+                   "group" => ":quack",
+                   "key" => ":meta",
+                   "value" => [":none"],
+                   "db" => [":meta"]
+                 },
+                 %{
+                   "group" => ":quack",
+                   "key" => ":webhook_url",
+                   "value" => "https://hooks.slack.com/services/KEY",
+                   "db" => [":webhook_url"]
+                 }
+               ]
+             }
+
+      assert Application.get_env(:quack, :level) == :info
+      assert Application.get_env(:quack, :meta) == [:none]
+      assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY"
+    end
+
+    test "saving config with partial update", %{conn: conn} do
+      config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
+
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]}
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => ":key1",
+                   "value" => [
+                     %{"tuple" => [":key1", 1]},
+                     %{"tuple" => [":key2", 2]},
+                     %{"tuple" => [":key3", 3]}
+                   ],
+                   "db" => [":key1", ":key2", ":key3"]
+                 }
+               ]
+             }
+    end
+
+    test "saving config with nested merge", %{conn: conn} do
+      config =
+        insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2]))
+
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{
+              group: config.group,
+              key: config.key,
+              value: [
+                %{"tuple" => [":key3", 3]},
+                %{
+                  "tuple" => [
+                    ":key2",
+                    [
+                      %{"tuple" => [":k2", 1]},
+                      %{"tuple" => [":k3", 3]}
+                    ]
+                  ]
+                }
+              ]
+            }
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => ":key1",
+                   "value" => [
+                     %{"tuple" => [":key1", 1]},
+                     %{"tuple" => [":key3", 3]},
+                     %{
+                       "tuple" => [
+                         ":key2",
+                         [
+                           %{"tuple" => [":k1", 1]},
+                           %{"tuple" => [":k2", 1]},
+                           %{"tuple" => [":k3", 3]}
+                         ]
+                       ]
+                     }
+                   ],
+                   "db" => [":key1", ":key3", ":key2"]
+                 }
+               ]
+             }
+    end
+
+    test "saving special atoms", %{conn: conn} do
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          "configs" => [
+            %{
+              "group" => ":pleroma",
+              "key" => ":key1",
+              "value" => [
+                %{
+                  "tuple" => [
+                    ":ssl_options",
+                    [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
+                  ]
+                }
+              ]
+            }
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => ":key1",
+                   "value" => [
+                     %{
+                       "tuple" => [
+                         ":ssl_options",
+                         [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
+                       ]
+                     }
+                   ],
+                   "db" => [":ssl_options"]
+                 }
+               ]
+             }
+
+      assert Application.get_env(:pleroma, :key1) == [
+               ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]]
+             ]
+    end
+
+    test "saving full setting if value is in full_key_update list", %{conn: conn} do
+      backends = Application.get_env(:logger, :backends)
+      on_exit(fn -> Application.put_env(:logger, :backends, backends) end)
+
+      config =
+        insert(:config,
+          group: ":logger",
+          key: ":backends",
+          value: :erlang.term_to_binary([])
+        )
+
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{
+              group: config.group,
+              key: config.key,
+              value: [":console", %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}]
+            }
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":logger",
+                   "key" => ":backends",
+                   "value" => [
+                     ":console",
+                     %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}
+                   ],
+                   "db" => [":backends"]
+                 }
+               ]
+             }
+
+      assert Application.get_env(:logger, :backends) == [
+               :console,
+               {ExSyslogger, :ex_syslogger}
+             ]
+
+      ExUnit.CaptureLog.capture_log(fn ->
+        require Logger
+        Logger.warn("Ooops...")
+      end) =~ "Ooops..."
+    end
+
+    test "saving full setting if value is not keyword", %{conn: conn} do
+      config =
+        insert(:config,
+          group: ":tesla",
+          key: ":adapter",
+          value: :erlang.term_to_binary(Tesla.Adapter.Hackey)
+        )
+
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"}
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":tesla",
+                   "key" => ":adapter",
+                   "value" => "Tesla.Adapter.Httpc",
+                   "db" => [":adapter"]
+                 }
+               ]
+             }
+    end
+
+    test "update config setting & delete with fallback to default value", %{
+      conn: conn,
+      admin: admin,
+      token: token
+    } do
+      ueberauth = Application.get_env(:ueberauth, Ueberauth)
+      config1 = insert(:config, key: ":keyaa1")
+      config2 = insert(:config, key: ":keyaa2")
+
+      config3 =
+        insert(:config,
+          group: ":ueberauth",
+          key: "Ueberauth"
+        )
+
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{group: config1.group, key: config1.key, value: "another_value"},
+            %{group: config2.group, key: config2.key, value: "another_value"}
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
                    "key" => config1.key,
-                   "value" => "another_value"
+                   "value" => "another_value",
+                   "db" => [":keyaa1"]
+                 },
+                 %{
+                   "group" => ":pleroma",
+                   "key" => config2.key,
+                   "value" => "another_value",
+                   "db" => [":keyaa2"]
                  }
                ]
              }
 
       assert Application.get_env(:pleroma, :keyaa1) == "another_value"
-      refute Application.get_env(:pleroma, :keyaa2)
+      assert Application.get_env(:pleroma, :keyaa2) == "another_value"
+      assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value)
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> assign(:token, token)
+        |> post("/api/pleroma/admin/config", %{
+          configs: [
+            %{group: config2.group, key: config2.key, delete: true},
+            %{
+              group: ":ueberauth",
+              key: "Ueberauth",
+              delete: true
+            }
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => []
+             }
+
+      assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
+      refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2)
     end
 
     test "common config example", %{conn: conn} do
+      adapter = Application.get_env(:tesla, :adapter)
+      on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
+
       conn =
         post(conn, "/api/pleroma/admin/config", %{
           configs: [
             %{
-              "group" => "pleroma",
+              "group" => ":pleroma",
               "key" => "Pleroma.Captcha.NotReal",
               "value" => [
                 %{"tuple" => [":enabled", false]},
@@ -2099,16 +2510,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                 %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]},
                 %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]},
                 %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]},
-                %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}
+                %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]},
+                %{"tuple" => [":name", "Pleroma"]}
               ]
+            },
+            %{
+              "group" => ":tesla",
+              "key" => ":adapter",
+              "value" => "Tesla.Adapter.Httpc"
             }
           ]
         })
 
+      assert Application.get_env(:tesla, :adapter) == Tesla.Adapter.Httpc
+      assert Pleroma.Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma"
+
       assert json_response(conn, 200) == %{
                "configs" => [
                  %{
-                   "group" => "pleroma",
+                   "group" => ":pleroma",
                    "key" => "Pleroma.Captcha.NotReal",
                    "value" => [
                      %{"tuple" => [":enabled", false]},
@@ -2120,8 +2540,28 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                      %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]},
                      %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]},
                      %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]},
-                     %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}
+                     %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]},
+                     %{"tuple" => [":name", "Pleroma"]}
+                   ],
+                   "db" => [
+                     ":enabled",
+                     ":method",
+                     ":seconds_valid",
+                     ":path",
+                     ":key1",
+                     ":partial_chain",
+                     ":regex1",
+                     ":regex2",
+                     ":regex3",
+                     ":regex4",
+                     ":name"
                    ]
+                 },
+                 %{
+                   "group" => ":tesla",
+                   "key" => ":adapter",
+                   "value" => "Tesla.Adapter.Httpc",
+                   "db" => [":adapter"]
                  }
                ]
              }
@@ -2132,7 +2572,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         post(conn, "/api/pleroma/admin/config", %{
           configs: [
             %{
-              "group" => "pleroma",
+              "group" => ":pleroma",
               "key" => "Pleroma.Web.Endpoint.NotReal",
               "value" => [
                 %{
@@ -2196,7 +2636,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert json_response(conn, 200) == %{
                "configs" => [
                  %{
-                   "group" => "pleroma",
+                   "group" => ":pleroma",
                    "key" => "Pleroma.Web.Endpoint.NotReal",
                    "value" => [
                      %{
@@ -2252,7 +2692,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                          ]
                        ]
                      }
-                   ]
+                   ],
+                   "db" => [":http"]
                  }
                ]
              }
@@ -2263,7 +2704,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         post(conn, "/api/pleroma/admin/config", %{
           configs: [
             %{
-              "group" => "pleroma",
+              "group" => ":pleroma",
               "key" => ":key1",
               "value" => [
                 %{"tuple" => [":key2", "some_val"]},
@@ -2293,7 +2734,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                %{
                  "configs" => [
                    %{
-                     "group" => "pleroma",
+                     "group" => ":pleroma",
                      "key" => ":key1",
                      "value" => [
                        %{"tuple" => [":key2", "some_val"]},
@@ -2314,7 +2755,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                            }
                          ]
                        }
-                     ]
+                     ],
+                     "db" => [":key2", ":key3"]
                    }
                  ]
                }
@@ -2325,7 +2767,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         post(conn, "/api/pleroma/admin/config", %{
           configs: [
             %{
-              "group" => "pleroma",
+              "group" => ":pleroma",
               "key" => ":key1",
               "value" => %{"key" => "some_val"}
             }
@@ -2336,83 +2778,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                %{
                  "configs" => [
                    %{
-                     "group" => "pleroma",
+                     "group" => ":pleroma",
                      "key" => ":key1",
-                     "value" => %{"key" => "some_val"}
+                     "value" => %{"key" => "some_val"},
+                     "db" => [":key1"]
                    }
                  ]
                }
     end
 
-    test "dispatch setting", %{conn: conn} do
-      conn =
-        post(conn, "/api/pleroma/admin/config", %{
-          configs: [
-            %{
-              "group" => "pleroma",
-              "key" => "Pleroma.Web.Endpoint.NotReal",
-              "value" => [
-                %{
-                  "tuple" => [
-                    ":http",
-                    [
-                      %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
-                      %{"tuple" => [":dispatch", ["{:_,
-       [
-         {\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-         {\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
-          {Phoenix.Transports.WebSocket,
-           {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
-         {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-       ]}"]]}
-                    ]
-                  ]
-                }
-              ]
-            }
-          ]
-        })
-
-      dispatch_string =
-        "{:_, [{\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, " <>
-          "{\"/websocket\", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, " <>
-          "{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}}, " <>
-          "{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}]}"
-
-      assert json_response(conn, 200) == %{
-               "configs" => [
-                 %{
-                   "group" => "pleroma",
-                   "key" => "Pleroma.Web.Endpoint.NotReal",
-                   "value" => [
-                     %{
-                       "tuple" => [
-                         ":http",
-                         [
-                           %{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
-                           %{
-                             "tuple" => [
-                               ":dispatch",
-                               [
-                                 dispatch_string
-                               ]
-                             ]
-                           }
-                         ]
-                       ]
-                     }
-                   ]
-                 }
-               ]
-             }
-    end
-
     test "queues key as atom", %{conn: conn} do
       conn =
         post(conn, "/api/pleroma/admin/config", %{
           configs: [
             %{
-              "group" => "oban",
+              "group" => ":oban",
               "key" => ":queues",
               "value" => [
                 %{"tuple" => [":federator_incoming", 50]},
@@ -2430,7 +2810,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert json_response(conn, 200) == %{
                "configs" => [
                  %{
-                   "group" => "oban",
+                   "group" => ":oban",
                    "key" => ":queues",
                    "value" => [
                      %{"tuple" => [":federator_incoming", 50]},
@@ -2440,6 +2820,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                      %{"tuple" => [":transmogrifier", 20]},
                      %{"tuple" => [":scheduled_activities", 10]},
                      %{"tuple" => [":background", 5]}
+                   ],
+                   "db" => [
+                     ":federator_incoming",
+                     ":federator_outgoing",
+                     ":web_push",
+                     ":mailer",
+                     ":transmogrifier",
+                     ":scheduled_activities",
+                     ":background"
                    ]
                  }
                ]
@@ -2449,7 +2838,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     test "delete part of settings by atom subkeys", %{conn: conn} do
       config =
         insert(:config,
-          key: "keyaa1",
+          key: ":keyaa1",
           value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3")
         )
 
@@ -2460,41 +2849,127 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
               group: config.group,
               key: config.key,
               subkeys: [":subkey1", ":subkey3"],
-              delete: "true"
+              delete: true
             }
           ]
         })
 
-      assert(
-        json_response(conn, 200) == %{
-          "configs" => [
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => ":keyaa1",
+                   "value" => [%{"tuple" => [":subkey2", "val2"]}],
+                   "db" => [":subkey2"]
+                 }
+               ]
+             }
+    end
+
+    test "proxy tuple localhost", %{conn: conn} do
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
             %{
-              "group" => "pleroma",
-              "key" => "keyaa1",
-              "value" => [%{"tuple" => [":subkey2", "val2"]}]
+              group: ":pleroma",
+              key: ":http",
+              value: [
+                %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
+                %{"tuple" => [":send_user_agent", false]}
+              ]
             }
           ]
-        }
-      )
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => ":http",
+                   "value" => [
+                     %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
+                     %{"tuple" => [":send_user_agent", false]}
+                   ],
+                   "db" => [":proxy_url", ":send_user_agent"]
+                 }
+               ]
+             }
+    end
+
+    test "proxy tuple domain", %{conn: conn} do
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{
+              group: ":pleroma",
+              key: ":http",
+              value: [
+                %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
+                %{"tuple" => [":send_user_agent", false]}
+              ]
+            }
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => ":http",
+                   "value" => [
+                     %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
+                     %{"tuple" => [":send_user_agent", false]}
+                   ],
+                   "db" => [":proxy_url", ":send_user_agent"]
+                 }
+               ]
+             }
+    end
+
+    test "proxy tuple ip", %{conn: conn} do
+      conn =
+        post(conn, "/api/pleroma/admin/config", %{
+          configs: [
+            %{
+              group: ":pleroma",
+              key: ":http",
+              value: [
+                %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
+                %{"tuple" => [":send_user_agent", false]}
+              ]
+            }
+          ]
+        })
+
+      assert json_response(conn, 200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => ":http",
+                   "value" => [
+                     %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
+                     %{"tuple" => [":send_user_agent", false]}
+                   ],
+                   "db" => [":proxy_url", ":send_user_agent"]
+                 }
+               ]
+             }
     end
   end
 
   describe "config mix tasks run" do
     setup do
-      temp_file = "config/test.exported_from_db.secret.exs"
-
       Mix.shell(Mix.Shell.Quiet)
 
       on_exit(fn ->
         Mix.shell(Mix.Shell.IO)
-        :ok = File.rm(temp_file)
       end)
 
       :ok
     end
 
-    clear_config([:instance, :dynamic_configuration]) do
-      Pleroma.Config.put([:instance, :dynamic_configuration], true)
+    clear_config(:configurable_from_database) do
+      Pleroma.Config.put(:configurable_from_database, true)
     end
 
     clear_config([:feed, :post_title]) do
@@ -2502,15 +2977,27 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
 
     test "transfer settings to DB and to file", %{conn: conn} do
-      assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
-      ret_conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
-      assert json_response(ret_conn, 200) == %{}
-      assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0
+      assert Repo.all(Pleroma.ConfigDB) == []
+      Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
+      assert Repo.aggregate(Pleroma.ConfigDB, :count, :id) > 0
 
-      ret_conn = get(conn, "/api/pleroma/admin/config/migrate_from_db")
+      conn = get(conn, "/api/pleroma/admin/config/migrate_from_db")
 
-      assert json_response(ret_conn, 200) == %{}
-      assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
+      assert json_response(conn, 200) == %{}
+      assert Repo.all(Pleroma.ConfigDB) == []
+    end
+
+    test "returns error if configuration from database is off", %{conn: conn} do
+      initial = Pleroma.Config.get(:configurable_from_database)
+      on_exit(fn -> Pleroma.Config.put(:configurable_from_database, initial) end)
+      Pleroma.Config.put(:configurable_from_database, false)
+
+      conn = get(conn, "/api/pleroma/admin/config/migrate_from_db")
+
+      assert json_response(conn, 400) ==
+               "To use this endpoint you need to enable configuration from database."
+
+      assert Repo.all(Pleroma.ConfigDB) == []
     end
   end
 
@@ -2979,6 +3466,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       assert ReportNote |> Repo.all() |> length() == 1
     end
   end
+
+  test "GET /api/pleroma/admin/config/descriptions", %{conn: conn} do
+    admin = insert(:user, is_admin: true)
+
+    conn =
+      assign(conn, :user, admin)
+      |> get("/api/pleroma/admin/config/descriptions")
+
+    assert [child | _others] = json_response(conn, 200)
+
+    assert child["children"]
+    assert child["key"]
+    assert String.starts_with?(child["group"], ":")
+    assert child["description"]
+  end
 end
 
 # Needed for testing
diff --git a/test/web/admin_api/config_test.exs b/test/web/admin_api/config_test.exs
deleted file mode 100644
index 204446b79..000000000
--- a/test/web/admin_api/config_test.exs
+++ /dev/null
@@ -1,497 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.AdminAPI.ConfigTest do
-  use Pleroma.DataCase, async: true
-  import Pleroma.Factory
-  alias Pleroma.Web.AdminAPI.Config
-
-  test "get_by_key/1" do
-    config = insert(:config)
-    insert(:config)
-
-    assert config == Config.get_by_params(%{group: config.group, key: config.key})
-  end
-
-  test "create/1" do
-    {:ok, config} = Config.create(%{group: "pleroma", key: "some_key", value: "some_value"})
-    assert config == Config.get_by_params(%{group: "pleroma", key: "some_key"})
-  end
-
-  test "update/1" do
-    config = insert(:config)
-    {:ok, updated} = Config.update(config, %{value: "some_value"})
-    loaded = Config.get_by_params(%{group: config.group, key: config.key})
-    assert loaded == updated
-  end
-
-  test "update_or_create/1" do
-    config = insert(:config)
-    key2 = "another_key"
-
-    params = [
-      %{group: "pleroma", key: key2, value: "another_value"},
-      %{group: config.group, key: config.key, value: "new_value"}
-    ]
-
-    assert Repo.all(Config) |> length() == 1
-
-    Enum.each(params, &Config.update_or_create(&1))
-
-    assert Repo.all(Config) |> length() == 2
-
-    config1 = Config.get_by_params(%{group: config.group, key: config.key})
-    config2 = Config.get_by_params(%{group: "pleroma", key: key2})
-
-    assert config1.value == Config.transform("new_value")
-    assert config2.value == Config.transform("another_value")
-  end
-
-  test "delete/1" do
-    config = insert(:config)
-    {:ok, _} = Config.delete(%{key: config.key, group: config.group})
-    refute Config.get_by_params(%{key: config.key, group: config.group})
-  end
-
-  describe "transform/1" do
-    test "string" do
-      binary = Config.transform("value as string")
-      assert binary == :erlang.term_to_binary("value as string")
-      assert Config.from_binary(binary) == "value as string"
-    end
-
-    test "boolean" do
-      binary = Config.transform(false)
-      assert binary == :erlang.term_to_binary(false)
-      assert Config.from_binary(binary) == false
-    end
-
-    test "nil" do
-      binary = Config.transform(nil)
-      assert binary == :erlang.term_to_binary(nil)
-      assert Config.from_binary(binary) == nil
-    end
-
-    test "integer" do
-      binary = Config.transform(150)
-      assert binary == :erlang.term_to_binary(150)
-      assert Config.from_binary(binary) == 150
-    end
-
-    test "atom" do
-      binary = Config.transform(":atom")
-      assert binary == :erlang.term_to_binary(:atom)
-      assert Config.from_binary(binary) == :atom
-    end
-
-    test "pleroma module" do
-      binary = Config.transform("Pleroma.Bookmark")
-      assert binary == :erlang.term_to_binary(Pleroma.Bookmark)
-      assert Config.from_binary(binary) == Pleroma.Bookmark
-    end
-
-    test "phoenix module" do
-      binary = Config.transform("Phoenix.Socket.V1.JSONSerializer")
-      assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer)
-      assert Config.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer
-    end
-
-    test "sigil" do
-      binary = Config.transform("~r/comp[lL][aA][iI][nN]er/")
-      assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/)
-      assert Config.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/
-    end
-
-    test "link sigil" do
-      binary = Config.transform("~r/https:\/\/example.com/")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/)
-      assert Config.from_binary(binary) == ~r/https:\/\/example.com/
-    end
-
-    test "link sigil with u modifier" do
-      binary = Config.transform("~r/https:\/\/example.com/u")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/u)
-      assert Config.from_binary(binary) == ~r/https:\/\/example.com/u
-    end
-
-    test "link sigil with i modifier" do
-      binary = Config.transform("~r/https:\/\/example.com/i")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/i)
-      assert Config.from_binary(binary) == ~r/https:\/\/example.com/i
-    end
-
-    test "link sigil with s modifier" do
-      binary = Config.transform("~r/https:\/\/example.com/s")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/s)
-      assert Config.from_binary(binary) == ~r/https:\/\/example.com/s
-    end
-
-    test "2 child tuple" do
-      binary = Config.transform(%{"tuple" => ["v1", ":v2"]})
-      assert binary == :erlang.term_to_binary({"v1", :v2})
-      assert Config.from_binary(binary) == {"v1", :v2}
-    end
-
-    test "tuple with n childs" do
-      binary =
-        Config.transform(%{
-          "tuple" => [
-            "v1",
-            ":v2",
-            "Pleroma.Bookmark",
-            150,
-            false,
-            "Phoenix.Socket.V1.JSONSerializer"
-          ]
-        })
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
-               )
-
-      assert Config.from_binary(binary) ==
-               {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
-    end
-
-    test "tuple with dispatch key" do
-      binary = Config.transform(%{"tuple" => [":dispatch", ["{:_,
-       [
-         {\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-         {\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
-          {Phoenix.Transports.WebSocket,
-           {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
-         {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-       ]}"]]})
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 {:dispatch,
-                  [
-                    {:_,
-                     [
-                       {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-                       {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
-                        {Phoenix.Transports.WebSocket,
-                         {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: "/websocket"]}}},
-                       {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-                     ]}
-                  ]}
-               )
-
-      assert Config.from_binary(binary) ==
-               {:dispatch,
-                [
-                  {:_,
-                   [
-                     {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-                     {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
-                      {Phoenix.Transports.WebSocket,
-                       {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: "/websocket"]}}},
-                     {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-                   ]}
-                ]}
-    end
-
-    test "map with string key" do
-      binary = Config.transform(%{"key" => "value"})
-      assert binary == :erlang.term_to_binary(%{"key" => "value"})
-      assert Config.from_binary(binary) == %{"key" => "value"}
-    end
-
-    test "map with atom key" do
-      binary = Config.transform(%{":key" => "value"})
-      assert binary == :erlang.term_to_binary(%{key: "value"})
-      assert Config.from_binary(binary) == %{key: "value"}
-    end
-
-    test "list of strings" do
-      binary = Config.transform(["v1", "v2", "v3"])
-      assert binary == :erlang.term_to_binary(["v1", "v2", "v3"])
-      assert Config.from_binary(binary) == ["v1", "v2", "v3"]
-    end
-
-    test "list of modules" do
-      binary = Config.transform(["Pleroma.Repo", "Pleroma.Activity"])
-      assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity])
-      assert Config.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity]
-    end
-
-    test "list of atoms" do
-      binary = Config.transform([":v1", ":v2", ":v3"])
-      assert binary == :erlang.term_to_binary([:v1, :v2, :v3])
-      assert Config.from_binary(binary) == [:v1, :v2, :v3]
-    end
-
-    test "list of mixed values" do
-      binary =
-        Config.transform([
-          "v1",
-          ":v2",
-          "Pleroma.Repo",
-          "Phoenix.Socket.V1.JSONSerializer",
-          15,
-          false
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary([
-                 "v1",
-                 :v2,
-                 Pleroma.Repo,
-                 Phoenix.Socket.V1.JSONSerializer,
-                 15,
-                 false
-               ])
-
-      assert Config.from_binary(binary) == [
-               "v1",
-               :v2,
-               Pleroma.Repo,
-               Phoenix.Socket.V1.JSONSerializer,
-               15,
-               false
-             ]
-    end
-
-    test "simple keyword" do
-      binary = Config.transform([%{"tuple" => [":key", "value"]}])
-      assert binary == :erlang.term_to_binary([{:key, "value"}])
-      assert Config.from_binary(binary) == [{:key, "value"}]
-      assert Config.from_binary(binary) == [key: "value"]
-    end
-
-    test "keyword with partial_chain key" do
-      binary =
-        Config.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}])
-
-      assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1)
-      assert Config.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1]
-    end
-
-    test "keyword" do
-      binary =
-        Config.transform([
-          %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
-          %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
-          %{"tuple" => [":migration_lock", nil]},
-          %{"tuple" => [":key1", 150]},
-          %{"tuple" => [":key2", "string"]}
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 types: Pleroma.PostgresTypes,
-                 telemetry_event: [Pleroma.Repo.Instrumenter],
-                 migration_lock: nil,
-                 key1: 150,
-                 key2: "string"
-               )
-
-      assert Config.from_binary(binary) == [
-               types: Pleroma.PostgresTypes,
-               telemetry_event: [Pleroma.Repo.Instrumenter],
-               migration_lock: nil,
-               key1: 150,
-               key2: "string"
-             ]
-    end
-
-    test "complex keyword with nested mixed childs" do
-      binary =
-        Config.transform([
-          %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
-          %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
-          %{"tuple" => [":link_name", true]},
-          %{"tuple" => [":proxy_remote", false]},
-          %{"tuple" => [":common_map", %{":key" => "value"}]},
-          %{
-            "tuple" => [
-              ":proxy_opts",
-              [
-                %{"tuple" => [":redirect_on_failure", false]},
-                %{"tuple" => [":max_body_length", 1_048_576]},
-                %{
-                  "tuple" => [
-                    ":http",
-                    [%{"tuple" => [":follow_redirect", true]}, %{"tuple" => [":pool", ":upload"]}]
-                  ]
-                }
-              ]
-            ]
-          }
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 uploader: Pleroma.Uploaders.Local,
-                 filters: [Pleroma.Upload.Filter.Dedupe],
-                 link_name: true,
-                 proxy_remote: false,
-                 common_map: %{key: "value"},
-                 proxy_opts: [
-                   redirect_on_failure: false,
-                   max_body_length: 1_048_576,
-                   http: [
-                     follow_redirect: true,
-                     pool: :upload
-                   ]
-                 ]
-               )
-
-      assert Config.from_binary(binary) ==
-               [
-                 uploader: Pleroma.Uploaders.Local,
-                 filters: [Pleroma.Upload.Filter.Dedupe],
-                 link_name: true,
-                 proxy_remote: false,
-                 common_map: %{key: "value"},
-                 proxy_opts: [
-                   redirect_on_failure: false,
-                   max_body_length: 1_048_576,
-                   http: [
-                     follow_redirect: true,
-                     pool: :upload
-                   ]
-                 ]
-               ]
-    end
-
-    test "common keyword" do
-      binary =
-        Config.transform([
-          %{"tuple" => [":level", ":warn"]},
-          %{"tuple" => [":meta", [":all"]]},
-          %{"tuple" => [":path", ""]},
-          %{"tuple" => [":val", nil]},
-          %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 level: :warn,
-                 meta: [:all],
-                 path: "",
-                 val: nil,
-                 webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
-               )
-
-      assert Config.from_binary(binary) == [
-               level: :warn,
-               meta: [:all],
-               path: "",
-               val: nil,
-               webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
-             ]
-    end
-
-    test "complex keyword with sigil" do
-      binary =
-        Config.transform([
-          %{"tuple" => [":federated_timeline_removal", []]},
-          %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
-          %{"tuple" => [":replace", []]}
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 federated_timeline_removal: [],
-                 reject: [~r/comp[lL][aA][iI][nN]er/],
-                 replace: []
-               )
-
-      assert Config.from_binary(binary) ==
-               [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []]
-    end
-
-    test "complex keyword with tuples with more than 2 values" do
-      binary =
-        Config.transform([
-          %{
-            "tuple" => [
-              ":http",
-              [
-                %{
-                  "tuple" => [
-                    ":key1",
-                    [
-                      %{
-                        "tuple" => [
-                          ":_",
-                          [
-                            %{
-                              "tuple" => [
-                                "/api/v1/streaming",
-                                "Pleroma.Web.MastodonAPI.WebsocketHandler",
-                                []
-                              ]
-                            },
-                            %{
-                              "tuple" => [
-                                "/websocket",
-                                "Phoenix.Endpoint.CowboyWebSocket",
-                                %{
-                                  "tuple" => [
-                                    "Phoenix.Transports.WebSocket",
-                                    %{
-                                      "tuple" => [
-                                        "Pleroma.Web.Endpoint",
-                                        "Pleroma.Web.UserSocket",
-                                        []
-                                      ]
-                                    }
-                                  ]
-                                }
-                              ]
-                            },
-                            %{
-                              "tuple" => [
-                                ":_",
-                                "Phoenix.Endpoint.Cowboy2Handler",
-                                %{"tuple" => ["Pleroma.Web.Endpoint", []]}
-                              ]
-                            }
-                          ]
-                        ]
-                      }
-                    ]
-                  ]
-                }
-              ]
-            ]
-          }
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 http: [
-                   key1: [
-                     _: [
-                       {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-                       {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
-                        {Phoenix.Transports.WebSocket,
-                         {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
-                       {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-                     ]
-                   ]
-                 ]
-               )
-
-      assert Config.from_binary(binary) == [
-               http: [
-                 key1: [
-                   {:_,
-                    [
-                      {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-                      {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
-                       {Phoenix.Transports.WebSocket,
-                        {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
-                      {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-                    ]}
-                 ]
-               ]
-             ]
-    end
-  end
-end