diff --git a/resources/sql/autopatches/20150124.subs.1.sql b/resources/sql/autopatches/20150124.subs.1.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20150124.subs.1.sql
@@ -0,0 +1,20 @@
+CREATE TABLE {$NAMESPACE}_phortune.phortune_subscription (
+  id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+  phid VARBINARY(64) NOT NULL,
+  accountPHID VARBINARY(64) NOT NULL,
+  merchantPHID VARBINARY(64) NOT NULL,
+  triggerPHID VARBINARY(64) NOT NULL,
+  authorPHID VARBINARY(64) NOT NULL,
+  subscriptionClassKey BINARY(12) NOT NULL,
+  subscriptionClass VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
+  subscriptionRefKey BINARY(12) NOT NULL,
+  subscriptionRef VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
+  status VARCHAR(32) 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),
+  UNIQUE KEY `key_subscription` (subscriptionClassKey, subscriptionRefKey),
+  KEY `key_account` (accountPHID),
+  KEY `key_merchant` (merchantPHID)
+) 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
@@ -2804,6 +2804,13 @@
     'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
     'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
     'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
+    'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
+    'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
+    'PhortuneSubscriptionListController' => 'applications/phortune/controller/PhortuneSubscriptionListController.php',
+    'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
+    'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
+    'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
+    'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
     'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
     'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
     'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
@@ -6144,6 +6151,15 @@
     'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
+    'PhortuneSubscription' => array(
+      'PhortuneDAO',
+      'PhabricatorPolicyInterface',
+    ),
+    'PhortuneSubscriptionListController' => 'PhortuneController',
+    'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
+    'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
+    'PhortuneSubscriptionTableView' => 'AphrontView',
     'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
     'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
     'PhragmentBrowseController' => 'PhragmentController',
diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
--- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php
+++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
@@ -45,6 +45,8 @@
           ),
           'order/(?:query/(?P<queryKey>[^/]+)/)?'
             => 'PhortuneCartListController',
+          'subscription/(?:query/(?P<queryKey>[^/]+)/)?'
+            => 'PhortuneSubscriptionListController',
           'charge/(?:query/(?P<queryKey>[^/]+)/)?'
             => 'PhortuneChargeListController',
         ),
@@ -77,8 +79,10 @@
         'merchant/' => array(
           '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhortuneMerchantListController',
           'edit/(?:(?P<id>\d+)/)?' => 'PhortuneMerchantEditController',
-          'orders/(?P<merchantID>\d+)/(?:query/(?P<querKey>[^/]+)/)?'
+          'orders/(?P<merchantID>\d+)/(?:query/(?P<queryKey>[^/]+)/)?'
             => 'PhortuneCartListController',
+          'subscription/(?P<merchantID>\d+)/(?:query/(?P<queryKey>[^/]+)/)?'
+            => 'PhortuneSubscriptionListController',
           '(?P<id>\d+)/' => 'PhortuneMerchantViewController',
         ),
       ),
diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php
--- a/src/applications/phortune/controller/PhortuneAccountViewController.php
+++ b/src/applications/phortune/controller/PhortuneAccountViewController.php
@@ -70,6 +70,8 @@
     $payment_methods = $this->buildPaymentMethodsSection($account);
     $purchase_history = $this->buildPurchaseHistorySection($account);
     $charge_history = $this->buildChargeHistorySection($account);
+    $subscriptions = $this->buildSubscriptionsSection($account);
+
     $timeline = $this->buildTransactionTimeline(
       $account,
       new PhortuneAccountTransactionQuery());
@@ -86,6 +88,7 @@
         $payment_methods,
         $purchase_history,
         $charge_history,
+        $subscriptions,
         $timeline,
       ),
       array(
@@ -259,6 +262,39 @@
       ->appendChild($table);
   }
 
