diff --git a/bootstrap.php b/bootstrap.php
index c37e3f6f04d6080f31e1c4ed15f0e4313e6bb2fd..ae64d8c932578169b3da1d75964af75b18322516 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -111,6 +111,8 @@ class Bootstrap {
         define('FILTER_TABLE', $prefix.'filter');
         define('FILTER_RULE_TABLE', $prefix.'filter_rule');
 
+        define('PLUGIN_TABLE', $prefix.'plugin');
+
         define('API_KEY_TABLE',$prefix.'api_key');
         define('TIMEZONE_TABLE',$prefix.'timezone');
     }
@@ -169,8 +171,7 @@ class Bootstrap {
 
     function loadCode() {
         #include required files
-        require(INCLUDE_DIR.'class.ostsession.php');
-        require(INCLUDE_DIR.'class.usersession.php');
+        require(INCLUDE_DIR.'class.auth.php');
         require(INCLUDE_DIR.'class.pagenate.php'); //Pagenate helper!
         require(INCLUDE_DIR.'class.log.php');
         require(INCLUDE_DIR.'class.crypto.php');
diff --git a/include/ajax.users.php b/include/ajax.users.php
index 04b29bec611ee3b19d167645590950b4d3cd40ab..dd7615e41c16974f735de40f38a97793cbd9f1bc 100644
--- a/include/ajax.users.php
+++ b/include/ajax.users.php
@@ -118,5 +118,26 @@ class UsersAjaxAPI extends AjaxController {
         return $resp;
     }
 
+    function searchStaff() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required for searching');
+        elseif (!$thisstaff->isAdmin())
+            Http::response(403,
+                'Administrative privilege is required for searching');
+        elseif (!isset($_REQUEST['q']))
+            Http::response(400, 'Query argument is required');
+
+        $users = array();
+        foreach (AuthenticationBackend::allRegistered() as $ab) {
+            if (!$ab->supportsSearch())
+                continue;
+
+            foreach ($ab->search($_REQUEST['q']) as $u)
+                $users[] = $u;
+        }
+        return $this->json_encode($users);
+    }
 }
 ?>
