diff --git a/include/ajax.upgrader.php b/include/ajax.upgrader.php
index 8b263ba0ced03b51cacc0341b20f0fc7b8dc01b1..331d23d26bef97b89eddbf2e80170947a794b72f 100644
--- a/include/ajax.upgrader.php
+++ b/include/ajax.upgrader.php
@@ -25,14 +25,7 @@ class UpgraderAjaxAPI extends AjaxController {
         if(!$thisstaff or !$thisstaff->isAdmin() or !$ost)
             Http::response(403, 'Access Denied');
 
-        $upgrader = new Upgrader($ost->getDBSignature(), TABLE_PREFIX, SQL_DIR);
-
-        //Just report the next action on the first call.
-        if(!$_SESSION['ost_upgrader'] || !$_SESSION['ost_upgrader'][$upgrader->getShash()]['progress']) {
-            $_SESSION['ost_upgrader'][$upgrader->getShash()]['progress'] = $upgrader->getNextAction();
-            Http::response(200, $upgrader->getNextAction());
-            exit;
-        }
+        $upgrader = new Upgrader(TABLE_PREFIX, UPGRADE_DIR.'streams/');
 
         if($upgrader->isAborted()) {
             Http::response(416, "We have a problem ... wait a sec.");
@@ -51,7 +44,7 @@ class UpgraderAjaxAPI extends AjaxController {
                     Http::response(200, "Upgraded to $version ... post-upgrade checks!");
                     exit;
                 }
-            } else { 
+            } else {
                 //Abort: Upgrade pending but NOT upgradable - invalid or wrong hash.
                 $upgrader->abort(sprintf('Upgrade Failed: Invalid or wrong hash [%s]',$ost->getDBSignature()));
             }
diff --git a/include/class.migrater.php b/include/class.migrater.php
index ba26d091e31defa230ab4da3f7420d3e044dbaf2..f3d6c775dc50240deaf460b7586d961770422aed 100644
--- a/include/class.migrater.php
+++ b/include/class.migrater.php
@@ -70,6 +70,36 @@ class DatabaseMigrater {
 
         return array_filter($patches);
     }
+
+    /**
+     * Reads update stream information from UPGRADE_DIR/<streams>/streams.cfg.
+     * Each declared stream folder should contain a file under the name of the
+     * stream with an 'sig' extension. The file will be the hash of the
+     * signature of the tip of the stream. A corresponding config variable
+     * 'schema_signature' should exist in the namespace of the stream itself.
+     * If the hash file doesn't match the schema_signature on record, then an
+     * update is triggered and the patches in the stream folder are used to
+     * upgrade the database.
+	 */
+	/* static */
+    function getUpgradeStreams($basedir) {
+		static $streams = array();
+        if ($streams) return $streams;
+
+        // TODO: Make the hash algo configurable in the streams
+        //       configuration ( core : md5 )
+        $config = @file_get_contents($basedir.'/streams.cfg');
+        if (!$config) $config = 'core';
+        foreach (explode("\n", $config) as $line) {
+            $line = trim(preg_replace('/#.*$/', '', $line));
+            if (!$line)
+                continue;
+            else if (file_exists($basedir."$line.sig") && is_dir($basedir.$line))
+                $streams[$line] =
+                    trim(file_get_contents($basedir."$line.sig"));
+        }
+        return $streams;
+    }
 }
 
 
diff --git a/include/class.osticket.php b/include/class.osticket.php
index 1a1945218d05ba79ba906d41ddaf8eb4ce942b2a..671bdca4ba7f34ab99698dba009e79a73e06e61c 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -20,6 +20,7 @@
 
 require_once(INCLUDE_DIR.'class.config.php'); //Config helper
 require_once(INCLUDE_DIR.'class.csrf.php'); //CSRF token class.
+require_once(INCLUDE_DIR.'class.migrater.php');
 
 define('LOG_WARN',LOG_WARNING);
 
