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
@@ -1762,23 +1762,32 @@
     'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php',
     'PassphraseCredentialAuthorPolicyRule' => 'applications/passphrase/policyrule/PassphraseCredentialAuthorPolicyRule.php',
     'PassphraseCredentialConduitController' => 'applications/passphrase/controller/PassphraseCredentialConduitController.php',
+    'PassphraseCredentialConduitTransaction' => 'applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php',
     'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php',
     'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php',
+    'PassphraseCredentialDescriptionTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php',
     'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php',
+    'PassphraseCredentialDestroyTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php',
     'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php',
     'PassphraseCredentialFulltextEngine' => 'applications/passphrase/search/PassphraseCredentialFulltextEngine.php',
     'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php',
     'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php',
+    'PassphraseCredentialLockTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLockTransaction.php',
+    'PassphraseCredentialLookedAtTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php',
+    'PassphraseCredentialNameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialNameTransaction.php',
     'PassphraseCredentialPHIDType' => 'applications/passphrase/phid/PassphraseCredentialPHIDType.php',
     'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php',
     'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php',
     'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php',
     'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php',
+    'PassphraseCredentialSecretIDTransaction' => 'applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php',
     'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php',
     'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php',
     'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php',
+    'PassphraseCredentialTransactionType' => 'applications/passphrase/xaction/PassphraseCredentialTransactionType.php',
     'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php',
     'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php',
+    'PassphraseCredentialUsernameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php',
     'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php',
     'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php',
     'PassphraseDefaultEditCapability' => 'applications/passphrase/capability/PassphraseDefaultEditCapability.php',
@@ -6801,23 +6810,32 @@
     ),
     'PassphraseCredentialAuthorPolicyRule' => 'PhabricatorPolicyRule',
     'PassphraseCredentialConduitController' => 'PassphraseController',
+    'PassphraseCredentialConduitTransaction' => 'PassphraseCredentialTransactionType',
     'PassphraseCredentialControl' => 'AphrontFormControl',
     'PassphraseCredentialCreateController' => 'PassphraseController',
+    'PassphraseCredentialDescriptionTransaction' => 'PassphraseCredentialTransactionType',
     'PassphraseCredentialDestroyController' => 'PassphraseController',
+    'PassphraseCredentialDestroyTransaction' => 'PassphraseCredentialTransactionType',
     'PassphraseCredentialEditController' => 'PassphraseController',
     'PassphraseCredentialFulltextEngine' => 'PhabricatorFulltextEngine',
     'PassphraseCredentialListController' => 'PassphraseController',
     'PassphraseCredentialLockController' => 'PassphraseController',
+    'PassphraseCredentialLockTransaction' => 'PassphraseCredentialTransactionType',
+    'PassphraseCredentialLookedAtTransaction' => 'PassphraseCredentialTransactionType',
+    'PassphraseCredentialNameTransaction' => 'PassphraseCredentialTransactionType',
     'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType',
     'PassphraseCredentialPublicController' => 'PassphraseController',
     'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PassphraseCredentialRevealController' => 'PassphraseController',
     'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine',
-    'PassphraseCredentialTransaction' => 'PhabricatorApplicationTransaction',
+    'PassphraseCredentialSecretIDTransaction' => 'PassphraseCredentialTransactionType',
+    'PassphraseCredentialTransaction' => 'PhabricatorModularTransaction',
     'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PassphraseCredentialTransactionType' => 'PhabricatorModularTransactionType',
     'PassphraseCredentialType' => 'Phobject',
     'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase',
+    'PassphraseCredentialUsernameTransaction' => 'PassphraseCredentialTransactionType',
     'PassphraseCredentialViewController' => 'PassphraseController',
     'PassphraseDAO' => 'PhabricatorLiskDAO',
     'PassphraseDefaultEditCapability' => 'PhabricatorPolicyCapability',
diff --git a/src/applications/passphrase/controller/PassphraseCredentialConduitController.php b/src/applications/passphrase/controller/PassphraseCredentialConduitController.php
--- a/src/applications/passphrase/controller/PassphraseCredentialConduitController.php
+++ b/src/applications/passphrase/controller/PassphraseCredentialConduitController.php
@@ -50,7 +50,8 @@
       $xactions = array();
 
       $xactions[] = id(new PassphraseCredentialTransaction())
