diff --git a/tailbone/static/js/tailbone.appsettings.js b/tailbone/static/js/tailbone.appsettings.js
new file mode 100644
index 00000000..ae378931
--- /dev/null
+++ b/tailbone/static/js/tailbone.appsettings.js
@@ -0,0 +1,29 @@
+
+/************************************************************
+ *
+ * tailbone.appsettings.js
+ *
+ * Logic for App Settings page.
+ *
+ ************************************************************/
+
+
+function show_group(group) {
+    if (group == "(All)") {
+        $('.panel').show();
+    } else {
+        $('.panel').hide();
+        $('.panel[data-groupname="' + group + '"]').show();
+    }
+}
+
+
+$(function() {
+
+    $('#settings-group').on('selectmenuchange', function(event, ui) {
+        show_group(ui.item.value);
+    });
+
+    show_group($('#settings-group').val());
+
+});
diff --git a/tailbone/templates/appsettings.mako b/tailbone/templates/appsettings.mako
new file mode 100644
index 00000000..b52d3b5c
--- /dev/null
+++ b/tailbone/templates/appsettings.mako
@@ -0,0 +1,99 @@
+## -*- coding: utf-8; -*-
+<%inherit file="/base.mako" />
+
+<%def name="title()">${self.app_title()} App Settings%def>
+
+<%def name="content_title()">%def>
+
+<%def name="extra_javascript()">
+  ${parent.extra_javascript()}
+  ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.appsettings.js') + '?ver={}'.format(tailbone.__version__))}
+%def>
+
+<%def name="extra_styles()">
+  ${parent.extra_styles()}
+  
+%def>
+
+
diff --git a/tailbone/templates/forms/deform.mako b/tailbone/templates/forms/deform.mako
index 36d53644..b3163557 100644
--- a/tailbone/templates/forms/deform.mako
+++ b/tailbone/templates/forms/deform.mako
@@ -39,7 +39,7 @@ ${h.csrf_token(request)}
                   
               % endif
               
-                
+                
                 
                   ${field.serialize()|n}
                 
diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py
index 4bc135a7..ee412bac 100644
--- a/tailbone/views/settings.py
+++ b/tailbone/views/settings.py
@@ -28,11 +28,18 @@ from __future__ import unicode_literals, absolute_import
 
 import re
 
-from rattail.db import model
+import six
+
+from rattail.db import model, api
+from rattail.settings import Setting
+from rattail.util import import_module_path
 
 import colander
+from webhelpers2.html import tags
 
-from tailbone.views import MasterView
+from tailbone import forms
+from tailbone.db import Session
+from tailbone.views import MasterView, View
 
 
 class SettingsView(MasterView):
@@ -77,5 +84,116 @@ class SettingsView(MasterView):
         return True
 
 
+class AppSettingsForm(forms.Form):
+
+    def get_label(self, key):
+        return self.labels.get(key, key)
+
+
+class AppSettingsView(View):
+    """
+    Core view which exposes "app settings" - aka. admin-friendly settings with
+    descriptions and type-specific form controls etc.
+    """
+
+    def __call__(self):
+        settings = sorted(self.iter_known_settings(),
+                          key=lambda setting: (setting.group,
+                                               setting.namespace,
+                                               setting.name))
+        groups = sorted(set([setting.group for setting in settings]))
+        current_group = None
+
+        form = self.make_form(settings)
+        form.cancel_url = self.request.current_route_url()
+        if form.validate(newstyle=True):
+            self.save_form(form)
+            group = self.request.POST.get('settings-group')
+            if group:
+                self.request.session['appsettings.current_group'] = group
+            self.request.session.flash("App Settings have been saved.")
+            return self.redirect(self.request.current_route_url())
+
+        if self.request.method == 'POST':
+            current_group = self.request.POST.get('settings-group')
+
+        if not current_group:
+            current_group = self.request.session.get('appsettings.current_group')
+
+        group_options = [tags.Option(group, group) for group in groups]
+        group_options.insert(0, tags.Option("(All)", "(All)"))
+        return {
+            'index_title': "App Settings",
+            'form': form,
+            'dform': form.make_deform_form(),
+            'groups': groups,
+            'group_options': group_options,
+            'current_group': current_group,
+            'settings': settings,
+        }
+
+    def make_form(self, known_settings):
+        schema = colander.MappingSchema()
+        helptext = {}
+        for setting in known_settings:
+            kwargs = {
+                'name': setting.node_name,
+                'default': self.get_setting_value(setting),
+            }
+            if setting.choices:
+                kwargs['validator'] = colander.OneOf(setting.choices)
+                kwargs['widget'] = forms.widgets.JQuerySelectWidget(
+                    values=[(val, val) for val in setting.choices])
+            schema.add(colander.SchemaNode(self.get_node_type(setting), **kwargs))
+            helptext[setting.node_name] = setting.__doc__.strip()
+        return AppSettingsForm(schema=schema, request=self.request, helptext=helptext)
+
+    def get_node_type(self, setting):
+        if setting.data_type is bool:
+            return colander.Bool()
+        return colander.String()
+
+    def save_form(self, form):
+        for setting in self.iter_known_settings():
+            value = form.validated[setting.node_name]
+            self.save_setting_value(setting, value)
+
+    def iter_known_settings(self):
+        """
+        Iterate over all known settings.
+        """
+        for module in self.rattail_config.getlist('rattail', 'settings', default=['rattail.settings']):
+            module = import_module_path(module)
+            for name in dir(module):
+                obj = getattr(module, name)
+                if isinstance(obj, type) and issubclass(obj, Setting) and obj is not Setting:
+                    # NOTE: we set this here, and reference it elsewhere
+                    obj.node_name = self.get_node_name(obj)
+                    yield obj
+
+    def get_node_name(self, setting):
+        return '[{}] {}'.format(setting.namespace, setting.name)
+
+    def get_setting_value(self, setting):
+        if setting.data_type is bool:
+            return self.rattail_config.getbool(setting.namespace, setting.name)
+        return self.rattail_config.get(setting.namespace, setting.name, default='')
+
+    def save_setting_value(self, setting, value):
+        legacy_name = '{}.{}'.format(setting.namespace, setting.name)
+        if setting.data_type is bool:
+            api.save_setting(Session(), legacy_name, 'true' if value else 'false')
+        else:
+            api.save_setting(Session(), legacy_name, six.text_type(value))
+
+    @classmethod
+    def defaults(cls, config):
+        config.add_route('appsettings', '/settings/app/')
+        config.add_view(cls, route_name='appsettings',
+                        renderer='/appsettings.mako',
+                        permission='settings.edit')
+
+
 def includeme(config):
+    AppSettingsView.defaults(config)
     SettingsView.defaults(config)