diff --git a/include/class.auth.php b/include/class.auth.php
new file mode 100644
index 0000000000000000000000000000000000000000..3609e2d8921a01ebe5ab38103e900380ce44f465
--- /dev/null
+++ b/include/class.auth.php
@@ -0,0 +1,233 @@
+<?php
+require(INCLUDE_DIR.'class.ostsession.php');
+require(INCLUDE_DIR.'class.usersession.php');
+
+class AuthenticatedUser {
+    // How the user was authenticated
+    var $backend;
+
+    // Get basic information
+    function getId() {}
+    function getUsername() {}
+}
+
+/**
+ * Authentication backend
+ *
+ * Authentication provides the basis of abstracting the link between the
+ * login page with a username and password and the staff member,
+ * administrator, or client using the system.
+ *
+ * The system works by allowing the AUTH_BACKENDS setting from
+ * ost-config.php to determine the list of authentication backends or
+ * providers and also specify the order they should be evaluated in.
+ *
+ * The authentication backend should define a authenticate() method which
+ * receives a username and optional password. If the authentication
+ * succeeds, an instance deriving from <User> should be returned.
+ */
+class AuthenticationBackend {
+    static private $registry = array();
+    static $name;
+    static $id;
+
+    /* static */
+    static function register($class) {
+        if (is_string($class))
+            $class = new $class();
+        static::$registry[] = $class;
+    }
+
+    static function allRegistered() {
+        return static::$registry;
+    }
+
+    /* static */
+    function process($username, $password=null, $backend=null, &$errors) {
+        global $ost;
+
+        foreach (static::$registry as $bk) {
+            if ($backend && $bk->supportsAuthentication() && $bk::$id != $backend)
+                // User cannot be authenticated against this backend
+                continue;
+            $result = $bk->authenticate($username, $password);
+            if ($result instanceof AuthenticatedUser) {
+                //Log debug info.
+                $ost->logDebug('Staff login',
+                    sprintf("%s logged in [%s], via %s", $result->getUserName(),
+                        $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.
+
+                if ($result instanceof Staff) {
+                    $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() '
+                        .' WHERE staff_id='.db_input($result->getId());
+                    db_query($sql);
+                    //Now set session crap and lets roll baby!
+                    $_SESSION['_staff'] = array(); //clear.
+                    $_SESSION['_staff']['userID'] = $username;
+                    $result->refreshSession(); //set the hash.
+
+                    $_SESSION['TZ_OFFSET'] = $result->getTZoffset();
+                    $_SESSION['TZ_DST'] = $result->observeDaylight();
+
+                    $_SESSION['_staff']['backend'] = $bk;
+                }
+
+                //Regenerate session id.
+                $sid = session_id(); //Current id
+                session_regenerate_id(true);
+                // Destroy old session ID - needed for PHP version < 5.1.0
+                // DELME: remove when we move to php 5.3 as min. requirement.
+                if(($session=$ost->getSession()) && is_object($session)
+                        && $sid!=session_id())
+                    $session->destroy($sid);
+
+                Signal::send('auth.login.succeeded', $result);
+
+                $result->cancelResetTokens();
+
+                return $result;
+            }
+            // TODO: Handle permission denied, for instance
+            elseif ($result instanceof AccessDenied) {
+                $errors['err'] = $result->reason;
+                break;
+            }
+        }
+        $info = array('username'=>$username, 'password'=>$password);
+        Signal::send('auth.login.failed', null, $info);
+    }
+
+    /**
+     * Fetches the friendly name of the backend
+     */
+    function getName() {
+        return static::$name;
+    }
+
+    /**
+     * Indicates if the backed supports authentication. Useful if the
+     * backend is used for logging or lockout only
+     */
+    function supportsAuthentication() {
+        return true;
+    }
+
+    /**
+     * Indicates if the backend can be used to search for user information.
+     * Lookup is performed to find user information based on a unique
+     * identifier.
+     */
+    function supportsLookup() {
+        return false;
+    }
+
+    /**
+     * Indicates if the backend supports searching for usernames. This is
+     * distinct from information lookup in that lookup is intended to lookup
+     * information based on a unique identifier
+     */
+    function supportsSearch() {
+        return false;
+    }
+
+    /**
+     * Indicates if the backend supports changing a user's password. This
+     * would be done in two fashions. Either the currently-logged in user
+     * want to change its own password or a user requests to have their
+     * password reset. This requires an administrative privilege which this
+     * backend might not possess, so it's defined in supportsPasswordReset()
+     */
+    function supportsPasswordChange() {
+        return false;
+    }
+
+    function supportsPasswordReset() {
+        return false;
+    }
+}
+
+class RemoteAuthenticationBackend {
+    var $create_unknown_user = false;
+}
+
+/**
+ * This will be an exception in later versions of PHP
+ */
+class AccessDenied {
+    function AccessDenied() {
+        call_user_func_array(array($this, '__construct'), func_get_args());
+    }
+    function __construct($reason) {
+        $this->reason = $reason;
+    }
+}
+
+/**
+ * Simple authentication backend which will lock the login form after a
+ * configurable number of attempts
+ */
+class AuthLockoutBackend extends AuthenticationBackend {
+
+    function authenticate($username, $password=null) {
+        global $cfg, $ost;
+
+        if($_SESSION['_staff']['laststrike']) {
+            if((time()-$_SESSION['_staff']['laststrike'])<$cfg->getStaffLoginTimeout()) {
+                $_SESSION['_staff']['laststrike'] = time(); //reset timer.
+                return new AccessDenied('Max. failed login attempts reached');
+            } else { //Timeout is over.
+                //Reset the counter for next round of attempts after the timeout.
+                $_SESSION['_staff']['laststrike']=null;
+                $_SESSION['_staff']['strikes']=0;
+            }
+        }
+
+        $_SESSION['_staff']['strikes']+=1;
+        if($_SESSION['_staff']['strikes']>$cfg->getStaffMaxLogins()) {
+            $_SESSION['_staff']['laststrike']=time();
+            $alert='Excessive login attempts by a staff member?'."\n".
+                   'Username: '.$username."\n"
+                   .'IP: '.$_SERVER['REMOTE_ADDR']."\n"
+                   .'TIME: '.date('M j, Y, g:i a T')."\n\n"
+                   .'Attempts #'.$_SESSION['_staff']['strikes']."\n"
+                   .'Timeout: '.($cfg->getStaffLoginTimeout()/60)." minutes \n\n";
+            $ost->logWarning('Excessive login attempts ('.$username.')', $alert,
+                    $cfg->alertONLoginError());
+            return new AccessDenied('Forgot your login info? Contact Admin.');
+        //Log every other failed login attempt as a warning.
+        } elseif($_SESSION['_staff']['strikes']%2==0) {
+            $alert='Username: '.$username."\n"
+                    .'IP: '.$_SERVER['REMOTE_ADDR']."\n"
+                    .'TIME: '.date('M j, Y, g:i a T')."\n\n"
+                    .'Attempts #'.$_SESSION['_staff']['strikes'];
+            $ost->logWarning('Failed staff login attempt ('.$username.')', $alert, false);
+        }
+    }
+
+    function supportsAuthentication() {
+        return false;
+    }
+}
+AuthenticationBackend::register(AuthLockoutBackend);
+
+class osTicketAuthentication extends AuthenticationBackend {
+    static $name = "Local Authenication";
+    static $id = "local";
+
+    function authenticate($username, $password) {
+        if (($user = new StaffSession($username)) && $user->getId() &&
+                $user->check_passwd($password)) {
+
+            //update last login && password reset stuff.
+            $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ';
+            if($user->isPasswdResetDue() && !$user->isAdmin())
+                $sql.=',change_passwd=1';
+            $sql.=' WHERE staff_id='.db_input($user->getId());
+            db_query($sql);
+
+            return $user;
+        }
+    }
+}
+AuthenticationBackend::register(osTicketAuthentication);
+?>
diff --git a/include/class.config.php b/include/class.config.php
index ca426bd2c4661f8f417f961cbebc884f3d977cfd..22c984e43be176facb73b977316214e5680b9427 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -14,8 +14,6 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-require_once(INCLUDE_DIR.'class.email.php');
-
 class Config {
     var $config = array();
 
@@ -105,7 +103,9 @@ class Config {
     }
 
     function update($key, $value) {
-        if (!isset($this->config[$key]))
+        if (!$key)
+            return false;
+        elseif (!isset($this->config[$key]))
             return $this->create($key, $value);
 
         $setting = &$this->config[$key];
diff --git a/include/class.cron.php b/include/class.cron.php
index 3aa0357c198ce61e25a22dae6bc9ea982954877e..999bf437dd60b281d00d8c9e2c6c55e1ef220dd4 100644
--- a/include/class.cron.php
+++ b/include/class.cron.php
@@ -3,7 +3,7 @@
     class.cron.php
 
     Nothing special...just a central location for all cron calls.
-    
+
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
     http://www.osticket.com
@@ -12,10 +12,12 @@
     See LICENSE.TXT for details.
 
     TODO: The plan is to make cron jobs db based.
-    
+
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 //TODO: Make it DB based!
+require_once INCLUDE_DIR.'class.signal.php';
+
 class Cron {
 
     function MailFetcher() {
@@ -27,7 +29,7 @@ class Cron {
         require_once(INCLUDE_DIR.'class.ticket.php');
         require_once(INCLUDE_DIR.'class.lock.php');
         Ticket::checkOverdue(); //Make stale tickets overdue
-        TicketLock::cleanup(); //Remove expired locks 
+        TicketLock::cleanup(); //Remove expired locks
     }
 
     function PurgeLogs() {
@@ -55,6 +57,8 @@ class Cron {
         self::PurgeLogs();
         self::CleanOrphanedFiles();
         self::PurgeDrafts();
+
+        Signal::send('cron');
     }
 }
 ?>
diff --git a/include/class.forms.php b/include/class.forms.php
index 0654a2648c9d1cb2438e35c0357927dc7d2588b7..35121204b94e991dcb25764a49a2fd2377bbace7 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -82,6 +82,8 @@ class Form {
         if (!$this->_clean) {
             $this->_clean = array();
             foreach ($this->getFields() as $key=>$field) {
+                if (!$field->hasData())
+                    continue;
                 $this->_clean[$key] = $this->_clean[$field->get('name')]
                     = $field->getClean();
             }
@@ -129,14 +131,14 @@ class FormField {
 
     static $types = array(
         'Basic Fields' => array(
-            'text'  => array('Short Answer', TextboxField),
-            'memo' => array('Long Answer', TextareaField),
-            'thread' => array('Thread Entry', ThreadEntryField, false),
-            'datetime' => array('Date and Time', DatetimeField),
-            'phone' => array('Phone Number', PhoneField),
-            'bool' => array('Checkbox', BooleanField),
-            'choices' => array('Choices', ChoiceField),
-            'break' => array('Section Break', SectionBreakField),
+            'text'  => array('Short Answer', 'TextboxField'),
+            'memo' => array('Long Answer', 'TextareaField'),
+            'thread' => array('Thread Entry', 'ThreadEntryField', false),
+            'datetime' => array('Date and Time', 'DatetimeField'),
+            'phone' => array('Phone Number', 'PhoneField'),
+            'bool' => array('Checkbox', 'BooleanField'),
+            'choices' => array('Choices', 'ChoiceField'),
+            'break' => array('Section Break', 'SectionBreakField'),
         ),
     );
     static $more_types = array();
@@ -441,7 +443,8 @@ class FormField {
         if (!static::$widget)
             throw new Exception('Widget not defined for this field');
         if (!isset($this->_widget)) {
-            $this->_widget = new static::$widget($this);
+            $wc = $this->get('widget') ? $this->get('widget') : static::$widget;
+            $this->_widget = new $wc($this);
             $this->_widget->parseValue();
         }
         return $this->_widget;
@@ -500,6 +503,18 @@ class TextboxField extends FormField {
     }
 }
 
+class PasswordField extends TextboxField {
+    static $widget = 'PasswordWidget';
+
+    function to_database($value) {
+        return Crypto::encrypt($value, SECRET_SALT, $this->getFormName());
+    }
+
+    function to_php($value) {
+        return Crypto::decrypt($value, SECRET_SALT, $this->getFormName());
+    }
+}
+
 class TextareaField extends FormField {
     static $widget = 'TextareaWidget';
 
@@ -841,6 +856,8 @@ class Widget {
 }
 
 class TextboxWidget extends Widget {
+    static $input_type = 'text';
+
     function render() {
         $config = $this->field->getConfiguration();
         if (isset($config['size']))
@@ -853,7 +870,8 @@ class TextboxWidget extends Widget {
             $autocomplete = 'autocomplete="'.($config['autocomplete']?'on':'off').'"';
         ?>
         <span style="display:inline-block">
-        <input type="text" id="<?php echo $this->name; ?>"
+        <input type="<?php echo static::$input_type; ?>"
+            id="<?php echo $this->name; ?>"
             <?php echo $size . " " . $maxlength; ?>
             <?php echo $classes.' '.$autocomplete; ?>
             name="<?php echo $this->name; ?>"
@@ -863,6 +881,19 @@ class TextboxWidget extends Widget {
     }
 }
 
+class PasswordWidget extends TextboxWidget {
+    static $input_type = 'password';
+
+    function parseValue() {
+        // Show empty box unless failed POST
+        if ($_SERVER['REQUEST_METHOD'] == 'POST'
+                && $this->field->getForm()->isValid())
+            parent::parseValue();
+        else
+            $this->value = '';
+    }
+}
+
 class TextareaWidget extends Widget {
     function render() {
         $config = $this->field->getConfiguration();
diff --git a/include/class.nav.php b/include/class.nav.php
index 5a391ff553967c1338828ab6796ea079a615a6dd..799c76a8b4dc0a78b74374c2b2aa673cf74c5132 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -213,6 +213,7 @@ class AdminNav extends StaffNav{
                     $subnav[]=array('desc'=>'Pages', 'href'=>'pages.php','title'=>'Pages','iconclass'=>'pages');
                     $subnav[]=array('desc'=>'Forms','href'=>'forms.php','iconclass'=>'forms');
                     $subnav[]=array('desc'=>'Lists','href'=>'lists.php','iconclass'=>'lists');
+                    $subnav[]=array('desc'=>'Plugins','href'=>'plugins.php','iconclass'=>'api');
                     break;
                 case 'emails':
                     $subnav[]=array('desc'=>'Emails','href'=>'emails.php', 'title'=>'Email Addresses', 'iconclass'=>'emailSettings');
diff --git a/include/class.osticket.php b/include/class.osticket.php
index 875dc13bf8e2e6217a881db22448c0251a019cc5..0e76fe3e6f4f02c6c79b72081182e90d2c002c91 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -20,6 +20,7 @@
 
 require_once(INCLUDE_DIR.'class.csrf.php'); //CSRF token class.
 require_once(INCLUDE_DIR.'class.migrater.php');
+require_once(INCLUDE_DIR.'class.plugin.php');
 
 define('LOG_WARN',LOG_WARNING);
 
@@ -46,6 +47,7 @@ class osTicket {
     var $session;
     var $csrf;
     var $company;
+    var $plugins;
 
     function osTicket() {
 
@@ -59,6 +61,8 @@ class osTicket {
         $this->csrf = new CSRF('__CSRFToken__');
 
         $this->company = new Company();
+
+        $this->plugins = new PluginManager();
     }
 
     function isSystemOnline() {
@@ -432,6 +436,9 @@ class osTicket {
         $_SESSION['TZ_OFFSET'] = $ost->getConfig()->getTZoffset();
         $_SESSION['TZ_DST'] = $ost->getConfig()->observeDaylightSaving();
 
+        // Bootstrap installed plugins
+        $ost->plugins->bootstrap();
+
         return $ost;
     }
 }
diff --git a/include/class.plugin.php b/include/class.plugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..76ad2d24c5aecc6b16e72310b632573e5883695e
--- /dev/null
+++ b/include/class.plugin.php
@@ -0,0 +1,324 @@
+<?php
+
+require_once(INCLUDE_DIR.'/class.config.php');
+class PluginConfig extends Config {
+    var $table = CONFIG_TABLE;
+    var $form;
+
+    function __construct($name) {
+        // Use parent constructor to place configurable information into the
+        // central config table in a namespace of "plugin.<id>"
+        parent::Config("plugin.$name");
+    }
+
+    /* abstract */
+    function getOptions() {
+        return array();
+    }
+
+    /**
+     * Retreive a Form instance for the configurable options offered in
+     * ::getOptions
+     */
+    function getForm() {
+        if (!isset($this->form)) {
+            $this->form = new Form($this->getOptions());
+            if ($_SERVER['REQUEST_METHOD'] != 'POST')
+                $this->form->data($this->getInfo());
+        }
+        return $this->form;
+    }
+
+    /**
+     * commit
+     *
+     * Used in the POST request of the configuration process. The
+     * ::getForm() method should be used to retrieve a configuration form
+     * for this plugin. That form should be submitted via a POST request,
+     * and this method should be called in that request. The data from the
+     * POST request will be interpreted and will adjust the configuration of
+     * this field
+     *
+     * Parameters:
+     * errors - (OUT array) receives validation errors of the parsed
+     *      configuration form
+     *
+     * Returns:
+     * (bool) true if the configuration was updated, false if there were
+     * errors. If false, the errors were written into the received errors
+     * array.
+     */
+    function commit(&$errors=array()) {
+        $f = $this->getForm();
+        if ($f->isValid()) {
+            $config = $f->getClean();
+            $this->pre_save($config, $errors);
+        }
+        $errors += $f->errors();
+        if (count($errors) === 0)
+            return $this->updateAll($config);
+        return false;
+    }
+
+    /**
+     * Pre-save hook to check configuration for errors (other than obvious
+     * validation errors) prior to saving
+     */
+    function pre_save($config, &$errors) {
+        return;
+    }
+
+    /**
+     * Remove all configuration for this plugin -- used when the plugin is
+     * uninstalled
+     */
+    function purge() {
+        $sql = 'DELETE FROM '.$this->table
+            .' WHERE `namespace`='.db_input($this->getNamespace());
+        return (db_query($sql) && db_affected_rows());
+    }
+}
+
+class PluginManager {
+    static private $plugin_info = array();
+
+    /**
+     * boostrap
+     *
+     * Used to bootstrap the plugin subsystem and initialize all the plugins
+     * currently enabled.
+     */
+    function bootstrap() {
+        foreach ($this->allActive() as $p)
+            $p->bootstrap();
+    }
+
+    /**
+     * allActive
+     *
+     * Scans the plugin registry to find all installed and active plugins.
+     * Those plugins are included, instanciated, and cached in a list.
+     *
+     * Returns:
+     * Array<Plugin> a cached list of instanciated plugins for all installed
+     * and active plugins
+     */
+    static function allInstalled() {
+        static $plugins = null;
+        if ($plugins !== null)
+            return $plugins;
+
+        $plugins = array();
+        $sql = 'SELECT * FROM '.PLUGIN_TABLE;
+        if (!($res = db_query($sql)))
+            return $plugins;
+
+        $infos = static::allInfos();
+        while ($ht = db_fetch_array($res)) {
+            // XXX: Only read active plugins here. allInfos() will
+            //      read all plugins
+            if (isset($infos[$ht['install_path']])) {
+                $info = $infos[$ht['install_path']];
+                if ($ht['isactive']) {
+                    list($path, $class) = explode(':', $info['plugin']);
+                    require_once(INCLUDE_DIR . '/' . $ht['install_path'] . '/' . $path);
+                    $plugins[$ht['install_path']] = new $class($ht['id']);
+                }
+                else {
+                    $plugins[$ht['install_path']] = $ht;
+                }
+            }
+        }
+        return $plugins;
+    }
+
+    static function allActive() {
+        $plugins = array();
+        foreach (static::allInstalled() as $p)
+            if ($p instanceof Plugin && $p->isActive())
+                $plugins[] = $p;
+        return $plugins;
+    }
+
+    /**
+     * allInfos
+     *
+     * Scans the plugin folders for installed plugins. For each one, the
+     * plugin.php file is included and the info array returned in added to
+     * the list returned.
+     *
+     * Returns:
+     * Information about all available plugins. The registry will have to be
+     * queried to determine if the plugin is installed
+     */
+    static function allInfos() {
+        static $defaults = array(
+            'include' => 'include/',
+            'stream' => false,
+        );
+
+        if (static::$plugin_info)
+            return static::$plugin_info;
+
+        foreach (glob(INCLUDE_DIR . 'plugins/*', GLOB_ONLYDIR) as $p) {
+            if (!is_file($p . '/plugin.php'))
+                // Invalid plugin -- must define "/plugin.php"
+                continue;
+            // plugin.php is require to return an array of informaiton about
+            // the plugin.
+            $info = array_merge($defaults, (include $p . '/plugin.php'));
+            $info['install_path'] = str_replace(INCLUDE_DIR, '', $p);
+
+            // XXX: Ensure 'id' key isset
+            static::$plugin_info[$info['install_path']] = $info;
+        }
+        return static::$plugin_info;
+    }
+
+    static function getInfoForPath($path) {
+        $infos = static::allInfos();
+        if (isset($infos[$path]))
+            return $infos[$path];
+        return null;
+    }
+
+    function getInstance($path) {
+        static $instances = array();
+        if (!isset($instances[$path])
+                && ($ps = static::allInstalled())
+                && ($ht = $ps[$path])
+                && ($info = static::getInfoForPath($path))) {
+            // $ht may be the plugin instance
+            if ($ht instanceof Plugin)
+                return $ht;
+            // Usually this happens when the plugin is being enabled
+            list($path, $class) = explode(':', $info['plugin']);
+            require_once(INCLUDE_DIR . $info['install_path'] . '/' . $path);
+            $instances[$path] = new $class($ht['id']);
+        }
+        return $instances[$path];
+    }
+
+    /**
+     * install
+     *
+     * Used to install a plugin that is in-place on the filesystem, but not
+     * registered in the plugin registry -- the %plugin table.
+     */
+    function install($path) {
+        if (!($info = $this->getInfoForPath($path)))
+            return false;
+
+        $sql='INSERT INTO '.PLUGIN_TABLE.'SET installed=NOW() '
+            .', install_path='.db_input($path)
+            .', name='.db_input($info['name']);
+        return (db_query($sql) && db_affected_rows());
+    }
+}
+
+/**
+ * Class: Plugin (abstract)
+ *
+ * Base class for plugins. Plugins should inherit from this class and define
+ * the useful pieces of the
+ */
+class Plugin {
+    /**
+     * Configuration manager for the plugin. Should be the name of a class
+     * that inherits from PluginConfig. This is abstract and must be defined
+     * by the plugin subclass.
+     */
+    var $config_class = null;
+    var $id;
+    var $info;
+
+    function Plugin($id) {
+        $this->id = $id;
+        $this->load();
+    }
+
+    function load() {
+        $sql = 'SELECT * FROM '.PLUGIN_TABLE.' WHERE
+            `id`='.db_input($this->id);
+        if (($res = db_query($sql)) && ($ht=db_fetch_array($res)))
+            $this->ht = $ht;
+        $this->info = PluginManager::getInfoForPath($this->ht['install_path']);
+    }
+
+    function getId() { return $this->id; }
+    function getName() { return $this->info['name']; }
+    function isActive() { return $this->ht['isactive']; }
+    function isPhar() { return $this->ht['isphar']; }
+    function getInstallDate() { return $this->ht['installed']; }
+
+    function getIncludePath() {
+        return realpath(INCLUDE_DIR . $this->info['install_path'] . '/'
+            . $this->info['include_path']) . '/';
+    }
+
+    /**
+     * uninstall
+     *
+     * Removes the plugin from the plugin registry. The files remain on the
+     * filesystem which would allow the plugin to be reinstalled. The
+     * configuration for the plugin is also removed. If the plugin is
+     * reinstalled, it will have to be reconfigured.
+     */
+    function uninstall() {
+        $sql = 'DELETE FROM '.PLUGIN_TABLE
+            .' WHERE id='.db_input($this->getId());
+        if (db_query($sql) && db_affected_rows())
+            return $this->getConfig()->purge();
+        return false;
+    }
+
+    function enable() {
+        $sql = 'UPDATE '.PLUGIN_TABLE
+            .' SET isactive=1 WHERE id='.db_input($this->getId());
+        return (db_query($sql) && db_affected_rows());
+    }
+
+    function disable() {
+        $sql = 'UPDATE '.PLUGIN_TABLE
+            .' SET isactive=0 WHERE id='.db_input($this->getId());
+        return (db_query($sql) && db_affected_rows());
+    }
+
+    /**
+     * upgrade
+     *
+     * Upgrade the plugin. This is used to migrate the database pieces of
+     * the plugin using the database migration stream packaged with the
+     * plugin.
+     */
+    function upgrade() {
+    }
+
+    function getConfig() {
+        static $config = null;
+        if ($config === null)
+            $config = new $this->config_class($this->getId());
+
+        return $config;
+    }
+
+    function source($what) {
+        $what = str_replace('\\', '/', $what);
+        if ($what && $what[0] != '/')
+            $what = $this->getIncludePath() . $what;
+        include_once $what;
+    }
+
+    static function lookup($id) { //Assuming local ID is the only lookup used!
+        $path = false;
+        if ($id && is_numeric($id)) {
+            $sql = 'SELECT install_path FROM '.PLUGIN_TABLE
+                .' WHERE id='.db_input($id);
+            $path = db_result(db_query($sql));
+        }
+        if ($path)
+           return PluginManager::getInstance($path);
+    }
+}
+
+?>
diff --git a/include/class.staff.php b/include/class.staff.php
index 93708bc0cccfbd71120f321f22e722f7af98834e..f2e3d6b384fbc5b846155d3108792aff24c6021e 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -20,8 +20,9 @@ include_once(INCLUDE_DIR.'class.team.php');
 include_once(INCLUDE_DIR.'class.group.php');
 include_once(INCLUDE_DIR.'class.passwd.php');
 include_once(INCLUDE_DIR.'class.user.php');
+include_once(INCLUDE_DIR.'class.auth.php');
 
-class Staff {
+class Staff extends AuthenticatedUser {
 
     var $ht;
     var $id;
@@ -762,13 +763,17 @@ class Staff {
             $errors['mobile']='Valid number required';
 
         if($vars['passwd1'] || $vars['passwd2'] || !$id) {
-            if(!$vars['passwd1'] && !$id) {
+            if($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) {
+                $errors['passwd2']='Password(s) do not match';
+            }
+            elseif ($vars['backend'] != 'local') {
+                // Password can be omitted
+            }
+            elseif(!$vars['passwd1'] && !$id) {
                 $errors['passwd1']='Temp. password required';
                 $errors['temppasswd']='Required';
             } elseif($vars['passwd1'] && strlen($vars['passwd1'])<6) {
                 $errors['passwd1']='Must be at least 6 characters';
-            } elseif($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) {
-                $errors['passwd2']='Password(s) do not match';
             }
         }
 
@@ -798,6 +803,7 @@ class Staff {
             .' ,firstname='.db_input($vars['firstname'])
             .' ,lastname='.db_input($vars['lastname'])
             .' ,email='.db_input($vars['email'])
+            .' ,backend='.db_input($vars['backend'])
             .' ,phone="'.db_input(Format::phone($vars['phone']),false).'"'
             .' ,phone_ext='.db_input($vars['phone_ext'])
             .' ,mobile="'.db_input(Format::phone($vars['mobile']),false).'"'
diff --git a/include/staff/plugin.inc.php b/include/staff/plugin.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..f57b12b93a7f86d969231f20bb88ae7819990311
--- /dev/null
+++ b/include/staff/plugin.inc.php
@@ -0,0 +1,41 @@
+<?php
+
+$info=array();
+if($plugin && $_REQUEST['a']!='add') {
+    $form = $plugin->getConfig()->getForm();
+    if ($_POST)
+        $form->isValid();
+    $title = 'Update Plugin';
+    $action = 'update';
+    $submit_text='Save Changes';
+    $info = $plugin->ht;
+    $newcount=2;
+} else {
+    $title = 'Install plugin';
+    $action = 'add';
+    $submit_text='Install';
+    $newcount=4;
+}
+$info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
+?>
+
+<form action="?id=<?php echo urlencode($_REQUEST['id']); ?>" method="post" id="save">
+    <?php csrf_token(); ?>
+    <input type="hidden" name="do" value="<?php echo $action; ?>">
+    <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
+    <h2>Manage Plugin
+        <br/><small><?php echo $plugin->getName(); ?></small></h2>
+
+    <h3>Configuration</h3>
+    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+    <tbody>
+<?php
+$form->render();
+?>
+    </tbody></table>
+<p class="centered">
+    <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
+    <input type="reset"  name="reset"  value="Reset">
+    <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'>
+</p>
+</form>
diff --git a/include/staff/plugins.inc.php b/include/staff/plugins.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..eab4a796ee6a3240cd30d35fac1e20f2071b17f0
--- /dev/null
+++ b/include/staff/plugins.inc.php
@@ -0,0 +1,104 @@
+<div style="width:700;padding-top:5px; float:left;">
+ <h2>Currently Installed Plugins</h2>
+</div>
+<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;">
+ <b><a href="plugins.php?a=add" class="Icon form-add">Add New Plugin</a></b></div>
+<div class="clear"></div>
+
+<?php
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = count($ost->plugins->allInstalled());
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+$pageNav->setURL('forms.php');
+$showing=$pageNav->showing().' forms';
+?>
+
+<form action="plugins.php" method="POST" name="forms">
+<?php csrf_token(); ?>
+<input type="hidden" name="do" value="mass_process" >
+<input type="hidden" id="action" name="a" value="" >
+<table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
+    <thead>
+        <tr>
+            <th width="7">&nbsp;</th>
+            <th>Plugin Name</th>
+            <th>Status</td>
+            <th>Date Installed</th>
+        </tr>
+    </thead>
+    <tbody>
+<?php
+foreach ($ost->plugins->allInstalled() as $p) {
+    if ($p instanceof Plugin) { ?>
+    <tr>
+        <td><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p->getId(); ?>"
+                <?php echo $sel?'checked="checked"':''; ?>></td>
+        <td><a href="plugins.php?id=<?php echo $p->getId(); ?>"
+            ><?php echo $p->getName(); ?></a></td>
+        <td>Enabled</td>
+        <td><?php echo Format::db_datetime($p->getInstallDate()); ?></td>
+    </tr>
+    <?php } else {
+        $p = $ost->plugins->getInfoForPath($p['install_path']); ?>
+    <tr>
+        <td><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p['install_path']; ?>"
+                <?php echo $sel?'checked="checked"':''; ?>></td>
+        <td><?php echo $p['name']; ?></td>
+        <td><strong>Disabled</strong></td>
+        <td></td>
+    </tr>
+    <?php } ?>
+<?php } ?>
+    </tbody>
+    <tfoot>
+     <tr>
+        <td colspan="4">
+            <?php if($count){ ?>
+            Select:&nbsp;
+            <a id="selectAll" href="#ckb">All</a>&nbsp;&nbsp;
+            <a id="selectNone" href="#ckb">None</a>&nbsp;&nbsp;
+            <a id="selectToggle" href="#ckb">Toggle</a>&nbsp;&nbsp;
+            <?php }else{
+                echo 'No extra forms defined yet &mdash; add one!';
+            } ?>
+        </td>
+     </tr>
+    </tfoot>
+</table>
+<?php
+if ($count) //Show options..
+    echo '<div>&nbsp;Page:'.$pageNav->getPageLinks().'&nbsp;</div>';
+?>
+<p class="centered" id="actions">
+    <input class="button" type="submit" name="delete" value="Delete">
+    <input class="button" type="submit" name="enable" value="Enable">
+    <input class="button" type="submit" name="disable" value="Disable">
+</p>
+</form>
+
+<div style="display:none;" class="dialog" id="confirm-action">
+    <h3>Please Confirm</h3>
+    <a class="close" href="">&times;</a>
+    <hr/>
+    <p class="confirm-action" style="display:none;" id="delete-confirm">
+        <font color="red"><strong>Are you sure you want to DELETE selected plugins?</strong></font>
+        <br><br>Deleted forms CANNOT be recovered.
+    </p>
+    <p class="confirm-action" style="display:none;" id="enable-confirm">
+        <font color="green"><strong>Are you ready to enable selected plugins?</strong></font>
+    </p>
+    <p class="confirm-action" style="display:none;" id="disable-confirm">
+        <font color="red"><strong>Are you sure you want to disable selected plugins?</strong></font>
+    </p>
+    <div>Please confirm to continue.</div>
+    <hr style="margin-top:1em"/>
+    <p class="full-width">
+        <span class="buttons" style="float:left">
+            <input type="button" value="No, Cancel" class="close">
+        </span>
+        <span class="buttons" style="float:right">
+            <input type="button" value="Yes, Do it!" class="confirm">
+        </span>
+     </p>
+    <div class="clear"></div>
+</div>
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 4567624842d7aaee268dfe7e034e4173b82c2afd..667b462312b603a205c912584dafb35fe15ac06c 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -17,12 +17,12 @@ if($staff && $_REQUEST['a']!='add'){
     $title='Add New Staff';
     $action='create';
     $submit_text='Add Staff';
-    $passwd_text='Temp. password required &nbsp;<span class="error">&nbsp;*</span>';
+    $passwd_text='Temporary password required only for "Local" authenication';
     //Some defaults for new staff.
     $info['change_passwd']=1;
     $info['isactive']=1;
     $info['isvisible']=1;
-    $info['isadmin']=0; 
+    $info['isadmin']=0;
     $qstr.='&a=add';
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
@@ -48,7 +48,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 Username:
             </td>
             <td>
-                <input type="text" size="30" name="username" value="<?php echo $info['username']; ?>">
+                <input type="text" size="30" class="staff-username typeahead"
+                     name="username" value="<?php echo $info['username']; ?>">
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['username']; ?></span>
             </td>
         </tr>
@@ -58,7 +59,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 First Name:
             </td>
             <td>
-                <input type="text" size="30" name="firstname" value="<?php echo $info['firstname']; ?>">
+                <input type="text" size="30" name="firstname" class="auto first"
+                     value="<?php echo $info['firstname']; ?>">
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['firstname']; ?></span>
             </td>
         </tr>
@@ -67,7 +69,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 Last Name:
             </td>
             <td>
-                <input type="text" size="30" name="lastname" value="<?php echo $info['lastname']; ?>">
+                <input type="text" size="30" name="lastname" class="auto last"
+                    value="<?php echo $info['lastname']; ?>">
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['lastname']; ?></span>
             </td>
         </tr>
@@ -76,7 +79,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 Email Address:
             </td>
             <td>
-                <input type="text" size="30" name="email" value="<?php echo $info['email']; ?>">
+                <input type="text" size="30" name="email" class="auto email"
+                    value="<?php echo $info['email']; ?>">
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['email']; ?></span>
             </td>
         </tr>
@@ -85,7 +89,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 Phone Number:
             </td>
             <td>
-                <input type="text" size="18" name="phone" value="<?php echo $info['phone']; ?>">
+                <input type="text" size="18" name="phone" class="auto phone"
+                    value="<?php echo $info['phone']; ?>">
                 &nbsp;<span class="error">&nbsp;<?php echo $errors['phone']; ?></span>
                 Ext <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>">
                 &nbsp;<span class="error">&nbsp;<?php echo $errors['phone_ext']; ?></span>
@@ -96,15 +101,39 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 Mobile Number:
             </td>
             <td>
-                <input type="text" size="18" name="mobile" value="<?php echo $info['mobile']; ?>">
+                <input type="text" size="18" name="mobile" class="auto mobile"
+                    value="<?php echo $info['mobile']; ?>">
                 &nbsp;<span class="error">&nbsp;<?php echo $errors['mobile']; ?></span>
             </td>
         </tr>
         <tr>
             <th colspan="2">
-                <em><strong>Account Password</strong>: <?php echo $passwd_text; ?> &nbsp;<span class="error">&nbsp;<?php echo $errors['temppasswd']; ?></span></em>
+                <em><strong>Authentication</strong>: <?php echo $passwd_text; ?> &nbsp;<span class="error">&nbsp;<?php echo $errors['temppasswd']; ?></span></em>
             </th>
         </tr>
+        <tr>
+            <td>Authentication Backend</td>
+            <td>
+            <select name="backend" onchange="javascript:
+                if (this.value != '' && this.value != 'local')
+                    $('#password-fields').hide();
+                else
+                    $('#password-fields').show();
+                ">
+                <option value="">&mdash; Use any available backend &mdash;</option>
+            <?php foreach (AuthenticationBackend::allRegistered() as $ab) {
+                if (!$ab->supportsAuthentication()) continue; ?>
+                <option value="<?php echo $ab::$id; ?>" <?php
+                    if ($info['backend'] == $ab::$id)
+                        echo 'selected="selected"'; ?>><?php
+                    echo $ab::$name; ?></option>
+            <?php } ?>
+            </select>
+            </td>
+        </tr>
+        </tbody>
+        <tbody id="password-fields" style="<?php if ($info['backend'] && $info['backend'] != 'local')
+            echo 'display:none;'; ?>">
         <tr>
             <td width="180">
                 Password:
@@ -133,6 +162,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <strong>Force</strong> password change on next login.
             </td>
         </tr>
+    </tbody>
+    <tbody>
         <tr>
             <th colspan="2">
                 <em><strong>Staff's Signature</strong>: Optional signature used on outgoing emails. &nbsp;<span class="error">&nbsp;<?php echo $errors['signature']; ?></span></em>
diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php
index f77fa928a74148164d24a6af8eb3527aa6593682..cfe25a45ad544779d916179e55f639edb6073a49 100644
--- a/include/staff/templates/dynamic-form.tmpl.php
+++ b/include/staff/templates/dynamic-form.tmpl.php
@@ -12,7 +12,7 @@
                 <?php
             }
             else { ?>
-                <td class="multi-line <?php if ($field->get('required')) echo 'required'; ?>">
+                <td class="multi-line <?php if ($field->get('required')) echo 'required'; ?>" style="min-width:120px;">
                 <?php echo Format::htmlchars($field->get('label')); ?>:</td>
                 <td><?php
             }
diff --git a/scp/ajax.php b/scp/ajax.php
index 106f3366a723a6e25a815796209c1dc5fed07f71..1dcbe44aad94e8ca9a26794266f5cb4243d06975 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -64,7 +64,8 @@ $dispatcher = patterns('',
         url_get('^/lookup/form$', 'getLookupForm'),
         url_post('^/lookup/form$', 'addUser'),
         url_get('^/select$', 'selectUser'),
-        url_get('^/select/(?P<id>\d+)$', 'selectUser')
+        url_get('^/select/(?P<id>\d+)$', 'selectUser'),
+        url_get('^/staff$', 'searchStaff')
     )),
     url('^/tickets/', patterns('ajax.tickets.php:TicketsAjaxAPI',
         url_get('^(?P<tid>\d+)/change-user$', 'changeUserForm'),
@@ -93,6 +94,8 @@ $dispatcher = patterns('',
     ))
 );
 
+Signal::send('ajax.scp', $dispatcher);
+
 # Call the respective function
 print $dispatcher->resolve($ost->get_path_info());
 ?>
diff --git a/scp/js/scp.js b/scp/js/scp.js
index b61e62057084e164bf1aac9e146f13f292ba9b3b..f01da5f197969a341596cabab1b830a07778e234 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -345,6 +345,26 @@ $(document).ready(function(){
         },
         property: "email"
     });
+    $('.staff-username.typeahead').typeahead({
+        source: function (typeahead, query) {
+            if(query.length > 2) {
+                $.ajax({
+                    url: "ajax.php/users/staff?q="+query,
+                    dataType: 'json',
+                    success: function (data) {
+                        typeahead.process(data);
+                    }
+                });
+            }
+        },
+        onselect: function (obj) {
+            var fObj=$('.staff-username.typeahead').closest('form');
+            $.each(['first','last','email','phone','mobile'], function(i,k) {
+                if (obj[k]) $('.auto.'+k, fObj).val(obj[k]);
+            });
+        },
+        property: "username"
+    });
 
     //Overlay
     $('#overlay').css({
diff --git a/scp/login.php b/scp/login.php
index 2f3cf2236e9f4996bb10b94764fb6d0a14d99d22..7c13cf68b495fd76b12e3a33f744fc85c16d0a5c 100644
--- a/scp/login.php
+++ b/scp/login.php
@@ -23,8 +23,15 @@ $dest = $_SESSION['_staff']['auth']['dest'];
 $msg = $_SESSION['_staff']['auth']['msg'];
 $msg = $msg?$msg:'Authentication Required';
 if($_POST) {
-    //$_SESSION['_staff']=array(); #Uncomment to disable login strikes.
-    if(($user=Staff::login($_POST['userid'], $_POST['passwd'], $errors))){
+    // Lookup support backends for this staff
+    $username = trim($_POST['userid']);
+    $sql = 'SELECT backend FROM '.STAFF_TABLE
+        .' WHERE username='.db_input($username)
+        .' OR email='.db_input($username);
+    $backend = db_result(db_query($sql));
+
+    if ($user = AuthenticationBackend::process($username,
+            $_POST['passwd'], $backend, $errors)) {
         $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php';
         @header("Location: $dest");
         require_once('index.php'); //Just incase header is messed up.
diff --git a/scp/plugins.php b/scp/plugins.php
new file mode 100644
index 0000000000000000000000000000000000000000..7fdcb12aec33022ea9641895c59b3e59bbc1e5cd
--- /dev/null
+++ b/scp/plugins.php
@@ -0,0 +1,47 @@
+<?php
+require('admin.inc.php');
+require_once(INCLUDE_DIR."/class.plugin.php");
+
+if($_REQUEST['id'] && !($plugin=Plugin::lookup($_REQUEST['id'])))
+    $errors['err']='Unknown or invalid plugin ID.';
+
+if($_POST) {
+    switch(strtolower($_POST['do'])) {
+    case 'update':
+        if ($plugin) {
+            $plugin->getConfig()->commit($errors);
+        }
+        break;
+    case 'mass_process':
+        if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) {
+            $errors['err'] = 'You must select at least one plugin';
+        } else {
+            $count = count($_POST['ids']);
+            switch(strtolower($_POST['a'])) {
+            case 'enable':
+                foreach ($_POST['ids'] as $path) {
+                    if ($p = $ost->plugins->getInstance($path)) {
+                        $p->enable();
+                    }
+                }
+                break;
+            case 'disable':
+                foreach ($_POST['ids'] as $id) {
+                    if ($p = Plugin::lookup($id)) {
+                        $p->disable();
+                    }
+                }
+            }
+        }
+    }
+}
+
+$page = 'plugins.inc.php';
+if ($plugin)
+    $page = 'plugin.inc.php';
+
+$nav->setTabActive('manage');
+require(STAFFINC_DIR.'header.inc.php');
+require(STAFFINC_DIR.$page);
+include(STAFFINC_DIR.'footer.inc.php');
+?>
diff --git a/setup/cli/package.php b/setup/cli/package.php
index 15080fd8ab11eda31d6d09f06e0274f48de99a3b..e0b02cdea6725d3a8bd36387ba7e156c0765c32d 100755
--- a/setup/cli/package.php
+++ b/setup/cli/package.php
@@ -115,7 +115,7 @@ mkdir("$stage_path/scripts/");
 package("setup/scripts/*", "scripts/", -1, "*stage");
 
 # Load the heart of the system
-package("include/{,.}*", "upload/include", -1, array('*ost-config.php', '*.sw[a-z]'));
+package("include/{,.}*", "upload/include", -1, array('*ost-config.php', '*.sw[a-z]','plugins/*'));
 
 # Include the installer
 package("setup/*.{php,txt,html}", "upload/setup", -1, array("*scripts","*test","*stage"));
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 8ab5676efd090558868eef5065c14b19882af0ed..94cfbd5e7e0c1f74d89aa85a24482b67f5848f5f 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -435,6 +435,7 @@ CREATE TABLE `%TABLE_PREFIX%staff` (
   `firstname` varchar(32) default NULL,
   `lastname` varchar(32) default NULL,
   `passwd` varchar(128) default NULL,
+  `backend` varchar(32) default NULL,
   `email` varchar(128) default NULL,
   `phone` varchar(24) NOT NULL default '',
   `phone_ext` varchar(6) default NULL,
@@ -676,6 +677,18 @@ CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%page` (
   UNIQUE KEY `name` (`name`)
 ) DEFAULT CHARSET=utf8;
 
+-- Plugins
+DROP TABLE IF EXISTS `%TABLE_PREFIX%plugin`;
+CREATE TABLE `%TABLE_PREFIX%plugin` (
+  `id` int(11) unsigned not null auto_increment,
+  `name` varchar(30) not null,
+  `install_path` varchar(60) not null,
+  `isphar` tinyint(1) not null default 0,
+  `isactive` tinyint(1) not null default 0,
+  `installed` datetime not null,
+  primary key (`id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%user`;
 CREATE TABLE `%TABLE_PREFIX%user` (
   `id` int(10) unsigned NOT NULL auto_increment,