diff --git a/flag_abuse.install b/flag_abuse.install
new file mode 100644
index 0000000..a6f578a
--- /dev/null
+++ b/flag_abuse.install
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Install and Update hooks for Flag Abuse module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function flag_abuse_schema() {
+  $schema['flag_abuse_whitelist'] = array(
+    'description' => 'Content that has been whitelisted by a moderator.',
+    'fields' => array(
+      'fid' => array(
+        'description' => 'The unqiue flag ID this content has been flagged with, from {flags}.',
+        'type' => 'int',
+        'size' => 'small',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'content_type' => array(
+        'description' => 'The flag type, one of "node", "comment", "user".',
+        'type' => 'varchar',
+        'length' => '32',
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'content_id' => array(
+        'description' => 'The unique ID of the content, such as either the {cid}, {uid}, or {nid}.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => 'The user ID that whitelisted the item.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'timestamp' => array(
+        'description' => 'The UNIX time stamp representing when the item was whitelisted.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+        'disp-size' => 11,
+      )
+    ),
+    'unique keys' => array(
+      'fid_content_type_content_id' => array('fid', 'content_type', 'content_id'),
+    ),
+  );
+  return $schema;
+}
+
+/**
+ * Load up all whitelisted flaggings currently stored in the variable table by
+ * Flag Abuse module, and import them into the new schema.
+ */
+function flag_abuse_update_7000() {
+  drupal_install_schema('flag_abuse');
+
+  $query = db_select('variable', 'v')
+    ->fields('v');
+
+  // We filter out all variable names in MySQL using REGEXP(), but I'm worried
+  // about escaping characters from the flag name. It's probably safer to just
+  // load all variables and iterate through them in PHP.
+  $variables = $query->fetchAllKeyed();
+
+  // An array of abuse flag names.
+  $abuse_flags = flag_abuse_get_abuse_flags();
+  // All flag objects, so that we can retrieve the fid and content_type.
+  $flags = flag_get_flags();
+
+  $successes = 0;
+  $failures  = array();
+  if ($escaped_names = array_map('preg_quote', $abuse_flags)) {
+    $pattern = '/^(' . implode('|', $escaped_names) . ')_([\d]+)$/';
+    foreach ($variables as $name => $serialized_value) {
+      if (preg_match($pattern, $name, $matches) && unserialize($serialized_value) === TRUE) {
+        array_shift($matches);
+        list($flag_name, $content_id) = $matches;
+        if ($flag = $flags[$flag_name]) {
+          $attempts++;
+          // We can't reliably determine the UID so we just pretend these were
+          // all whitelisted by user 1.
+          $account      = new stdClass();
+          $account->uid = 1;
+          if (flag_abuse_whitelist_content($flag, $content_id, $account)) {
+            variable_del($name);
+            $successes++;
+          }
+          else {
+            $failures[] = $name;
+          }
+        }
+      }
+    }
+  }
+
+  if (count($failures)) {
+    $message = "There were @count failures during this update. This may simply mean that there is a variable that looks like it was created by Flag Abuse, but it really wasn't. The failed variable names are listed here: %failures.";
+    $t_args = array(
+      '@count' => count($failures),
+      '%failures' => implode(', ', $failures),
+    );
+    watchdog(__FUNCTION__, $message, $t_args, WATCHDOG_WARNING);
+    throw new DrupalUpdateException(t($message, $t_args));
+  }
+  else {
+    $singular = 'One whitelisted flagging was migrated to the new schema.';
+    $plural   = '@count whitelisted flaggings were migrated to the new schema.';
+    return format_plural($successes, $singular, $plural);
+  }
+}
diff --git a/flag_abuse.module b/flag_abuse.module
index 8d6edfc..9f2eed3 100644
--- a/flag_abuse.module
+++ b/flag_abuse.module
@@ -1,6 +1,13 @@
 <?php
 
 /**
+ * @file
+ * Flag Abuse module.
+ */
+
+require_once 'includes/flag_abuse.crud.inc';
+
+/**
  * Implements hook_menu().
  */
 function flag_abuse_menu() {
@@ -87,7 +94,7 @@ function flag_abuse_flag($event, $flag, $content_id, $account) {
       // user if they reset what they thought was one flag and we report two.
       $rows--;
       drupal_set_message(t('Reset !rows flags.', array('!rows' => $rows)));
-      variable_set($flag->name . '-' . $content_id, TRUE);
+      flag_abuse_whitelist_content($flag, $content_id, $account);
     }
   }
 }
@@ -98,7 +105,7 @@ function flag_abuse_flag($event, $flag, $content_id, $account) {
 function flag_abuse_flag_access($flag, $content_id, $action, $account) {
   if (flag_abuse_is_abuse_flag($flag->name)) {
     // Check to see if this flag has already been reset.
-    if (variable_get($flag->name . '-' . $content_id, FALSE)) {
+    if (flag_abuse_is_whitelisted($flag->fid, $content_id)) {
       return FALSE;
     }
   }
diff --git a/includes/flag_abuse.crud.inc b/includes/flag_abuse.crud.inc
new file mode 100644
index 0000000..a8417ed
--- /dev/null
+++ b/includes/flag_abuse.crud.inc
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Flag Abuse Crud functions.
+ */
+
+/**
+ * Whitelist a particular content item, so it cannot be marked as "abuse".
+ *
+ * @param stdClass $flag
+ *   The Flag Abuse Flag object.
+ * @param mixed
+ *   The content id of the content to be whitelisted.
+ * @param stdClass $account
+ *   The user object of the user whitelisting. If empty, the currently logged
+ *   in user will be used.
+ * @param int $timestamp
+ *   The timestamp when this item was whitelisted. If empty, REQUEST_TIME will
+ *   be used.
+ * @return mixed
+ *   The record that was saved, if successful, or boolean FALSE otherwise.
+ */
+function flag_abuse_whitelist_content(stdClass $flag, $content_id, stdClass $account = NULL, $timestamp = NULL) {
+  if (empty($account)) {
+    global $user;
+    $account = $user;
+  }
+  if (!isset($timestamp)) {
+    $timestamp = REQUEST_TIME;
+  }
+
+  $record               = new stdClass();
+  $record->fid          = $flag->fid;
+  $record->content_type = $flag->content_type;
+  $record->content_id   = $content_id;
+  $record->uid          = $account->uid;
+  $record->timestamp    = $timestamp;
+
+  if (flag_abuse_whitelist_record_save($record)) {
+    return $record;
+  }
+  return FALSE;
+}
+
+/**
+ * Save a whitelisted item to the database.
+ *
+ * @param stdClass $record
+ *   The record to be saved. Must include the Flag ID (fid), Content type
+ *   (content_type) and Content ID (content_id). Optional values are timestamp
+ *   and the uid.
+ * @return
+ *   If the record insert or update failed, returns FALSE. If it succeeded,
+ *   returns SAVED_NEW.
+ */
+function flag_abuse_whitelist_record_save(stdClass $record) {
+  if (empty($record->fid) || empty($record->content_id) || empty($record->content_type)) {
+    throw new Exception(t('Flag abuse whitelist record is missing required attributes.'));
+    return FALSE;
+  }
+  if (flag_abuse_flagging_is_whitelisted($record->fid, $record->content_id)) {
+    return FALSE;
+  }
+  // If there's no uid on the record, set it to the currently logged in user.
+  if (!isset($record->uid)) {
+    global $user;
+    $record->uid = $user->uid;
+  }
+  // If no timestamp has been set, set it to the time of the initial request.
+  if (empty($record->timestamp)) {
+    $record->timestamp = REQUEST_TIME;
+  }
+  return drupal_write_record('flag_abuse_whitelist', $record);
+}
+
+/**
+ * Determine if a particular content item has been whitelisted.
+ */
+function flag_abuse_flagging_is_whitelisted($fid, $content_id) {
+  $query = db_select('flag_abuse_whitelist', 'f')
+    ->fields('f')
+    ->condition('f.fid', $fid)
+    ->condition('f.content_id', $content_id)
+    ->addTag('slave-safe');
+  return $query->execute()->fetchObject();
+}