+  private function buildSubscriptionsSection(PhortuneAccount $account) {
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
+
+    $subscriptions = id(new PhortuneSubscriptionQuery())
+      ->setViewer($viewer)
+      ->withAccountPHIDs(array($account->getPHID()))
+      ->setLimit(10)
+      ->execute();
+
+    $subscriptions_uri = $this->getApplicationURI(
+      $account->getID().'/subscription/');
+
+    $table = id(new PhortuneSubscriptionTableView())
+      ->setUser($viewer)
+      ->setSubscriptions($subscriptions);
+
+    $header = id(new PHUIHeaderView())
+      ->setHeader(pht('Recent Subscriptions'))
+      ->addActionLink(
+        id(new PHUIButtonView())
+          ->setTag('a')
+          ->setIcon(
+            id(new PHUIIconView())
+              ->setIconFont('fa-list'))
+          ->setHref($subscriptions_uri)
+          ->setText(pht('View All Subscriptions')));
+
+    return id(new PHUIObjectBoxView())
+      ->setHeader($header)
+      ->appendChild($table);
+  }
+
   protected function buildApplicationCrumbs() {
     $crumbs = parent::buildApplicationCrumbs();
 
diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php
--- a/src/applications/phortune/controller/PhortuneMerchantViewController.php
+++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php
@@ -186,6 +186,14 @@
         ->setDisabled(!$can_edit)
         ->setWorkflow(!$can_edit));
 
+    $view->addAction(
+      id(new PhabricatorActionView())
+        ->setName(pht('View Subscriptions'))
+        ->setIcon('fa-moon-o')
+        ->setHref($this->getApplicationURI("merchant/subscription/{$id}/"))
+        ->setDisabled(!$can_edit)
+        ->setWorkflow(!$can_edit));
+
     return $view;
   }
 
