diff --git a/doc/manual/installation.xml b/doc/manual/installation.xml
index a1c410d5..4d1c6b2a 100644
--- a/doc/manual/installation.xml
+++ b/doc/manual/installation.xml
@@ -163,15 +163,16 @@ hydra-init</screen>
     </para>
 
     <para>
-      To add a user <emphasis>root</emphasis> with
-      <emphasis>admin</emphasis> privileges, execute:
-      <screen>
-echo "INSERT INTO Users(userName, emailAddress, password) VALUES ('root', 'some@email.adress.com', '$(echo -n foobar | sha1sum | cut -c1-40)');" | psql hydra
-echo "INSERT INTO UserRoles(userName, role) values('root', 'admin');" | psql hydra</screen>
+      To create projects, you need to create a user with
+      <emphasis>admin</emphasis> privileges.  This can be done using
+      the command <command>hydra-create-user</command>:
 
-      For SQLite the same commands can be used, with <command>psql
-      hydra</command> replaced by <command>sqlite3
-      /path/to/hydra.sqlite</command>.
+<screen>
+$ hydra-create-user alice --full-name 'Alice Q. User' \
+    --email-address 'alice@example.org' --password foobar --role admin
+</screen>
+
+      Additional users can be created through the web interface.
     </para>
 
   </section>
diff --git a/hydra-module.nix b/hydra-module.nix
index 1b8d8663..8034ef77 100644
--- a/hydra-module.nix
+++ b/hydra-module.nix
@@ -9,28 +9,31 @@ let
 
   hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
 
-  env =
-    { NIX_REMOTE = "daemon";
-      HYDRA_DBI = cfg.dbi;
+  hydraEnv =
+    { HYDRA_DBI = cfg.dbi;
       HYDRA_CONFIG = "${baseDir}/data/hydra.conf";
       HYDRA_DATA = "${baseDir}/data";
-      HYDRA_PORT = "${toString cfg.port}";
+    };
+
+  env =
+    { NIX_REMOTE = "daemon";
       OPENSSL_X509_CERT_FILE = "/etc/ssl/certs/ca-bundle.crt";
       GIT_SSL_CAINFO = "/etc/ssl/certs/ca-bundle.crt";
-    };
-  
+    } // hydraEnv;
+
   serverEnv = env //