@@ -60,8 +61,11 @@ class osTicket {
     }
 
     function isUpgradePending() {
-        return strcasecmp(SCHEMA_SIGNATURE,
-                $this->getConfig()->getSchemaSignature());
+		foreach (DatabaseMigrater::getUpgradeStreams(UPGRADE_DIR.'streams/') as $stream=>$hash)
+			if (strcasecmp($hash,
+					$this->getConfig()->getSchemaSignature($stream)))
+				return true;
+		return false;
     }
 
     function getSession() {
@@ -72,8 +76,8 @@ class osTicket {
         return $this->config;
     }
 
-    function getDBSignature() {
-        return $this->getConfig()->getSchemaSignature();
+    function getDBSignature($namespace='core') {
+        return $this->getConfig()->getSchemaSignature($namespace);
     }
 
     function getVersion() {
diff --git a/include/class.upgrader.php b/include/class.upgrader.php
index 8f6fea22342aacb2c2a23da4547f7c9d935a7e47..e17b0a4b2626bb21cc3e7b2103c48b5a3a9c4568 100644
--- a/include/class.upgrader.php
+++ b/include/class.upgrader.php
@@ -17,7 +17,130 @@
 require_once INCLUDE_DIR.'class.setup.php';
 require_once INCLUDE_DIR.'class.migrater.php';
 
-class Upgrader extends SetupWizard {
+class Upgrader {
+    function Upgrader($prefix, $basedir) {
+        global $ost;
+
+        $this->streams = array();
+        foreach (DatabaseMigrater::getUpgradeStreams($basedir) as $stream=>$hash) {
+            $signature = $ost->getConfig()->getSchemaSignature($stream);
+            $this->streams[$stream] = new StreamUpgrader($signature, $hash, $stream,
+                $prefix, $basedir.$stream.'/', $this);
+        }
+
+        //Init persistent state of upgrade.
+        $this->state = &$_SESSION['ost_upgrader']['state'];
+
+        $this->mode = &$_SESSION['ost_upgrader']['mode'];
+
+        $this->current = &$_SESSION['ost_upgrader']['stream'];
+        if (!$this->current || $this->getCurrentStream()->isFinished()) {
+            $streams = array_keys($this->streams);
+            do {
+                $this->current = array_shift($streams);
+            } while ($this->current && $this->getCurrentStream()->isFinished());
+        }
+    }
+
+    function getCurrentStream() {
+        return $this->streams[$this->current];
+    }
+
+    function isUpgradable() {
+        if ($this->isAborted())
+            return false;
+
+        foreach ($this->streams as $s)
+            if (!$s->isUpgradable())
+                return false;
+
+        return true;
+    }
+
+    function isAborted() {
+        return !strcasecmp($this->getState(), 'aborted');
+    }
+
+    function getState() {
+        return $this->state;
+    }
+
+    function setState($state) {
+        $this->state = $state;
+    }
+
+    function getMode() {
+        return $this->mode;
+    }
+
+    function setMode($mode) {
+        $this->mode = $mode;
+    }
+
+    function upgrade() {
+        if (!$this->current)
+            return true;
+
+        return $this->getCurrentStream()->upgrade();
+    }
+
+    function check_prereq() {
+        if ($this->getCurrentStream())
+            return $this->getCurrentStream()->check_prereq();
+    }
+    function check_php() {
+        if ($this->getCurrentStream())
+            return $this->getCurrentStream()->check_php();
+    }
+    function check_mysql() {
+        if ($this->getCurrentStream())
+            return $this->getCurrentStream()->check_mysql();
+    }
+
+    function getNumPendingTasks() {
+        if ($this->getCurrentStream())
+            return $this->getCurrentStream()->getNumPendingTasks();
+    }
+
+    function doTasks() {
+        if ($this->getNumPendingTasks())
+            return $this->getCurrentStream()->doTasks();
+    }
+
+    function getErrors() {
+        return $this->getCurrentStream()->getError();
+    }
+
+    function getNextAction() {
+        if ($this->getCurrentStream())
+            return $this->getCurrentStream()->getNextAction();
+    }
+
+    function getNextVersion() {
+        return $this->getCurrentStream()->getNextVersion();
+    }
+
+    function getSchemaSignature() {
+        if ($this->getCurrentStream())
+            return $this->getCurrentStream()->getSchemaSignature();
+    }
+
+    function getSHash() {
+        if ($this->getCurrentStream())
+            return $this->getCurrentStream()->getSHash();
+    }
+}
+
+/**
+ * Updates a single database stream. In the classical sense, osTicket only
+ * maintained a single database update stream. In that model, this
+ * represents upgrading that single stream. In multi-stream mode,
+ * customizations and plugins are supported to have their own respective
+ * database update streams. The Upgrader class is used to coordinate updates
+ * for all the streams, whereas the work to upgrade each stream is done in
+ * this class
+ */
+class StreamUpgrader extends SetupWizard {
 
     var $prefix;
     var $sqldir;
@@ -26,22 +149,32 @@ class Upgrader extends SetupWizard {
     var $state;
     var $mode;
 
-    function Upgrader($signature, $prefix, $sqldir) {
-
-        $this->signature = $signature;
+    /**
+     * Parameters:
+     * schema_signature - (string<hash-hex>) Current database-reflected (via
+     *      config table) version of the stream
+     * target - (stream<hash-hex>) Current stream tip, as reflected by
+     *      streams/<stream>.sig
+     * stream - (string) Name of the stream (folder)
+     * prefix - (string) Database table prefix
+     * sqldir - (string<path>) Path of sql patches
+     * upgrader - (Upgrader) Parent coordinator of parallel stream updates
+     */
+    function StreamUpgrader($schema_signature, $target, $stream, $prefix, $sqldir, $upgrader) {
+
+        $this->signature = $schema_signature;
+        $this->target = $target;
         $this->prefix = $prefix;
         $this->sqldir = $sqldir;
         $this->errors = array();
         $this->mode = 'ajax'; //
+        $this->upgrader = $upgrader;
+        $this->name = $stream;
 
         //Disable time limit if - safe mode is set.
         if(!ini_get('safe_mode'))
             set_time_limit(0);
 
-        //Init persistent state of upgrade.
-        $this->state = &$_SESSION['ost_upgrader']['state'];
-
-        $this->mode = &$_SESSION['ost_upgrader']['mode'];
 
         //Init the task Manager.
         if(!isset($_SESSION['ost_upgrader'][$this->getShash()]))
@@ -57,9 +190,10 @@ class Upgrader extends SetupWizard {
     function onError($error) {
         global $ost, $thisstaff;
 
-        $ost->logError('Upgrader Error', $error);
+        $subject = '['.$this->name.']: Upgrader Error';
+        $ost->logError($subject, $error);
         $this->setError($error);
-        $this->setState('aborted');
+        $this->upgrader->setState('aborted');
 
         //Alert staff upgrading the system - if the email is not same as admin's
         // admin gets alerted on error log (above)
@@ -70,7 +204,6 @@ class Upgrader extends SetupWizard {
         if(!($email=$ost->getConfig()->getAlertEmail()))
             $email=$ost->getConfig()->getDefaultEmail(); //will take the default email.
 
-        $subject = 'Upgrader Error';
         if($email) {
             $email->sendAlert($thisstaff->getEmail(), $subject, $error);
         } else {//no luck - try the system mail.
@@ -80,11 +213,7 @@ class Upgrader extends SetupWizard {
     }
 
     function isUpgradable() {
-        return (!$this->isAborted() && $this->getNextPatch());
-    }
-
-    function isAborted() {
-        return !strcasecmp($this->getState(), 'aborted');
+        return $this->getNextPatch();
     }
 
     function getSchemaSignature() {
@@ -103,25 +232,9 @@ class Upgrader extends SetupWizard {
         return $this->sqldir;
     }
 
-    function getState() {
-        return $this->state;
-    }
-
-    function setState($state) {
-        $this->state = $state;
-    }
-
-    function getMode() {
-        return $this->mode;
-    }
-
-    function setMode($mode) {
-        $this->mode = $mode;
-    }
-
     function getMigrater() {
         if(!$this->migrater)
-            $this->migrater = new DatabaseMigrater($this->signature, SCHEMA_SIGNATURE, $this->sqldir);
+            $this->migrater = new DatabaseMigrater($this->signature, $this->target, $this->sqldir);
 
         return  $this->migrater;
     }
@@ -146,6 +259,12 @@ class Upgrader extends SetupWizard {
         return $info['version'];
     }
 
+    function isFinished() {
+        # TODO: 1. Check if current and target hashes match,
+        #       2. Any pending tasks
+        return !($this->getNextPatch() || $this->getPendingTasks());
+    }
+
     function readPatchInfo($patch) {
         $info = array();
         if (preg_match(':/\*\*(.*)\*/:s', file_get_contents($patch), $matches)) {
@@ -170,7 +289,7 @@ class Upgrader extends SetupWizard {
             $action = "Upgrade to $nextversion";
         }
 
-        return $action;
+        return '['.$this->name.'] '.$action;
     }
 
     function getNumPendingTasks() {
diff --git a/setup/inc/sql/osTicket-mysql.sql.md5 b/include/upgrader/streams/core.sig
similarity index 100%
rename from setup/inc/sql/osTicket-mysql.sql.md5
rename to include/upgrader/streams/core.sig
diff --git a/include/upgrader/sql/00ff231f-9f3b454c.patch.sql b/include/upgrader/streams/core/00ff231f-9f3b454c.patch.sql
similarity index 100%
rename from include/upgrader/sql/00ff231f-9f3b454c.patch.sql
rename to include/upgrader/streams/core/00ff231f-9f3b454c.patch.sql
diff --git a/include/upgrader/sql/02decaa2-60fcbee1.patch.sql b/include/upgrader/streams/core/02decaa2-60fcbee1.patch.sql
similarity index 100%
rename from include/upgrader/sql/02decaa2-60fcbee1.patch.sql
rename to include/upgrader/streams/core/02decaa2-60fcbee1.patch.sql
diff --git a/include/upgrader/sql/15719536-dd0022fb.patch.sql b/include/upgrader/streams/core/15719536-dd0022fb.patch.sql
similarity index 100%
rename from include/upgrader/sql/15719536-dd0022fb.patch.sql
rename to include/upgrader/streams/core/15719536-dd0022fb.patch.sql
diff --git a/include/upgrader/sql/15af7cd3-98ae1ed2.patch.sql b/include/upgrader/streams/core/15af7cd3-98ae1ed2.patch.sql
similarity index 100%
rename from include/upgrader/sql/15af7cd3-98ae1ed2.patch.sql
rename to include/upgrader/streams/core/15af7cd3-98ae1ed2.patch.sql
diff --git a/include/upgrader/sql/15b30765-dd0022fb.cleanup.sql b/include/upgrader/streams/core/15b30765-dd0022fb.cleanup.sql
similarity index 100%
rename from include/upgrader/sql/15b30765-dd0022fb.cleanup.sql
rename to include/upgrader/streams/core/15b30765-dd0022fb.cleanup.sql
diff --git a/include/upgrader/sql/15b30765-dd0022fb.patch.sql b/include/upgrader/streams/core/15b30765-dd0022fb.patch.sql
similarity index 100%
rename from include/upgrader/sql/15b30765-dd0022fb.patch.sql
rename to include/upgrader/streams/core/15b30765-dd0022fb.patch.sql
diff --git a/include/upgrader/sql/1da1bcba-15b30765.patch.sql b/include/upgrader/streams/core/1da1bcba-15b30765.patch.sql
similarity index 100%
rename from include/upgrader/sql/1da1bcba-15b30765.patch.sql
rename to include/upgrader/streams/core/1da1bcba-15b30765.patch.sql
diff --git a/include/upgrader/sql/2e20a0eb-98ae1ed2.patch.sql b/include/upgrader/streams/core/2e20a0eb-98ae1ed2.patch.sql
similarity index 100%
rename from include/upgrader/sql/2e20a0eb-98ae1ed2.patch.sql
rename to include/upgrader/streams/core/2e20a0eb-98ae1ed2.patch.sql
diff --git a/include/upgrader/sql/2e7531a2-d0e37dca.patch.sql b/include/upgrader/streams/core/2e7531a2-d0e37dca.patch.sql
similarity index 100%
rename from include/upgrader/sql/2e7531a2-d0e37dca.patch.sql
rename to include/upgrader/streams/core/2e7531a2-d0e37dca.patch.sql
diff --git a/include/upgrader/sql/32de1766-852ca89e.patch.sql b/include/upgrader/streams/core/32de1766-852ca89e.patch.sql
similarity index 100%
rename from include/upgrader/sql/32de1766-852ca89e.patch.sql
rename to include/upgrader/streams/core/32de1766-852ca89e.patch.sql
diff --git a/include/upgrader/sql/435c62c3-2e7531a2.cleanup.sql b/include/upgrader/streams/core/435c62c3-2e7531a2.cleanup.sql
similarity index 100%
rename from include/upgrader/sql/435c62c3-2e7531a2.cleanup.sql
rename to include/upgrader/streams/core/435c62c3-2e7531a2.cleanup.sql
diff --git a/include/upgrader/sql/435c62c3-2e7531a2.patch.sql b/include/upgrader/streams/core/435c62c3-2e7531a2.patch.sql
similarity index 100%
rename from include/upgrader/sql/435c62c3-2e7531a2.patch.sql
rename to include/upgrader/streams/core/435c62c3-2e7531a2.patch.sql
diff --git a/include/upgrader/sql/49478749-c2d2fabf.patch.sql b/include/upgrader/streams/core/49478749-c2d2fabf.patch.sql
similarity index 100%
rename from include/upgrader/sql/49478749-c2d2fabf.patch.sql
rename to include/upgrader/streams/core/49478749-c2d2fabf.patch.sql
diff --git a/include/upgrader/sql/522e5b78-02decaa2.patch.sql b/include/upgrader/streams/core/522e5b78-02decaa2.patch.sql
similarity index 100%
rename from include/upgrader/sql/522e5b78-02decaa2.patch.sql
rename to include/upgrader/streams/core/522e5b78-02decaa2.patch.sql
diff --git a/include/upgrader/sql/60fcbee1-f8856d56.patch.sql b/include/upgrader/streams/core/60fcbee1-f8856d56.patch.sql
similarity index 100%
rename from include/upgrader/sql/60fcbee1-f8856d56.patch.sql
rename to include/upgrader/streams/core/60fcbee1-f8856d56.patch.sql
diff --git a/include/upgrader/sql/7be60a84-522e5b78.patch.sql b/include/upgrader/streams/core/7be60a84-522e5b78.patch.sql
similarity index 100%
rename from include/upgrader/sql/7be60a84-522e5b78.patch.sql
rename to include/upgrader/streams/core/7be60a84-522e5b78.patch.sql
diff --git a/include/upgrader/sql/852ca89e-740428f9.patch.sql b/include/upgrader/streams/core/852ca89e-740428f9.patch.sql
similarity index 100%
rename from include/upgrader/sql/852ca89e-740428f9.patch.sql
rename to include/upgrader/streams/core/852ca89e-740428f9.patch.sql
diff --git a/include/upgrader/sql/98ae1ed2-e342f869.cleanup.sql b/include/upgrader/streams/core/98ae1ed2-e342f869.cleanup.sql
similarity index 100%
rename from include/upgrader/sql/98ae1ed2-e342f869.cleanup.sql
rename to include/upgrader/streams/core/98ae1ed2-e342f869.cleanup.sql
diff --git a/include/upgrader/sql/98ae1ed2-e342f869.patch.sql b/include/upgrader/streams/core/98ae1ed2-e342f869.patch.sql
similarity index 100%
rename from include/upgrader/sql/98ae1ed2-e342f869.patch.sql
rename to include/upgrader/streams/core/98ae1ed2-e342f869.patch.sql
diff --git a/include/upgrader/sql/9f3b454c-c0fd16f4.patch.sql b/include/upgrader/streams/core/9f3b454c-c0fd16f4.patch.sql
similarity index 100%
rename from include/upgrader/sql/9f3b454c-c0fd16f4.patch.sql
rename to include/upgrader/streams/core/9f3b454c-c0fd16f4.patch.sql
diff --git a/include/upgrader/sql/a67ba35e-98ae1ed2.patch.sql b/include/upgrader/streams/core/a67ba35e-98ae1ed2.patch.sql
similarity index 100%
rename from include/upgrader/sql/a67ba35e-98ae1ed2.patch.sql
rename to include/upgrader/streams/core/a67ba35e-98ae1ed2.patch.sql
diff --git a/include/upgrader/sql/aa4664af-b19dc97d.patch.sql b/include/upgrader/streams/core/aa4664af-b19dc97d.patch.sql
similarity index 100%
rename from include/upgrader/sql/aa4664af-b19dc97d.patch.sql
rename to include/upgrader/streams/core/aa4664af-b19dc97d.patch.sql
diff --git a/include/upgrader/sql/abe9c0cb-bbb021fb.patch.sql b/include/upgrader/streams/core/abe9c0cb-bbb021fb.patch.sql
similarity index 100%
rename from include/upgrader/sql/abe9c0cb-bbb021fb.patch.sql
rename to include/upgrader/streams/core/abe9c0cb-bbb021fb.patch.sql
diff --git a/include/upgrader/sql/aee589ab-98ae1ed2.patch.sql b/include/upgrader/streams/core/aee589ab-98ae1ed2.patch.sql
similarity index 100%
rename from include/upgrader/sql/aee589ab-98ae1ed2.patch.sql
rename to include/upgrader/streams/core/aee589ab-98ae1ed2.patch.sql
diff --git a/include/upgrader/sql/b19dc97d-435c62c3.patch.sql b/include/upgrader/streams/core/b19dc97d-435c62c3.patch.sql
similarity index 100%
rename from include/upgrader/sql/b19dc97d-435c62c3.patch.sql
rename to include/upgrader/streams/core/b19dc97d-435c62c3.patch.sql
diff --git a/include/upgrader/sql/bbb021fb-49478749.patch.sql b/include/upgrader/streams/core/bbb021fb-49478749.patch.sql
similarity index 100%
rename from include/upgrader/sql/bbb021fb-49478749.patch.sql
rename to include/upgrader/streams/core/bbb021fb-49478749.patch.sql
diff --git a/include/upgrader/sql/c00511c7-7be60a84.cleanup.sql b/include/upgrader/streams/core/c00511c7-7be60a84.cleanup.sql
similarity index 100%
rename from include/upgrader/sql/c00511c7-7be60a84.cleanup.sql
rename to include/upgrader/streams/core/c00511c7-7be60a84.cleanup.sql
diff --git a/include/upgrader/sql/c00511c7-7be60a84.patch.sql b/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql
similarity index 100%
rename from include/upgrader/sql/c00511c7-7be60a84.patch.sql
rename to include/upgrader/streams/core/c00511c7-7be60a84.patch.sql
diff --git a/include/upgrader/sql/c0fd16f4-d959a00e.patch.sql b/include/upgrader/streams/core/c0fd16f4-d959a00e.patch.sql
similarity index 100%
rename from include/upgrader/sql/c0fd16f4-d959a00e.patch.sql
rename to include/upgrader/streams/core/c0fd16f4-d959a00e.patch.sql
diff --git a/include/upgrader/sql/c2d2fabf-aa4664af.patch.sql b/include/upgrader/streams/core/c2d2fabf-aa4664af.patch.sql
similarity index 100%
rename from include/upgrader/sql/c2d2fabf-aa4664af.patch.sql
rename to include/upgrader/streams/core/c2d2fabf-aa4664af.patch.sql
diff --git a/include/upgrader/sql/d0e37dca-1da1bcba.patch.sql b/include/upgrader/streams/core/d0e37dca-1da1bcba.patch.sql
similarity index 100%
rename from include/upgrader/sql/d0e37dca-1da1bcba.patch.sql
rename to include/upgrader/streams/core/d0e37dca-1da1bcba.patch.sql
diff --git a/include/upgrader/sql/d959a00e-32de1766.patch.sql b/include/upgrader/streams/core/d959a00e-32de1766.patch.sql
similarity index 100%
rename from include/upgrader/sql/d959a00e-32de1766.patch.sql
rename to include/upgrader/streams/core/d959a00e-32de1766.patch.sql
diff --git a/include/upgrader/sql/dd0022fb-f4da0c9b.patch.sql b/include/upgrader/streams/core/dd0022fb-f4da0c9b.patch.sql
similarity index 100%
rename from include/upgrader/sql/dd0022fb-f4da0c9b.patch.sql
rename to include/upgrader/streams/core/dd0022fb-f4da0c9b.patch.sql
diff --git a/include/upgrader/sql/e342f869-c00511c7.patch.sql b/include/upgrader/streams/core/e342f869-c00511c7.patch.sql
similarity index 100%
rename from include/upgrader/sql/e342f869-c00511c7.patch.sql
rename to include/upgrader/streams/core/e342f869-c00511c7.patch.sql
diff --git a/include/upgrader/sql/f4da0c9b-00ff231f.patch.sql b/include/upgrader/streams/core/f4da0c9b-00ff231f.patch.sql
similarity index 100%
rename from include/upgrader/sql/f4da0c9b-00ff231f.patch.sql
rename to include/upgrader/streams/core/f4da0c9b-00ff231f.patch.sql
diff --git a/include/upgrader/sql/f8856d56-abe9c0cb.patch.sql b/include/upgrader/streams/core/f8856d56-abe9c0cb.patch.sql
similarity index 100%
rename from include/upgrader/sql/f8856d56-abe9c0cb.patch.sql
rename to include/upgrader/streams/core/f8856d56-abe9c0cb.patch.sql
diff --git a/main.inc.php b/main.inc.php
index 6f7c932045fa46099de9d707b69dc105bdfecfba..e5d19c0a417ce78d71ccfdb5b3a1772bff9ec042 100644
--- a/main.inc.php
+++ b/main.inc.php
@@ -70,13 +70,11 @@
     define('SETUP_DIR',INCLUDE_DIR.'setup/');
 
     define('UPGRADE_DIR', INCLUDE_DIR.'upgrader/');
-    define('SQL_DIR', UPGRADE_DIR.'sql/');
 
     /*############## Do NOT monkey with anything else beyond this point UNLESS you really know what you are doing ##############*/
 
     #Current version && schema signature (Changes from version to version)
     define('THIS_VERSION','1.7.0+'); //Shown on admin panel
-    define('SCHEMA_SIGNATURE', '740428f9986da6ad85f88ec841b57bfe'); //MD5 signature of the db schema. (used to trigger upgrades)
     #load config info
     $configfile='';
     if(file_exists(ROOT_DIR.'ostconfig.php')) //Old installs prior to v 1.6 RC5
diff --git a/scp/upgrade.php b/scp/upgrade.php
index c66230a22288d131b3666a1a789df065397880f1..4be64dc3b851e490e40a97911fb018bea52215d3 100644
--- a/scp/upgrade.php
+++ b/scp/upgrade.php
@@ -17,7 +17,7 @@ require_once 'admin.inc.php';
 require_once INCLUDE_DIR.'class.upgrader.php';
 
 //$_SESSION['ost_upgrader']=null;
-$upgrader = new Upgrader($cfg->getSchemaSignature(), TABLE_PREFIX, SQL_DIR);
+$upgrader = new Upgrader(TABLE_PREFIX, UPGRADE_DIR.'streams/');
 $errors=array();
 if($_POST && $_POST['s'] && !$upgrader->isAborted()) {
     switch(strtolower($_POST['s'])) {
diff --git a/setup/doc/streams.md b/setup/doc/streams.md
new file mode 100644
index 0000000000000000000000000000000000000000..823e7ee5c3921f7c3b226161884dc66312b4b04c
--- /dev/null
+++ b/setup/doc/streams.md
@@ -0,0 +1,114 @@
+osTicket Database Migration
+===========================
+
+Database Upgrade Streams
+------------------------
+Database upgrade streams are registered in the
+`INCLUDE_DIR/upgrader/streams/streams.cfg`. This file contains the names of
+the upgrade streams in the order the streams should be applied in. The stock
+osTicket install does not ship with a `streams.cfg`, so that updates to the
+source code base will not destroy your `streams.cfg` file. If the file does
+not exist, only the osTicket `core` stream will be updated.
+
+Streams folders
+---------------
+Stream folders are created in the `INCLUDE_DIR/upgrader/streams` folder. The
+name of the stream will be the name of the folder. For instance, the core
+osTicket stream exists in the `core` folder, and so is called `core`. Each
+stream folder should also have an accompanying hash file which gives the md5
+hash of the tip of the stream. How you generate the hashes is up to you. For
+the core stream, we use the md5 hash of the install SQL file. Changes made
+to the main install file result in changes to the md5 hash of the file.
+Then, the update file placed in the upgrade stream will have the changes
+between to two hashes.
+
+Upgrade Streams
+---------------
+Upgrade patches are used to migrate the database from snapshot to snapshot.
+The system will start upgrading by consulting the `schema_signature`
+configuration setting in the `config` table in the namespace of your stream
+name. It will look for an upgrade patch file that starts with the first
+eight characters of the current signature.
+
+Each upgrade in a stream should set the `schema_signature` configuration
+option in the `config` when completed. The plan is to make this automatic,
+but currently, it is still a manual process. Whatever the hash of then last
+executed patch is, it should be reflected in the config table at the
+completion of the upgrade patch.
+
+The migration process will continue until the hash reflected in the
+`schema_signature` setting is the same as the value given in the stream
+signature file. If no patch files are given to migrate from the current
+`schema_signature` value to the value listed in the md5 file, then the
+migrater will fail and complain that the system is not upgradeable.
+
+Patch Files
+-----------
+Patch files should live in your stream folder and should have the name of
+
+    12345678-00abcdef.patch.sql
+
+and should contain only SQL text. Double-dash comments are only supported if
+started at the beginning of a line. For instance, do not write then inline
+as part of a long running SQL statement. The filename format is the first
+eight chars of the starting and ending database `schema_signature` values.
+
+Your patch process can be separated into two parts if you like. A cleanup file can be used to cleanup database objects after the completion of the patch process. Cleanup files must have the name of
+
+    12345678-00abcdef.cleanup.sql
+
+Where the starting and ending hashes are listed with a hyphen in between.
+The idea is that PHP code can be run between the two SQL patch files.
+//Currently, support for this is hardcoded, but will hopefully be redesigned
+to include a `patch.php` file at some point in the future.//
+
+If you want to use numeric serial numbers, make sure the first eight digits
+change for every upgrade. For instance, use 00000001-00000002. Technically,
+there is no current requirement for the hash file to be an actual md5 or even
+have 32 hex chars in it.
+
+Patch files should contain a header with some common information about the
+patch. The header should be formatted similar to
+
+    /* osTicket database migration patch
+     *
+     * @version 0.0.0
+     * @signature 0123456789abcdef0000000000000000
+     *
+     * Details about the migration patch are listed here
+     */
+
+Eventually the `@signature` line will be automatically inspected and forced
+into the config table for the `schema_signature` setting in the namespace of
+your stream at the completion of the patch process. Please add it to your
+patches to keep them future-minded. The `@version` is a string that will be
+shown to clients during the upgrade process indicating the version numbers
+as each patch is applied.
+
+Customizing osTicket
+====================
+osTicket now supports database customizations using a separation technique
+called *streams*. Separating the database upgrade path into streams allows
+the upstream osTicket database upgrades to be kept separate from your own,
+custom upgrade streams. Streams are registered in `streams.cfg` located in
+the `UPGRADE_DIR/streams`.
+
+Example streams.cfg
+-------------------
+    # Write the names of the stream folders to be enabled in this file.
+    # The order is significant. The upgrade process will run updates for the
+    # respective streams in the order they are listed in this file.
+
+    core            # The upstream osTicket upgrade stream
+
+    # Add custom upgrade streams here, which will be applied in the order
+    # listed in this file
+
+Database Customization Rules
+---------------------------
+1. Leave the upstream osTicket tables unchanged. If your customization makes
+   changes to the main osTicket tables, you will likely get merge conflicts
+   when the *core* stream is updated. If you need to add columns to an
+   upstream table, add another table with the extra columns and link the
+   data to the upstream table using the primary key of the upstream table.
+   This will keep your data model separate from the upstream data model.
diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php
index 40d9137dc6ea723ea5c4cd9d357b4523bc234a43..5562f33ab120196550aff8690984b4d610d27309 100644
--- a/setup/inc/class.installer.php
+++ b/setup/inc/class.installer.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR.'class.migrater.php';
 require_once INCLUDE_DIR.'class.setup.php';
 
 class Installer extends SetupWizard {
@@ -109,24 +110,34 @@ class Installer extends SetupWizard {
         define('ADMIN_EMAIL',$vars['admin_email']); //Needed to report SQL errors during install.
         define('PREFIX',$vars['prefix']); //Table prefix
 
-        $schemaFile =INC_DIR.'sql/osTicket-mysql.sql'; //DB dump.
-        $debug = true; //XXX:Change it to true to show SQL errors.
+        $debug = true; // Change it to false to squelch SQL errors.
 
         //Last minute checks.
-        if(!file_exists($schemaFile) || !($fp = fopen($schemaFile, 'rb')))
-            $this->errors['err']='Internal Error - please make sure your download is the latest (#1)';
-        elseif(
-                !($signature=trim(file_get_contents("$schemaFile.md5")))
-                || !($hash=md5(fread($fp, filesize($schemaFile))))
-                || strcasecmp($signature, $hash))
-            $this->errors['err']='Unknown or invalid schema signature ('
-                .$signature.' .. '.$hash.')';
-        elseif(!file_exists($this->getConfigFile()) || !($configFile=file_get_contents($this->getConfigFile())))
+        if(!file_exists($this->getConfigFile()) || !($configFile=file_get_contents($this->getConfigFile())))
             $this->errors['err']='Unable to read config file. Permission denied! (#2)';
         elseif(!($fp = @fopen($this->getConfigFile(),'r+')))
             $this->errors['err']='Unable to open config file for writing. Permission denied! (#3)';
-        elseif(!$this->load_sql_file($schemaFile,$vars['prefix'], true, $debug))
-            $this->errors['err']='Error parsing SQL schema! Get help from developers (#4)';
+
+        else {
+            foreach (DatabaseMigrater::getUpgradeStreams(INCLUDE_DIR.'upgrader/streams/')
+                    as $stream=>$signature) {
+                $schemaFile = INC_DIR."streams/$stream/install-mysql.sql";
+                if (!file_exists($schemaFile) || !($fp2 = fopen($schemaFile, 'rb')))
+                    $this->errors['err'] = $stream
+                        . ': Internal Error - please make sure your download is the latest (#1)';
+                elseif (
+                        // TODO: Make the hash algo configurable in the streams
+                        //       configuration ( core : md5 )
+                        !($hash = md5(fread($fp2, filesize($schemaFile))))
+                        || strcasecmp($signature, $hash))
+                    $this->errors['err'] = $stream
+                        .': Unknown or invalid schema signature ('
+                        .$signature.' .. '.$hash.')';
+                elseif (!$this->load_sql_file($schemaFile, $vars['prefix'], true, $debug))
+                    $this->errors['err'] = $stream
+                        .': Error parsing SQL schema! Get help from developers (#4)';
+            }
+        }
 
         $sql='SELECT `id` FROM '.PREFIX.'sla ORDER BY `id` LIMIT 1';
         $sla_id_1 = db_result(db_query($sql, false), 0);
@@ -173,6 +184,7 @@ class Installer extends SetupWizard {
 
             //Create config settings---default settings!
             //XXX: rename ostversion  helpdesk_* ??
+            // XXX: Some of this can go to the core install file
 			$defaults = array('isonline'=>'0', 'default_email_id'=>$support_email_id,
 				'alert_email_id'=>$alert_email_id, 'default_dept_id'=>$dept_id_1, 'default_sla_id'=>$sla_id_1,
 				'default_timezone_id'=>$eastern_timezone, 'default_template_id'=>$template_id_1,
diff --git a/setup/inc/sql/osTicket-mysql.sql b/setup/inc/streams/core/install-mysql.sql
similarity index 100%
rename from setup/inc/sql/osTicket-mysql.sql
rename to setup/inc/streams/core/install-mysql.sql