diff --git a/qa-include/Q2A/Plugin/AbstractPointModule.php b/qa-include/Q2A/Plugin/AbstractPointModule.php
new file mode 100644
index 000000000..03177bf8c
--- /dev/null
+++ b/qa-include/Q2A/Plugin/AbstractPointModule.php
@@ -0,0 +1,20 @@
+<?php
+
+abstract class Q2A_Plugin_AbstractPointModule implements Q2A_Plugin_IPointModule
+{
+	/**
+	 * Return the amount of points that the module expects to change for the given user id. This default implementation
+	 * just calls the getPointsForUsers() function wrapping the given user id as the only user in the array. It can be
+	 * overridden in subclasses
+	 *
+	 * @param int|string $userId The user id
+	 *
+	 * @return int The number of points that the module should change for the given user
+	 */
+	public function getPointsForUser($userId)
+	{
+		$userIdPoints = $this->getPointsForUsers(array($userId));
+
+		return reset($userIdPoints);
+	}
+}
diff --git a/qa-include/Q2A/Plugin/IPointModule.php b/qa-include/Q2A/Plugin/IPointModule.php
new file mode 100644
index 000000000..8de3f2a39
--- /dev/null
+++ b/qa-include/Q2A/Plugin/IPointModule.php
@@ -0,0 +1,22 @@
+<?php
+
+interface Q2A_Plugin_IPointModule
+{
+	/**
+	 * Return the amount of points that the module expects to change for the given user id
+	 *
+	 * @param int|string $userId The user id
+	 *
+	 * @return int The number of points that the module should change for the given user
+	 */
+	public function getPointsForUser($userId);
+
+	/**
+	 * Return the amount of points that the module expects to change for the given array of user ids
+	 *
+	 * @param array $userIds The array of user ids
+	 *
+	 * @return array The array containing as keys the user ids and as values the amount of points to change
+	 */
+	public function getPointsForUsers($userIds);
+}
diff --git a/qa-include/app/recalc.php b/qa-include/app/recalc.php
index 0fdf6174c..2eaab774c 100644
--- a/qa-include/app/recalc.php
+++ b/qa-include/app/recalc.php
@@ -246,7 +246,29 @@ function qa_recalc_perform_step(&$state)
 
 			if ($recalccount > 0) {
 				$lastuserid = $userids[$recalccount - 1];
-				qa_db_users_recalc_points($next, $lastuserid);
+
+				// Get points from all registered point modules
+
+				$userIdPointsMap = array();
+
+				$modules = qa_load_modules_for_type('point');
+				if (!empty($modules)) {
+					$processUserIds = array_slice($userids, 0, $recalccount);
+
+					foreach ($modules as $module) {
+						$currentPluginUserIdPointsMap = $module->getPointsForUsers($processUserIds);
+						foreach ($currentPluginUserIdPointsMap as $userId => $points) {
+							if (!isset($userIdPointsMap[$userId])) {
+								$userIdPointsMap[$userId] = 0;
+							}
+
+							$userIdPointsMap[$userId] += $points;
+						}
+					}
+				}
+
+				qa_db_users_recalc_points($next, $lastuserid, $userIdPointsMap);
+
 				$done += $recalccount;
 
 			} else {
diff --git a/qa-include/db/points.php b/qa-include/db/points.php
index ec282f14d..6189b0929 100644
--- a/qa-include/db/points.php
+++ b/qa-include/db/points.php
@@ -162,7 +162,7 @@ function qa_db_points_calculations()
  * @param $columns
  * @return mixed
  */
-function qa_db_points_update_ifuser($userid, $columns)
+function qa_db_points_update_ifuser($userid, $columns = null)
 {
 	if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
 
@@ -202,8 +202,15 @@ function qa_db_points_update_ifuser($userid, $columns)
 			$updatepoints .= '+(' . $multiple . '*' . (isset($keycolumns[$field]) ? '@_' : '') . $field . ')';
 		}
 
+		$pluginPoints = 0;
+
+		$modules = qa_load_modules_for_type('point');
+		foreach ($modules as $module) {
+			$pluginPoints += $module->getPointsForUser($userid);
+		}
+
 		$query = 'INSERT INTO ^userpoints (' . $insertfields . 'points) VALUES (' . $insertvalues . $insertpoints . ') ' .
-			'ON DUPLICATE KEY UPDATE ' . $updates . 'points=' . $updatepoints . '+bonus';
+			'ON DUPLICATE KEY UPDATE ' . $updates . 'points=' . $updatepoints . '+ bonus + ' . ((string) $pluginPoints);
 
 		// build like this so that a #, $ or ^ character in the $userid (if external integration) isn't substituted
 		qa_db_query_raw(str_replace('~', "='" . qa_db_escape_string($userid) . "'", qa_db_apply_sub($query, array($userid))));
diff --git a/qa-include/db/recalc.php b/qa-include/db/recalc.php
index 351cd751b..1ac218d27 100644
--- a/qa-include/db/recalc.php
+++ b/qa-include/db/recalc.php
@@ -266,11 +266,13 @@ function qa_db_users_get_for_recalc_points($startuserid, $count)
 
 
 /**
- * Recalculate all userpoints columns for users $firstuserid to $lastuserid in the database
+ * Recalculate all userpoints columns for users $firstuserid to $lastuserid in the database. Send a user id to point map containing
+ * the result of applying all plugins point recalculations
  * @param $firstuserid
  * @param $lastuserid
+ * @param $userIdPointsMap
  */
-function qa_db_users_recalc_points($firstuserid, $lastuserid)
+function qa_db_users_recalc_points($firstuserid, $lastuserid, $userIdPointsMap)
 {
 	require_once QA_INCLUDE_DIR . 'db/points.php';
 
@@ -315,8 +317,34 @@ function qa_db_users_recalc_points($firstuserid, $lastuserid)
 		$updatepoints .= '+(' . ((int)$calculation['multiple']) . '*' . $field . ')';
 	}
 
+	$pluginLeftJoin = '';
+	$pluginExtraPoints = '';
+
+	if (!empty($userIdPointsMap)) {
+		// Start the left join
+
+		$pluginLeftJoin = 'LEFT JOIN (';
+
+		// Build the derived table
+
+		reset($userIdPointsMap);
+		$userId = key($userIdPointsMap);
+		$pluginLeftJoin .= sprintf('SELECT %s userid, %d points ', $userId, $userIdPointsMap[$userId]);
+		unset($userIdPointsMap[$userId]);
+
+		foreach ($userIdPointsMap as $userId => $points) {
+			$pluginLeftJoin .= sprintf('UNION SELECT %s, %d ', $userId, $userIdPointsMap[$userId]);
+		}
+
+		// Finish the left join
+
+		$pluginLeftJoin .= ') plugin_points ON up.userid = plugin_points.userid ';
+
+		$pluginExtraPoints = '+ COALESCE(plugin_points.points, 0) ';
+	}
+
 	qa_db_query_sub(
-		'UPDATE ^userpoints SET points=' . $updatepoints . '+bonus WHERE userid>=# AND userid<=#',
+		'UPDATE ^userpoints up ' . $pluginLeftJoin . 'SET up.points=' . $updatepoints . '+ bonus ' . $pluginExtraPoints . 'WHERE up.userid >= # AND up.userid <= #',
 		$firstuserid, $lastuserid
 	);
 }