-    { HYDRA_LOGO = if cfg.logo != null then cfg.logo else "";
-      HYDRA_TRACKER = cfg.tracker;
+    { HYDRA_TRACKER = cfg.tracker;
     } // (optionalAttrs cfg.debugServer { DBIC_TRACE = 1; });
 in
 
 {
   ###### interface
   options = {
+
     services.hydra = rec {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to run Hydra services.
@@ -38,27 +41,29 @@ in
       };
 
       dbi = mkOption {
-        default = "dbi:Pg:dbname=hydra;host=localhost;user=hydra;";
-        example = "dbi:SQLite:/home/hydra/db/hydra.sqlite";
+        type = types.string;
+        default = "dbi:Pg:dbname=hydra;user=hydra;";
+        example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
         description = ''
           The DBI string for Hydra database connection.
         '';
       };
 
-      hydra = mkOption {
+      package = mkOption {
+        type = types.path;
         #default = pkgs.hydra;
-        description = ''
-          Location of hydra
-        '';
+        description = "The Hydra package.";
       };
 
       hydraURL = mkOption {
+        type = types.str;
         description = ''
           The base URL for the Hydra webserver instance. Used for links in emails.
         '';
       };
 
       listenHost = mkOption {
+        type = types.str;
         default = "*";
         example = "localhost";
         description = ''
@@ -68,6 +73,7 @@ in
       };
 
       port = mkOption {
+        type = types.int;
         default = 3000;
         description = ''
           TCP port the web server should listen to.
@@ -75,26 +81,30 @@ in
       };
 
       minimumDiskFree = mkOption {
+        type = types.int;
         default = 5;
         description = ''
-          Threshold of minimum disk space (G) to determine if queue runner should run or not.
+          Threshold of minimum disk space (GiB) to determine if queue runner should run or not.
         '';
       };
 
       minimumDiskFreeEvaluator = mkOption {
+        type = types.int;
         default = 2;
         description = ''
-          Threshold of minimum disk space (G) to determine if evaluator should run or not.
+          Threshold of minimum disk space (GiB) to determine if evaluator should run or not.
         '';
       };
 
       notificationSender = mkOption {
+        type = types.str;
         description = ''
           Sender email address used for email notifications.
         '';
       };
 
       tracker = mkOption {
+        type = types.str;
         default = "";
         description = ''
           Piece of HTML that is included on all pages.
@@ -102,22 +112,16 @@ in
       };
 
       logo = mkOption {
+        type = types.nullOr types.path;
         default = null;
         description = ''
           File name of an alternate logo to be displayed on the web pages.
         '';
       };
 
-      useWAL = mkOption {
-        default = true;
-        description = ''
-          Whether to use SQLite's Write-Ahead Logging, which may improve performance.
-        '';
-      };
-
       debugServer = mkOption {
-        default = false;
         type = types.bool;
+        default = false;
         description = "Whether to run the server in debug mode";
       };
 
@@ -134,15 +138,21 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+
     services.hydra.extraConfig =
       ''
         using_frontend_proxy 1
         base_uri ${cfg.hydraURL}
         notification_sender ${cfg.notificationSender}
         max_servers 25
+        ${optionalString (cfg.logo != null) ''
+          hydra_logo ${cfg.logo}
+        ''}
       '';
 
-    environment.systemPackages = [ cfg.hydra ];
+    environment.systemPackages = [ cfg.package ];
+
+    environment.variables = hydraEnv;
 
     users.extraUsers.hydra =
       { description = "Hydra";
@@ -163,12 +173,10 @@ in
       build-cache-failure = true
 
       build-poll-interval = 10
-      
+
       # Online log compression makes it impossible to get the tail of
       # builds that are in progress.
       build-compress-log = false
-
-      use-sqlite-wal = ${if cfg.useWAL then "true" else "false"}
     '';
 
     systemd.services."hydra-init" =
@@ -177,41 +185,32 @@ in
         after = [ "postgresql.service" ];
         environment = env;
         script = ''
-          mkdir -p ${baseDir}/data
+          mkdir -m 0700 -p ${baseDir}/data
           chown hydra ${baseDir}/data
           ln -sf ${hydraConf} ${baseDir}/data/hydra.conf
-          pass=$(HOME=/root ${pkgs.openssl}/bin/openssl rand -base64 32)
-          if [ ! -f ${baseDir}/.pgpass ]; then
-              ${config.services.postgresql.package}/bin/psql postgres << EOF
-          CREATE USER hydra PASSWORD '$pass';
-          EOF
+          ${optionalString (cfg.dbi == "dbi:Pg:dbname=hydra;user=hydra;") ''
+            if ! [ -e ${baseDir}/.db-created ]; then
+              ${config.services.postgresql.package}/bin/createuser hydra
               ${config.services.postgresql.package}/bin/createdb -O hydra hydra
-              cat > ${baseDir}/.pgpass-tmp << EOF
-          localhost:*:hydra:hydra:$pass
-          EOF
-              chown hydra ${baseDir}/.pgpass-tmp
-              chmod 600 ${baseDir}/.pgpass-tmp
-              mv ${baseDir}/.pgpass-tmp ${baseDir}/.pgpass
-          fi
-          ${pkgs.shadow}/bin/su hydra -c ${cfg.hydra}/bin/hydra-init
-          ${config.services.postgresql.package}/bin/psql hydra << EOF
-            BEGIN;
-            INSERT INTO Users(userName, emailAddress, password) VALUES ('admin', '${cfg.notificationSender}', '$(echo -n $pass | sha1sum | cut -c1-40)');
-            INSERT INTO UserRoles(userName, role) values('admin', 'admin');
-            COMMIT;
-          EOF
+              touch ${baseDir}/.db-created
+            fi
+          ''}
+          ${pkgs.shadow}/bin/su hydra -c ${cfg.package}/bin/hydra-init
         '';
         serviceConfig.Type = "oneshot";
         serviceConfig.RemainAfterExit = true;
       };
-    
+
     systemd.services."hydra-server" =
       { wantedBy = [ "multi-user.target" ];
-        wants = [ "hydra-init.service" ];
+        requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" ];
         environment = serverEnv;
         serviceConfig =
-          { ExecStart = "@${cfg.hydra}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' --max_spare_servers 5 --max_servers 25 --max_requests 100${optionalString cfg.debugServer " -d"}";
+          { ExecStart =
+              "@${cfg.package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
+              + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 "
+              + "--max_requests 100 ${optionalString cfg.debugServer "-d"}";
             User = "hydra";
             Restart = "always";
           };
@@ -219,13 +218,13 @@ in
 
     systemd.services."hydra-queue-runner" =
       { wantedBy = [ "multi-user.target" ];
-        wants = [ "hydra-init.service" ];
+        requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" "network.target" ];
         path = [ pkgs.nettools ];
         environment = env;
         serviceConfig =
-          { ExecStartPre = "${cfg.hydra}/bin/hydra-queue-runner --unlock";
-            ExecStart = "@${cfg.hydra}/bin/hydra-queue-runner hydra-queue-runner";
+          { ExecStartPre = "${cfg.package}/bin/hydra-queue-runner --unlock";
+            ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner";
             User = "hydra";
             Restart = "always";
           };
@@ -233,25 +232,26 @@ in
 
     systemd.services."hydra-evaluator" =
       { wantedBy = [ "multi-user.target" ];
-        wants = [ "hydra-init.service" ];
+        requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" "network.target" ];
         path = [ pkgs.nettools ];
         environment = env;
         serviceConfig =
-          { ExecStart = "@${cfg.hydra}/bin/hydra-evaluator hydra-evaluator";
+          { ExecStart = "@${cfg.package}/bin/hydra-evaluator hydra-evaluator";
             User = "hydra";
             Restart = "always";
           };
       };
 
     systemd.services."hydra-update-gc-roots" =
-      { wants = [ "hydra-init.service" ];
+      { requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" ];
         environment = env;
         serviceConfig =
-          { ExecStart = "@${cfg.hydra}/bin/hydra-update-gc-roots hydra-update-gc-roots";
+          { ExecStart = "@${cfg.package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
             User = "hydra";
           };
+        startAt = "02:15";
       };
 
     services.cron.systemCronJobs =
@@ -278,7 +278,6 @@ in
       in
         [ "*/5 * * * * root  ${checkSpace} &> ${baseDir}/data/checkspace.log"
           "15 5 * * * root  ${compressLogs} &> ${baseDir}/data/compress.log"
-          "15 2 * * * root  ${pkgs.systemd}/bin/systemctl start hydra-update-gc-roots.service"
         ];
   };
 }
diff --git a/release.nix b/release.nix
index 2be29519..6004c88d 100644
--- a/release.nix
+++ b/release.nix
@@ -8,6 +8,23 @@ let
 
   genAttrs' = pkgs.lib.genAttrs [ "x86_64-linux" ];
 
+  hydraServer = hydraPkg:
+    { config, pkgs, ... }:
+    { imports = [ ./hydra-module.nix ];
+
+      virtualisation.memorySize = 1024;
+
+      services.hydra.enable = true;
+      services.hydra.package = hydraPkg;
+      services.hydra.hydraURL = "http://hydra.example.org";
+      services.hydra.notificationSender = "admin@hydra.example.org";
+
+      services.postgresql.enable = true;
+      services.postgresql.package = pkgs.postgresql92;
+
+      environment.systemPackages = [ pkgs.perlPackages.LWP pkgs.perlPackages.JSON ];
+    };
+
 in rec {
 
   tarball =
@@ -150,67 +167,45 @@ in rec {
 
   tests.install = genAttrs' (system:
     with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
-    let hydra = builtins.getAttr system build; in # build.${system}
     simpleTest {
-      machine =
-        { config, pkgs, ... }:
-        { services.postgresql.enable = true;
-          services.postgresql.package = pkgs.postgresql92;
-          environment.systemPackages = [ hydra ];
-        };
-
+      machine = hydraServer (builtins.getAttr system build); # build.${system}
       testScript =
         ''
-          $machine->waitForJob("postgresql");
-
-          # Initialise the database and the state.
-          $machine->mustSucceed
-              ( "createdb -O root hydra",
-              , "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
-              , "mkdir /var/lib/hydra"
-              );
-
-          # Start the web interface.
-          $machine->mustSucceed("HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=hydra;' hydra-server >&2 &");
+          $machine->waitForJob("hydra-init");
+          $machine->waitForJob("hydra-server");
+          $machine->waitForJob("hydra-evaluator");
+          $machine->waitForJob("hydra-queue-runner");
           $machine->waitForOpenPort("3000");
+          $machine->succeed("curl --fail http://localhost:3000/");
         '';
     });
 
   tests.api = genAttrs' (system:
     with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
-    let hydra = builtins.getAttr system build; in # build."${system}"
     simpleTest {
-      machine =
-        { config, pkgs, ... }:
-        { services.postgresql.enable = true;
-          services.postgresql.package = pkgs.postgresql92;
-          environment.systemPackages = [ hydra pkgs.perlPackages.LWP pkgs.perlPackages.JSON ];
-          virtualisation.memorySize = 2047;
-          boot.kernelPackages = pkgs.linuxPackages_3_10;
-        };
-
+      machine = hydraServer (builtins.getAttr system build); # build.${system}
       testScript =
+        let dbi = "dbi:Pg:dbname=hydra;user=root;"; in
         ''
-          $machine->waitForJob("postgresql");
+          $machine->waitForJob("hydra-init");
 
-          # Initialise the database and the state.
-          $machine->mustSucceed
-              ( "createdb -O root hydra"
-              , "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
-              , "mkdir /var/lib/hydra"
-              , "echo \"insert into Users(userName, emailAddress, password) values('root', 'e.dolstra\@tudelft.nl', '\$(echo -n foobar | sha1sum | cut -c1-40)');\" | psql hydra"
-              , "echo \"insert into UserRoles(userName, role) values('root', 'admin');\" | psql hydra"
-              , "mkdir /run/jobset"
-              , "chmod 755 /run/jobset"
+          # Create an admin account and some other state.
+          $machine->succeed
+              ( "su hydra -c \"hydra-create-user root --email-address 'e.dolstra\@tudelft.nl' --password foobar --role admin\""
+              , "mkdir /run/jobset /tmp/nix"
+              , "chmod 755 /run/jobset /tmp/nix"
               , "cp ${./tests/api-test.nix} /run/jobset/default.nix"
               , "chmod 644 /run/jobset/default.nix"
+              , "chown -R hydra /run/jobset /tmp/nix"
               );
 
-          # Start the web interface.
-          $machine->mustSucceed("NIX_STORE_DIR=/run/nix NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' LOGNAME=root DBIC_TRACE=1 hydra-server -d >&2 &");
+          # Start the web interface with some weird settings.
+          $machine->succeed("systemctl stop hydra-server hydra-evaluator hydra-queue-runner");
+          $machine->mustSucceed("su hydra -c 'NIX_STORE_DIR=/tmp/nix/store NIX_LOG_DIR=/tmp/nix/var/log/nix NIX_STATE_DIR=/tmp/nix/var/nix DBIC_TRACE=1 hydra-server -d' >&2 &");
           $machine->waitForOpenPort("3000");
 
-          $machine->mustSucceed("perl ${./tests/api-test.pl} >&2");
+          # Run the API tests.
+          $machine->mustSucceed("su hydra -c 'perl ${./tests/api-test.pl}' >&2");
         '';
   });
 
@@ -236,7 +231,7 @@ in rec {
           $machine->waitForJob("postgresql");
 
           # Initialise the database and the state.
-          $machine->mustSucceed
+          $machine->succeed
               ( "createdb -O root hydra"
               , "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
               , "mkdir /var/lib/hydra"
@@ -246,10 +241,10 @@ in rec {
               );
 
           # start fakes3
-          $machine->mustSucceed("fakes3 --root /tmp/s3 --port 80 &>/dev/null &");
+          $machine->succeed("fakes3 --root /tmp/s3 --port 80 &>/dev/null &");
           $machine->waitForOpenPort("80");
 
-          $machine->mustSucceed("cd /tmp && LOGNAME=root AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' HYDRA_CONFIG=${./tests/s3-backup-test.config} perl -I ${hydra}/libexec/hydra/lib -I ${hydra.perlDeps}/lib/perl5/site_perl ./s3-backup-test.pl >&2");
+          $machine->succeed("cd /tmp && LOGNAME=root AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' HYDRA_CONFIG=${./tests/s3-backup-test.config} perl -I ${hydra}/libexec/hydra/lib -I ${hydra.perlDeps}/lib/perl5/site_perl ./s3-backup-test.pl >&2");
         '';
   });
 }
diff --git a/src/lib/Hydra.pm b/src/lib/Hydra.pm
index 42da5f65..6bef5cd8 100644
--- a/src/lib/Hydra.pm
+++ b/src/lib/Hydra.pm
@@ -53,8 +53,9 @@ __PACKAGE__->config(
         expose_stash => 'json'
     },
     'Plugin::Session' => {
-        expires => 3600 * 24 * 2,
-        storage => Hydra::Model::DB::getHydraPath . "/session_data"
+        expires => 3600 * 24 * 7,
+        storage => Hydra::Model::DB::getHydraPath . "/session_data",
+        unlink_on_exit => 0
     },
     'Plugin::AccessLog' => {
         formatter => {
diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm
index fc88d16c..ad34ae40 100644
--- a/src/lib/Hydra/Controller/Root.pm
+++ b/src/lib/Hydra/Controller/Root.pm
@@ -20,10 +20,11 @@ sub begin :Private {
     $c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
     $c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
     $c->stash->{curTime} = time;
-    $c->stash->{logo} = $ENV{"HYDRA_LOGO"} ? "/logo" : "";
+    $c->stash->{logo} = ($c->config->{hydra_logo} // $ENV{"HYDRA_LOGO"}) ? "/logo" : "";
     $c->stash->{tracker} = $ENV{"HYDRA_TRACKER"};
     $c->stash->{flashMsg} = $c->flash->{flashMsg};
     $c->stash->{successMsg} = $c->flash->{successMsg};
+    $c->stash->{personaEnabled} = $c->config->{enable_persona} // "0" eq "1";
 
     if (scalar(@args) == 0 || $args[0] ne "static") {
         $c->stash->{nrRunningBuilds} = $c->model('DB::Builds')->search({ finished => 0, busy => 1 }, {})->count();
@@ -75,20 +76,6 @@ sub queue_GET {
 }
 
 
-sub timeline :Local {
-    my ($self, $c) = @_;
-    my $pit = time();
-    $c->stash->{pit} = $pit;
-    $pit = $pit-(24*60*60)-1;
-
-    $c->stash->{template} = 'timeline.tt';
-    $c->stash->{builds} = [ $c->model('DB::Builds')->search
-        ( { finished => 1, stoptime => { '>' => $pit } }
-        , { order_by => ["starttime"] }
-        ) ];
-}
-
-
 sub status :Local :Args(0) :ActionClass('REST') { }
 
 sub status_GET {
@@ -117,7 +104,7 @@ sub machines :Local Args(0) {
             { order_by => 'stoptime desc', rows => 1 });
         ${$machines}{$m}{'idle'} = $idle ? $idle->stoptime : 0;
     }
-    
+
     $c->stash->{machines} = $machines;
     $c->stash->{steps} = [ $c->model('DB::BuildSteps')->search(
         { finished => 0, 'me.busy' => 1, 'build.busy' => 1, },
@@ -273,7 +260,7 @@ sub narinfo :LocalRegex('^([a-z0-9]+).narinfo$') :Args(0) {
 
 sub logo :Local {
     my ($self, $c) = @_;
-    my $path = $ENV{"HYDRA_LOGO"} or die("Logo not set!");
+    my $path = $c->config->{hydra_logo} // $ENV{"HYDRA_LOGO"} // die("Logo not set!");
     $c->serve_static_file($path);
 }
 
diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm
index 1ee214d0..6a1a8552 100644
--- a/src/lib/Hydra/Controller/User.pm
+++ b/src/lib/Hydra/Controller/User.pm
@@ -8,65 +8,83 @@ use Crypt::RandPasswd;
 use Digest::SHA1 qw(sha1_hex);
 use Hydra::Helper::Nix;
 use Hydra::Helper::CatalystUtils;
+use LWP::UserAgent;
+use JSON;
+use HTML::Entities;
 
 
 __PACKAGE__->config->{namespace} = '';
 
 
-sub login :Local :Args(0) :ActionClass('REST::ForBrowsers') { }
-
-sub login_GET {
-    my ($self, $c) = @_;
-
-    my $baseurl = $c->uri_for('/');
-    my $referer = $c->request->referer;
-    $c->session->{referer} = $referer if defined $referer && $referer =~ m/^($baseurl)/;
-
-    $c->stash->{template} = 'login.tt';
-}
+sub login :Local :Args(0) :ActionClass('REST') { }
 
 sub login_POST {
     my ($self, $c) = @_;
 
-    my $username;
-    my $password;
+    my $username = $c->stash->{params}->{username} // "";
+    my $password = $c->stash->{params}->{password} // "";
 
-    $username = $c->stash->{params}->{username};
-    $password = $c->stash->{params}->{password};
+    error($c, "You must specify a user name.") if $username eq "";
+    error($c, "You must specify a password.") if $password eq "";
 
-    if ($username && $password) {
-        if ($c->authenticate({username => $username, password => $password})) {
-            if ($c->request->looks_like_browser) {
-                backToReferer($c);
-            } else {
-                currentUser_GET($self, $c);
-            }
-        } else {
-            $self->status_forbidden($c, message => "Bad username or password.");
-            if ($c->request->looks_like_browser) {
-                login_GET($self, $c);
-            }
-        }
-    }
+    accessDenied($c, "Bad username or password.")
+        if !$c->authenticate({username => $username, password => $password});
+
+    currentUser_GET($self, $c);
 }
 
 
-sub logout :Local :Args(0) :ActionClass('REST::ForBrowsers') { }
+sub logout :Local :Args(0) :ActionClass('REST') { }
 
 sub logout_POST {
     my ($self, $c) = @_;
+    $c->flash->{flashMsg} = "You are no longer signed in." if $c->user_exists();
     $c->logout;
-    if ($c->request->looks_like_browser) {
-        $c->response->redirect($c->request->referer || $c->uri_for('/'));
-    } else {
-        $self->status_no_content($c);
-    }
+    $self->status_no_content($c);
 }
 
-sub logout_GET {
-    # Probably a better way to do this
+
+sub persona_login :Path('/persona-login') Args(0) {
     my ($self, $c) = @_;
-    logout_POST($self, $c);
+    requirePost($c);
+
+    error($c, "Persona support is not enabled.") unless $c->stash->{personaEnabled};
+
+    my $assertion = $c->req->params->{assertion} or die;
+
+    my $ua = new LWP::UserAgent;
+    my $response = $ua->post(
+        'https://verifier.login.persona.org/verify',
+        { assertion => $assertion,
+          audience => $c->uri_for('/')
+        });
+    error($c, "Did not get a response from Persona.") unless $response->is_success;
+
+    my $d = decode_json($response->decoded_content) or die;
+    error($c, "Persona says: $d->{reason}") if $d->{status} ne "okay";
+
+    my $email = $d->{email} or die;
+
+    # Be paranoid about the email address format, since we do use it
+    # in URLs.
+    die "Illegal email address." unless $email =~ /^[a-zA-Z0-9\.\-\_]+@[a-zA-Z0-9\.\-\_]+$/;
+
+    my $user = $c->find_user({ username => $email });
+
+    if (!$user) {
+        $c->model('DB::Users')->create(
+            { username => $email
+            , password => "!"
+            , emailaddress => $email,
+            , type => "persona"
+            });
+        $user = $c->find_user({ username => $email }) or die;
+    }
+
+    $c->set_authenticated($user);
+
+    $self->status_no_content($c);
+    $c->flash->{successMsg} = "You are now signed in as <tt>" . encode_entities($email) . "</tt>.";
 }
 
 
@@ -91,57 +109,81 @@ sub setPassword {
 sub register :Local Args(0) {
     my ($self, $c) = @_;
 
-    die "Not implemented!\n";
+    accessDenied($c, "User registration is currently not implemented.") unless isAdmin($c);
 
-    $c->stash->{template} = 'user.tt';
-    $c->stash->{create} = 1;
-    return if $c->request->method ne "POST";
-
-    my $userName = trim $c->req->params->{username};
-    my $fullName = trim $c->req->params->{fullname};
-    my $password = trim $c->req->params->{password};
-    $c->stash->{username} = $userName;
-    $c->stash->{fullname} = $fullName;
-
-    sub fail {
-        my ($c, $msg) = @_;
-        $c->stash->{errorMsg} = $msg;
+    if ($c->request->method eq "GET") {
+        $c->stash->{template} = 'user.tt';
+        $c->stash->{create} = 1;
+        return;
     }
 
-    return fail($c, "You did not enter the correct digits from the security image.")
-        unless $c->validate_captcha($c->req->param('captcha'));
+    die unless $c->request->method eq "PUT";
 
-    return fail($c, "Your user name is invalid. It must start with a lower-case letter followed by lower-case letters, digits, dots or underscores.")
+    my $userName = trim $c->req->params->{username};
+    $c->stash->{username} = $userName;
+
+    error($c, "You did not enter the correct digits from the security image.")
+        unless isAdmin($c) || $c->validate_captcha($c->req->param('captcha'));
+
+    error($c, "Your user name is invalid. It must start with a lower-case letter followed by lower-case letters, digits, dots or underscores.")
         if $userName !~ /^$userNameRE$/;
 
-    return fail($c, "Your user name is already taken.")
+    error($c, "Your user name is already taken.")
         if $c->find_user({ username => $userName });
 
-    return fail($c, "Your must specify your full name.") if $fullName eq "";
-
-    return fail($c, "You must specify a password of at least 6 characters.")
-        unless isValidPassword($password);
-
-    return fail($c, "The passwords you specified did not match.")
-        if $password ne trim $c->req->params->{password2};
-
     txn_do($c->model('DB')->schema, sub {
         my $user = $c->model('DB::Users')->create(
             { username => $userName
-            , fullname => $fullName
             , password => "!"
             , emailaddress => "",
+            , type => "hydra"
             });
-        setPassword($user, $password);
+        updatePreferences($c, $user);
     });
 
     unless ($c->user_exists) {
-        $c->authenticate({username => $userName, password => $password})
+        $c->set_authenticated({username => $userName})
             or error($c, "Unable to authenticate the new user!");
     }
 
     $c->flash->{successMsg} = "User <tt>$userName</tt> has been created.";
-    backToReferer($c);
+    $self->status_no_content($c);
+}
+
+
+sub updatePreferences {
+    my ($c, $user) = @_;
+
+    my $fullName = trim($c->req->params->{fullname} // "");
+    error($c, "Your must specify your full name.") if $fullName eq "";
+
+    my $password = trim($c->req->params->{password} // "");
+    if ($user->type eq "hydra" && ($user->password eq "!" || $password ne "")) {
+        error($c, "You must specify a password of at least 6 characters.")
+            unless isValidPassword($password);
+
+        error($c, "The passwords you specified did not match.")
+            if $password ne trim $c->req->params->{password2};
+
+        setPassword($user, $password);
+    }
+
+    my $emailAddress = trim($c->req->params->{emailaddress} // "");
+    # FIXME: validate email address?
+
+    $user->update(
+        { fullname => $fullName
+        , emailonerror => $c->stash->{params}->{"emailonerror"} ? 1 : 0
+        });
+
+    if (isAdmin($c)) {
+        $user->update({ emailaddress => $emailAddress })
+            if $user->type eq "hydra";
+
+        $user->userroles->delete;
+        $user->userroles->create({ role => $_ })
+            foreach paramToList($c, "roles");
+    }
 }
 
 
@@ -152,8 +194,7 @@ sub currentUser_GET {
 
     requireUser($c);
 
-    $self->status_ok(
-        $c,
+    $self->status_ok($c,
         entity => $c->model("DB::Users")->find($c->user->username)
     );
 }
@@ -172,96 +213,69 @@ sub user :Chained('/') PathPart('user') CaptureArgs(1) {
 }
 
 
-sub deleteUser {
-    my ($self, $c, $user) = @_;
-    my ($project) = $c->model('DB::Projects')->search({ owner => $user->username });
-    error($c, "User " . $user->username . " is still owner of project " . $project->name . ".")
-        if defined $project;
-    $c->logout() if $user->username eq $c->user->username;
-    $user->delete;
-}
-
-
-sub edit :Chained('user') :Args(0) :ActionClass('REST::ForBrowsers') { }
+sub edit :Chained('user') :PathPart('') :Args(0) :ActionClass('REST::ForBrowsers') { }
 
 sub edit_GET {
     my ($self, $c) = @_;
-
-    my $user = $c->stash->{user};
-
     $c->stash->{template} = 'user.tt';
-
-    $c->session->{referer} = $c->request->referer if !defined $c->session->{referer};
-
-    $c->stash->{fullname} = $user->fullname;
-
-    $c->stash->{emailonerror} = $user->emailonerror;
 }
 
-sub edit_POST {
+sub edit_PUT {
     my ($self, $c) = @_;
-
     my $user = $c->stash->{user};
 
-    $c->stash->{template} = 'user.tt';
-
-    $c->session->{referer} = $c->request->referer if !defined $c->session->{referer};
-
-    if (($c->stash->{params}->{submit} // "") eq "delete") {
-        deleteUser($self, $c, $user);
-        backToReferer($c);
-    }
-
     if (($c->stash->{params}->{submit} // "") eq "reset-password") {
-        $c->stash->{json} = {};
-        error($c, "No email address is set for this user.")
-            unless $user->emailaddress;
-        my $password = Crypt::RandPasswd->word(8,10);
-        setPassword($user, $password);
-        sendEmail($c,
-            $user->emailaddress,
-            "Hydra password reset",
-            "Hi,\n\n".
-            "Your password has been reset. Your new password is '$password'.\n\n".
-            "You can change your password at " . $c->uri_for($self->action_for('edit'), [$user->username]) . ".\n\n".
-            "With regards,\n\nHydra.\n"
-        );
         return;
     }
 
-    my $fullName = trim $c->stash->{params}->{fullname};
-
     txn_do($c->model('DB')->schema, sub {
-
-        error($c, "Your must specify your full name.") if $fullName eq "";
-
-        $user->update(
-            { fullname => $fullName
-            , emailonerror => $c->stash->{params}->{"emailonerror"} ? 1 : 0
-            });
-
-        my $password = $c->stash->{params}->{password} // "";
-        if ($password ne "") {
-            error($c, "You must specify a password of at least 6 characters.")
-                unless isValidPassword($password);
-            error($c, "The passwords you specified did not match.")
-                if $password ne trim $c->stash->{params}->{password2};
-            setPassword($user, $password);
-        }
-
-        if (isAdmin($c)) {
-            $user->userroles->delete;
-            $user->userroles->create({ role => $_})
-                foreach paramToList($c, "roles");
-        }
-
+        updatePreferences($c, $user);
     });
 
-    if ($c->request->looks_like_browser) {
-        backToReferer($c);
-    } else {
-        $self->status_no_content($c);
-    }
+    $c->flash->{successMsg} = "Your preferences have been updated.";
+    $self->status_no_content($c);
+}
+
+sub edit_DELETE {
+    my ($self, $c) = @_;
+    my $user = $c->stash->{user};
+
+    my ($project) = $c->model('DB::Projects')->search({ owner => $user->username });
+    error($c, "User " . $user->username . " is still owner of project " . $project->name . ".")
+        if defined $project;
+
+    $c->logout() if $user->username eq $c->user->username;
+
+    $user->delete;
+
+    $c->flash->{successMsg} = "The user has been deleted.";
+    $self->status_no_content($c);
+}
+
+
+sub reset_password :Chained('user') :PathPart('reset-password') :Args(0) {
+    my ($self, $c) = @_;
+    my $user = $c->stash->{user};
+
+    requirePost($c);
+
+    error($c, "This user's password cannot be reset.") if $user->type ne "hydra";
+    error($c, "No email address is set for this user.")
+        unless $user->emailaddress;
+
+    my $password = Crypt::RandPasswd->word(8,10);
+    setPassword($user, $password);
+    sendEmail($c,
+        $user->emailaddress,
+        "Hydra password reset",
+        "Hi,\n\n".
+        "Your password has been reset. Your new password is '$password'.\n\n".
+        "You can change your password at " . $c->uri_for($self->action_for('edit'), [$user->username]) . ".\n\n".
+        "With regards,\n\nHydra.\n"
+    );
+
+    $c->flash->{successMsg} = "A new password has been sent to ${\$user->emailaddress}.";
+    $self->status_no_content($c);
 }
 
 
@@ -280,4 +294,39 @@ sub dashboard :Chained('user') :Args(0) {
 }
 
 
+sub my_jobs_tab :Chained('user') :PathPart('my-jobs-tab') :Args(0) {
+    my ($self, $c) = @_;
+    $c->stash->{template} = 'dashboard-my-jobs-tab.tt';
+
+    die unless $c->stash->{user}->emailaddress;
+
+    # Get all current builds of which this user is a maintainer.
+    $c->stash->{builds} = [$c->model('DB::Builds')->search(
+        { iscurrent => 1
+        , maintainers => { ilike => "%" . $c->stash->{user}->emailaddress . "%" }
+        , "project.enabled" => 1
+        , "jobset.enabled" => 1
+        },
+        { order_by => ["project", "jobset", "job"]
+        , join => ["project", "jobset"]
+        })];
+}
+
+
+sub my_jobsets_tab :Chained('user') :PathPart('my-jobsets-tab') :Args(0) {
+    my ($self, $c) = @_;
+    $c->stash->{template} = 'dashboard-my-jobsets-tab.tt';
+
+    my $jobsets = $c->model('DB::Jobsets')->search(
+        { "project.enabled" => 1, "me.enabled" => 1,
+        , owner => $c->stash->{user}->username
+        },
+        { order_by => ["project", "name"]
+        , join => ["project"]
+        });
+
+    $c->stash->{jobsets} = [jobsetOverview_($c, $jobsets)];
+}
+
+
 1;
diff --git a/src/lib/Hydra/Helper/CatalystUtils.pm b/src/lib/Hydra/Helper/CatalystUtils.pm
index 83413e2d..e83bd0ea 100644
--- a/src/lib/Hydra/Helper/CatalystUtils.pm
+++ b/src/lib/Hydra/Helper/CatalystUtils.pm
@@ -90,7 +90,8 @@ sub getPreviousSuccessfulBuild {
 
 
 sub error {
-    my ($c, $msg) = @_;
+    my ($c, $msg, $status) = @_;
+    $c->response->status($status) if defined $status;
     $c->error($msg);
     $c->detach; # doesn't return
 }
@@ -98,15 +99,13 @@ sub error {
 
 sub notFound {
     my ($c, $msg) = @_;
-    $c->response->status(404);
-    error($c, $msg);
+    error($c, $msg, 404);
 }
 
 
 sub accessDenied {
     my ($c, $msg) = @_;
-    $c->response->status(403);
-    error($c, $msg);
+    error($c, $msg, 403);
 }
 
 
@@ -121,8 +120,7 @@ sub backToReferer {
 sub forceLogin {
     my ($c) = @_;
     $c->session->{referer} = $c->request->uri;
-    $c->response->redirect($c->uri_for('/login'));
-    $c->detach; # doesn't return
+    accessDenied($c, "This page requires you to sign in.");
 }
 
 
diff --git a/src/lib/Hydra/Helper/Nix.pm b/src/lib/Hydra/Helper/Nix.pm
index a7fa06ed..76e10b47 100644
--- a/src/lib/Hydra/Helper/Nix.pm
+++ b/src/lib/Hydra/Helper/Nix.pm
@@ -17,7 +17,8 @@ our @EXPORT = qw(
     getPrimaryBuildsForView
     getPrimaryBuildTotal
     getViewResult getLatestSuccessfulViewResult
-    jobsetOverview removeAsciiEscapes getDrvLogPath findLog logContents
+    jobsetOverview jobsetOverview_
+    removeAsciiEscapes getDrvLogPath findLog logContents
     getMainOutput
     getEvals getMachines
     pathIsInsidePrefix
@@ -173,9 +174,9 @@ sub findLastJobForBuilds {
 }
 
 
-sub jobsetOverview {
-    my ($c, $project) = @_;
-    return $project->jobsets->search( isProjectOwner($c, $project) ? {} : { hidden => 0 },
+sub jobsetOverview_ {
+    my ($c, $jobsets) = @_;
+    return $jobsets->search({},
         { order_by => "name"
         , "+select" =>
           [ "(select count(*) from Builds as a where a.finished = 0 and me.project = a.project and me.name = a.jobset and a.isCurrent = 1)"
@@ -188,6 +189,13 @@ sub jobsetOverview {
 }
 
 
+sub jobsetOverview {
+    my ($c, $project) = @_;
+    my $jobsets = $project->jobsets->search(isProjectOwner($c, $project) ? {} : { hidden => 0 });
+    return jobsetOverview_($c, $jobsets);
+}
+
+
 sub getViewResult {
     my ($primaryBuild, $jobs, $finished) = @_;
 
diff --git a/src/lib/Hydra/Schema/Users.pm b/src/lib/Hydra/Schema/Users.pm
index 245f44ee..a16370d3 100644
--- a/src/lib/Hydra/Schema/Users.pm
+++ b/src/lib/Hydra/Schema/Users.pm
@@ -61,6 +61,12 @@ __PACKAGE__->table("Users");
   default_value: 0
   is_nullable: 0
 
+=head2 type
+
+  data_type: 'text'
+  default_value: 'hydra'
+  is_nullable: 0
+
 =cut
 
 __PACKAGE__->add_columns(
@@ -74,6 +80,8 @@ __PACKAGE__->add_columns(
   { data_type => "text", is_nullable => 0 },
   "emailonerror",
   { data_type => "integer", default_value => 0, is_nullable => 0 },
+  "type",
+  { data_type => "text", default_value => "hydra", is_nullable => 0 },
 );
 
 =head1 PRIMARY KEY
@@ -176,8 +184,8 @@ Composing rels: L</projectmembers> -> project
 __PACKAGE__->many_to_many("projects", "projectmembers", "project");
 
 
-# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Hv9Ukqud0d3uIUot0ErKeg
+# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-11-05 10:22:03
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Gd8KwFcnVShZ/WihvwfgQw
 
 my %hint = (
     columns => [
diff --git a/src/root/common.tt b/src/root/common.tt
index 48366567..056c9f7e 100644
--- a/src/root/common.tt
+++ b/src/root/common.tt
@@ -452,7 +452,7 @@ BLOCK makePopover %]
 
 BLOCK menuItem %]
   <li class="[% IF "${root}${curUri}" == uri %]active[% END %]" [% IF confirmmsg %]onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>
-    <a href="[% uri %]" [% IF modal %]data-toggle="modal"[% END %]>
+    <a [% HTML.attributes(href => uri) %] [%+ IF modal %]data-toggle="modal"[% END %]>
       [% IF icon %]<i class="[% icon %] icon-black"></i> [%+ END %]
       [% title %]
     </a>
@@ -464,4 +464,65 @@ BLOCK makeStar %]
 <span class="star" data-post="[% starUri %]">[% IF starred; "★"; ELSE; "☆"; END %]</span>
 [% END;
 
+
+BLOCK renderJobsetOverview %]
+<table class="table table-striped table-condensed clickable-rows">
+  <thead>
+    <tr>
+      <th></th>
+      <th>Name</th>
+      <th>Description</th>
+      <th>Last evaluated</th>
+      <th colspan="2">Success</th>
+    </tr>
+  </thead>
+  <tbody>
+    [% FOREACH j IN jobsets %]
+      [% successrate = 0 %]
+      <tr>
+        <td>
+          [% IF j.get_column('nrscheduled') > 0 %]
+            <img src="[% c.uri_for("/static/images/help_16.png") %]" alt="Scheduled" />
+          [% ELSIF j.get_column('nrfailed') == 0  %]
+            <img src="[% c.uri_for("/static/images/checkmark_16.png") %]" alt="Succeeded" />
+          [% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %]
+            <img src="[% c.uri_for("/static/images/error_some_16.png") %]" alt="Some Failed" />
+          [% ELSE %]
+            <img src="[% c.uri_for("/static/images/error_16.png") %]" alt="All Failed" />
+          [% END %]
+        </td>
+        <td><span class="[% IF !j.enabled %]disabled-jobset[% END %] [%+ IF j.hidden %]hidden-jobset[% END %]">[% IF showProject; INCLUDE renderFullJobsetName project=j.get_column('project') jobset=j.name inRow=1; ELSE; INCLUDE renderJobsetName project=j.get_column('project') jobset=j.name inRow=1; END %]</span></td>
+        <td>[% HTML.escape(j.description) %]</td>
+        <td>[% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %]</td>
+        [% IF j.get_column('nrtotal') > 0 %]
+          [% successrate = ( j.get_column('nrsucceeded') / j.get_column('nrtotal') )*100 %]
+          [% IF j.get_column('nrscheduled') > 0 %]
+            [% class = 'label' %]
+          [% ELSIF successrate < 25 %]
+            [% class = 'label label-important' %]
+          [% ELSIF successrate < 75 %]
+            [% class = 'label label-warning' %]
+          [% ELSIF successrate <= 100 %]
+            [% class = 'label label-success' %]
+          [% END %]
+        [% END %]
+        <td><span class="[% class %]">[% successrate FILTER format('%d') %]%</span></td>
+        <td>
+          [% IF j.get_column('nrsucceeded') > 0 %]
+            <span class="label label-success">[% j.get_column('nrsucceeded') %]</span>
+          [% END %]
+          [% IF j.get_column('nrfailed') > 0 %]
+            <span class="label label-important">[% j.get_column('nrfailed') %]</span>
+          [% END %]
+          [% IF j.get_column('nrscheduled') > 0 %]
+            <span class="label label">[% j.get_column('nrscheduled') %]</span>
+          [% END %]
+        </td>
+      </tr>
+    [% END %]
+  </tbody>
+</table>
+[% END;
+
+
 %]
diff --git a/src/root/dashboard-my-jobs-tab.tt b/src/root/dashboard-my-jobs-tab.tt
new file mode 100644
index 00000000..a1e82612
--- /dev/null
+++ b/src/root/dashboard-my-jobs-tab.tt
@@ -0,0 +1,17 @@
+[% PROCESS common.tt %]
+
+[% IF builds.size == 0 %]
+
+  <div class="alert alert-warning">You are not the maintainer of any
+  job.  You can become a maintainer by setting the
+  <tt>meta.maintainer</tt> field of a job to <tt>[%
+  HTML.escape(user.emailaddress) %]</tt>.</div>
+
+[% ELSE %]
+
+  <p>Below are the most recent builds of the [% builds.size %] jobs of which you
+  (<tt>[% HTML.escape(user.emailaddress) %]</tt>) are a maintainer.</p>
+
+  [% INCLUDE renderBuildList %]
+
+[% END %]
diff --git a/src/root/dashboard-my-jobsets-tab.tt b/src/root/dashboard-my-jobsets-tab.tt
new file mode 100644
index 00000000..ab4fff0c
--- /dev/null
+++ b/src/root/dashboard-my-jobsets-tab.tt
@@ -0,0 +1,12 @@
+[% PROCESS common.tt %]
+
+[% IF jobsets.size == 0 %]
+
+  <div class="alert alert-warning">You are not the owner of any
+  jobset.</div>
+
+[% ELSE %]
+
+  [% INCLUDE renderJobsetOverview showProject=1 %]
+
+[% END %]
diff --git a/src/root/dashboard.tt b/src/root/dashboard.tt
index d33eb887..07e48eb5 100644
--- a/src/root/dashboard.tt
+++ b/src/root/dashboard.tt
@@ -3,6 +3,8 @@
 
 <ul class="nav nav-tabs">
   <li class="active"><a href="#tabs-starred-jobs" data-toggle="tab">Starred jobs</a></li>
+  <li><a href="#tabs-my-jobs" data-toggle="tab">My jobs</a></li>
+  <li><a href="#tabs-my-jobsets" data-toggle="tab">My jobsets</a></li>
 </ul>
 
 <div id="generic-tabs" class="tab-content">
@@ -31,12 +33,17 @@
 
     [% ELSE %]
 
-      <div class="alert alert-warning">You have no starred jobs.  You can add them by visiting a job page and clicking on the ☆ icon.</div>
+      <div class="alert alert-warning">You have no starred jobs.  You
+      can add them by visiting a job page and clicking on the ☆
+      icon.</div>
 
     [% END %]
 
   </div>
 
+  [% INCLUDE makeLazyTab tabName="tabs-my-jobs" uri=c.uri_for(c.controller('User').action_for('my_jobs_tab'), [user.username]) %]
+  [% INCLUDE makeLazyTab tabName="tabs-my-jobsets" uri=c.uri_for(c.controller('User').action_for('my_jobsets_tab'), [user.username]) %]
+
 </div>
 
 [% END %]
diff --git a/src/root/layout.tt b/src/root/layout.tt
index 0fd1397f..bee4d8a7 100644
--- a/src/root/layout.tt
+++ b/src/root/layout.tt
@@ -8,7 +8,8 @@
   <head>
     <title>Hydra - [% HTML.escape(title) %]</title>
 
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
 
     <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
     <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/jquery-ui.min.js"></script>
@@ -64,28 +65,26 @@
     </div>
 
     <div class="container">
+      <div class="skip-topbar"></div>
 
       [% IF flashMsg %]
-        <br />
         <div class="alert alert-info">[% flashMsg %]</div>
       [% END %]
 
       [% IF successMsg %]
-        <br />
         <div class="alert alert-success">[% successMsg %]</div>
       [% END %]
 
       [% IF errorMsg %]
-        <br />
         <div class="alert alert-warning">Error: [% errorMsg %]</div>
       [% END %]
 
       [% IF !hideHeader %]
         <div class="page-header">
-          <h1><small>[% IF c.user_exists && starUri; INCLUDE makeStar; " "; END; HTML.escape(title) %]</small></h1>
+          [% IF c.user_exists && starUri; INCLUDE makeStar; " "; END; HTML.escape(title) %]
         </div>
       [% ELSE %]
-        <br />
+        [% IF first %]<br />[% first = 0; END; %]
       [% END %]
 
       [% content %]
@@ -95,13 +94,96 @@
         <small>
           <em><a href="http://nixos.org/hydra" target="_blank">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]).</em>
           [% IF c.user_exists %]
-          You are logged in as <tt>[% c.user.username %]</tt>.
+          You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>[% IF c.user.type == 'persona' %] via Persona[% END %].
           [% END %]
         </small>
       </footer>
 
     </div>
 
+    <script>
+      function doLogout() {
+        [% IF c.user_exists %]
+          $.post("[% c.uri_for('/logout') %]")
+            .done(function(data) {
+              window.location.reload();
+            })
+            .fail(function() { bootbox.alert("Server request failed!"); });
+        [% END %]
+      }
+    </script>
+
+    [% IF c.user_exists && c.user.type == 'hydra' %]
+      <script>
+        $("#persona-signout").click(doLogout);
+      </script>
+    [% ELSE %]
+      <script src="https://login.persona.org/include.js"></script>
+
+      <script>
+        navigator.id.watch({
+          loggedInUser: [% c.user_exists ? '"' _ HTML.escape(c.user.username) _ '"' : "null" %],
+          onlogin: function(assertion) {
+            requestJSON({
+              url: "[% c.uri_for('/persona-login') %]",
+              data: "assertion=" + assertion,
+              type: 'POST',
+              success: function(data) { window.location.reload(); },
+              postError: function() { navigator.id.logout(); }
+            });
+          },
+          onlogout: doLogout
+        });
+
+        $("#persona-signin").click(function() {
+          navigator.id.request({ siteName: 'Hydra' });
+        });
+
+        $("#persona-signout").click(function() {
+          navigator.id.logout();
+        });
+      </script>
+    [% END %]
+
+    [% IF !c.user_exists %]
+      <div id="hydra-signin" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
+        <form class="form-horizontal">
+          <div class="modal-body">
+            <div class="control-group">
+              <label class="control-label">User name</label>
+              <div class="controls">
+                <input type="text" class="span3" name="username" value=""/>
+              </div>
+            </div>
+            <div class="control-group">
+              <label class="control-label">Password</label>
+              <div class="controls">
+                <input type="password" class="span3" name="password" value=""/>
+              </div>
+            </div>
+          </div>
+          <div class="modal-footer">
+            <button id="do-signin" class="btn btn-primary">Sign in</button>
+            <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
+          </div>
+        </form>
+      </div>
+
+      <script>
+          $("#do-signin").click(function() {
+            requestJSON({
+              url: "[% c.uri_for('/login') %]",
+              data: $(this).parents("form").serialize(),
+              type: 'POST',
+              success: function(data) {
+                window.location.reload();
+              }
+            });
+            return false;
+          });
+      </script>
+    [% END %]
+
   </body>
 
 </html>
diff --git a/src/root/login.tt b/src/root/login.tt
deleted file mode 100644
index 4e57077f..00000000
--- a/src/root/login.tt
+++ /dev/null
@@ -1,44 +0,0 @@
-[% WRAPPER layout.tt title="Sign in" %]
-[% PROCESS common.tt %]
-
-[% IF c.user_exists %]
-<p class="btn-info btn-large">
-You are already logged in as <tt>[% c.user.username %]</tt>.
-You can <a href="[% c.uri_for('/logout') %]">logout</a> here.
-</p>
-[% ELSE %]
-
-<!--
-<p>Don't have an account yet? Please <a href="[%
-c.uri_for('/register') %]">register</a> first.</p>
--->
-
-<br/>
-
-<form class="form-horizontal" method="post" action="[% c.uri_for('/login') %]">
-
-  <fieldset>
-    <div class="control-group">
-      <label class="control-label">User name</label>
-      <div class="controls">
-        <input type="text" class="span3" name="username" value=""/>
-      </div>
-    </div>
-
-    <div class="control-group">
-      <label class="control-label">Password</label>
-      <div class="controls">
-        <input type="password" class="span3" name="password" value=""/>
-      </div>
-    </div>
-
-    <div class="form-actions">
-      <input type="submit" name="login" value="Sign in" class="btn btn-primary" />
-    </div>
-  </fieldset>
-
-</form>
-
-[% END %]
-
-[% END %]
diff --git a/src/root/overview.tt b/src/root/overview.tt
index 9b31aeb5..ebc4cea7 100644
--- a/src/root/overview.tt
+++ b/src/root/overview.tt
@@ -13,7 +13,7 @@
   </div>
 [% END %]
 
-<h2>Projects</h2>
+<div class="page-header">Projects</div>
 
 [% IF projects.size != 0 %]
 
diff --git a/src/root/project.tt b/src/root/project.tt
index d88474dd..4e2dbd8f 100644
--- a/src/root/project.tt
+++ b/src/root/project.tt
@@ -30,64 +30,7 @@
   <div id="tabs-project" class="tab-pane active">
     [% IF project.jobsets %]
       <p>This project has the following jobsets:</p>
-
-      <table class="table table-striped table-condensed clickable-rows">
-        <thead>
-          <tr>
-            <th></th>
-            <th>Id</th>
-            <th>Description</th>
-            <th>Last evaluated</th>
-            <th colspan="2">Success</th>
-          </tr>
-        </thead>
-        <tbody>
-          [% FOREACH j IN jobsets %]
-            [% successrate = 0 %]
-            <tr>
-              <td>
-                [% IF j.get_column('nrscheduled') > 0 %]
-                  <img src="[% c.uri_for("/static/images/help_16.png") %]" alt="Scheduled" />
-                [% ELSIF j.get_column('nrfailed') == 0  %]
-                  <img src="[% c.uri_for("/static/images/checkmark_16.png") %]" alt="Succeeded" />
-                [% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %]
-                  <img src="[% c.uri_for("/static/images/error_some_16.png") %]" alt="Some Failed" />
-                [% ELSE %]
-                  <img src="[% c.uri_for("/static/images/error_16.png") %]" alt="All Failed" />
-                [% END %]
-              </td>
-              <td><span class="[% IF !j.enabled %]disabled-jobset[% END %] [%+ IF j.hidden %]hidden-jobset[% END %]">[% INCLUDE renderJobsetName project=project.name jobset=j.name inRow=1 %]</span></td>
-              <td>[% HTML.escape(j.description) %]</td>
-              <td>[% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %]</td>
-              [% IF j.get_column('nrtotal') > 0 %]
-                [% successrate = ( j.get_column('nrsucceeded') / j.get_column('nrtotal') )*100 %]
-                [% IF j.get_column('nrscheduled') > 0 %]
-                  [% class = 'label' %]
-                [% ELSIF successrate < 25 %]
-                  [% class = 'label label-important' %]
-                [% ELSIF successrate < 75 %]
-                  [% class = 'label label-warning' %]
-                [% ELSIF successrate <= 100 %]
-                  [% class = 'label label-success' %]
-                [% END %]
-              [% END %]
-              <td><span class="[% class %]">[% successrate FILTER format('%d') %]%</span></td>
-              <td>
-                [% IF j.get_column('nrsucceeded') > 0 %]
-                  <span class="label label-success">[% j.get_column('nrsucceeded') %]</span>
-                [% END %]
-                [% IF j.get_column('nrfailed') > 0 %]
-                  <span class="label label-important">[% j.get_column('nrfailed') %]</span>
-                [% END %]
-                [% IF j.get_column('nrscheduled') > 0 %]
-                  <span class="label label">[% j.get_column('nrscheduled') %]</span>
-                [% END %]
-              </td>
-            </tr>
-          [% END %]
-        </tbody>
-      </table>
-
+      [% INCLUDE renderJobsetOverview %]
     [% ELSE %]
       <p>No jobsets have been defined yet.</p>
     [% END %]
diff --git a/src/root/static/css/hydra.css b/src/root/static/css/hydra.css
index 4fac2c3c..59ef91c1 100644
--- a/src/root/static/css/hydra.css
+++ b/src/root/static/css/hydra.css
@@ -1,6 +1,12 @@
-@media (min-width: 768px) {
-    body {
-        padding-top: 40px;
+div.skip-topbar {
+    padding-top: 40px;
+    margin-bottom: 1.5em;
+}
+
+@media (max-width: 979px) {
+    div.skip-topbar {
+        padding-top: 0;
+        margin-bottom: 0;
     }
 }
 
@@ -68,16 +74,13 @@ h3 {
 }
 
 div.page-header {
-    margin-top: 0em;
+    margin-top: 1em;
     margin-bottom: 1em;
-}
-
-div.page-header h1 {
-    margin-bottom: 0em;
-}
-
-div.page-header h1 small {
-    font-size: 45%;
+    padding-top: 0em;
+    padding-bottom: 0.5em;
+    color: #999999;
+    font-size: 123.809%;
+    font-weight: normal;
 }
 
 .shell-prompt {
@@ -107,4 +110,4 @@ td.nowrap {
 
 .star:hover {
     cursor: pointer;
-}
\ No newline at end of file
+}
diff --git a/src/root/static/js/common.js b/src/root/static/js/common.js
index 891fd4e6..68a65d2a 100644
--- a/src/root/static/js/common.js
+++ b/src/root/static/js/common.js
@@ -135,6 +135,7 @@ function requestJSON(args) {
             bootbox.alert("Server error: " + escapeHTML(data.responseText));
         else
             bootbox.alert("Unknown server error!");
+        if (args.postError) args.postError(data);
     };
     return $.ajax(args);
 };
@@ -145,3 +146,9 @@ function redirectJSON(args) {
     };
     return requestJSON(args);
 };
+
+function backToReferrer() {
+    // FIXME: should only do this if the referrer is another Hydra
+    // page.
+    window.location = document.referrer;
+}
diff --git a/src/root/timeline.tt b/src/root/timeline.tt
deleted file mode 100644
index 2f665ad0..00000000
--- a/src/root/timeline.tt
+++ /dev/null
@@ -1,64 +0,0 @@
-[% USE date %]
-
-[% WRAPPER layout.tt title="Timeline" %]
-
-[% PROCESS common.tt %]
-
-<script type="text/javascript">
-Timeline_urlPrefix="http://simile.mit.edu/timeline/api/";
-</script>
-<script src="http://simile.mit.edu/timeline/api/timeline-api.js" type="text/javascript"></script>
-
-
-<script type="text/javascript">
-  $(function() {
-    doItNow()
-  });
-
- var tl;
- function doItNow() {
-   var eventSource = new Timeline.DefaultEventSource();
-   var bandInfos = [
-     Timeline.createBandInfo({
-         eventSource:    eventSource,
-         width:          "100%",
-         intervalUnit:   Timeline.DateTime.HOUR,
-         intervalPixels: 200
-     })
-   ];
-
-   tl = Timeline.create(document.getElementById("my-timeline"), bandInfos);
-
-   var centerd = Timeline.DateTime.parseIso8601DateTime("[% date.format(pit, '%Y-%m-%dT%H:%M:%S') %]");
-   tl.getBand(0).setCenterVisibleDate( centerd );
-
-   var event_data =
-       { "dateTimeFormat": "iso8601", "events":[
-          { "start": "[% date.format(pit, '%Y-%m-%dT%H:%M:%S') %]",
-            "end": "[% date.format(pit, '%Y-%m-%dT%H:%M:%S') %]",
-            "title": "Now"
-          }
-
-     [% FOREACH build IN builds %]
-          , { "start": "[% date.format(build.get_column("starttime"), '%Y-%m-%dT%H:%M:%S')  %]",
-              "end": "[% date.format(build.get_column("stoptime"), '%Y-%m-%dT%H:%M:%S') %]",
-              "isDuration": "true",
-              "title": "[% build.id %]",
-              "link": "[% c.uri_for('/build' build.id) %]",
-              "color": "[% IF build.get_column("buildstatus") == 0 %]green[%ELSE%]red[% END%]"
-            }
-     [% END %]
-    ]};
-
-   eventSource.loadJSON(event_data, document.location.href);
-
-
- }
-</script>
-
-<div id="my-timeline" style="height: 700px; width: 100%;border: 1px solid #aaa"></div>
-<noscript>
-This page uses Javascript to show you a Timeline. Please enable Javascript in your browser to see the full page. Thank you.
-</noscript>
-
-[% END %]
diff --git a/src/root/topbar.tt b/src/root/topbar.tt
index 642e40d2..6e3cd264 100644
--- a/src/root/topbar.tt
+++ b/src/root/topbar.tt
@@ -119,9 +119,27 @@
 
   [% IF c.user_exists %]
     [% INCLUDE menuItem uri = c.uri_for(c.controller('User').action_for('edit'), [c.user.username]) title = "Preferences" %]
-    [% INCLUDE menuItem uri = c.uri_for(c.controller('Root').action_for('logout')) title = "Sign out" %]
+    <li>
+      <a href="#" id="persona-signout">Sign out</a>
+    </li>
   [% ELSE %]
-    [% INCLUDE menuItem uri = c.uri_for(c.controller('Root').action_for('login')) title = "Sign in" %]
+    [% IF personaEnabled %]
+      [% WRAPPER makeSubMenu title="Sign in" %]
+        <li>
+          <a href="#" id="persona-signin">
+            <img src="https://developer.mozilla.org/files/3963/persona_sign_in_blue.png" alt="Sign in with Persona" />
+          </a>
+        </li>
+        <li class="divider"></li>
+        <li>
+          <a href="#hydra-signin" data-toggle="modal">Sign in with a Hydra account</a>
+        </li>
+      [% END %]
+    [% ELSE %]
+      <li>
+        <a href="#hydra-signin" data-toggle="modal">Sign in</a>
+      </li>
+    [% END %]
   [% END %]
 
 </ul>
diff --git a/src/root/user.tt b/src/root/user.tt
index 18de0ebb..8f577929 100644
--- a/src/root/user.tt
+++ b/src/root/user.tt
@@ -1,4 +1,4 @@
-[% WRAPPER layout.tt title=(create ? "Register new user" : "Editing user $user.username") %]
+[% WRAPPER layout.tt title=(create ? "Add new user" : "Editing user $user.username") %]
 [% PROCESS common.tt %]
 
 [% BLOCK roleoption %]
@@ -14,7 +14,7 @@
     >[% role %]</option>
 [% END %]
 
-<form class="form-horizontal" method="post">
+<form class="form-horizontal">
 
   <fieldset>
 
@@ -30,10 +30,11 @@
     <div class="control-group">
       <label class="control-label">Full name</label>
       <div class="controls">
-        <input type="text" class="span3" name="fullname" [% HTML.attributes(value => fullname) %]/>
+        <input type="text" class="span3" name="fullname" [% HTML.attributes(value => create ? '' : user.fullname) %]/>
       </div>
     </div>
 
+    [% IF create || user.type == 'hydra' %]
     <div class="control-group">
       <label class="control-label">Password</label>
       <div class="controls">
@@ -47,31 +48,28 @@
         <input type="password" class="span3" name="password2" value=""/>
       </div>
     </div>
+    [% END %]
 
-    <!--
     <div class="control-group">
       <label class="control-label">Email</label>
       <div class="controls">
-        <input type="text" class="span3" name="emailaddress" [% HTML.attributes(value => user.emailaddress) %]/>
+        <input type="text" class="span3" name="emailaddress" [% IF !create && user.type == 'persona' %]disabled="disabled"[% END %] [%+ HTML.attributes(value => user.emailaddress) %]/>
       </div>
     </div>
-    -->
 
-    [% IF !create %]
-      <div class="control-group">
-        <div class="controls">
-          <label class="checkbox">
-            <input type="checkbox" name="emailonerror" [% IF emailonerror; 'checked="checked"'; END %]/>Receive evaluation error notifications
-          </label>
-        </div>
+    <div class="control-group">
+      <div class="controls">
+        <label class="checkbox">
+          <input type="checkbox" name="emailonerror" [% IF !create && user.emailonerror; 'checked="checked"'; END %]/>Receive evaluation error notifications
+        </label>
       </div>
-    [% END %]
+    </div>
 
-    [% IF !create && c.check_user_roles('admin') %]
+    [% IF !create || c.check_user_roles('admin') %]
       <div class="control-group">
         <label class="control-label">Roles</label>
         <div class="controls">
-          <select multiple="multiple" name="roles" class="span3">
+          <select multiple="multiple" name="roles" class="span3" [% IF !c.check_user_roles('admin') %]disabled="disabled"[% END %]>
             [% INCLUDE roleoption role="admin" %]
             [% INCLUDE roleoption role="create-projects" %]
           </select>
@@ -79,7 +77,7 @@
       </div>
     [% END %]
 
-    [% IF create %]
+    [% IF create && !c.check_user_roles('admin') %]
       <div class="control-group">
         <div class="controls">
           <img src="[% c.uri_for('/captcha') %]" alt="CAPTCHA"/>
@@ -95,44 +93,21 @@
     [% END %]
 
     <div class="form-actions">
-      <button type="submit" class="btn btn-primary">
+      <button id="submit-user" class="btn btn-primary">
         <i class="icon-ok icon-white"></i>
         [%IF create %]Create[% ELSE %]Apply changes[% END %]
       </button>
-      [% IF !create && c.check_user_roles('admin') %]
-        <button id="reset-password" type="submit" class="btn btn-warning" name="submit" value="reset-password">
+      [% IF !create && c.check_user_roles('admin') && user.type == 'hydra' %]
+        <button id="reset-password" class="btn btn-warning">
           <i class="icon-trash icon-white"></i>
           Reset password
         </button>
-        <script type="text/javascript">
-          $("#reset-password").click(function() {
-            bootbox.confirm(
-              'Are you sure you want to reset the password for this user?',
-              function(c) {
-                if (!c) return;
-                $.post("[% c.uri_for(c.controller('User').action_for('edit'), [user.username]) %]", { submit: 'reset-password' })
-                  .done(function(data) {
-                    if (data.error)
-                      bootbox.alert("Unable to reset password: " + data.error);
-                    else
-                      bootbox.alert("An email containing the new password has been sent to the user.");
-                  })
-                  .fail(function() { bootbox.alert("Server request failed!"); });
-              });
-            return false;
-          });
-        </script>
       [% END %]
       [% IF !create %]
-        <button id="delete-user" type="submit" class="btn btn-danger" name="submit" value="delete">
+        <button id="delete-user" class="btn btn-danger">
           <i class="icon-trash icon-white"></i>
           Delete this user
         </button>
-        <script type="text/javascript">
-          $("#delete-user").click(function() {
-            return confirm("Are you sure you want to delete this user?");
-          });
-        </script>
       [% END %]
     </div>
 
@@ -140,4 +115,48 @@
 
 </form>
 
+<script>
+  $("#submit-user").click(function() {
+    requestJSON({
+      [% IF create %]
+        url: "[% c.uri_for(c.controller('User').action_for('register')) %]",
+      [% ELSE %]
+        url: "[% c.uri_for(c.controller('User').action_for('edit'), c.req.captures) %]",
+      [% END %]
+      data: $(this).parents("form").serialize(),
+      type: 'PUT',
+      success: backToReferrer
+    });
+    return false;
+  });
+
+  $("#reset-password").click(function() {
+    bootbox.confirm(
+      'Are you sure you want to reset the password for this user?',
+      function(c) {
+        if (!c) return;
+        requestJSON({
+          url: "[% c.uri_for(c.controller('User').action_for('reset_password'), [user.username]) %]",
+          type: 'POST',
+          success: backToReferrer
+        });
+      });
+    return false;
+  });
+
+  $("#delete-user").click(function() {
+    bootbox.confirm(
+      'Are you sure you want to delete this user?',
+      function(c) {
+        if (!c) return;
+        requestJSON({
+          url: "[% c.uri_for(c.controller('User').action_for('edit'), c.req.captures) %]",
+          type: 'DELETE',
+          success: backToReferrer
+        });
+      });
+    return false;
+  });
+</script>
+
 [% END %]
diff --git a/src/root/users.tt b/src/root/users.tt
index f9178b8e..45f5e5e7 100644
--- a/src/root/users.tt
+++ b/src/root/users.tt
@@ -4,7 +4,7 @@
 <table class="table table-striped table-condensed clickable-rows">
   <thead>
     <tr>
-      <th>Username</th>
+      <th>User name</th>
       <th>Name</th>
       <th>Email</th>
       <th>Roles</th>
@@ -14,9 +14,9 @@
   <tbody>
     [% FOREACH u IN users %]
       <tr>
-        <td><a class="row-link" href="[% c.uri_for(c.controller('User').action_for('edit'), [u.username]) %]">[% u.username %]</a></td>
-        <td>[% u.fullname %]</td>
-        <td>[% u.emailaddress %]</td>
+        <td><a class="row-link" href="[% c.uri_for(c.controller('User').action_for('edit'), [u.username]) %]">[% HTML.escape(u.username) %]</a></td>
+        <td>[% HTML.escape(u.fullname) %]</td>
+        <td>[% HTML.escape(u.emailaddress) %]</td>
         <td>[% FOREACH r IN u.userroles %]<i>[% r.role %]</i> [% END %]</td>
         <td>[% IF u.emailonerror %]Yes[% ELSE %]No[% END %]</td>
       </tr>
diff --git a/src/script/Makefile.am b/src/script/Makefile.am
index 3994684c..c05c9a12 100644
--- a/src/script/Makefile.am
+++ b/src/script/Makefile.am
@@ -1,6 +1,5 @@
 EXTRA_DIST =					\
   $(distributable_scripts)			\
-  hydra-control					\
   hydra-eval-guile-jobs.in
 
 distributable_scripts =				\
@@ -11,6 +10,7 @@ distributable_scripts =				\
   hydra-server					\
   hydra-update-gc-roots				\
   hydra-s3-backup-collect-garbage		\
+  hydra-create-user				\
   nix-prefetch-git				\
   nix-prefetch-bzr				\
   nix-prefetch-hg
diff --git a/src/script/hydra-control b/src/script/hydra-control
deleted file mode 100755
index 7349e303..00000000
--- a/src/script/hydra-control
+++ /dev/null
@@ -1,42 +0,0 @@
-#! /bin/sh
-
-action="$1"
-
-if test -z "$HYDRA_DATA"; then
-    echo "Error: \$HYDRA_DATA is not set";
-    exit 1
-fi
-
-if test "$action" = "start"; then
-
-    hydra-server > $HYDRA_DATA/server.log 2>&1 &
-    echo $! > $HYDRA_DATA/server.pid
-
-    hydra-evaluator > $HYDRA_DATA/evaluator.log 2>&1 &
-    echo $! > $HYDRA_DATA/evaluator.pid
-
-    hydra-queue-runner > $HYDRA_DATA/queue-runner.log 2>&1 &
-    echo $! > $HYDRA_DATA/queue_runner.pid
-
-elif test "$action" = "stop"; then
-
-    kill $(cat $HYDRA_DATA/server.pid)
-    kill $(cat $HYDRA_DATA/evaluator.pid)
-    kill $(cat $HYDRA_DATA/queue_runner.pid)
-
-elif test "$action" = "status"; then
-
-    echo -n "Hydra web server... "
-    (kill -0 $(cat $HYDRA_DATA/server.pid) 2> /dev/null && echo "ok") || echo "not running"
-
-    echo -n "Hydra evaluator... "
-    (kill -0 $(cat $HYDRA_DATA/evaluator.pid) 2> /dev/null && echo "ok") || echo "not running"
-
-    echo -n "Hydra queue runner... "
-    (kill -0 $(cat $HYDRA_DATA/queue_runner.pid) 2> /dev/null && echo "ok") || echo "not running"
-
-
-else
-    echo "Syntax: $0 [start|stop|status]"
-    exit 1
-fi
diff --git a/src/script/hydra-create b/src/script/hydra-create
deleted file mode 100755
index d05c0c6c..00000000
--- a/src/script/hydra-create
+++ /dev/null
@@ -1,60 +0,0 @@
-#! @perl@
-
-use strict;
-use warnings;
-
-use Catalyst::ScriptRunner;
-Catalyst::ScriptRunner->run('Hydra', 'Create');
-
-1;
-
-=head1 NAME
-
-hydra_create.pl - Create a new Catalyst Component
-
-=head1 SYNOPSIS
-
-hydra_create.pl [options] model|view|controller name [helper] [options]
-
- Options:
-   --force        don't create a .new file where a file to be created exists
-   --mechanize    use Test::WWW::Mechanize::Catalyst for tests if available
-   --help         display this help and exits
-
- Examples:
-   hydra_create.pl controller My::Controller
-   hydra_create.pl -mechanize controller My::Controller
-   hydra_create.pl view My::View
-   hydra_create.pl view HTML TT
-   hydra_create.pl model My::Model
-   hydra_create.pl model SomeDB DBIC::Schema MyApp::Schema create=dynamic\
-   dbi:SQLite:/tmp/my.db
-   hydra_create.pl model AnotherDB DBIC::Schema MyApp::Schema create=static\
-   [Loader opts like db_schema, naming] dbi:Pg:dbname=foo root 4321
-   [connect_info opts like quote_char, name_sep]
-
- See also:
-   perldoc Catalyst::Manual
-   perldoc Catalyst::Manual::Intro
-   perldoc Catalyst::Helper::Model::DBIC::Schema
-   perldoc Catalyst::Model::DBIC::Schema
-   perldoc Catalyst::View::TT
-
-=head1 DESCRIPTION
-
-Create a new Catalyst Component.
-
-Existing component files are not overwritten.  If any of the component files
-to be created already exist the file will be written with a '.new' suffix.
-This behavior can be suppressed with the C<-force> option.
-
-=head1 AUTHORS
-
-Catalyst Contributors, see Catalyst.pm
-
-=head1 COPYRIGHT
-
-This library is free software. You can redistribute it and/or modify
-it under the same terms as Perl itself.
-
-=cut
diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user
new file mode 100755
index 00000000..1fcf4728
--- /dev/null
+++ b/src/script/hydra-create-user
@@ -0,0 +1,88 @@
+#! /var/run/current-system/sw/bin/perl -w
+
+use strict;
+use Hydra::Schema;
+use Hydra::Helper::Nix;
+use Hydra::Model::DB;
+use Getopt::Long qw(:config gnu_getopt);
+use Digest::SHA1 qw(sha1_hex);
+
+sub showHelp {
+    print <<EOF;
+Usage: $0 NAME
+  [--rename-from NAME]
+  [--type hydra|persona]
+  [--full-name FULLNAME]
+  [--email-address EMAIL-ADDRESS]
+  [--password PASSWORD]
+  [--wipe-roles]
+  [--role ROLE]...
+
+Create a new Hydra user account, or update or an existing one.  The
+--role flag can be given multiple times.  If the account already
+exists, roles are added to the existing roles unless --wipe-roles is
+specified.  If --rename-from is given, the specified account is
+renamed.
+
+Example:
+  \$ hydra-create-user alice --password foobar --role admin
+EOF
+    exit 0;
+}
+
+my ($renameFrom, $type, $fullName, $emailAddress, $password);
+my $wipeRoles = 0;
+my @roles;
+
+GetOptions("rename-from=s" => \$renameFrom,
+           "type=s" => \$type,
+           "full-name=s" => \$fullName,
+           "email-address=s" => \$emailAddress,
+           "password=s" => \$password,
+           "wipe-roles" => \$wipeRoles,
+           "role=s" => \@roles,
+           "help" => sub { showHelp() }
+    ) or exit 1;
+
+die "$0: one user name required\n" if scalar @ARGV != 1;
+my $userName = $ARGV[0];
+
+die "$0: type must be `hydra' or `persona'\n"
+    if defined $type && $type ne "hydra" && $type ne "persona";
+
+my $db = Hydra::Model::DB->new();
+
+txn_do($db, sub {
+    my $user = $db->resultset('Users')->find({ username => $renameFrom // $userName });
+    if ($renameFrom) {
+        die "$0: user `$renameFrom' does not exist\n" unless $user;
+        $user->update({ username => $userName });
+    } elsif ($user) {
+        print STDERR "updating existing user `$userName'\n";
+    } else {
+        print STDERR "creating new user `$userName'\n";
+        $user = $db->resultset('Users')->create(
+            { username => $userName, type => "hydra", emailaddress => "", password => "!" });
+    }
+
+    die "$0: Persona user names must be email addresses\n"
+        if $user->type eq "persona" && $userName !~ /\@/;
+
+    $user->update({ type => $type }) if defined $type;
+
+    $user->update({ fullname => $fullName eq "" ? undef : $fullName }) if defined $fullName;
+
+    if ($user->type eq "persona") {
+        die "$0: Persona accounts do not have an explicitly set email address.\n"
+            if defined $emailAddress;
+        die "$0: Persona accounts do not have a password.\n"
+            if defined $password;
+        $user->update({ emailaddress => $userName, password => "!" });
+    } else {
+        $user->update({ emailaddress => $emailAddress }) if defined $emailAddress;
+        $user->update({ password => sha1_hex($password) }) if defined $password;
+    }
+
+    $user->userroles->delete if $wipeRoles;
+    $user->userroles->update_or_create({ role => $_ }) foreach @roles;
+});
diff --git a/src/sql/hydra.sql b/src/sql/hydra.sql
index 8e5f3093..a91013c8 100644
--- a/src/sql/hydra.sql
+++ b/src/sql/hydra.sql
@@ -9,7 +9,8 @@ create table Users (
     fullName      text,
     emailAddress  text not null,
     password      text not null, -- sha256 hash
-    emailOnError  integer not null default 0
+    emailOnError  integer not null default 0,
+    type          text not null default 'hydra' -- either "hydra" or "persona"
 );
 
 
diff --git a/src/sql/upgrade-25.sql b/src/sql/upgrade-25.sql
new file mode 100644
index 00000000..d191d4fb
--- /dev/null
+++ b/src/sql/upgrade-25.sql
@@ -0,0 +1 @@
+alter table Users add column type text not null default 'hydra';
diff --git a/tests/Makefile.am b/tests/Makefile.am
index ab848d85..f5f88488 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -27,7 +27,7 @@ TESTS = \
 
 clean:
 	chmod -R a+w nix || true
-	rm -rf db.sqlite data nix git-repo hg-repo svn-repo svn-checkout svn-checkout-repo bzr-repo bzr-checkout-repo
+	rm -rf db.sqlite data nix git-repo hg-repo svn-repo svn-checkout svn-checkout-repo bzr-repo bzr-checkout-repo darcs-repo
 	rm -f .*-state
 
 check_SCRIPTS = db.sqlite repos
diff --git a/tests/api-test.pl b/tests/api-test.pl
index f9068dd4..c71f7909 100644
--- a/tests/api-test.pl
+++ b/tests/api-test.pl
@@ -1,6 +1,6 @@
 use LWP::UserAgent;
 use JSON;
-use Test::Simple tests => 16;
+use Test::Simple tests => 17;
 
 my $ua = LWP::UserAgent->new;
 $ua->cookie_jar({});
@@ -17,6 +17,9 @@ sub request_json {
     return $res;
 }
 
+my $result = request_json({ uri => "/login", method => "POST", data => { username => "root", password => "wrong" } });
+ok($result->code() == 403, "Incorrect password rejected.");
+
 my $result = request_json({ uri => "/login", method => "POST", data => { username => "root", password => "foobar" } });
 
 my $user = decode_json($result->content());
@@ -46,7 +49,7 @@ ok(exists $jobset->{jobsetinputs}->{"my-src"}, "The new jobset has a 'my-src' in
 
 ok($jobset->{jobsetinputs}->{"my-src"}->{jobsetinputalts}->[0] eq "/run/jobset", "The 'my-src' input is in /run/jobset");
 
-system("LOGNAME=root NIX_STORE_DIR=/run/nix/store NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' hydra-evaluator sample default");
+system("NIX_STORE_DIR=/tmp/nix/store NIX_LOG_DIR=/tmp/nix/var/log/nix NIX_STATE_DIR=/tmp/nix/var/nix hydra-evaluator sample default");
 $result = request_json({ uri => '/jobset/sample/default/evals' });
 ok($result->code() == 200, "Can get evals of a jobset");
 my $evals = decode_json($result->content())->{evals};
@@ -55,11 +58,11 @@ ok($eval->{hasnewbuilds} == 1, "The first eval of a jobset has new builds");
 
 # Ugh, cached for 30s
 sleep 30;
-system("echo >> /run/jobset/default.nix; LOGNAME=root NIX_STORE_DIR=/run/nix/store NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' hydra-evaluator sample default");
+system("echo >> /run/jobset/default.nix; NIX_STORE_DIR=/tmp/nix/store NIX_LOG_DIR=/tmp/nix/var/log/nix NIX_STATE_DIR=/tmp/nix/var/nix hydra-evaluator sample default");
 my $evals = decode_json(request_json({ uri => '/jobset/sample/default/evals' })->content())->{evals};
 ok($evals->[0]->{jobsetevalinputs}->{"my-src"}->{revision} != $evals->[1]->{jobsetevalinputs}->{"my-src"}->{revision}, "Changing a jobset source changes its revision");
 
 my $build = decode_json(request_json({ uri => "/build/" . $evals->[0]->{builds}->[0] })->content());
 ok($build->{job} eq "job", "The build's job name is job");
 ok($build->{finished} == 0, "The build isn't finished yet");
-ok($build->{buildoutputs}->{out}->{path} =~ /^\/run\/nix\/store\/[a-zA-Z0-9]{32}-job$/, "The build's outpath is in the nix store and named 'job'");
+ok($build->{buildoutputs}->{out}->{path} =~ /^\/tmp\/nix\/store\/[a-zA-Z0-9]{32}-job$/, "The build's outpath is in the nix store and named 'job'");