diff --git a/apps/.htaccess b/apps/.htaccess
new file mode 100644
index 0000000000000000000000000000000000000000..184048348cd306d635249908e82c174a4c0e6f60
--- /dev/null
+++ b/apps/.htaccess
@@ -0,0 +1,11 @@
+<IfModule mod_rewrite.c>
+
+RewriteEngine On
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_URI} (.*/apps)
+
+RewriteRule ^(.*)$ %1/dispatcher.php/$1 [L]
+
+</IfModule>
diff --git a/apps/dispatcher.php b/apps/dispatcher.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a3ed9d342ffea060923a11213ecd6317a62939e
--- /dev/null
+++ b/apps/dispatcher.php
@@ -0,0 +1,32 @@
+<?php
+/*********************************************************************
+    dispatcher.php
+
+    Dispatcher for client applications
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+function clientLoginPage($msg='Unauthorized') {
+    Http::response(403,'Must login: '.Format::htmlchars($msg));
+    exit;
+}
+
+require('client.inc.php');
+
+if(!defined('INCLUDE_DIR'))	Http::response(500, 'Server configuration error');
+require_once INCLUDE_DIR.'/class.dispatcher.php';
+
+$dispatcher = patterns('',
+);
+
+Signal::send('ajax.client', $dispatcher);
+print $dispatcher->resolve($ost->get_path_info());
diff --git a/include/class.app.php b/include/class.app.php
new file mode 100644
index 0000000000000000000000000000000000000000..8153c32dfcc25a7ec14fe17e3dd6ea2f9c8ad9a8
--- /dev/null
+++ b/include/class.app.php
@@ -0,0 +1,52 @@
+<?php
+/*********************************************************************
+    class.app.php
+
+    Application registration system
+    Apps, usually to be distributed as plugins, can register themselves
+    using this utility class, and navigation links will be added to the
+    staff and client interfaces.
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2014 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+class Application {
+    private static $client_apps;
+    private static $staff_apps;
+    private static $admin_apps;
+
+    function registerStaffApp($desc, $href, $info=array()) {
+        self::$staff_apps[] = array_merge($info,
+            array('desc'=>$desc, 'href'=>$href));
+    }
+
+    function getStaffApps() {
+        return self::$staff_apps;
+    }
+
+    function registerClientApp($desc, $href, $info=array()) {
+        self::$client_apps[] = array_merge($info,
+            array('desc'=>$desc, 'href'=>$href));
+    }
+
+    function getClientApps() {
+        return self::$client_apps;
+    }
+
+    function registerAdminApp($desc, $href, $info=array()) {
+        self::$admin_apps[] = array_merge($info,
+            array('desc'=>$desc, 'href'=>$href));
+    }
+
+    function getAdminApps() {
+        return self::$admin_apps;
+    }
+}
diff --git a/include/class.nav.php b/include/class.nav.php
index c744e24d141e4ec319ed1eef1bd45345bc33869d..20246ad4ba0df9e0b7895c6d41cef932c04ecc65 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once(INCLUDE_DIR.'class.app.php');
 
 class StaffNav {
     var $tabs=array();
@@ -43,6 +44,10 @@ class StaffNav {
         return (!$this->isAdminPanel());
     }
 
+    function getRegisteredApps() {
+        return Application::getStaffApps();
+    }
+
     function setTabActive($tab, $menu=''){
 
         if($this->tabs[$tab]){
@@ -100,8 +105,8 @@ class StaffNav {
             $this->tabs['users'] = array('desc' => 'Users', 'href' => 'users.php', 'title' => 'User Directory');
             $this->tabs['tickets'] = array('desc'=>'Tickets','href'=>'tickets.php','title'=>'Ticket Queue');
             $this->tabs['kbase'] = array('desc'=>'Knowledgebase','href'=>'kb.php','title'=>'Knowledgebase');
-            // TODO: If at least one app is installed
-            $this->tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications');
+            if (count($this->getRegisteredApps()))
+                $this->tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications');
         }
 
         return $this->tabs;
@@ -151,7 +156,8 @@ class StaffNav {
                     }
                    break;
                 case 'apps':
-                    $subnav[]=array('desc'=>'Equipment', 'href'=>'apps?a=equipment','iconclass'=>'icon-bug');
+                    foreach ($this->getRegisteredApps() as $app)
+                        $subnav[] = $app;
                     break;
             }
             if($subnav)
@@ -178,6 +184,10 @@ class AdminNav extends StaffNav{
         parent::StaffNav($staff, 'admin');
     }
 
+    function getRegisteredApps() {
+        return Application::getAdminApps();
+    }
+
     function getTabs(){
 
 
@@ -189,6 +199,8 @@ class AdminNav extends StaffNav{
             $tabs['manage']=array('desc'=>'Manage','href'=>'helptopics.php','title'=>'Manage Options');
             $tabs['emails']=array('desc'=>'Emails','href'=>'emails.php','title'=>'Email Settings');
             $tabs['staff']=array('desc'=>'Staff','href'=>'staff.php','title'=>'Manage Staff');
+            if (count($this->getRegisteredApps()))
+                $tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications');
             $this->tabs=$tabs;
         }
 
@@ -239,6 +251,10 @@ class AdminNav extends StaffNav{
                     $subnav[]=array('desc'=>'Groups','href'=>'groups.php','iconclass'=>'groups');
                     $subnav[]=array('desc'=>'Departments','href'=>'departments.php','iconclass'=>'departments');
                     break;
+                case 'apps':
+                    foreach ($this->getRegisteredApps() as $app)
+                        $subnav[] = $app;
+                    break;
             }
             if($subnav)
                 $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav;
@@ -263,6 +279,10 @@ class UserNav {
             $this->setActiveNav($active);
     }
 
+    function getRegisteredApps() {
+        return Application::getClientApps();
+    }
+
     function setActiveNav($nav){
 
         if($nav && $this->navs[$nav]){
diff --git a/scp/apps/.htaccess b/scp/apps/.htaccess
new file mode 100644
index 0000000000000000000000000000000000000000..184048348cd306d635249908e82c174a4c0e6f60
--- /dev/null
+++ b/scp/apps/.htaccess
@@ -0,0 +1,11 @@
+<IfModule mod_rewrite.c>
+
+RewriteEngine On
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_URI} (.*/apps)
+
+RewriteRule ^(.*)$ %1/dispatcher.php/$1 [L]
+
+</IfModule>
diff --git a/scp/apps/dispatcher.php b/scp/apps/dispatcher.php
new file mode 100644
index 0000000000000000000000000000000000000000..d1cf05fd81fc6a1b8fcf11e71346338e5eb8b161
--- /dev/null
+++ b/scp/apps/dispatcher.php
@@ -0,0 +1,41 @@
+<?php
+/*********************************************************************
+    dispatcher.php
+
+    Dispatcher for staff applications
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+# Override staffLoginPage() defined in staff.inc.php to return an
+# HTTP/Forbidden status rather than the actual login page.
+# XXX: This should be moved to the AjaxController class
+function staffLoginPage($msg='Unauthorized') {
+    Http::response(403,'Must login: '.Format::htmlchars($msg));
+    exit;
+}
+
+require('staff.inc.php');
+
+//Clean house...don't let the world see your crap.
+ini_set('display_errors','0'); //Disable error display
+ini_set('display_startup_errors','0');
+
+//TODO: disable direct access via the browser? i,e All request must have REFER?
+if(!defined('INCLUDE_DIR'))	Http::response(500, 'Server configuration error');
+
+require_once INCLUDE_DIR.'/class.dispatcher.php';
+$dispatcher = patterns('',
+);
+
+Signal::send('apps.scp', $dispatcher);
+
+# Call the respective function
+print $dispatcher->resolve($ost->get_path_info());
diff --git a/web.config b/web.config
index fd61b6a95f6776289e5b1fd85660f05ceec5174f..ee754443fd21e93c111e2cd962fd019ded871c30 100644
--- a/web.config
+++ b/web.config
@@ -34,6 +34,16 @@
                     </conditions>
                     <action type="Rewrite" url="{R:1}pages/index.php/{R:2}"/>
                 </rule>
+                <rule name="Staff applications" stopProcessing="true">
+                    <match url="^(.*/)?scp/apps/(.*)$" ignoreCase="true"/>
+                    <conditions>
+                        <add input="{REQUEST_FILENAME}" matchType="IsFile"
+                            ignoreCase="false" negate="true" />
+                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory"
+                            ignoreCase="false" negate="true" />
+                    </conditions>
+                    <action type="Rewrite" url="{R:1}scp/apps/dispatcher.php/{R:2}"/>
+                </rule>
             </rules>
         </rewrite>
         <defaultDocument>