diff --git a/src/applications/phortune/controller/PhortuneSubscriptionListController.php b/src/applications/phortune/controller/PhortuneSubscriptionListController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/controller/PhortuneSubscriptionListController.php
@@ -0,0 +1,110 @@
+<?php
+
+final class PhortuneSubscriptionListController
+  extends PhortuneController {
+
+  private $accountID;
+  private $merchantID;
+  private $queryKey;
+
+  private $merchant;
+  private $account;
+
+  public function willProcessRequest(array $data) {
+    $this->merchantID = idx($data, 'merchantID');
+    $this->accountID = idx($data, 'accountID');
+    $this->queryKey = idx($data, 'queryKey');
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
+
+    $engine = new PhortuneSubscriptionSearchEngine();
+
+    if ($this->merchantID) {
+      $merchant = id(new PhortuneMerchantQuery())
+        ->setViewer($viewer)
+        ->withIDs(array($this->merchantID))
+        ->requireCapabilities(
+          array(
+            PhabricatorPolicyCapability::CAN_VIEW,
+            PhabricatorPolicyCapability::CAN_EDIT,
+          ))
+        ->executeOne();
+      if (!$merchant) {
+        return new Aphront404Response();
+      }
+      $this->merchant = $merchant;
+      $engine->setMerchant($merchant);
+    } else if ($this->accountID) {
+      $account = id(new PhortuneAccountQuery())
+        ->setViewer($viewer)
+        ->withIDs(array($this->accountID))
+        ->requireCapabilities(
+          array(
+            PhabricatorPolicyCapability::CAN_VIEW,
+            PhabricatorPolicyCapability::CAN_EDIT,
+          ))
+        ->executeOne();
+      if (!$account) {
+        return new Aphront404Response();
+      }
+      $this->account = $account;
+      $engine->setAccount($account);
+    } else {
+      return new Aphront404Response();
+    }
+
+    $controller = id(new PhabricatorApplicationSearchController())
+      ->setQueryKey($this->queryKey)
+      ->setSearchEngine($engine)
+      ->setNavigation($this->buildSideNavView());
+
+    return $this->delegateToController($controller);
+  }
+
+  public function buildSideNavView() {
+    $viewer = $this->getRequest()->getUser();
+
+    $nav = new AphrontSideNavFilterView();
+    $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
+
+    id(new PhortuneSubscriptionSearchEngine())
+      ->setViewer($viewer)
+      ->addNavigationItems($nav->getMenu());
+
+    $nav->selectFilter(null);
+
+    return $nav;
+  }
+
+  protected function buildApplicationCrumbs() {
+    $crumbs = parent::buildApplicationCrumbs();
+
+    $merchant = $this->merchant;
+    if ($merchant) {
+      $id = $merchant->getID();
+      $crumbs->addTextCrumb(
+        $merchant->getName(),
+        $this->getApplicationURI("merchant/{$id}/"));
+      $crumbs->addTextCrumb(
+        pht('Subscriptions'),
+        $this->getApplicationURI("merchant/subscriptions/{$id}/"));
+    }
+
+    $account = $this->account;
+    if ($account) {
+      $id = $account->getID();
+      $crumbs->addTextCrumb(
+        $account->getName(),
+        $this->getApplicationURI("{$id}/"));
+      $crumbs->addTextCrumb(
+        pht('Subscriptions'),
+        $this->getApplicationURI("{$id}/subscription/"));
+    }
+
+    return $crumbs;
+  }
+
+}
diff --git a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php
@@ -0,0 +1,38 @@
+<?php
+
+final class PhortuneSubscriptionPHIDType extends PhabricatorPHIDType {
+
+  const TYPECONST = 'PSUB';
+
+  public function getTypeName() {
+    return pht('Phortune Subscription');
+  }
+
+  public function newObject() {
+    return new PhortuneSubscription();
+  }
+
+  protected function buildQueryForObjects(
+    PhabricatorObjectQuery $query,
+    array $phids) {
+
+    return id(new PhortuneSubscriptionQuery())
+      ->withPHIDs($phids);
+  }
+
+  public function loadHandles(
+    PhabricatorHandleQuery $query,
+    array $handles,
+    array $objects) {
+
+    foreach ($handles as $phid => $handle) {
+      $subscription = $objects[$phid];
+
+      $id = $subscription->getID();
+
+      // TODO: Flesh this out.
+
+    }
+  }
+
+}
diff --git a/src/applications/phortune/query/PhortuneSubscriptionQuery.php b/src/applications/phortune/query/PhortuneSubscriptionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/query/PhortuneSubscriptionQuery.php
@@ -0,0 +1,152 @@
+<?php
+
+final class PhortuneSubscriptionQuery
+  extends PhabricatorCursorPagedPolicyAwareQuery {
+
+  private $ids;
+  private $phids;
+  private $accountPHIDs;
+  private $merchantPHIDs;
+  private $statuses;
+
+  public function withIDs(array $ids) {
+    $this->ids = $ids;
+    return $this;
+  }
+
+  public function withPHIDs(array $phids) {
+    $this->phids = $phids;
+    return $this;
+  }
+
+  public function withAccountPHIDs(array $account_phids) {
+    $this->accountPHIDs = $account_phids;
+    return $this;
+  }
+
+  public function withMerchantPHIDs(array $merchant_phids) {
+    $this->merchantPHIDs = $merchant_phids;
+    return $this;
+  }
+
+  public function withStatuses(array $statuses) {
+    $this->statuses = $statuses;
+    return $this;
+  }
+
+  protected function loadPage() {
+    $table = new PhortuneSubscription();
+    $conn = $table->establishConnection('r');
+
+    $rows = queryfx_all(
+      $conn,
+      'SELECT subscription.* FROM %T subscription %Q %Q %Q',
+      $table->getTableName(),
+      $this->buildWhereClause($conn),
+      $this->buildOrderClause($conn),
+      $this->buildLimitClause($conn));
+
+    return $table->loadAllFromArray($rows);
+  }
+
+  protected function willFilterPage(array $subscriptions) {
+    $accounts = id(new PhortuneAccountQuery())
+      ->setViewer($this->getViewer())
+      ->withPHIDs(mpull($subscriptions, 'getAccountPHID'))
+      ->execute();
+    $accounts = mpull($accounts, null, 'getPHID');
+
+    foreach ($subscriptions as $key => $subscription) {
+      $account = idx($accounts, $subscription->getAccountPHID());
+      if (!$account) {
+        unset($subscriptions[$key]);
+        continue;
+      }
+      $subscription->attachAccount($account);
+    }
+
+    $merchants = id(new PhortuneMerchantQuery())
+      ->setViewer($this->getViewer())
+      ->withPHIDs(mpull($subscriptions, 'getMerchantPHID'))
+      ->execute();
+    $merchants = mpull($merchants, null, 'getPHID');
+
+    foreach ($subscriptions as $key => $subscription) {
+      $merchant = idx($merchants, $subscription->getMerchantPHID());
+      if (!$merchant) {
+        unset($subscriptions[$key]);
+        continue;
+      }
+      $subscription->attachMerchant($merchant);
+    }
+
+    $implementations = array();
+
+    $subscription_map = mgroup($subscriptions, 'getSubscriptionClass');
+    foreach ($subscription_map as $class => $class_subscriptions) {
+      $sub = newv($class, array());
+      $implementations += $sub->loadImplementationsForSubscriptions(
+        $this->getViewer(),
+        $class_subscriptions);
+    }
+
+    foreach ($subscriptions as $key => $subscription) {
+      $implementation = idx($implementations, $key);
+      if (!$implementation) {
+        unset($subscriptions[$key]);
+        continue;
+      }
+      $subscription->attachImplementation($implementation);
+    }
+
+    return $subscriptions;
+  }
+
+  private function buildWhereClause(AphrontDatabaseConnection $conn) {
+    $where = array();
+
+    $where[] = $this->buildPagingClause($conn);
+
+    if ($this->ids !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'subscription.id IN (%Ld)',
+        $this->ids);
+    }
+
+    if ($this->phids !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'subscription.phid IN (%Ls)',
+        $this->phids);
+    }
+
+    if ($this->accountPHIDs !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'subscription.accountPHID IN (%Ls)',
+        $this->accountPHIDs);
+    }
+
+    if ($this->merchantPHIDs !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'subscription.merchantPHID IN (%Ls)',
+        $this->merchantPHIDs);
+    }
+
+    if ($this->statuses !== null) {
+      $where[] = qsprintf(
+        $conn,
+        'subscription.status IN (%Ls)',
+        $this->statuses);
+    }
+
+    return $this->formatWhereClause($where);
+  }
+
+  public function getQueryApplicationClass() {
+    return 'PhabricatorPhortuneApplication';
+  }
+
+}
diff --git a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php
@@ -0,0 +1,154 @@
+<?php
+
+final class PhortuneSubscriptionSearchEngine
+  extends PhabricatorApplicationSearchEngine {
+
+  private $merchant;
+  private $account;
+
+  public function setAccount(PhortuneAccount $account) {
+    $this->account = $account;
+    return $this;
+  }
+
+  public function getAccount() {
+    return $this->account;
+  }
+
+  public function setMerchant(PhortuneMerchant $merchant) {
+    $this->merchant = $merchant;
+    return $this;
+  }
+
+  public function getMerchant() {
+    return $this->merchant;
+  }
+
+  public function getResultTypeDescription() {
+    return pht('Phortune Subscriptions');
+  }
+
+  public function buildSavedQueryFromRequest(AphrontRequest $request) {
+    $saved = new PhabricatorSavedQuery();
+
+    return $saved;
+  }
+
+  public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
+    $query = id(new PhortuneSubscriptionQuery());
+
+    $viewer = $this->requireViewer();
+
+    $merchant = $this->getMerchant();
+    $account = $this->getAccount();
+    if ($merchant) {
+      $can_edit = PhabricatorPolicyFilter::hasCapability(
+        $viewer,
+        $merchant,
+        PhabricatorPolicyCapability::CAN_EDIT);
+      if (!$can_edit) {
+        throw new Exception(
+          pht(
+            'You can not query subscriptions for a merchant you do not '.
+            'control.'));
+      }
+      $query->withMerchantPHIDs(array($merchant->getPHID()));
+    } else if ($account) {
+      $can_edit = PhabricatorPolicyFilter::hasCapability(
+        $viewer,
+        $account,
+        PhabricatorPolicyCapability::CAN_EDIT);
+      if (!$can_edit) {
+        throw new Exception(
+          pht(
+            'You can not query subscriptions for an account you are not '.
+            'a member of.'));
+      }
+      $query->withAccountPHIDs(array($account->getPHID()));
+    } else {
+      $accounts = id(new PhortuneAccountQuery())
+        ->withMemberPHIDs(array($viewer->getPHID()))
+        ->execute();
+      if ($accounts) {
+        $query->withAccountPHIDs(mpull($accounts, 'getPHID'));
+      } else {
+        throw new Exception(pht('You have no accounts!'));
+      }
+    }
+
+    return $query;
+  }
+
+  public function buildSearchForm(
+    AphrontFormView $form,
+    PhabricatorSavedQuery $saved_query) {}
+
+  protected function getURI($path) {
+    $merchant = $this->getMerchant();
+    $account = $this->getAccount();
+    if ($merchant) {
+      return '/phortune/merchant/'.$merchant->getID().'/subscription/'.$path;
+    } else if ($account) {
+      return '/phortune/'.$account->getID().'/subscription/';
+    } else {
+      return '/phortune/subscription/'.$path;
+    }
+  }
+
+  protected function getBuiltinQueryNames() {
+    $names = array(
+      'all' => pht('All Subscriptions'),
+    );
+
+    return $names;
+  }
+
+  public function buildSavedQueryFromBuiltin($query_key) {
+
+    $query = $this->newSavedQuery();
+    $query->setQueryKey($query_key);
+
+    switch ($query_key) {
+      case 'all':
+        return $query;
+    }
+
+    return parent::buildSavedQueryFromBuiltin($query_key);
+  }
+
+  protected function getRequiredHandlePHIDsForResultList(
+    array $subscriptions,
+    PhabricatorSavedQuery $query) {
+    $phids = array();
+    foreach ($subscriptions as $subscription) {
+      $phids[] = $subscription->getPHID();
+      $phids[] = $subscription->getMerchantPHID();
+      $phids[] = $subscription->getAuthorPHID();
+    }
+    return $phids;
+  }
+
+  protected function renderResultList(
+    array $subscriptions,
+    PhabricatorSavedQuery $query,
+    array $handles) {
+    assert_instances_of($subscriptions, 'PhortuneSubscription');
+
+    $viewer = $this->requireViewer();
+
+    $table = id(new PhortuneSubscriptionTableView())
+      ->setUser($viewer)
+      ->setSubscriptions($subscriptions);
+
+    $merchant = $this->getMerchant();
+    if ($merchant) {
+      $header = pht('Subscriptions for %s', $merchant->getName());
+    } else {
+      $header = pht('Your Subscriptions');
+    }
+
+    return id(new PHUIObjectBoxView())
+      ->setHeaderText($header)
+      ->appendChild($table);
+  }
+}
diff --git a/src/applications/phortune/storage/PhortuneSubscription.php b/src/applications/phortune/storage/PhortuneSubscription.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/storage/PhortuneSubscription.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * A subscription bills users regularly.
+ */
+final class PhortuneSubscription extends PhortuneDAO
+  implements PhabricatorPolicyInterface {
+
+  const STATUS_ACTIVE = 'active';
+  const STATUS_CANCELLED = 'cancelled';
+
+  protected $accountPHID;
+  protected $merchantPHID;
+  protected $triggerPHID;
+  protected $authorPHID;
+  protected $subscriptionClassKey;
+  protected $subscriptionClass;
+  protected $subscriptionRefKey;
+  protected $subscriptionRef;
+  protected $status;
+  protected $metadata = array();
+
+  private $merchant = self::ATTACHABLE;
+  private $account = self::ATTACHABLE;
+  private $implementation = self::ATTACHABLE;
+  private $trigger = self::ATTACHABLE;
+
+  protected function getConfiguration() {
+    return array(
+      self::CONFIG_AUX_PHID => true,
+      self::CONFIG_SERIALIZATION => array(
+        'metadata' => self::SERIALIZATION_JSON,
+      ),
+      self::CONFIG_COLUMN_SCHEMA => array(
+        'subscriptionClassKey' => 'bytes12',
+        'subscriptionClass' => 'text128',
+        'subscriptionRefKey' => 'bytes12',
+        'subscriptionRef' => 'text128',
+        'status' => 'text32',
+      ),
+      self::CONFIG_KEY_SCHEMA => array(
+        'key_subscription' => array(
+          'columns' => array('subscriptionClassKey', 'subscriptionRefKey'),
+          'unique' => true,
+        ),
+        'key_account' => array(
+          'columns' => array('accountPHID'),
+        ),
+        'key_merchant' => array(
+          'columns' => array('merchantPHID'),
+        ),
+      ),
+    ) + parent::getConfiguration();
+  }
+
+  public function generatePHID() {
+    return PhabricatorPHID::generateNewPHID(
+      PhortuneSubscriptionPHIDType::TYPECONST);
+  }
+
+  public static function initializeNewSubscription() {
+    return id(new PhortuneSubscription());
+  }
+
+  public function attachImplementation(
+    PhortuneSubscriptionImplementation $impl) {
+    $this->implementation = $impl;
+  }
+
+  public function getImplementation() {
+    return $this->assertAttached($this->implementation);
+  }
+
+  public function save() {
+    $this->subscriptionClassKey = PhabricatorHash::digestForIndex(
+      $this->subscriptionClass);
+
+    $this->subscriptionRefKey = PhabricatorHash::digestForIndex(
+      $this->subscriptionRef);
+
+    return parent::save();
+  }
+
+
+/* -(  PhabricatorPolicyInterface  )----------------------------------------- */
+
+
+  public function getCapabilities() {
+    return array(
+      PhabricatorPolicyCapability::CAN_VIEW,
+      PhabricatorPolicyCapability::CAN_EDIT,
+    );
+  }
+
+  public function getPolicy($capability) {
+    // NOTE: Both view and edit use the account's edit policy. We punch a hole
+    // through this for merchants, below.
+    return $this
+      ->getAccount()
+      ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
+  }
+
+  public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+    if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
+      return true;
+    }
+
+    // If the viewer controls the merchant this subscription bills to, they can
+    // view the subscription.
+    if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
+      $can_admin = PhabricatorPolicyFilter::hasCapability(
+        $viewer,
+        $this->getMerchant(),
+        PhabricatorPolicyCapability::CAN_EDIT);
+      if ($can_admin) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  public function describeAutomaticCapability($capability) {
+    return array(
+      pht('Subscriptions inherit the policies of the associated account.'),
+      pht(
+        'The merchant you are subscribed with can review and manage the '.
+        'subscription.'),
+    );
+  }
+}
diff --git a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
@@ -0,0 +1,18 @@
+<?php
+
+abstract class PhortuneSubscriptionImplementation {
+
+  abstract public function loadImplementationsForRefs(
+    PhabricatorUser $viewer,
+    array $refs);
+
+  abstract public function getRef();
+  abstract public function getName(PhortuneSubscription $subscription);
+
+  protected function getContentSource() {
+    return PhabricatorContentSource::newForSource(
+      PhabricatorContentSource::SOURCE_PHORTUNE,
+      array());
+  }
+
+}
diff --git a/src/applications/phortune/view/PhortuneSubscriptionTableView.php b/src/applications/phortune/view/PhortuneSubscriptionTableView.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/view/PhortuneSubscriptionTableView.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhortuneSubscriptionTableView extends AphrontView {
+
+  private $subscriptions;
+  private $handles;
+
+  public function setHandles(array $handles) {
+    $this->handles = $handles;
+    return $this;
+  }
+
+  public function getHandles() {
+    return $this->handles;
+  }
+
+  public function setSubscriptions(array $subscriptions) {
+    $this->subscriptions = $subscriptions;
+    return $this;
+  }
+
+  public function getSubscriptions() {
+    return $this->subscriptions;
+  }
+
+  public function render() {
+    $subscriptions = $this->getSubscriptions();
+    $handles = $this->getHandles();
+    $viewer = $this->getUser();
+
+    $rows = array();
+    $rowc = array();
+    foreach ($subscriptions as $subscription) {
+      $subscription_link = $handles[$subscription->getPHID()]->renderLink();
+      $rows[] = array(
+        $subscription->getID(),
+        phabricator_datetime($subscription->getDateCreated(), $viewer),
+      );
+    }
+
+    $table = id(new AphrontTableView($rows))
+      ->setHeaders(
+        array(
+          pht('ID'),
+          pht('Created'),
+        ))
+      ->setColumnClasses(
+        array(
+          '',
+          'right',
+        ));
+
+    return $table;
+  }
+
+}