-        ->setTransactionType(PassphraseCredentialTransaction::TYPE_CONDUIT)
+        ->setTransactionType(
+          PassphraseCredentialConduitTransaction::TRANSACTIONTYPE)
         ->setNewValue(!$credential->getAllowConduit());
 
       $editor = id(new PassphraseCredentialTransactionEditor())
diff --git a/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php
--- a/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php
+++ b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php
@@ -32,7 +32,8 @@
 
       $xactions = array();
       $xactions[] = id(new PassphraseCredentialTransaction())
-        ->setTransactionType(PassphraseCredentialTransaction::TYPE_DESTROY)
+        ->setTransactionType(
+          PassphraseCredentialDestroyTransaction::TRANSACTIONTYPE)
         ->setNewValue(1);
 
       $editor = id(new PassphraseCredentialTransactionEditor())
diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php
--- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php
+++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php
@@ -117,12 +117,19 @@
       }
 
       if (!$errors) {
-        $type_name = PassphraseCredentialTransaction::TYPE_NAME;
-        $type_desc = PassphraseCredentialTransaction::TYPE_DESCRIPTION;
-        $type_username = PassphraseCredentialTransaction::TYPE_USERNAME;
-        $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY;
-        $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID;
-        $type_is_locked = PassphraseCredentialTransaction::TYPE_LOCK;
+        $type_name =
+          PassphraseCredentialNameTransaction::TRANSACTIONTYPE;
+        $type_desc =
+          PassphraseCredentialDescriptionTransaction::TRANSACTIONTYPE;
+        $type_username =
+          PassphraseCredentialUsernameTransaction::TRANSACTIONTYPE;
+        $type_destroy =
+          PassphraseCredentialDestroyTransaction::TRANSACTIONTYPE;
+        $type_secret_id =
+          PassphraseCredentialSecretIDTransaction::TRANSACTIONTYPE;
+        $type_is_locked =
+          PassphraseCredentialLockTransaction::TRANSACTIONTYPE;
+
         $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY;
         $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY;
         $type_space = PhabricatorTransactions::TYPE_SPACE;
diff --git a/src/applications/passphrase/controller/PassphraseCredentialLockController.php b/src/applications/passphrase/controller/PassphraseCredentialLockController.php
--- a/src/applications/passphrase/controller/PassphraseCredentialLockController.php
+++ b/src/applications/passphrase/controller/PassphraseCredentialLockController.php
@@ -40,11 +40,13 @@
       $xactions = array();
 
       $xactions[] = id(new PassphraseCredentialTransaction())
-        ->setTransactionType(PassphraseCredentialTransaction::TYPE_CONDUIT)
+        ->setTransactionType(
+          PassphraseCredentialConduitTransaction::TRANSACTIONTYPE)
         ->setNewValue(0);
 
       $xactions[] = id(new PassphraseCredentialTransaction())
-        ->setTransactionType(PassphraseCredentialTransaction::TYPE_LOCK)
+        ->setTransactionType(
+          PassphraseCredentialLockTransaction::TRANSACTIONTYPE)
         ->setNewValue(1);
 
       $editor = id(new PassphraseCredentialTransactionEditor())
diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php
--- a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php
+++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php
@@ -67,7 +67,7 @@
         ->setDisableWorkflowOnCancel(true)
         ->addCancelButton($view_uri, pht('Done'));
 
-      $type_secret = PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET;
+      $type_secret = PassphraseCredentialLookedAtTransaction::TRANSACTIONTYPE;
       $xactions = array(
         id(new PassphraseCredentialTransaction())
           ->setTransactionType($type_secret)
diff --git a/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php b/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php
--- a/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php
+++ b/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php
@@ -17,185 +17,15 @@
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
-    $types[] = PassphraseCredentialTransaction::TYPE_NAME;
-    $types[] = PassphraseCredentialTransaction::TYPE_DESCRIPTION;
-    $types[] = PassphraseCredentialTransaction::TYPE_USERNAME;
-    $types[] = PassphraseCredentialTransaction::TYPE_SECRET_ID;
-    $types[] = PassphraseCredentialTransaction::TYPE_DESTROY;
-    $types[] = PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET;
-    $types[] = PassphraseCredentialTransaction::TYPE_LOCK;
-    $types[] = PassphraseCredentialTransaction::TYPE_CONDUIT;
-
     return $types;
   }
 
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-    switch ($xaction->getTransactionType()) {
-      case PassphraseCredentialTransaction::TYPE_NAME:
-        if ($this->getIsNewObject()) {
-          return null;
-        }
-        return $object->getName();
-      case PassphraseCredentialTransaction::TYPE_DESCRIPTION:
-        return $object->getDescription();
-      case PassphraseCredentialTransaction::TYPE_USERNAME:
-        return $object->getUsername();
-      case PassphraseCredentialTransaction::TYPE_SECRET_ID:
-        return $object->getSecretID();
-      case PassphraseCredentialTransaction::TYPE_DESTROY:
-        return (int)$object->getIsDestroyed();
-      case PassphraseCredentialTransaction::TYPE_LOCK:
-        return (int)$object->getIsLocked();
-      case PassphraseCredentialTransaction::TYPE_CONDUIT:
-        return (int)$object->getAllowConduit();
-      case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET:
-        return null;
-    }
-
-    return parent::getCustomTransactionOldValue($object, $xaction);
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-    switch ($xaction->getTransactionType()) {
-      case PassphraseCredentialTransaction::TYPE_NAME:
-      case PassphraseCredentialTransaction::TYPE_DESCRIPTION:
-      case PassphraseCredentialTransaction::TYPE_USERNAME:
-      case PassphraseCredentialTransaction::TYPE_SECRET_ID:
-      case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET:
-        return $xaction->getNewValue();
-      case PassphraseCredentialTransaction::TYPE_DESTROY:
-      case PassphraseCredentialTransaction::TYPE_LOCK:
-        return (int)$xaction->getNewValue();
-      case PassphraseCredentialTransaction::TYPE_CONDUIT:
-        return (int)$xaction->getNewValue();
-    }
-    return parent::getCustomTransactionNewValue($object, $xaction);
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-    switch ($xaction->getTransactionType()) {
-      case PassphraseCredentialTransaction::TYPE_NAME:
-        $object->setName($xaction->getNewValue());
-        return;
-      case PassphraseCredentialTransaction::TYPE_DESCRIPTION:
-        $object->setDescription($xaction->getNewValue());
-        return;
-      case PassphraseCredentialTransaction::TYPE_USERNAME:
-        $object->setUsername($xaction->getNewValue());
-        return;
-      case PassphraseCredentialTransaction::TYPE_SECRET_ID:
-        $old_id = $object->getSecretID();
-        if ($old_id) {
-          $this->destroySecret($old_id);
-        }
-        $object->setSecretID($xaction->getNewValue());
-        return;
-      case PassphraseCredentialTransaction::TYPE_DESTROY:
-        // When destroying a credential, wipe out its secret.
-        $is_destroyed = $xaction->getNewValue();
-        $object->setIsDestroyed($is_destroyed);
-        if ($is_destroyed) {
-          $secret_id = $object->getSecretID();
-          if ($secret_id) {
-            $this->destroySecret($secret_id);
-            $object->setSecretID(null);
-          }
-        }
-        return;
-      case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET:
-        return;
-      case PassphraseCredentialTransaction::TYPE_LOCK:
-        $object->setIsLocked((int)$xaction->getNewValue());
-        return;
-      case PassphraseCredentialTransaction::TYPE_CONDUIT:
-        $object->setAllowConduit((int)$xaction->getNewValue());
-        return;
-    }
-
-    return parent::applyCustomInternalTransaction($object, $xaction);
-  }
-
-  protected function applyCustomExternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PassphraseCredentialTransaction::TYPE_NAME:
-      case PassphraseCredentialTransaction::TYPE_DESCRIPTION:
-      case PassphraseCredentialTransaction::TYPE_USERNAME:
-      case PassphraseCredentialTransaction::TYPE_SECRET_ID:
-      case PassphraseCredentialTransaction::TYPE_DESTROY:
-      case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET:
-      case PassphraseCredentialTransaction::TYPE_LOCK:
-      case PassphraseCredentialTransaction::TYPE_CONDUIT:
-        return;
-    }
-
-    return parent::applyCustomExternalTransaction($object, $xaction);
-  }
-
-  private function destroySecret($secret_id) {
-    $table = new PassphraseSecret();
-    queryfx(
-      $table->establishConnection('w'),
-      'DELETE FROM %T WHERE id = %d',
-      $table->getTableName(),
-      $secret_id);
+  public function getCreateObjectTitle($author, $object) {
+    return pht('%s created this credential.', $author);
   }
 
-  protected function validateTransaction(
-    PhabricatorLiskDAO $object,
-    $type,
-    array $xactions) {
-
-    $errors = parent::validateTransaction($object, $type, $xactions);
-
-    switch ($type) {
-      case PassphraseCredentialTransaction::TYPE_NAME:
-        $missing = $this->validateIsEmptyTextField(
-          $object->getName(),
-          $xactions);
-
-        if ($missing) {
-          $error = new PhabricatorApplicationTransactionValidationError(
-            $type,
-            pht('Required'),
-            pht('Credential name is required.'),
-            nonempty(last($xactions), null));
-
-          $error->setIsMissingFieldError(true);
-          $errors[] = $error;
-        }
-        break;
-      case PassphraseCredentialTransaction::TYPE_USERNAME:
-        $credential_type = $object->getImplementation();
-        if (!$credential_type->shouldRequireUsername()) {
-          break;
-        }
-        $missing = $this->validateIsEmptyTextField(
-          $object->getUsername(),
-          $xactions);
-
-        if ($missing) {
-          $error = new PhabricatorApplicationTransactionValidationError(
-            $type,
-            pht('Required'),
-            pht('Username is required.'),
-            nonempty(last($xactions), null));
-
-          $error->setIsMissingFieldError(true);
-          $errors[] = $error;
-        }
-        break;
-    }
-
-    return $errors;
+  public function getCreateObjectTitleForFeed($author, $object) {
+    return pht('%s created %s.', $author, $object);
   }
 
   protected function supportsSearch() {
diff --git a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php
--- a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php
+++ b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php
@@ -1,16 +1,7 @@
 <?php
 
 final class PassphraseCredentialTransaction
-  extends PhabricatorApplicationTransaction {
-
-  const TYPE_NAME = 'passphrase:name';
-  const TYPE_DESCRIPTION = 'passphrase:description';
-  const TYPE_USERNAME = 'passphrase:username';
-  const TYPE_SECRET_ID = 'passphrase:secretID';
-  const TYPE_DESTROY = 'passphrase:destroy';
-  const TYPE_LOOKEDATSECRET = 'passphrase:lookedAtSecret';
-  const TYPE_LOCK = 'passphrase:lock';
-  const TYPE_CONDUIT = 'passphrase:conduit';
+  extends PhabricatorModularTransaction {
 
   public function getApplicationName() {
     return 'passphrase';
@@ -24,116 +15,8 @@
     return null;
   }
 
-  public function shouldHide() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-    switch ($this->getTransactionType()) {
-      case self::TYPE_DESCRIPTION:
-        return ($old === null);
-      case self::TYPE_LOCK:
-        return ($old === null);
-      case self::TYPE_USERNAME:
-        return !strlen($old);
-      case self::TYPE_LOOKEDATSECRET:
-        return false;
-      case self::TYPE_DESTROY:
-        // Don't show "undestroy" transactions because they're a bit confusing
-        // and redundant with restoring a secret.
-        if (!$new) {
-          return true;
-        }
-    }
-    return parent::shouldHide();
-  }
-
-  public function getTitle() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-    $author_phid = $this->getAuthorPHID();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        if ($old === null) {
-          return pht(
-            '%s created this credential.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s renamed this credential from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-        break;
-      case self::TYPE_DESCRIPTION:
-        return pht(
-          '%s updated the description for this credential.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_USERNAME:
-        if (strlen($old)) {
-          return pht(
-            '%s changed the username for this credential from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        } else {
-          return pht(
-            '%s set the username for this credential to "%s".',
-            $this->renderHandleLink($author_phid),
-            $new);
-        }
-        break;
-      case self::TYPE_SECRET_ID:
-        if ($old === null) {
-          return pht(
-            '%s attached a new secret to this credential.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s updated the secret for this credential.',
-            $this->renderHandleLink($author_phid));
-        }
-      case self::TYPE_DESTROY:
-        return pht(
-          '%s destroyed the secret for this credential.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_LOOKEDATSECRET:
-        return pht(
-          '%s examined the secret plaintext for this credential.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_LOCK:
-        return pht(
-          '%s locked this credential.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_CONDUIT:
-        if ($old) {
-          return pht(
-            '%s disallowed Conduit API access to this credential.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s allowed Conduit API access to this credential.',
-            $this->renderHandleLink($author_phid));
-        }
-        break;
-    }
-
-    return parent::getTitle();
-  }
-
-  public function hasChangeDetails() {
-    switch ($this->getTransactionType()) {
-      case self::TYPE_DESCRIPTION:
-        return true;
-    }
-    return parent::hasChangeDetails();
-  }
-
-  public function renderChangeDetails(PhabricatorUser $viewer) {
-    return $this->renderTextCorpusChangeDetails(
-      $viewer,
-      json_encode($this->getOldValue()),
-      json_encode($this->getNewValue()));
+  public function getBaseTransactionClass() {
+    return 'PassphraseCredentialTransactionType';
   }
 
 }
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php
@@ -0,0 +1,48 @@
+<?php
+
+final class PassphraseCredentialConduitTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:conduit';
+
+  public function generateOldValue($object) {
+    return $object->getAllowConduit();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setAllowConduit((int)$value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    if ($old) {
+      return pht(
+        '%s disallowed Conduit API access to this credential.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s allowed Conduit API access to this credential.',
+        $this->renderAuthor());
+    }
+  }
+
+  public function getTitleForFeed() {
+    $old = $this->getOldValue();
+    if ($old) {
+      return pht(
+        '%s disallowed Conduit API access to credential %s.',
+        $this->renderAuthor(),
+        $this->renderObject());
+    } else {
+      return pht(
+        '%s allowed Conduit API access to credential %s.',
+        $this->renderAuthor(),
+        $this->renderObject());
+    }
+  }
+
+  public function getIcon() {
+    return 'fa-tty';
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php
@@ -0,0 +1,64 @@
+<?php
+
+final class PassphraseCredentialDescriptionTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:description';
+
+  public function generateOldValue($object) {
+    return $object->getDescription();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDescription($value);
+  }
+
+  public function shouldHide() {
+    $old = $this->getOldValue();
+    if (!strlen($old)) {
+      return true;
+    }
+    return false;
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the description for this credential.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated the description for credential %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function hasChangeDetailView() {
+    return true;
+  }
+
+  public function getMailDiffSectionHeader() {
+    return pht('CHANGES TO CREDENTIAL DESCRIPTION');
+  }
+
+  public function newChangeDetailView() {
+    $viewer = $this->getViewer();
+
+    return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+      ->setViewer($viewer)
+      ->setOldText($this->getOldValue())
+      ->setNewText($this->getNewValue());
+  }
+
+  public function newRemarkupChanges() {
+    $changes = array();
+
+    $changes[] = $this->newRemarkupChange()
+      ->setOldValue($this->getOldValue())
+      ->setNewValue($this->getNewValue());
+
+    return $changes;
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php
@@ -0,0 +1,52 @@
+<?php
+
+final class PassphraseCredentialDestroyTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:destroy';
+
+  public function generateOldValue($object) {
+    return $object->getIsDestroyed();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $is_destroyed = $value;
+    $object->setIsDestroyed($is_destroyed);
+    if ($is_destroyed) {
+      $secret_id = $object->getSecretID();
+      if ($secret_id) {
+        $this->destroySecret($secret_id);
+        $object->setSecretID(null);
+      }
+    }
+  }
+
+  public function shouldHide() {
+    $new = $this->getNewValue();
+    if (!$new) {
+      return true;
+    }
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s destroyed the secret for this credential.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s destroyed the secret for credential %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function getIcon() {
+    return 'fa-ban';
+  }
+
+  public function getColor() {
+    return 'red';
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialLockTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialLockTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialLockTransaction.php
@@ -0,0 +1,41 @@
+<?php
+
+final class PassphraseCredentialLockTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:lock';
+
+  public function generateOldValue($object) {
+    return $object->getIsLocked();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setIsLocked((int)$value);
+  }
+
+  public function shouldHide() {
+    $new = $this->getNewValue();
+    if ($new === null) {
+      return true;
+    }
+    return false;
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s locked this credential.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s locked credential %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function getIcon() {
+    return 'fa-lock';
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PassphraseCredentialLookedAtTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:lookedAtSecret';
+
+  public function generateOldValue($object) {
+    return null;
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s examined the secret plaintext for this credential.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s examined the secret plaintext for credential %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function getIcon() {
+    return 'fa-eye';
+  }
+
+  public function getColor() {
+    return 'blue';
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialNameTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialNameTransaction.php
@@ -0,0 +1,71 @@
+<?php
+
+final class PassphraseCredentialNameTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:name';
+
+  public function generateOldValue($object) {
+    return $object->getName();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setName($value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    if (!strlen($old)) {
+      return pht(
+        '%s created this credential.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s renamed this credential from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function getTitleForFeed() {
+    $old = $this->getOldValue();
+    if (!strlen($old)) {
+      return pht(
+        '%s created %s.',
+        $this->renderAuthor(),
+        $this->renderObject());
+    } else {
+      return pht(
+        '%s renamed %s credential %s to %s.',
+        $this->renderAuthor(),
+        $this->renderObject(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
+      $errors[] = $this->newRequiredError(
+        pht('Credentials must have a name.'));
+    }
+
+    $max_length = $object->getColumnMaximumByteLength('name');
+    foreach ($xactions as $xaction) {
+      $new_value = $xaction->getNewValue();
+
+      $new_length = strlen($new_value);
+      if ($new_length > $max_length) {
+        $errors[] = $this->newInvalidError(
+          pht('The name can be no longer than %s characters.',
+          new PhutilNumber($max_length)));
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PassphraseCredentialSecretIDTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:secretID';
+
+  public function generateOldValue($object) {
+    return $object->getSecretID();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $old_id = $object->getSecretID();
+    if ($old_id) {
+      $this->destroySecret($old_id);
+    }
+    $object->setSecretID($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s destroyed the secret for this credential.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s destroyed the secret for credential %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialTransactionType.php b/src/applications/passphrase/xaction/PassphraseCredentialTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialTransactionType.php
@@ -0,0 +1,15 @@
+<?php
+
+abstract class PassphraseCredentialTransactionType
+  extends PhabricatorModularTransactionType {
+
+  public function destroySecret($secret_id) {
+    $table = new PassphraseSecret();
+    queryfx(
+      $table->establishConnection('w'),
+      'DELETE FROM %T WHERE id = %d',
+      $table->getTableName(),
+      $secret_id);
+  }
+
+}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PassphraseCredentialUsernameTransaction
+  extends PassphraseCredentialTransactionType {
+
+  const TRANSACTIONTYPE = 'passphrase:username';
+
+  public function generateOldValue($object) {
+    return $object->getUsername();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setUsername($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s set the username for this credential to %s.',
+      $this->renderAuthor(),
+      $this->renderNewValue());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s set the username for credential %s to %s.',
+      $this->renderAuthor(),
+      $this->renderObject(),
+      $this->renderNewValue());
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $credential_type = $object->getImplementation();
+    if ($credential_type->shouldRequireUsername()) {
+      if ($this->isEmptyTextTransaction($object->getUsername(), $xactions)) {
+        $errors[] = $this->newRequiredError(
+          pht('This credential must have a username.'));
+      }
+    }
+
+    $max_length = $object->getColumnMaximumByteLength('username');
+    foreach ($xactions as $xaction) {
+      $new_value = $xaction->getNewValue();
+      $new_length = strlen($new_value);
+      if ($new_length > $max_length) {
+        $errors[] = $this->newInvalidError(
+          pht('The username can be no longer than %s characters.',
+          new PhutilNumber($max_length)));
+      }
+    }
+
+    return $errors;
+  }
+
+}