diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -7,7 +7,7 @@
  */
 return array(
   'names' => array(
-    'core.pkg.css' => '1716a671',
+    'core.pkg.css' => '0730cb6d',
     'core.pkg.js' => 'a590b451',
     'darkconsole.pkg.js' => 'e7393ebb',
     'differential.pkg.css' => '9451634c',
@@ -142,10 +142,10 @@
     'rsrc/css/phui/phui-info-view.css' => '5b16bac6',
     'rsrc/css/phui/phui-list.css' => '125599df',
     'rsrc/css/phui/phui-object-box.css' => '3db9f358',
-    'rsrc/css/phui/phui-object-item-list-view.css' => 'fc19bfc1',
+    'rsrc/css/phui/phui-object-item-list-view.css' => 'a1b990b7',
     'rsrc/css/phui/phui-pager.css' => 'bea33d23',
     'rsrc/css/phui/phui-pinboard-view.css' => '2495140e',
-    'rsrc/css/phui/phui-property-list-view.css' => '1baf23eb',
+    'rsrc/css/phui/phui-property-list-view.css' => 'aeb09581',
     'rsrc/css/phui/phui-remarkup-preview.css' => '867f85b3',
     'rsrc/css/phui/phui-spacing.css' => '042804d6',
     'rsrc/css/phui/phui-status.css' => '888cedb8',
@@ -797,10 +797,10 @@
     'phui-inline-comment-view-css' => '9fadd6b8',
     'phui-list-view-css' => '125599df',
     'phui-object-box-css' => '3db9f358',
-    'phui-object-item-list-view-css' => 'fc19bfc1',
+    'phui-object-item-list-view-css' => 'a1b990b7',
     'phui-pager-css' => 'bea33d23',
     'phui-pinboard-view-css' => '2495140e',
-    'phui-property-list-view-css' => '1baf23eb',
+    'phui-property-list-view-css' => 'aeb09581',
     'phui-remarkup-preview-css' => '867f85b3',
     'phui-spacing-css' => '042804d6',
     'phui-status-list-view-css' => '888cedb8',
diff --git a/resources/sql/autopatches/20150712.badges.1.sql b/resources/sql/autopatches/20150712.badges.1.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20150712.badges.1.sql
@@ -0,0 +1,52 @@
+CREATE TABLE {$NAMESPACE}_badges.badges_badge (
+  id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+  phid VARBINARY(64) NOT NULL,
+  name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+  flavor VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+  description LONGTEXT NOT NULL,
+  icon VARCHAR(255) NOT NULL,
+  quality VARCHAR(255) NOT NULL,
+  status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+  dateCreated INT UNSIGNED NOT NULL,
+  dateModified INT UNSIGNED NOT NULL,
+  viewPolicy VARBINARY(64) NOT NULL,
+  editPolicy VARBINARY(64) NOT NULL,
+  UNIQUE KEY `key_phid` (phid)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_badges.badges_transaction (
+  id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  phid VARBINARY(64) NOT NULL,
+  authorPHID VARBINARY(64) NOT NULL,
+  objectPHID VARBINARY(64) NOT NULL,
+  viewPolicy VARBINARY(64) NOT NULL,
+  editPolicy VARBINARY(64) NOT NULL,
+  commentPHID VARBINARY(64),
+  commentVersion INT UNSIGNED NOT NULL,
+  transactionType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+  oldValue LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+  newValue LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+  contentSource LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+  metadata LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+  dateCreated INT UNSIGNED NOT NULL,
+  dateModified INT UNSIGNED NOT NULL,
+  UNIQUE KEY `key_phid` (phid),
+  KEY `key_object` (objectPHID)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_badges.edge (
+  src VARBINARY(64) NOT NULL,
+  type INT UNSIGNED NOT NULL,
+  dst VARBINARY(64) NOT NULL,
+  dateCreated INT UNSIGNED NOT NULL,
+  seq INT UNSIGNED NOT NULL,
+  dataID INT UNSIGNED,
+  PRIMARY KEY (src, type, dst),
+  KEY `src` (src, type, dateCreated, seq),
+  UNIQUE KEY `key_dst` (dst, type, src)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_badges.edgedata (
+  id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1600,6 +1600,30 @@
     'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
     'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
     'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php',
+    'PhabricatorBadge' => 'applications/badges/storage/PhabricatorBadge.php',
+    'PhabricatorBadgeHasRecipientEdgeType' => 'applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php',
+    'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php',
+    'PhabricatorBadgesController' => 'applications/badges/controller/PhabricatorBadgesController.php',
+    'PhabricatorBadgesCreateCapability' => 'applications/badges/capability/PhabricatorBadgesCreateCapability.php',
+    'PhabricatorBadgesDAO' => 'applications/badges/storage/PhabricatorBadgesDAO.php',
+    'PhabricatorBadgesDefaultEditCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php',
+    'PhabricatorBadgesDefaultViewCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultViewCapability.php',
+    'PhabricatorBadgesEditController' => 'applications/badges/controller/PhabricatorBadgesEditController.php',
+    'PhabricatorBadgesEditIconController' => 'applications/badges/controller/PhabricatorBadgesEditIconController.php',
+    'PhabricatorBadgesEditRecipientsController' => 'applications/badges/controller/PhabricatorBadgesEditRecipientsController.php',
+    'PhabricatorBadgesEditor' => 'applications/badges/editor/PhabricatorBadgesEditor.php',
+    'PhabricatorBadgesIcon' => 'applications/badges/icon/PhabricatorBadgesIcon.php',
+    'PhabricatorBadgesInterface' => 'applications/badges/interface/PhabricatorBadgesInterface.php',
+    'PhabricatorBadgesListController' => 'applications/badges/controller/PhabricatorBadgesListController.php',
+    'PhabricatorBadgesPHIDType' => 'applications/badges/phid/PhabricatorBadgesPHIDType.php',
+    'PhabricatorBadgesQuery' => 'applications/badges/query/PhabricatorBadgesQuery.php',
+    'PhabricatorBadgesRecipientsListView' => 'applications/badges/view/PhabricatorBadgesRecipientsListView.php',
+    'PhabricatorBadgesRemoveRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php',
+    'PhabricatorBadgesSchemaSpec' => 'applications/badges/storage/PhabricatorBadgesSchemaSpec.php',
+    'PhabricatorBadgesSearchEngine' => 'applications/badges/query/PhabricatorBadgesSearchEngine.php',
+    'PhabricatorBadgesTransaction' => 'applications/badges/storage/PhabricatorBadgesTransaction.php',
+    'PhabricatorBadgesTransactionQuery' => 'applications/badges/query/PhabricatorBadgesTransactionQuery.php',
+    'PhabricatorBadgesViewController' => 'applications/badges/controller/PhabricatorBadgesViewController.php',
     'PhabricatorBarePageUIExample' => 'applications/uiexample/examples/PhabricatorBarePageUIExample.php',
     'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
     'PhabricatorBaseURISetupCheck' => 'applications/config/check/PhabricatorBaseURISetupCheck.php',
@@ -2533,6 +2557,7 @@
     'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
     'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
     'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
+    'PhabricatorRecipientHasBadgeEdgeType' => 'applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php',
     'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
     'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
     'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
@@ -5286,6 +5311,35 @@
     'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
     'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorAutoEventListener' => 'PhabricatorEventListener',
+    'PhabricatorBadge' => array(
+      'PhabricatorBadgesDAO',
+      'PhabricatorPolicyInterface',
+      'PhabricatorApplicationTransactionInterface',
+      'PhabricatorDestructibleInterface',
+    ),
+    'PhabricatorBadgeHasRecipientEdgeType' => 'PhabricatorEdgeType',
+    'PhabricatorBadgesApplication' => 'PhabricatorApplication',
+    'PhabricatorBadgesController' => 'PhabricatorController',
+    'PhabricatorBadgesCreateCapability' => 'PhabricatorPolicyCapability',
+    'PhabricatorBadgesDAO' => 'PhabricatorLiskDAO',
+    'PhabricatorBadgesDefaultEditCapability' => 'PhabricatorPolicyCapability',
+    'PhabricatorBadgesDefaultViewCapability' => 'PhabricatorPolicyCapability',
+    'PhabricatorBadgesEditController' => 'PhabricatorBadgesController',
+    'PhabricatorBadgesEditIconController' => 'PhabricatorBadgesController',
+    'PhabricatorBadgesEditRecipientsController' => 'PhabricatorBadgesController',
+    'PhabricatorBadgesEditor' => 'PhabricatorApplicationTransactionEditor',
+    'PhabricatorBadgesIcon' => 'Phobject',
+    'PhabricatorBadgesInterface' => 'PhabricatorPHIDInterface',
+    'PhabricatorBadgesListController' => 'PhabricatorBadgesController',
+    'PhabricatorBadgesPHIDType' => 'PhabricatorPHIDType',
+    'PhabricatorBadgesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'PhabricatorBadgesRecipientsListView' => 'AphrontTagView',
+    'PhabricatorBadgesRemoveRecipientsController' => 'PhabricatorBadgesController',
+    'PhabricatorBadgesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
+    'PhabricatorBadgesSearchEngine' => 'PhabricatorApplicationSearchEngine',
+    'PhabricatorBadgesTransaction' => 'PhabricatorApplicationTransaction',
+    'PhabricatorBadgesTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhabricatorBadgesViewController' => 'PhabricatorBadgesController',
     'PhabricatorBarePageUIExample' => 'PhabricatorUIExample',
     'PhabricatorBarePageView' => 'AphrontPageView',
     'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck',
@@ -6375,6 +6429,7 @@
       'Iterator',
     ),
     'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
+    'PhabricatorRecipientHasBadgeEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorRedirectController' => 'PhabricatorController',
     'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
     'PhabricatorRegistrationProfile' => 'Phobject',
diff --git a/src/applications/badges/application/PhabricatorBadgesApplication.php b/src/applications/badges/application/PhabricatorBadgesApplication.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/application/PhabricatorBadgesApplication.php
@@ -0,0 +1,77 @@
+<?php
+
+final class PhabricatorBadgesApplication extends PhabricatorApplication {
+
+  public function getName() {
+    return pht('Badges');
+  }
+
+  public function getBaseURI() {
+    return '/badges/';
+  }
+
+  public function getShortDescription() {
+    return pht('Achievements and Notority');
+  }
+
+  public function getFontIcon() {
+    return 'fa-trophy';
+  }
+
+  public function getFlavorText() {
+    return pht('Build self esteem through gamification.');
+  }
+
+  public function getApplicationGroup() {
+    return self::GROUP_UTILITIES;
+  }
+
+  public function canUninstall() {
+    return true;
+  }
+
+  public function isPrototype() {
+    return true;
+  }
+
+  public function getRoutes() {
+    return array(
+      '/badges/' => array(
+        '(?:query/(?P<queryKey>[^/]+)/)?'
+          => 'PhabricatorBadgesListController',
+        'create/'
+          => 'PhabricatorBadgesEditController',
+        'edit/(?:(?P<id>\d+)/)?'
+          => 'PhabricatorBadgesEditController',
+        'view/(?:(?P<id>\d+)/)?'
+          => 'PhabricatorBadgesViewController',
+        'icon/(?P<id>[1-9]\d*)/'
+          => 'PhabricatorBadgesEditIconController',
+        'icon/'
+          => 'PhabricatorBadgesEditIconController',
+        'recipients/(?P<id>[1-9]\d*)/'
+          => 'PhabricatorBadgesEditRecipientsController',
+        'recipients/(?P<id>[1-9]\d*)/remove/'
+          => 'PhabricatorBadgesRemoveRecipientsController',
+
+      ),
+    );
+  }
+
+  protected function getCustomCapabilities() {
+    return array(
+      PhabricatorBadgesCreateCapability::CAPABILITY => array(
+        'default' => PhabricatorPolicies::POLICY_ADMIN,
+        'caption' => pht('Default create policy for badges.'),
+      ),
+      PhabricatorBadgesDefaultEditCapability::CAPABILITY => array(
+        'default' => PhabricatorPolicies::POLICY_ADMIN,
+        'caption' => pht('Default edit policy for badges.'),
+      ),
+      PhabricatorBadgesDefaultViewCapability::CAPABILITY => array(
+        'caption' => pht('Default view policy for badges.'),
+      ),
+    );
+  }
+
+}
diff --git a/src/applications/badges/capability/PhabricatorBadgesCreateCapability.php b/src/applications/badges/capability/PhabricatorBadgesCreateCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/capability/PhabricatorBadgesCreateCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorBadgesCreateCapability
+  extends PhabricatorPolicyCapability {
+
+  const CAPABILITY = 'badges.default.create';
+
+  public function getCapabilityName() {
+    return pht('Can Create Badges');
+  }
+
+  public function describeCapabilityRejection() {
+    return pht('You do not have permission to create badges.');
+  }
+
+}
diff --git a/src/applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php b/src/applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorBadgesDefaultEditCapability
+  extends PhabricatorPolicyCapability {
+
+  const CAPABILITY = 'badges.default.edit';
+
+  public function getCapabilityName() {
+    return pht('Can Edit Badges');
+  }
+
+  public function describeCapabilityRejection() {
+    return pht('You do not have permission to edit badges.');
+  }
+
+}
diff --git a/src/applications/badges/capability/PhabricatorBadgesDefaultViewCapability.php b/src/applications/badges/capability/PhabricatorBadgesDefaultViewCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/capability/PhabricatorBadgesDefaultViewCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorBadgesDefaultViewCapability extends
+  PhabricatorPolicyCapability {
+
+  const CAPABILITY = 'badges.default.view';
+
+  public function getCapabilityName() {
+    return pht('Default View Policy');
+  }
+
+  public function shouldAllowPublicPolicySetting() {
+    return true;
+  }
+
+}
diff --git a/src/applications/badges/controller/PhabricatorBadgesController.php b/src/applications/badges/controller/PhabricatorBadgesController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/controller/PhabricatorBadgesController.php
@@ -0,0 +1,3 @@
+<?php
+
+abstract class PhabricatorBadgesController extends PhabricatorController {}
diff --git a/src/applications/badges/controller/PhabricatorBadgesEditController.php b/src/applications/badges/controller/PhabricatorBadgesEditController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/controller/PhabricatorBadgesEditController.php
@@ -0,0 +1,228 @@
+<?php
+
+final class PhabricatorBadgesEditController
+  extends PhabricatorBadgesController {
+
+  private $id;
+
+  public function willProcessRequest(array $data) {
+    $this->id = idx($data, 'id');
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
+
+    if ($this->id) {
+      $badge = id(new PhabricatorBadgesQuery())
+        ->setViewer($viewer)
+        ->withIDs(array($this->id))
+        ->requireCapabilities(
+          array(
+            PhabricatorPolicyCapability::CAN_VIEW,
+            PhabricatorPolicyCapability::CAN_EDIT,
+          ))
+        ->executeOne();
+      if (!$badge) {
+        return new Aphront404Response();
+      }
+      $is_new = false;
+    } else {
+      $badge = PhabricatorBadge::initializeNewBadge($viewer);
+      $is_new = true;
+    }
+
+    if ($is_new) {
+      $title = pht('Create Badge');
+      $button_text = pht('Create Badge');
+      $cancel_uri = $this->getApplicationURI();
+    } else {
+      $title = pht(
+        'Edit %s',
+        $badge->getName());
+      $button_text = pht('Save Changes');
+      $cancel_uri = $this->getApplicationURI().'view/'.$this->id.'/';
+    }
+
+    $e_name = true;
+    $v_name = $badge->getName();
+
+    $e_icon = true;
+    $v_icon = $badge->getIcon();
+
+    $v_flav = $badge->getFlavor();
+    $v_desc = $badge->getDescription();
+    $v_qual = $badge->getQuality();
+    $v_stat = $badge->getStatus();
+
+    $validation_exception = null;
+    if ($request->isFormPost()) {
+      $v_name = $request->getStr('name');
+      $v_flav = $request->getStr('flavor');
+      $v_desc = $request->getStr('description');
+      $v_icon = $request->getStr('icon');
+      $v_stat = $request->getStr('status');
+      $v_qual = $request->getStr('quality');
+
+      $v_view = $request->getStr('viewPolicy');
+      $v_edit = $request->getStr('editPolicy');
+
+      $type_name = PhabricatorBadgesTransaction::TYPE_NAME;
+      $type_flav = PhabricatorBadgesTransaction::TYPE_FLAVOR;
+      $type_desc = PhabricatorBadgesTransaction::TYPE_DESCRIPTION;
+      $type_icon = PhabricatorBadgesTransaction::TYPE_ICON;
+      $type_qual = PhabricatorBadgesTransaction::TYPE_QUALITY;
+      $type_stat = PhabricatorBadgesTransaction::TYPE_STATUS;
+
+      $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
+      $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
+
+      $xactions = array();
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_name)
+        ->setNewValue($v_name);
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_flav)
+        ->setNewValue($v_flav);
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_desc)
+        ->setNewValue($v_desc);
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_icon)
+        ->setNewValue($v_icon);
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_qual)
+        ->setNewValue($v_qual);
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_stat)
+        ->setNewValue($v_stat);
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_view)
+        ->setNewValue($v_view);
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType($type_edit)
+        ->setNewValue($v_edit);
+
+      $editor = id(new PhabricatorBadgesEditor())
+        ->setActor($viewer)
+        ->setContentSourceFromRequest($request)
+        ->setContinueOnNoEffect(true);
+
+      try {
+        $editor->applyTransactions($badge, $xactions);
+
+        return id(new AphrontRedirectResponse())
+          ->setURI('/badges/');
+      } catch (PhabricatorApplicationTransactionValidationException $ex) {
+        $validation_exception = $ex;
+
+        $e_name = $ex->getShortMessage($type_name);
+        $e_desc = $ex->getShortMessage($type_desc);
+
+        $badge->setViewPolicy($v_view);
+        $badge->setEditPolicy($v_edit);
+      }
+    }
+
+    if ($is_new) {
+      $icon_uri = $this->getApplicationURI('icon/');
+    } else {
+      $icon_uri = $this->getApplicationURI('icon/'.$badge->getID().'/');
+    }
+    $icon_display = PhabricatorBadgesIcon::renderIconForChooser($v_icon);
+
+    $policies = id(new PhabricatorPolicyQuery())
+      ->setViewer($viewer)
+      ->setObject($badge)
+      ->execute();
+
+    $form = id(new AphrontFormView())
+      ->setUser($viewer)
+      ->appendChild(
+        id(new AphrontFormTextControl())
+          ->setName('name')
+          ->setLabel(pht('Name'))
+          ->setValue($v_name)
+          ->setError($e_name))
+      ->appendChild(
+        id(new AphrontFormTextControl())
+          ->setName('flavor')
+          ->setLabel(pht('Flavor Text'))
+          ->setValue($v_flav))
+      ->appendChild(
+        id(new AphrontFormChooseButtonControl())
+          ->setLabel(pht('Icon'))
+          ->setName('icon')
+          ->setDisplayValue($icon_display)
+          ->setButtonText(pht('Choose Icon...'))
+          ->setChooseURI($icon_uri)
+          ->setValue($v_icon))
+      ->appendChild(
+        id(new AphrontFormSelectControl())
+          ->setName('quality')
+          ->setLabel(pht('Quality'))
+          ->setValue($v_qual)
+          ->setOptions($badge->getQualityNameMap()))
+      ->appendChild(
+        id(new AphrontFormSelectControl())
+          ->setLabel(pht('Status'))
+          ->setName('status')
+          ->setValue($v_stat)
+          ->setOptions($badge->getStatusNameMap()))
+      ->appendChild(
+        id(new PhabricatorRemarkupControl())
+          ->setUser($viewer)
+          ->setName('description')
+          ->setLabel(pht('Description'))
+          ->setValue($v_desc))
+      ->appendChild(
+        id(new AphrontFormPolicyControl())
+          ->setName('viewPolicy')
+          ->setPolicyObject($badge)
+          ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
+          ->setPolicies($policies))
+      ->appendChild(
+        id(new AphrontFormPolicyControl())
+          ->setName('editPolicy')
+          ->setPolicyObject($badge)
+          ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
+          ->setPolicies($policies))
+      ->appendChild(
+        id(new AphrontFormSubmitControl())
+          ->setValue($button_text)
+          ->addCancelButton($cancel_uri));
+
+    $crumbs = $this->buildApplicationCrumbs();
+    if ($is_new) {
+      $crumbs->addTextCrumb(pht('Create Badge'));
+    } else {
+      $crumbs->addTextCrumb(
+        $badge->getName(),
+        '/badges/view/'.$badge->getID().'/');
+      $crumbs->addTextCrumb(pht('Edit'));
+    }
+
+    $box = id(new PHUIObjectBoxView())
+      ->setValidationException($validation_exception)
+      ->setHeaderText($title)
+      ->appendChild($form);
+
+    return $this->buildApplicationPage(
+      array(
+        $crumbs,
+        $box,
+      ),
+      array(
+        'title' => $title,
+      ));
+  }
+
+}
diff --git a/src/applications/badges/controller/PhabricatorBadgesEditIconController.php b/src/applications/badges/controller/PhabricatorBadgesEditIconController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/controller/PhabricatorBadgesEditIconController.php
@@ -0,0 +1,107 @@
+<?php
+
+final class PhabricatorBadgesEditIconController
+  extends PhabricatorBadgesController {
+
+  private $id;
+
+  public function willProcessRequest(array $data) {
+    $this->id = idx($data, 'id');
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
+
+    if ($this->id) {
+      $badge = id(new PhabricatorBadgesQuery())
+        ->setViewer($viewer)
+        ->withIDs(array($this->id))
+        ->requireCapabilities(
+          array(
+            PhabricatorPolicyCapability::CAN_VIEW,
+            PhabricatorPolicyCapability::CAN_EDIT,
+          ))
+          ->executeOne();
+      if (!$badge) {
+        return new Aphront404Response();
+      }
+      $cancel_uri =
+        $this->getApplicationURI('view/'.$badge->getID().'/');
+      $badge_icon = $badge->getIcon();
+    } else {
+      $this->requireApplicationCapability(
+        PhabricatorBadgesCreateCapability::CAPABILITY);
+
+      $cancel_uri = '/badges/';
+      $badge_icon = $request->getStr('value');
+    }
+
+    require_celerity_resource('project-icon-css');
+    Javelin::initBehavior('phabricator-tooltips');
+
+    $badge_icons = PhabricatorBadgesIcon::getIconMap();
+
+    if ($request->isFormPost()) {
+      $v_icon = $request->getStr('icon');
+
+      return id(new AphrontAjaxResponse())->setContent(
+        array(
+          'value' => $v_icon,
+          'display' => PhabricatorBadgesIcon::renderIconForChooser($v_icon),
+        ));
+    }
+
+    $ii = 0;
+    $buttons = array();
+    foreach ($badge_icons as $icon => $label) {
+      $view = id(new PHUIIconView())
+        ->setIconFont($icon);
+
+      $aural = javelin_tag(
+        'span',
+        array(
+          'aural' => true,
+        ),
+        pht('Choose "%s" Icon', $label));
+
+      if ($icon == $badge_icon) {
+        $class_extra = ' selected';
+      } else {
+        $class_extra = null;
+      }
+
+      $buttons[] = javelin_tag(
+        'button',
+        array(
+          'class' => 'icon-button'.$class_extra,
+          'name' => 'icon',
+          'value' => $icon,
+          'type' => 'submit',
+          'sigil' => 'has-tooltip',
+          'meta' => array(
+            'tip' => $label,
+          ),
+        ),
+        array(
+          $aural,
+          $view,
+        ));
+      if ((++$ii % 4) == 0) {
+        $buttons[] = phutil_tag('br');
+      }
+    }
+
+    $buttons = phutil_tag(
+      'div',
+      array(
+        'class' => 'icon-grid',
+      ),
+      $buttons);
+
+    return $this->newDialog()
+      ->setTitle(pht('Choose Badge Icon'))
+      ->appendChild($buttons)
+      ->addCancelButton($cancel_uri);
+  }
+}
diff --git a/src/applications/badges/controller/PhabricatorBadgesEditRecipientsController.php b/src/applications/badges/controller/PhabricatorBadgesEditRecipientsController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/controller/PhabricatorBadgesEditRecipientsController.php
@@ -0,0 +1,126 @@
+<?php
+
+final class PhabricatorBadgesEditRecipientsController
+  extends PhabricatorBadgesController {
+
+  private $id;
+
+  public function willProcessRequest(array $data) {
+    $this->id = $data['id'];
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $user = $request->getUser();
+    $id = $request->getURIData('id');
+
+    $badge = id(new PhabricatorBadgesQuery())
+      ->setViewer($user)
+      ->withIDs(array($this->id))
+      ->needRecipients(true)
+      ->requireCapabilities(
+        array(
+          PhabricatorPolicyCapability::CAN_VIEW,
+        ))
+      ->executeOne();
+    if (!$badge) {
+      return new Aphront404Response();
+    }
+
+    $recipient_phids = $badge->getRecipientPHIDs();
+
+    if ($request->isFormPost()) {
+      $recipient_spec = array();
+
+      $remove = $request->getStr('remove');
+      if ($remove) {
+        $recipient_spec['-'] = array_fuse(array($remove));
+      }
+
+      $add_recipients = $request->getArr('phids');
+      if ($add_recipients) {
+        $recipient_spec['+'] = array_fuse($add_recipients);
+      }
+
+      $type_recipient = PhabricatorBadgeHasRecipientEdgeType::EDGECONST;
+
+      $xactions = array();
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+        ->setMetadataValue('edge:type', $type_recipient)
+        ->setNewValue($recipient_spec);
+
+      $editor = id(new PhabricatorBadgesEditor($badge))
+        ->setActor($user)
+        ->setContentSourceFromRequest($request)
+        ->setContinueOnNoEffect(true)
+        ->setContinueOnMissingFields(true)
+        ->applyTransactions($badge, $xactions);
+
+      return id(new AphrontRedirectResponse())
+        ->setURI($request->getRequestURI());
+    }
+
+    $recipient_phids = array_reverse($recipient_phids);
+    $handles = $this->loadViewerHandles($recipient_phids);
+
+    $state = array();
+    foreach ($handles as $handle) {
+      $state[] = array(
+        'phid' => $handle->getPHID(),
+        'name' => $handle->getFullName(),
+      );
+    }
+
+    $can_edit = PhabricatorPolicyFilter::hasCapability(
+      $user,
+      $badge,
+      PhabricatorPolicyCapability::CAN_EDIT);
+
+    $form_box = null;
+    $title = pht('Add Recipient');
+    if ($can_edit) {
+      $header_name = pht('Edit Recipients');
+      $view_uri = $this->getApplicationURI('view/'.$badge->getID().'/');
+
+      $form = new AphrontFormView();
+      $form
+        ->setUser($user)
+        ->appendControl(
+          id(new AphrontFormTokenizerControl())
+            ->setName('phids')
+            ->setLabel(pht('Add Recipients'))
+            ->setDatasource(new PhabricatorPeopleDatasource()))
+        ->appendChild(
+          id(new AphrontFormSubmitControl())
+            ->addCancelButton($view_uri)
+            ->setValue(pht('Add Recipients')));
+      $form_box = id(new PHUIObjectBoxView())
+        ->setHeaderText($title)
+        ->setForm($form);
+    }
+
+    $recipient_list = id(new PhabricatorBadgesRecipientsListView())
+      ->setBadge($badge)
+      ->setHandles($handles)
+      ->setUser($user);
+
+    $badge_url = $this->getApplicationURI().'view/'.$this->id.'/';
+
+    $crumbs = $this->buildApplicationCrumbs();
+    $crumbs->addTextCrumb($badge->getName(), $badge_url);
+    $crumbs->addTextCrumb(pht('Recipients'));
+
+    return $this->buildApplicationPage(
+      array(
+        $crumbs,
+        $form_box,
+        $recipient_list,
+      ),
+      array(
+        'title' => $title,
+      ));
+  }
+
+}
diff --git a/src/applications/badges/controller/PhabricatorBadgesListController.php b/src/applications/badges/controller/PhabricatorBadgesListController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/controller/PhabricatorBadgesListController.php
@@ -0,0 +1,57 @@
+<?php
+
+final class PhabricatorBadgesListController
+  extends PhabricatorBadgesController {
+
+  private $queryKey;
+
+  public function shouldAllowPublic() {
+    return true;
+  }
+
+  public function willProcessRequest(array $data) {
+    $this->queryKey = idx($data, 'queryKey');
+  }
+
+  public function processRequest() {
+    $controller = id(new PhabricatorApplicationSearchController())
+      ->setQueryKey($this->queryKey)
+      ->setSearchEngine(new PhabricatorBadgesSearchEngine())
+      ->setNavigation($this->buildSideNavView());
+
+    return $this->delegateToController($controller);
+  }
+
+  public function buildSideNavView() {
+    $user = $this->getRequest()->getUser();
+
+    $nav = new AphrontSideNavFilterView();
+    $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
+
+    id(new PhabricatorBadgesSearchEngine())
+      ->setViewer($user)
+      ->addNavigationItems($nav->getMenu());
+
+    $nav->selectFilter(null);
+
+    return $nav;
+  }
+
+  protected function buildApplicationCrumbs() {
+    $crumbs = parent::buildApplicationCrumbs();
+
+    $can_create = $this->hasApplicationCapability(
+      PhabricatorBadgesCreateCapability::CAPABILITY);
+
+    $crumbs->addAction(
+      id(new PHUIListItemView())
+        ->setName(pht('Create Badge'))
+        ->setHref($this->getApplicationURI('create/'))
+        ->setIcon('fa-plus-square')
+        ->setDisabled(!$can_create)
+        ->setWorkflow(!$can_create));
+
+    return $crumbs;
+  }
+
+}
diff --git a/src/applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php b/src/applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php
@@ -0,0 +1,83 @@
+<?php
+
+final class PhabricatorBadgesRemoveRecipientsController
+  extends PhabricatorBadgesController {
+
+  private $id;
+
+  public function willProcessRequest(array $data) {
+    $this->id = $data['id'];
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
+
+    $badge = id(new PhabricatorBadgesQuery())
+      ->setViewer($viewer)
+      ->withIDs(array($this->id))
+      ->needRecipients(true)
+      ->requireCapabilities(
+        array(
+          PhabricatorPolicyCapability::CAN_VIEW,
+          PhabricatorPolicyCapability::CAN_EDIT,
+        ))
+      ->executeOne();
+    if (!$badge) {
+      return new Aphront404Response();
+    }
+
+    $recipient_phids = $badge->getRecipientPHIDs();
+    $remove_phid = $request->getStr('phid');
+
+    if (!in_array($remove_phid, $recipient_phids)) {
+      return new Aphront404Response();
+    }
+
+    $recipients_uri =
+      $this->getApplicationURI('recipients/'.$badge->getID().'/');
+
+    if ($request->isFormPost()) {
+      $recipient_spec = array();
+      $recipient_spec['-'] = array($remove_phid => $remove_phid);
+
+      $type_recipient = PhabricatorBadgeHasRecipientEdgeType::EDGECONST;
+
+      $xactions = array();
+
+      $xactions[] = id(new PhabricatorBadgesTransaction())
+        ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+        ->setMetadataValue('edge:type', $type_recipient)
+        ->setNewValue($recipient_spec);
+
+      $editor = id(new PhabricatorBadgesEditor($badge))
+        ->setActor($viewer)
+        ->setContentSourceFromRequest($request)
+        ->setContinueOnNoEffect(true)
+        ->setContinueOnMissingFields(true)
+        ->applyTransactions($badge, $xactions);
+
+      return id(new AphrontRedirectResponse())
+        ->setURI($recipients_uri);
+    }
+
+    $handle = id(new PhabricatorHandleQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($remove_phid))
+      ->executeOne();
+
+    $dialog = id(new AphrontDialogView())
+      ->setUser($viewer)
+      ->setTitle(pht('Really Revoke Badge?'))
+      ->appendParagraph(
+        pht(
+          'Really revoke the badge "%s" from %s?',
+          phutil_tag('strong', array(), $badge->getName()),
+          phutil_tag('strong', array(), $handle->getName())))
+      ->addCancelButton($recipients_uri)
+      ->addSubmitButton(pht('Revoke Badge'));
+
+    return id(new AphrontDialogResponse())->setDialog($dialog);
+  }
+
+}
diff --git a/src/applications/badges/controller/PhabricatorBadgesViewController.php b/src/applications/badges/controller/PhabricatorBadgesViewController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/controller/PhabricatorBadgesViewController.php
@@ -0,0 +1,163 @@
+<?php
+
+final class PhabricatorBadgesViewController
+  extends PhabricatorBadgesController {
+
+  private $id;
+
+  public function shouldAllowPublic() {
+    return true;
+  }
+
+  public function willProcessRequest(array $data) {
+    $this->id = $data['id'];
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
+
+    $badge = id(new PhabricatorBadgesQuery())
+      ->setViewer($viewer)
+      ->withIDs(array($this->id))
+      ->needRecipients(true)
+      ->executeOne();
+    if (!$badge) {
+      return new Aphront404Response();
+    }
+
+    $crumbs = $this->buildApplicationCrumbs();
+    $crumbs->addTextCrumb($badge->getName());
+    $title = $badge->getName();
+
+    if ($badge->isClosed()) {
+      $status_icon = 'fa-ban';
+      $status_color = 'dark';
+    } else {
+      $status_icon = 'fa-check';
+      $status_color = 'bluegrey';
+    }
+    $status_name = idx(
+      PhabricatorBadge::getStatusNameMap(),
+      $badge->getStatus());
+
+    $header = id(new PHUIHeaderView())
+      ->setHeader($badge->getName())
+      ->setUser($viewer)
+      ->setPolicyObject($badge)
+      ->setStatus($status_icon, $status_color, $status_name);
+
+    $properties = $this->buildPropertyListView($badge);
+    $actions = $this->buildActionListView($badge);
+    $properties->setActionList($actions);
+
+    $box = id(new PHUIObjectBoxView())
+      ->setHeader($header)
+      ->addPropertyList($properties);
+
+    $timeline = $this->buildTransactionTimeline(
+      $badge,
+      new PhabricatorBadgesTransactionQuery());
+    $timeline
+      ->setShouldTerminate(true);
+
+    $recipient_phids = $badge->getRecipientPHIDs();
+    $recipient_phids = array_reverse($recipient_phids);
+    $handles = $this->loadViewerHandles($recipient_phids);
+
+    $recipient_list = id(new PhabricatorBadgesRecipientsListView())
+      ->setBadge($badge)
+      ->setHandles($handles)
+      ->setUser($viewer);
+
+    return $this->buildApplicationPage(
+      array(
+        $crumbs,
+        $box,
+        $recipient_list,
+        $timeline,
+      ),
+      array(
+        'title' => $title,
+        'pageObjects' => array($badge->getPHID()),
+      ));
+  }
+
+  private function buildPropertyListView(PhabricatorBadge $badge) {
+    $viewer = $this->getRequest()->getUser();
+
+    $view = id(new PHUIPropertyListView())
+      ->setUser($viewer)
+      ->setObject($badge);
+
+    $quality = idx($badge->getQualityNameMap(), $badge->getQuality());
+    $icon = idx($badge->getIconNameMap(), $badge->getIcon());
+
+    $view->addProperty(
+      pht('Quality'),
+      $quality);
+
+    $view->addProperty(
+      pht('Icon'),
+      $icon);
+
+    $view->addProperty(
+      pht('Flavor'),
+      $badge->getFlavor());
+
+    $view->invokeWillRenderEvent();
+
+    $description = $badge->getDescription();
+    if (strlen($description)) {
+      $description = PhabricatorMarkupEngine::renderOneObject(
+        id(new PhabricatorMarkupOneOff())->setContent($description),
+        'default',
+        $viewer);
+
+      $view->addSectionHeader(pht('Description'));
+      $view->addTextContent($description);
+    }
+
+    $badge = id(new PHUIBadgeView())
+      ->setIcon($badge->getIcon())
+      ->setHeader($badge->getName())
+      ->setSubhead($badge->getFlavor())
+      ->setQuality($badge->getQuality());
+
+    $view->addTextContent($badge);
+
+    return $view;
+  }
+
+  private function buildActionListView(PhabricatorBadge $badge) {
+    $viewer = $this->getRequest()->getUser();
+    $id = $badge->getID();
+
+    $can_edit = PhabricatorPolicyFilter::hasCapability(
+      $viewer,
+      $badge,
+      PhabricatorPolicyCapability::CAN_EDIT);
+
+    $view = id(new PhabricatorActionListView())
+      ->setUser($viewer)
+      ->setObject($badge);
+
+    $view->addAction(
+      id(new PhabricatorActionView())
+        ->setName(pht('Edit Badge'))
+        ->setIcon('fa-pencil')
+        ->setDisabled(!$can_edit)
+        ->setWorkflow(!$can_edit)
+        ->setHref($this->getApplicationURI("/edit/{$id}/")));
+
+    $view->addAction(
+      id(new PhabricatorActionView())
+        ->setName('Manage Recipients')
+        ->setIcon('fa-users')
+        ->setDisabled(!$can_edit)
+        ->setHref($this->getApplicationURI("/recipients/{$id}/")));
+
+    return $view;
+  }
+
+}
diff --git a/src/applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php b/src/applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/edge/PhabricatorBadgeHasRecipientEdgeType.php
@@ -0,0 +1,103 @@
+<?php
+
+final class PhabricatorBadgeHasRecipientEdgeType
+  extends PhabricatorEdgeType {
+
+  const EDGECONST = 101;
+
+  public function getInverseEdgeConstant() {
+    return PhabricatorRecipientHasBadgeEdgeType::EDGECONST;
+  }
+
+  public function shouldWriteInverseTransactions() {
+    return true;
+  }
+
+  public function getTransactionAddString(
+    $actor,
+    $add_count,
+    $add_edges) {
+
+    return pht(
+      '%s awarded %s recipients(s): %s.',
+      $actor,
+      $add_count,
+      $add_edges);
+  }
+
+  public function getTransactionRemoveString(
+    $actor,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s revoked %s recipients(s): %s.',
+      $actor,
+      $rem_count,
+      $rem_edges);
+  }
+
+  public function getTransactionEditString(
+    $actor,
+    $total_count,
+    $add_count,
+    $add_edges,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s edited recipient(s), awarded %s: %s; revoked %s: %s.',
+      $actor,
+      $add_count,
+      $add_edges,
+      $rem_count,
+      $rem_edges);
+  }
+
+  public function getFeedAddString(
+    $actor,
+    $object,
+    $add_count,
+    $add_edges) {
+
+    return pht(
+      '%s awarded %s recipient(s) for %s: %s.',
+      $actor,
+      $add_count,
+      $object,
+      $add_edges);
+  }
+
+  public function getFeedRemoveString(
+    $actor,
+    $object,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s revoked %s recipient(s) for %s: %s.',
+      $actor,
+      $rem_count,
+      $object,
+      $rem_edges);
+  }
+
+  public function getFeedEditString(
+    $actor,
+    $object,
+    $total_count,
+    $add_count,
+    $add_edges,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s edited recipient(s) for %s, awarded %s: %s; revoked %s: %s.',
+      $actor,
+      $object,
+      $add_count,
+      $add_edges,
+      $rem_count,
+      $rem_edges);
+  }
+}
diff --git a/src/applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php b/src/applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php
@@ -0,0 +1,103 @@
+<?php
+
+final class PhabricatorRecipientHasBadgeEdgeType
+  extends PhabricatorEdgeType {
+
+  const EDGECONST = 100;
+
+  public function getInverseEdgeConstant() {
+    return PhabricatorBadgeHasRecipientEdgeType::EDGECONST;
+  }
+
+  public function shouldWriteInverseTransactions() {
+    return true;
+  }
+
+  public function getTransactionAddString(
+    $actor,
+    $add_count,
+    $add_edges) {
+
+    return pht(
+      '%s added %s badge(s): %s.',
+      $actor,
+      $add_count,
+      $add_edges);
+  }
+
+  public function getTransactionRemoveString(
+    $actor,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s revoked %s badge(s): %s.',
+      $actor,
+      $rem_count,
+      $rem_edges);
+  }
+
+  public function getTransactionEditString(
+    $actor,
+    $total_count,
+    $add_count,
+    $add_edges,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s edited badge(s), added %s: %s; revoked %s: %s.',
+      $actor,
+      $add_count,
+      $add_edges,
+      $rem_count,
+      $rem_edges);
+  }
+
+  public function getFeedAddString(
+    $actor,
+    $object,
+    $add_count,
+    $add_edges) {
+
+    return pht(
+      '%s added %s badge(s) for %s: %s.',
+      $actor,
+      $add_count,
+      $object,
+      $add_edges);
+  }
+
+  public function getFeedRemoveString(
+    $actor,
+    $object,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s revoked %s badge(s) for %s: %s.',
+      $actor,
+      $rem_count,
+      $object,
+      $rem_edges);
+  }
+
+  public function getFeedEditString(
+    $actor,
+    $object,
+    $total_count,
+    $add_count,
+    $add_edges,
+    $rem_count,
+    $rem_edges) {
+
+    return pht(
+      '%s edited badge(s) for %s, added %s: %s; revoked %s: %s.',
+      $actor,
+      $object,
+      $add_count,
+      $add_edges,
+      $rem_count,
+      $rem_edges);
+  }
+}
diff --git a/src/applications/badges/editor/PhabricatorBadgesEditor.php b/src/applications/badges/editor/PhabricatorBadgesEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/editor/PhabricatorBadgesEditor.php
@@ -0,0 +1,145 @@
+<?php
+
+final class PhabricatorBadgesEditor
+  extends PhabricatorApplicationTransactionEditor {
+
+  public function getEditorApplicationClass() {
+    return 'PhabricatorBadgesApplication';
+  }
+
+  public function getEditorObjectsDescription() {
+    return pht('Badges');
+  }
+
+  public function getTransactionTypes() {
+    $types = parent::getTransactionTypes();
+
+    $types[] = PhabricatorBadgesTransaction::TYPE_NAME;
+    $types[] = PhabricatorBadgesTransaction::TYPE_FLAVOR;
+    $types[] = PhabricatorBadgesTransaction::TYPE_DESCRIPTION;
+    $types[] = PhabricatorBadgesTransaction::TYPE_ICON;
+    $types[] = PhabricatorBadgesTransaction::TYPE_STATUS;
+    $types[] = PhabricatorBadgesTransaction::TYPE_QUALITY;
+
+    $types[] = PhabricatorTransactions::TYPE_EDGE;
+    $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
+    $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
+
+    return $types;
+  }
+
+  protected function getCustomTransactionOldValue(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+    switch ($xaction->getTransactionType()) {
+      case PhabricatorBadgesTransaction::TYPE_NAME:
+        return $object->getName();
+      case PhabricatorBadgesTransaction::TYPE_FLAVOR:
+        return $object->getFlavor();
+      case PhabricatorBadgesTransaction::TYPE_DESCRIPTION:
+        return $object->getDescription();
+      case PhabricatorBadgesTransaction::TYPE_ICON:
+        return $object->getIcon();
+      case PhabricatorBadgesTransaction::TYPE_QUALITY:
+        return $object->getQuality();
+      case PhabricatorBadgesTransaction::TYPE_STATUS:
+        return $object->getStatus();
+    }
+
+    return parent::getCustomTransactionOldValue($object, $xaction);
+  }
+
+  protected function getCustomTransactionNewValue(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+
+    switch ($xaction->getTransactionType()) {
+      case PhabricatorBadgesTransaction::TYPE_NAME:
+      case PhabricatorBadgesTransaction::TYPE_FLAVOR:
+      case PhabricatorBadgesTransaction::TYPE_DESCRIPTION:
+      case PhabricatorBadgesTransaction::TYPE_ICON:
+      case PhabricatorBadgesTransaction::TYPE_STATUS:
+      case PhabricatorBadgesTransaction::TYPE_QUALITY:
+        return $xaction->getNewValue();
+    }
+
+    return parent::getCustomTransactionNewValue($object, $xaction);
+  }
+
+  protected function applyCustomInternalTransaction(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+
+    $type = $xaction->getTransactionType();
+    switch ($type) {
+      case PhabricatorBadgesTransaction::TYPE_NAME:
+        $object->setName($xaction->getNewValue());
+        return;
+      case PhabricatorBadgesTransaction::TYPE_FLAVOR:
+        $object->setFlavor($xaction->getNewValue());
+        return;
+      case PhabricatorBadgesTransaction::TYPE_DESCRIPTION:
+        $object->setDescription($xaction->getNewValue());
+        return;
+      case PhabricatorBadgesTransaction::TYPE_ICON:
+        $object->setIcon($xaction->getNewValue());
+        return;
+      case PhabricatorBadgesTransaction::TYPE_QUALITY:
+        $object->setQuality($xaction->getNewValue());
+        return;
+      case PhabricatorBadgesTransaction::TYPE_STATUS:
+        $object->setStatus($xaction->getNewValue());
+        return;
+    }
+
+    return parent::applyCustomInternalTransaction($object, $xaction);
+  }
+
+  protected function applyCustomExternalTransaction(
+    PhabricatorLiskDAO $object,
+    PhabricatorApplicationTransaction $xaction) {
+
+    $type = $xaction->getTransactionType();
+    switch ($type) {
+      case PhabricatorBadgesTransaction::TYPE_NAME:
+      case PhabricatorBadgesTransaction::TYPE_FLAVOR:
+      case PhabricatorBadgesTransaction::TYPE_DESCRIPTION:
+      case PhabricatorBadgesTransaction::TYPE_ICON:
+      case PhabricatorBadgesTransaction::TYPE_STATUS:
+      case PhabricatorBadgesTransaction::TYPE_QUALITY:
+        return;
+    }
+
+    return parent::applyCustomExternalTransaction($object, $xaction);
+  }
+
+  protected function validateTransaction(
+    PhabricatorLiskDAO $object,
+    $type,
+    array $xactions) {
+
+    $errors = parent::validateTransaction($object, $type, $xactions);
+
+    switch ($type) {
+      case PhabricatorBadgesTransaction::TYPE_NAME:
+        $missing = $this->validateIsEmptyTextField(
+          $object->getName(),
+          $xactions);
+
+        if ($missing) {
+          $error = new PhabricatorApplicationTransactionValidationError(
+            $type,
+            pht('Required'),
+            pht('Badge name is required.'),
+            nonempty(last($xactions), null));
+
+          $error->setIsMissingFieldError(true);
+          $errors[] = $error;
+        }
+      break;
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/badges/icon/PhabricatorBadgesIcon.php b/src/applications/badges/icon/PhabricatorBadgesIcon.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/icon/PhabricatorBadgesIcon.php
@@ -0,0 +1,53 @@
+<?php
+
+final class PhabricatorBadgesIcon extends Phobject {
+
+  public static function getIconMap() {
+    return
+      array(
+        'fa-star' => pht('Superstar'),
+        'fa-user' => pht('Average Person'),
+        'fa-bug' => pht('Ladybug'),
+        'fa-users' => pht('Triplets'),
+
+        'fa-book' => pht('Nominomicon'),
+        'fa-rocket' => pht('Escape Route'),
+        'fa-life-ring' => pht('Foam Circle'),
+        'fa-birthday-cake' => pht('Cake Day'),
+
+        'fa-camera-retro' => pht('Leica Enthusiast'),
+        'fa-beer' => pht('Liquid Lunch'),
+        'fa-gift' => pht('Free Stuff'),
+        'fa-eye' => pht('Eye See You'),
+
+        'fa-heart' => pht('Love is Love'),
+        'fa-trophy' => pht('Winner at Things'),
+        'fa-umbrella' => pht('Rain Defender'),
+        'fa-graduation-cap' => pht('In Debt'),
+
+      );
+  }
+
+  public static function getLabel($key) {
+    $map = self::getIconMap();
+    return $map[$key];
+  }
+
+  public static function getAPIName($key) {
+    return substr($key, 3);
+  }
+
+  public static function renderIconForChooser($icon) {
+    $badge_icons = self::getIconMap();
+
+    return phutil_tag(
+      'span',
+      array(),
+      array(
+        id(new PHUIIconView())->setIconFont($icon),
+        ' ',
+        idx($badge_icons, $icon, pht('Unknown Icon')),
+      ));
+  }
+
+}
diff --git a/src/applications/badges/interface/PhabricatorBadgesInterface.php b/src/applications/badges/interface/PhabricatorBadgesInterface.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/interface/PhabricatorBadgesInterface.php
@@ -0,0 +1,3 @@
+<?php
+
+interface PhabricatorBadgesInterface extends PhabricatorPHIDInterface {}
diff --git a/src/applications/badges/phid/PhabricatorBadgesPHIDType.php b/src/applications/badges/phid/PhabricatorBadgesPHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/phid/PhabricatorBadgesPHIDType.php
@@ -0,0 +1,73 @@
+<?php
+
+final class PhabricatorBadgesPHIDType extends PhabricatorPHIDType {
+
+  const TYPECONST = 'BDGE';
+
+  public function getTypeName() {
+    return pht('Badge');
+  }
+
+  public function newObject() {
+    return new PhabricatorBadge();
+  }
+
+  public function getPHIDTypeApplicationClass() {
+    return 'PhabricatorBadgesApplication';
+  }
+
+  protected function buildQueryForObjects(
+    PhabricatorObjectQuery $query,
+    array $phids) {
+
+    return id(new PhabricatorBadgesQuery())
+      ->withPHIDs($phids);
+  }
+
+  public function loadHandles(
+    PhabricatorHandleQuery $query,
+    array $handles,
+    array $objects) {
+
+    foreach ($handles as $phid => $handle) {
+      $badge = $objects[$phid];
+
+      $id = $badge->getID();
+      $name = $badge->getName();
+
+      if ($badge->isClosed()) {
+        $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
+      }
+
+      $handle->setName($name);
+      $handle->setFullName("{$name}");
+      $handle->setURI("/badges/view/{$id}/");
+    }
+  }
+
+  public function loadNamedObjects(
+    PhabricatorObjectQuery $query,
+    array $names) {
+
+    $id_map = array();
+    foreach ($names as $name) {
+      $id = (int)substr($name, 1);
+      $id_map[$id][] = $name;
+    }
+
+    $objects = id(new PhabricatorBadgesQuery())
+      ->setViewer($query->getViewer())
+      ->withIDs(array_keys($id_map))
+      ->execute();
+
+    $results = array();
+    foreach ($objects as $id => $object) {
+      foreach (idx($id_map, $id, array()) as $name) {
+        $results[$name] = $object;
+      }
+    }
+
+    return $results;
+  }
+
+}
diff --git a/src/applications/badges/query/PhabricatorBadgesQuery.php b/src/applications/badges/query/PhabricatorBadgesQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/query/PhabricatorBadgesQuery.php
@@ -0,0 +1,122 @@
+<?php
+
+final class PhabricatorBadgesQuery
+  extends PhabricatorCursorPagedPolicyAwareQuery {
+
+  private $ids;
+  private $phids;
+  private $qualities;
+  private $statuses;
+  private $recipientPHIDs;
+
+  private $needRecipients;
+
+  public function withIDs(array $ids) {
+    $this->ids = $ids;
+    return $this;
+  }
+
+  public function withPHIDs(array $phids) {
+    $this->phids = $phids;
+    return $this;
+  }
+
+  public function withQualities(array $qualities) {
+    $this->qualities = $qualities;
+    return $this;
+  }
+
+  public function withStatuses(array $statuses) {
+    $this->statuses = $statuses;
+    return $this;
+  }
+
+  public function withRecipientPHIDs(array $recipient_phids) {
+    $this->recipientPHIDs = $recipient_phids;
+    return $this;
+  }
+
+  public function needRecipients($need_recipients) {
+    $this->needRecipients = $need_recipients;
+    return $this;
+  }
+
+  protected function loadPage() {
+    $table = new PhabricatorBadge();
+    $conn_r = $table->establishConnection('r');
+
+    $rows = queryfx_all(
+      $conn_r,
+      'SELECT * FROM %T %Q %Q %Q',
+      $table->getTableName(),
+      $this->buildWhereClause($conn_r),
+      $this->buildOrderClause($conn_r),
+      $this->buildLimitClause($conn_r));
+
+    $badges = $table->loadAllFromArray($rows);
+    $viewer_phid = $this->getViewer()->getPHID();
+    $badge_phids = mpull($badges, 'getPHID');
+
+    $recipient_type = PhabricatorBadgeHasRecipientEdgeType::EDGECONST;
+    $need_edge_types = array();
+    $need_edge_types[] = $recipient_type;
+
+    $edges = id(new PhabricatorEdgeQuery())
+      ->withSourcePHIDs($badge_phids)
+      ->withEdgeTypes($need_edge_types)
+      ->execute();
+
+    foreach ($badges as $badge) {
+      $phid = $badge->getPHID();
+      $badge->attachRecipientPHIDs(
+        array_keys($edges[$phid][$recipient_type]));
+    }
+
+    return $badges;
+  }
+
+  protected function didFilterPage(array $badges) {
+    return $badges;
+  }
+
+  protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
+    $where = array();
+
+    $where[] = $this->buildPagingClause($conn_r);
+
+    if ($this->ids !== null) {
+      $where[] = qsprintf(
+        $conn_r,
+        'id IN (%Ld)',
+        $this->ids);
+    }
+
+    if ($this->phids !== null) {
+      $where[] = qsprintf(
+        $conn_r,
+        'phid IN (%Ls)',
+        $this->phids);
+    }
+
+    if ($this->qualities !== null) {
+      $where[] = qsprintf(
+        $conn_r,
+        'quality IN (%Ls)',
+        $this->qualities);
+    }
+
+    if ($this->statuses !== null) {
+      $where[] = qsprintf(
+        $conn_r,
+        'status IN (%Ls)',
+        $this->statuses);
+    }
+
+    return $this->formatWhereClause($where);
+  }
+
+  public function getQueryApplicationClass() {
+    return 'PhabricatorBadgesApplication';
+  }
+
+}
diff --git a/src/applications/badges/query/PhabricatorBadgesSearchEngine.php b/src/applications/badges/query/PhabricatorBadgesSearchEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/query/PhabricatorBadgesSearchEngine.php
@@ -0,0 +1,140 @@
+<?php
+
+final class PhabricatorBadgesSearchEngine
+  extends PhabricatorApplicationSearchEngine {
+
+  public function getResultTypeDescription() {
+    return pht('Badge');
+  }
+
+  public function getApplicationClassName() {
+    return 'PhabricatorBadgesApplication';
+  }
+
+  public function newQuery() {
+    return new PhabricatorBadgesQuery();
+  }
+
+  public function buildSavedQueryFromRequest(AphrontRequest $request) {
+    $saved = new PhabricatorSavedQuery();
+
+    $saved->setParameter(
+      'statuses',
+      $this->readListFromRequest($request, 'statuses'));
+
+    return $saved;
+  }
+
+  public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
+    $query = new PhabricatorBadgesQuery();
+
+    $statuses = $saved->getParameter('statuses');
+    if ($statuses) {
+      $query->withStatuses($statuses);
+    }
+
+    return $query;
+  }
+
+  protected function buildCustomSearchFields() {
+    return array(
+      id(new PhabricatorSearchCheckboxesField())
+        ->setKey('statuses')
+        ->setLabel(pht('Status'))
+        ->setOptions(
+          id(new PhabricatorBadge())
+            ->getStatusNameMap()),
+    );
+  }
+
+  protected function buildQueryFromParameters(array $map) {
+    $query = $this->newQuery();
+
+    if ($map['statuses']) {
+      $query->withStatuses($map['statuses']);
+    }
+
+    return $query;
+  }
+
+  protected function getURI($path) {
+    return '/badges/'.$path;
+  }
+
+  protected function getBuiltinQueryNames() {
+    $names = array();
+
+    $names['open'] = pht('Active Badges');
+    $names['all'] = pht('All Badges');
+
+    return $names;
+  }
+
+  public function buildSavedQueryFromBuiltin($query_key) {
+    $query = $this->newSavedQuery();
+    $query->setQueryKey($query_key);
+
+    switch ($query_key) {
+      case 'all':
+        return $query;
+      case 'open':
+        return $query->setParameter(
+          'statuses',
+          array(
+            PhabricatorBadge::STATUS_OPEN,
+          ));
+    }
+
+    return parent::buildSavedQueryFromBuiltin($query_key);
+  }
+
+  protected function getRequiredHandlePHIDsForResultList(
+    array $badges,
+    PhabricatorSavedQuery $query) {
+
+    $phids = array();
+
+    return $phids;
+  }
+
+  protected function renderResultList(
+    array $badges,
+    PhabricatorSavedQuery $query,
+    array $handles) {
+    assert_instances_of($badges, 'PhabricatorBadge');
+
+    $viewer = $this->requireViewer();
+
+    $list = id(new PHUIObjectItemListView());
+    foreach ($badges as $badge) {
+
+      $quality = idx($badge->getQualityNameMap(), $badge->getQuality());
+      $mini_badge = id(new PHUIBadgeMiniView())
+        ->setHeader($badge->getName())
+        ->setIcon($badge->getIcon())
+        ->setQuality($badge->getQuality());
+
+      $item = id(new PHUIObjectItemView())
+        ->setHeader($badge->getName())
+        ->setBadge($mini_badge)
+        ->setHref('/badges/view/'.$badge->getID().'/')
+        ->addAttribute($quality)
+        ->addAttribute($badge->getFlavor());
+
+      if ($badge->isClosed()) {
+        $item->setDisabled(true);
+        $item->addIcon('fa-ban', pht('Archived'));
+      }
+
+      $list->addItem($item);
+    }
+
+    $result = new PhabricatorApplicationSearchResultView();
+    $result->setObjectList($list);
+    $result->setNoDataString(pht('No badges found.'));
+
+    return $result;
+
+  }
+
+}
diff --git a/src/applications/badges/query/PhabricatorBadgesTransactionQuery.php b/src/applications/badges/query/PhabricatorBadgesTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/query/PhabricatorBadgesTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorBadgesTransactionQuery
+  extends PhabricatorApplicationTransactionQuery {
+
+  public function getTemplateApplicationTransaction() {
+    return new PhabricatorBadgesTransaction();
+  }
+
+}
diff --git a/src/applications/badges/storage/PhabricatorBadge.php b/src/applications/badges/storage/PhabricatorBadge.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/storage/PhabricatorBadge.php
@@ -0,0 +1,188 @@
+<?php
+
+final class PhabricatorBadge extends PhabricatorBadgesDAO
+  implements
+    PhabricatorPolicyInterface,
+    PhabricatorApplicationTransactionInterface,
+    PhabricatorDestructibleInterface {
+
+  protected $name;
+  protected $flavor;
+  protected $description;
+  protected $icon;
+  protected $quality;
+  protected $viewPolicy;
+  protected $editPolicy;
+  protected $status;
+
+  private $recipientPHIDs = self::ATTACHABLE;
+
+  const STATUS_OPEN = 'open';
+  const STATUS_CLOSED = 'closed';
+
+  const DEFAULT_ICON = 'fa-star';
+  const DEFAULT_QUALITY = 'green';
+
+  const POOR = 'grey';
+  const COMMON = 'white';
+  const UNCOMMON = 'green';
+  const RARE = 'blue';
+  const EPIC = 'indigo';
+  const LEGENDARY = 'orange';
+  const HEIRLOOM = 'yellow';
+
+  public static function getStatusNameMap() {
+    return array(
+      self::STATUS_OPEN => pht('Active'),
+      self::STATUS_CLOSED => pht('Archived'),
+    );
+  }
+
+  public static function getQualityNameMap() {
+    return array(
+      self::POOR => pht('Poor'),
+      self::COMMON => pht('Common'),
+      self::UNCOMMON => pht('Uncommon'),
+      self::RARE => pht('Rare'),
+      self::EPIC => pht('Epic'),
+      self::LEGENDARY => pht('Legendary'),
+      self::HEIRLOOM => pht('Heirloom'),
+    );
+  }
+
+  public static function getIconNameMap() {
+    return PhabricatorBadgesIcon::getIconMap();
+  }
+
+  public static function initializeNewBadge(PhabricatorUser $actor) {
+    $app = id(new PhabricatorApplicationQuery())
+      ->setViewer($actor)
+      ->withClasses(array('PhabricatorBadgesApplication'))
+      ->executeOne();
+
+    $view_policy =
+      $app->getPolicy(PhabricatorBadgesDefaultViewCapability::CAPABILITY);
+
+    $edit_policy =
+      $app->getPolicy(PhabricatorBadgesDefaultEditCapability::CAPABILITY);
+
+    return id(new PhabricatorBadge())
+      ->setIcon(self::DEFAULT_ICON)
+      ->setQuality(self::DEFAULT_QUALITY)
+      ->setViewPolicy($view_policy)
+      ->setEditPolicy($edit_policy)
+      ->setStatus(self::STATUS_OPEN);
+  }
+
+  protected function getConfiguration() {
+    return array(
+      self::CONFIG_AUX_PHID => true,
+      self::CONFIG_COLUMN_SCHEMA => array(
+        'name' => 'text255',
+        'flavor' => 'text255',
+        'description' => 'text',
+        'icon' => 'text255',
+        'quality' => 'text255',
+        'status' => 'text32',
+      ),
+    ) + parent::getConfiguration();
+  }
+
+  public function generatePHID() {
+    return
+      PhabricatorPHID::generateNewPHID(PhabricatorBadgesPHIDType::TYPECONST);
+  }
+
+  public function isClosed() {
+    return ($this->getStatus() == self::STATUS_CLOSED);
+  }
+
+  public function attachRecipientPHIDs(array $phids) {
+    $this->recipientPHIDs = $phids;
+    return $this;
+  }
+
+  public function getRecipientPHIDs() {
+    return $this->assertAttached($this->recipientPHIDs);
+  }
+
+
+/* -(  PhabricatorPolicyInterface  )----------------------------------------- */
+
+
+  public function getCapabilities() {
+    return array(
+      PhabricatorPolicyCapability::CAN_VIEW,
+      PhabricatorPolicyCapability::CAN_EDIT,
+    );
+  }
+
+  public function getPolicy($capability) {
+    switch ($capability) {
+      case PhabricatorPolicyCapability::CAN_VIEW:
+        return $this->getViewPolicy();
+      case PhabricatorPolicyCapability::CAN_EDIT:
+        return $this->getEditPolicy();
+    }
+  }
+
+  public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+    return false;
+  }
+
+  public function describeAutomaticCapability($capability) {
+    return null;
+  }
+
+
+/* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
+
+
+  public function getApplicationTransactionEditor() {
+    return new PhabricatorBadgesEditor();
+  }
+
+  public function getApplicationTransactionObject() {
+    return $this;
+  }
+
+  public function getApplicationTransactionTemplate() {
+    return new PhabricatorBadgesTransaction();
+  }
+
+  public function willRenderTimeline(
+    PhabricatorApplicationTransactionView $timeline,
+    AphrontRequest $request) {
+
+    return $timeline;
+  }
+
+
+/* -(  PhabricatorSubscribableInterface  )----------------------------------- */
+
+
+  public function isAutomaticallySubscribed($phid) {
+    return false;
+  }
+
+  public function shouldShowSubscribersProperty() {
+    return false;
+  }
+
+  public function shouldAllowSubscription($phid) {
+    return false;
+  }
+
+
+/* -(  PhabricatorDestructibleInterface  )----------------------------------- */
+
+
+  public function destroyObjectPermanently(
+    PhabricatorDestructionEngine $engine) {
+
+    $this->openTransaction();
+      $this->delete();
+    $this->saveTransaction();
+  }
+
+}
diff --git a/src/applications/badges/storage/PhabricatorBadgesDAO.php b/src/applications/badges/storage/PhabricatorBadgesDAO.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/storage/PhabricatorBadgesDAO.php
@@ -0,0 +1,9 @@
+<?php
+
+abstract class PhabricatorBadgesDAO extends PhabricatorLiskDAO {
+
+  public function getApplicationName() {
+    return 'badges';
+  }
+
+}
diff --git a/src/applications/badges/storage/PhabricatorBadgesSchemaSpec.php b/src/applications/badges/storage/PhabricatorBadgesSchemaSpec.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/storage/PhabricatorBadgesSchemaSpec.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorBadgesSchemaSpec
+  extends PhabricatorConfigSchemaSpec {
+
+  public function buildSchemata() {
+    $this->buildEdgeSchemata(new PhabricatorBadge());
+  }
+
+}
diff --git a/src/applications/badges/storage/PhabricatorBadgesTransaction.php b/src/applications/badges/storage/PhabricatorBadgesTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/storage/PhabricatorBadgesTransaction.php
@@ -0,0 +1,195 @@
+<?php
+
+final class PhabricatorBadgesTransaction
+  extends PhabricatorApplicationTransaction {
+
+  const TYPE_NAME = 'badges:name';
+  const TYPE_DESCRIPTION = 'badges:description';
+  const TYPE_QUALITY = 'badges:quality';
+  const TYPE_ICON = 'badges:icon';
+  const TYPE_STATUS = 'badges:status';
+  const TYPE_FLAVOR = 'badges:flavor';
+
+  public function getApplicationName() {
+    return 'badges';
+  }
+
+  public function getApplicationTransactionType() {
+    return PhabricatorBadgesPHIDType::TYPECONST;
+  }
+
+  public function getApplicationTransactionCommentObject() {
+    return null;
+  }
+
+  public function getTitle() {
+    $author_phid = $this->getAuthorPHID();
+    $object_phid = $this->getObjectPHID();
+
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    $type = $this->getTransactionType();
+    switch ($type) {
+      case self::TYPE_NAME:
+        if ($old === null) {
+          return pht(
+            '%s created this badge.',
+            $this->renderHandleLink($author_phid));
+        } else {
+          return pht(
+            '%s renamed this badge from "%s" to "%s".',
+            $this->renderHandleLink($author_phid),
+            $old,
+            $new);
+        }
+        break;
+      case self::TYPE_FLAVOR:
+        if ($old === null) {
+          return pht(
+            '%s set the flavor text for this badge.',
+            $this->renderHandleLink($author_phid));
+        } else {
+          return pht(
+            '%s updated the flavor text for this badge.',
+            $this->renderHandleLink($author_phid));
+        }
+        break;
+      case self::TYPE_DESCRIPTION:
+        if ($old === null) {
+          return pht(
+            '%s set the description for this badge.',
+            $this->renderHandleLink($author_phid));
+        } else {
+          return pht(
+            '%s updated the description for this badge.',
+            $this->renderHandleLink($author_phid));
+        }
+        break;
+      case self::TYPE_ICON:
+        if ($old === null) {
+          return pht(
+            '%s set the icon for this badge as "%s".',
+            $this->renderHandleLink($author_phid),
+            $new);
+        } else {
+          $icon_map = PhabricatorBadge::getIconNameMap();
+          $icon_new = idx($icon_map, $new, $new);
+          $icon_old = idx($icon_map, $old, $old);
+          return pht(
+            '%s updated the icon for this badge from "%s" to "%s".',
+            $this->renderHandleLink($author_phid),
+            $icon_old,
+            $icon_new);
+        }
+        break;
+      case self::TYPE_QUALITY:
+        if ($old === null) {
+          return pht(
+            '%s set the quality for this badge as "%s".',
+            $this->renderHandleLink($author_phid),
+            $new);
+        } else {
+          $qual_map = PhabricatorBadge::getQualityNameMap();
+          $qual_new = idx($qual_map, $new, $new);
+          $qual_old = idx($qual_map, $old, $old);
+          return pht(
+            '%s updated the quality for this badge from "%s" to "%s".',
+            $this->renderHandleLink($author_phid),
+            $qual_old,
+            $qual_new);
+        }
+        break;
+    }
+
+    return parent::getTitle();
+  }
+
+  public function getTitleForFeed() {
+    $author_phid = $this->getAuthorPHID();
+    $object_phid = $this->getObjectPHID();
+
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    $type = $this->getTransactionType();
+    switch ($type) {
+      case self::TYPE_NAME:
+        if ($old === null) {
+          return pht(
+            '%s created %s.',
+            $this->renderHandleLink($author_phid),
+            $this->renderHandleLink($object_phid));
+
+        } else {
+          return pht(
+            '%s renamed %s.',
+            $this->renderHandleLink($author_phid),
+            $this->renderHandleLink($object_phid));
+        }
+      break;
+      case self::TYPE_FLAVOR:
+        return pht(
+          '%s updated the flavor text for %s.',
+          $this->renderHandleLink($author_phid),
+          $this->renderHandleLink($object_phid));
+      case self::TYPE_ICON:
+        return pht(
+          '%s updated the icon for %s.',
+          $this->renderHandleLink($author_phid),
+          $this->renderHandleLink($object_phid));
+      case self::TYPE_QUALITY:
+        return pht(
+          '%s updated the quality level for %s.',
+          $this->renderHandleLink($author_phid),
+          $this->renderHandleLink($object_phid));
+      case self::TYPE_DESCRIPTION:
+        return pht(
+          '%s updated the description for %s.',
+          $this->renderHandleLink($author_phid),
+          $this->renderHandleLink($object_phid));
+      case self::TYPE_STATUS:
+        switch ($new) {
+          case PhabricatorBadge::STATUS_OPEN:
+            return pht(
+              '%s activated %s.',
+              $this->renderHandleLink($author_phid),
+              $this->renderHandleLink($object_phid));
+          case PhabricatorBadge::STATUS_CLOSED:
+            return pht(
+              '%s archived %s.',
+              $this->renderHandleLink($author_phid),
+              $this->renderHandleLink($object_phid));
+        }
+        break;
+    }
+
+    return parent::getTitleForFeed();
+  }
+
+
+  public function shouldHide() {
+    $old = $this->getOldValue();
+    switch ($this->getTransactionType()) {
+      case self::TYPE_DESCRIPTION:
+        return ($old === null);
+    }
+    return parent::shouldHide();
+  }
+
+  public function hasChangeDetails() {
+    switch ($this->getTransactionType()) {
+      case self::TYPE_DESCRIPTION:
+        return ($this->getOldValue() !== null);
+    }
+
+    return parent::hasChangeDetails();
+  }
+
+  public function renderChangeDetails(PhabricatorUser $viewer) {
+    return $this->renderTextCorpusChangeDetails(
+      $viewer,
+      $this->getOldValue(),
+      $this->getNewValue());
+  }
+}
diff --git a/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php b/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php
new file mode 100644
--- /dev/null
+++ b/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php
@@ -0,0 +1,61 @@
+<?php
+
+final class PhabricatorBadgesRecipientsListView extends AphrontTagView {
+
+  private $badge;
+  private $handles;
+
+  public function setBadge(PhabricatorBadge $badge) {
+    $this->badge = $badge;
+    return $this;
+  }
+
+  public function setHandles(array $handles) {
+    $this->handles = $handles;
+    return $this;
+  }
+
+  protected function getTagContent() {
+
+    $viewer = $this->user;
+
+    $badge = $this->badge;
+    $handles = $this->handles;
+
+    $can_edit = PhabricatorPolicyFilter::hasCapability(
+      $viewer,
+      $badge,
+      PhabricatorPolicyCapability::CAN_EDIT);
+
+    $list = id(new PHUIObjectItemListView())
+      ->setNoDataString(pht('This badge does not have any recipients.'));
+
+    foreach ($handles as $handle) {
+      $remove_uri = '/badges/recipients/'.
+        $badge->getID().'/remove/?phid='.$handle->getPHID();
+
+      $item = id(new PHUIObjectItemView())
+        ->setHeader($handle->getFullName())
+        ->setHref($handle->getURI())
+        ->setImageURI($handle->getImageURI());
+
+      if ($can_edit) {
+        $item->addAction(
+          id(new PHUIListItemView())
+            ->setIcon('fa-times')
+            ->setName(pht('Remove'))
+            ->setHref($remove_uri)
+            ->setWorkflow(true));
+      }
+
+      $list->addItem($item);
+    }
+
+    $box = id(new PHUIObjectBoxView())
+      ->setHeaderText(pht('Recipients'))
+      ->setObjectList($list);
+
+    return $box;
+  }
+
+}
diff --git a/src/applications/tokens/application/PhabricatorTokensApplication.php b/src/applications/tokens/application/PhabricatorTokensApplication.php
--- a/src/applications/tokens/application/PhabricatorTokensApplication.php
+++ b/src/applications/tokens/application/PhabricatorTokensApplication.php
@@ -11,7 +11,7 @@
   }
 
   public function getFontIcon() {
-    return 'fa-trophy';
+    return 'fa-heart';
   }
 
   public function getTitleGlyph() {
diff --git a/src/applications/uiexample/examples/PHUIBadgeExample.php b/src/applications/uiexample/examples/PHUIBadgeExample.php
--- a/src/applications/uiexample/examples/PHUIBadgeExample.php
+++ b/src/applications/uiexample/examples/PHUIBadgeExample.php
@@ -89,7 +89,7 @@
     $badges2[] = id(new PHUIBadgeView())
       ->setIcon('fa-users')
       ->setHeader(pht('Security Team'))
-      ->setSubhead(pht('<sripts>alert(1);</script>'))
+      ->setSubhead(pht('<script>alert(1);</script>'))
       ->setQuality(PHUIBadgeView::EPIC)
       ->setSource('Projects (automatic)')
       ->addByline(pht('Dec 31, 1969'))
@@ -152,7 +152,7 @@
     $flex2->addItems($badges2);
 
     $box2 = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Acheivements'))
+      ->setHeaderText(pht('Achievements'))
       ->appendChild($flex2);
 
     $flex3 = new PHUIBadgeBoxView();
diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
--- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
@@ -106,6 +106,7 @@
       'db.almanac' => array(),
       'db.multimeter' => array(),
       'db.spaces' => array(),
+      'db.badges' => array(),
       '0000.legacy.sql' => array(
         'legacy' => 0,
       ),
diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php
--- a/src/view/phui/PHUIObjectItemView.php
+++ b/src/view/phui/PHUIObjectItemView.php
@@ -23,6 +23,7 @@
   private $fontIcon;
   private $imageIcon;
   private $titleText;
+  private $badge;
 
   const AGE_FRESH = 'fresh';
   const AGE_STALE = 'stale';
@@ -99,6 +100,11 @@
     return $this;
   }
 
