diff --git a/rbdemo/rbdemo/auth_backends.py b/rbdemo/rbdemo/auth_backends.py
new file mode 100644
index 0000000000000000000000000000000000000000..12d3c88015cd72cd9549adf5fde3e2d300e9b6f2
--- /dev/null
+++ b/rbdemo/rbdemo/auth_backends.py
@@ -0,0 +1,102 @@
+from __future__ import unicode_literals
+
+import random
+
+from django.contrib.auth.models import User
+from django.utils.six.moves import range
+from django.utils.translation import ugettext_lazy as _
+from reviewboard.accounts.backends import AuthBackend
+from reviewboard.reviews.models import Group
+
+from rbdemo.forms import DemoAuthSettingsForm
+
+
+class DemoAuthBackend(AuthBackend):
+    """Authentication backend for the demo server.
+
+    This allows people to log in with a pre-generated username and password,
+    which will be shown on the login page.
+
+    The page will try to show a username that has not already been created.
+    This is generated by taking a prefix (defaults to "guest") and appending
+    a random number in a range, and showing that as the username.
+
+    The goal is to give people a clean session. Depending on the random
+    numbers generated and the traffic on the server, this may not be
+    feasible without too many queries, so we'll fall back on the last
+    attempt.
+
+    After creating a user for the first time, that user will be added to
+    the configured review groups, in order to give them something they can
+    see in their dashboard.
+    """
+    backend_id = 'demo'
+    name = _('Demo Server')
+    settings_form = DemoAuthSettingsForm
+
+    MAX_USER_CHECKS = 10
+
+    @property
+    def login_instructions(self):
+        settings = self.extension.settings
+        user_prefix = settings.get('auth_user_prefix')
+        max_guest_id = settings.get('auth_user_max_id')
+        demo_password = settings.get('auth_password')
+
+        # Try to find a username that hasn't been taken yet. We'll only
+        # try up to a certain number of times, though.
+        for i in range(self.MAX_USER_CHECKS):
+            username = '%s%d' % (user_prefix, random.randint(1, max_guest_id))
+
+            if not User.objects.filter(username=username).exists():
+                break
+
+        return (_('To log into the demo server, use username "%s", '
+                  'password "%s"')
+                % (username, demo_password))
+
+    def __init__(self, *args, **kwargs):
+        from rbdemo.extension import DemoExtension
+
+        super(DemoAuthBackend, self).__init__(*args, **kwargs)
+        self.extension = DemoExtension.instance
+
+    def authenticate(self, username, password):
+        settings = self.extension.settings
+        user_prefix = settings.get('auth_user_prefix')
+        demo_password = settings.get('auth_password')
+
+        username = username.strip()
+
+        if password == demo_password and username.startswith(user_prefix):
+            # We know this is a guest user, and the password is valid.
+            # We're going to take the ID from the end of the username,
+            # make sure it's an integer in the expected range, and then
+            # validate.
+            guest_id = username[len(user_prefix):]
+
+            try:
+                # Check that the ID is in the allowed range.
+                if 0 < int(guest_id) <= settings.get('auth_user_max_id'):
+                    return self.get_or_create_user(username, None, password)
+            except ValueError:
+                pass
+
+        return None
+
+    def get_or_create_user(self, username, request, password=None):
+        user, is_new = User.objects.get_or_create(username=username)
+
+        if is_new:
+            user.set_unusable_password()
+            user.save()
+
+            group_names = self.extension.settings.get('auth_default_groups')
+
+            # Add the user to the default groups, so that they can see some
+            # review requests on their dashboard.
+            for group_name in group_names:
+                groups = Group.objects.get(name=group_name)
+                groups.users.add(user)
+
+        return user
diff --git a/rbdemo/rbdemo/extension.py b/rbdemo/rbdemo/extension.py
new file mode 100644
index 0000000000000000000000000000000000000000..246e6dcb5d8ca265cb2ab6e53f809bf75f1583ea
--- /dev/null
+++ b/rbdemo/rbdemo/extension.py
@@ -0,0 +1,24 @@
+from __future__ import unicode_literals
+
+from reviewboard.extensions.base import Extension
+from reviewboard.extensions.hooks import AuthBackendHook
+
+from rbdemo.auth_backends import DemoAuthBackend
+
+
+class DemoExtension(Extension):
+    metadata = {
+        'Name': 'Demo Server Extension',
+        'Summary': 'Provides authentication and management for the '
+                   'demo server.',
+    }
+
+    default_settings = {
+        'auth_user_prefix': 'guest',
+        'auth_user_max_id': 10000,
+        'auth_password': 'demo',
+        'auth_default_groups': [],
+    }
+
+    def initialize(self):
+        AuthBackendHook(self, DemoAuthBackend)
diff --git a/rbdemo/rbdemo/forms.py b/rbdemo/rbdemo/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..adca642db6b139c60406aef2fb9ea54774660d1f
--- /dev/null
+++ b/rbdemo/rbdemo/forms.py
@@ -0,0 +1,64 @@
+from __future__ import unicode_literals
+
+import re
+
+from django import forms
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+from djblets.extensions.forms import SettingsForm
+from reviewboard.reviews.models import Group
+
+
+class DemoAuthSettingsForm(SettingsForm):
+    """Configuration form for the demo authentication settings."""
+    auth_user_prefix = forms.CharField(
+        label=_('Username prefix'),
+        help_text=_('The prefix that generated usernames will start with.'))
+
+    auth_user_max_id = forms.IntegerField(
+        label=_('Max numeric suffix on username'),
+        help_text=_('The maximum number used for generating the numeric '
+                    'suffix for the username.'))
+
+    auth_password = forms.CharField(
+        label=_('Demo password'),
+        help_text=_('The password used for authenticating to any demo user.'))
+
+    auth_default_groups = forms.CharField(
+        label=_('Default groups'),
+        help_text=_('Comma-separated list of default group IDs.'),
+        widget=forms.TextInput(attrs={'size': 40}),
+        required=False)
+
+    def __init__(self, siteconfig, *args, **kwargs):
+        from rbdemo.extension import DemoExtension
+
+        super(DemoAuthSettingsForm, self).__init__(
+            DemoExtension.instance, *args, **kwargs)
+
+    def clean_auth_default_groups(self):
+        """Validates and serializes a list of groups."""
+        group_list = re.split(r',\s*',
+                              self.cleaned_data['auth_default_groups'])
+
+        for group_name in group_list:
+            try:
+                Group.objects.get(name=group_name)
+            except Group.DoesNotExist:
+                raise ValidationError(
+                    _('%(group_name)s is not a valid group'),
+                    params={
+                        'group_name': group_name,
+                    },
+                    code='invalid-group')
+
+        return group_list
+
+    def load(self):
+        super(DemoAuthSettingsForm, self).load()
+
+        self.fields['auth_default_groups'].initial = \
+            ', '.join(self.settings['auth_default_groups'])
+
+    class Meta:
+        title = _('Demo Authentication Settings')
diff --git a/rbdemo/rbdemo/management/commands/dump-demo-data.py b/rbdemo/rbdemo/management/commands/dump-demo-data.py
new file mode 100644
index 0000000000000000000000000000000000000000..93920e3cac0d78172a008387d44035bd032dd79f
--- /dev/null
+++ b/rbdemo/rbdemo/management/commands/dump-demo-data.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+
+import sys
+
+from django.core.management import execute_from_command_line
+from django.core.management.base import NoArgsCommand
+
+
+class Command(NoArgsCommand):
+    help = 'Dumps the contents of the demo server for use in resets.'
+
+    EXCLUDE_APPS = [
+        'contenttypes',
+    ]
+
+    def handle_noargs(self, **options):
+        # Reset the state of the database.
+        cmd = [sys.argv[0], 'dumpdata', '--indent=2']
+
+        for app in self.EXCLUDE_APPS:
+            cmd += ['-e', app]
+
+        execute_from_command_line(cmd)
diff --git a/rbdemo/rbdemo/management/commands/reset-demo.py b/rbdemo/rbdemo/management/commands/reset-demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..caaf24e4ef7be5099b22b0ce16978ed36a8f1959
--- /dev/null
+++ b/rbdemo/rbdemo/management/commands/reset-demo.py
@@ -0,0 +1,68 @@
+from __future__ import unicode_literals
+
+import os
+import shutil
+import sys
+
+from django.conf import settings
+from django.core.management import execute_from_command_line
+from django.core.management.base import CommandError, NoArgsCommand
+from django.utils import timezone
+from reviewboard.changedescs.models import ChangeDescription
+from reviewboard.diffviewer.models import DiffSet
+from reviewboard.reviews.models import (Comment, FileAttachmentComment,
+                                        ReviewRequest, Review)
+
+
+class Command(NoArgsCommand):
+    help = 'Resets the state of the demo server.'
+
+    def handle_noargs(self, **options):
+        demo_fixtures = getattr(settings, 'DEMO_FIXTURES', None)
+        demo_upload_path = getattr(settings, 'DEMO_UPLOAD_PATH', None)
+        demo_upload_owner = getattr(settings, 'DEMO_UPLOAD_PATH_OWNER', None)
+
+        if not demo_fixtures:
+            raise CommandError(
+                'settings.DEMO_FIXTURES must be set to a list of valid '
+                'paths')
+
+        if not demo_upload_path or not os.path.exists(demo_upload_path)
+            raise CommandError(
+                'settings.DEMO_UPLOAD_PATH must be set to a valid path')
+
+        if not demo_upload_owner:
+            raise CommandError(
+                'settings.DEMO_UPLOAD_PATH_OWNER must be set to '
+                '(uid, gid)')
+
+        cmd = sys.argv[0]
+
+        # Reset the state of the database.
+        execute_from_command_line([cmd, 'flush', '--noinput',
+                                   '--no-initial-data'])
+
+        # Now load in the new fixtures.
+        execute_from_command_line([cmd, 'loaddata'] + demo_fixtures)
+
+        # Update the timestamps on everything.
+        now = timezone.now()
+
+        ReviewRequest.objects.update(
+            time_added=now,
+            last_updated=now,
+            last_review_activity_timestamp=now)
+        Review.objects.update(timestamp=now)
+        Comment.objects.update(timestamp=now)
+        FileAttachmentComment.objects.update(timestamp=now)
+        ChangeDescription.objects.update(timestamp=now)
+        DiffSet.objects.update(timestamp=now)
+
+        # Replace the uploaded filess.
+        dest_uploaded_path = os.path.join(settings.MEDIA_ROOT, 'uploaded')
+
+        if os.path.exists(dest_uploaded_path):
+            shutil.rmtree(dest_uploaded_path)
+
+        shutil.copytree(demo_upload_path, dest_uploaded_path)
+        os.chown(demo_upload_path, *demo_upload_owner)
diff --git a/rbdemo/setup.py b/rbdemo/setup.py
new file mode 100755
index 0000000000000000000000000000000000000000..7a32ed5ad00d4927c61d789b8cea2d7f81538515
--- /dev/null
+++ b/rbdemo/setup.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+from reviewboard.extensions.packaging import setup
+
+
+PACKAGE = 'rbdemo'
+VERSION = '0.1'
+
+setup(
+    name=PACKAGE,
+    version=VERSION,
+    description='Handles the demo management for demo.reviewboard.org',
+    url='http://www.beanbaginc.com/',
+    author='Beanbag, Inc.',
+    author_email='support@beanbaginc.com',
+    maintainer='Beanbag, Inc.',
+    maintainer_email='support@beanbaginc.com',
+    packages=['rbdemo'],
+    install_requires=[
+        'ReviewBoard>=2.0beta4.dev',
+    ],
+    entry_points={
+        'reviewboard.extensions': [
+            'rbdemo = rbdemo.extension:DemoExtension',
+        ],
+    },
+)