diff --git a/qa-include/qa-base.php b/qa-include/qa-base.php
index 01b22fc6a..7b31a0696 100644
--- a/qa-include/qa-base.php
+++ b/qa-include/qa-base.php
@@ -942,7 +942,7 @@ function qa_load_module($type, $name)
 }
 
 /**
- * Return an array of instantiated clases for modules which have defined $method
+ * Return an array of instantiated classes for modules which have defined $method
  * (all modules are loaded but not included in the returned array)
  * @param $method
  * @return array
@@ -966,7 +966,28 @@ function qa_load_all_modules_with($method)
 }
 
 /**
- * Return an array of instantiated clases for modules of $type which have defined $method
+ * Return an array of instantiated classes for modules of type $type
+ * @param $type
+ * @return array
+ */
+function qa_load_modules_for_type($type)
+{
+	$modules = array();
+
+	$trynames = qa_list_modules($type);
+
+	foreach ($trynames as $tryname) {
+		$module = qa_load_module($type, $tryname);
+		if (isset($module)) {
+			$modules[$tryname] = $module;
+		}
+	}
+
+	return $modules;
+}
+
+/**
+ * Return an array of instantiated classes for modules of $type which have defined $method
  * (other modules of that type are also loaded but not included in the returned array)
  * @param $type
  * @param $method