+  public function setBadge(PHUIBadgeMiniView $badge) {
+    $this->badge = $badge;
+    return $this;
+  }
+
   public function setTitleText($title_text) {
     $this->titleText = $title_text;
     return $this;
@@ -588,6 +594,16 @@
         $status);
     }
 
+    /* Can't set a Badge and a Status Icon */
+    if ($this->badge) {
+      $column0 = phutil_tag(
+        'div',
+        array(
+          'class' => 'phui-object-item-col0 phui-object-item-badge',
+        ),
+        $this->badge);
+    }
+
     $column1 = phutil_tag(
       'div',
       array(
diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css
--- a/webroot/rsrc/css/phui/phui-object-item-list-view.css
+++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css
@@ -617,6 +617,17 @@
   border: none;
 }
 
+/* - Badges ---------------------------------------------------------------- */
+
+.phui-object-item-col0.phui-object-item-badge {
+  width: 28px;
+}
+
+.phui-object-item-col0.phui-object-item-badge .phui-icon-view {
+  left: 0;
+}
+
+
 /* - Dashboards ------------------------------------------------------------ */
 
 .phui-object-box .phui-object-item-list-view .phui-object-item-frame {
diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css
--- a/webroot/rsrc/css/phui/phui-property-list-view.css
+++ b/webroot/rsrc/css/phui/phui-property-list-view.css
@@ -98,6 +98,12 @@
   border-width: 1px 0 0;
 }
 
+.phui-property-list-container + .phui-property-list-text-content {
+  border-color: {$thinblueborder};
+  border-style: solid;
+  border-width: 1px 0 0;
+}
+
 .phui-property-list-section-noninitial .phui-property-list-section-header {
   border-top: none;
 }