diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -415,8 +415,11 @@
     'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
     'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5',
     'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
-    'rsrc/js/application/projects/Workboard.js' => '088b2495',
-    'rsrc/js/application/projects/behavior-project-boards.js' => '37eb99e4',
+    'rsrc/js/application/projects/WorkboardBoard.js' => '069d6dd3',
+    'rsrc/js/application/projects/WorkboardCard.js' => '2fcefa17',
+    'rsrc/js/application/projects/WorkboardColumn.js' => 'e8f303bb',
+    'rsrc/js/application/projects/WorkboardController.js' => 'fa1378c3',
+    'rsrc/js/application/projects/behavior-project-boards.js' => 'e1b56d72',
     'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
     'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
     'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
@@ -656,7 +659,7 @@
     'javelin-behavior-phui-profile-menu' => '12884df9',
     'javelin-behavior-policy-control' => 'd0c516d5',
     'javelin-behavior-policy-rule-editor' => '5e9f347c',
-    'javelin-behavior-project-boards' => '37eb99e4',
+    'javelin-behavior-project-boards' => 'e1b56d72',
     'javelin-behavior-project-create' => '065227cc',
     'javelin-behavior-quicksand-blacklist' => '7927a7d3',
     'javelin-behavior-recurring-edit' => '5f1c4d5f',
@@ -723,7 +726,10 @@
     'javelin-view-renderer' => '6c2b09a2',
     'javelin-view-visitor' => 'efe49472',
     'javelin-websocket' => 'e292eaf4',
-    'javelin-workboard' => '088b2495',
+    'javelin-workboard-board' => '069d6dd3',
+    'javelin-workboard-card' => '2fcefa17',
+    'javelin-workboard-column' => 'e8f303bb',
+    'javelin-workboard-controller' => 'fa1378c3',
     'javelin-workflow' => '5b2e3e2b',
     'lightbox-attachment-css' => '7acac05d',
     'maniphest-batch-editor' => 'b0f0b6d5',
@@ -913,6 +919,15 @@
       'javelin-stratcom',
       'javelin-workflow',
     ),
+    '069d6dd3' => array(
+      'javelin-install',
+      'javelin-dom',
+      'javelin-util',
+      'javelin-stratcom',
+      'javelin-workflow',
+      'phabricator-draggable-list',
+      'javelin-workboard-column',
+    ),
     '06c32383' => array(
       'javelin-behavior',
       'javelin-typeahead-ondemand-source',
@@ -930,16 +945,6 @@
       'javelin-stratcom',
       'javelin-vector',
     ),
-    '088b2495' => array(
-      'javelin-install',
-      'javelin-dom',
-      'javelin-util',
-      'javelin-vector',
-      'javelin-stratcom',
-      'javelin-workflow',
-      'phabricator-draggable-list',
-      'phabricator-drag-and-drop-file-upload',
-    ),
     '0a3f3021' => array(
       'javelin-behavior',
       'javelin-stratcom',
@@ -1077,6 +1082,9 @@
     '2ee659ce' => array(
       'javelin-install',
     ),
+    '2fcefa17' => array(
+      'javelin-install',
+    ),
     '327a00d1' => array(
       'javelin-behavior',
       'javelin-stratcom',
@@ -1096,17 +1104,6 @@
       'javelin-vector',
       'phuix-autocomplete',
     ),
-    '37eb99e4' => array(
-      'javelin-behavior',
-      'javelin-dom',
-      'javelin-util',
-      'javelin-vector',
-      'javelin-stratcom',
-      'javelin-workflow',
-      'phabricator-draggable-list',
-      'phabricator-drag-and-drop-file-upload',
-      'javelin-workboard',
-    ),
     '3ab51e2c' => array(
       'javelin-behavior',
       'javelin-behavior-device',
@@ -1937,6 +1934,15 @@
       'javelin-dom',
       'phabricator-prefab',
     ),
+    'e1b56d72' => array(
+      'javelin-behavior',
+      'javelin-dom',
+      'javelin-util',
+      'javelin-vector',
+      'javelin-stratcom',
+      'javelin-workflow',
+      'javelin-workboard-controller',
+    ),
     'e1d25dfb' => array(
       'javelin-behavior',
       'javelin-stratcom',
@@ -2004,6 +2010,10 @@
     'e6e25838' => array(
       'javelin-install',
     ),
+    'e8f303bb' => array(
+      'javelin-install',
+      'javelin-workboard-card',
+    ),
     'e9581f08' => array(
       'javelin-behavior',
       'javelin-stratcom',
@@ -2088,6 +2098,16 @@
       'javelin-vector',
       'javelin-magical-init',
     ),
+    'fa1378c3' => array(
+      'javelin-install',
+      'javelin-dom',
+      'javelin-util',
+      'javelin-vector',
+      'javelin-stratcom',
+      'javelin-workflow',
+      'phabricator-drag-and-drop-file-upload',
+      'javelin-workboard-board',
+    ),
     'fb20ac8d' => array(
       'javelin-behavior',
       'javelin-aphlict',
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
@@ -1820,6 +1820,7 @@
     'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php',
     'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php',
     'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php',
+    'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php',
     'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
     'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
     'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
@@ -6058,6 +6059,7 @@
     'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider',
     'PhabricatorBoardLayoutEngine' => 'Phobject',
     'PhabricatorBoardRenderingEngine' => 'Phobject',
+    'PhabricatorBoardResponseEngine' => 'Phobject',
     'PhabricatorBot' => 'PhabricatorDaemon',
     'PhabricatorBotChannel' => 'PhabricatorBotTarget',
     'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -9,6 +9,7 @@
       ->addContextParameter('responseType')
       ->addContextParameter('columnPHID')
       ->addContextParameter('order')
+      ->addContextParameter('visiblePHIDs')
       ->buildResponse();
   }
 
diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php
--- a/src/applications/maniphest/editor/ManiphestEditEngine.php
+++ b/src/applications/maniphest/editor/ManiphestEditEngine.php
@@ -289,7 +289,11 @@
     $viewer = $request->getViewer();
 
     $column_phid = $request->getStr('columnPHID');
-    $order = $request->getStr('order');
+
+    $visible_phids = $request->getStrList('visiblePHIDs');
+    if (!$visible_phids) {
+      $visible_phids = array();
+    }
 
     $column = id(new PhabricatorProjectColumnQuery())
       ->setViewer($viewer)
@@ -299,98 +303,15 @@
       return new Aphront404Response();
     }
 
-    // If the workboard's project and all descendant projects have been removed
-    // from the card's project list, we are going to remove it from the board
-    // completely.
-
-    // TODO: If the user did something sneaky and changed a subproject, we'll
-    // currently leave the card where it was but should really move it to the
-    // proper new column.
-
     $board_phid = $column->getProjectPHID();
+    $object_phid = $task->getPHID();
 
-    $descendant_projects = id(new PhabricatorProjectQuery())
-      ->setViewer($viewer)
-      ->withAncestorProjectPHIDs(array($column->getProjectPHID()))
-      ->execute();
-    $board_phids = mpull($descendant_projects, 'getPHID', 'getPHID');
-    $board_phids[$board_phid] = $board_phid;
-
-    $project_map = array_fuse($task->getProjectPHIDs());
-    $remove_card = !array_intersect_key($board_phids, $project_map);
-
-    // TODO: Maybe the caller should pass a list of visible task PHIDs so we
-    // know which ones we need to reorder? This is a HUGE overfetch.
-    $objects = id(new ManiphestTaskQuery())
-      ->setViewer($viewer)
-      ->withEdgeLogicPHIDs(
-        PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
-        PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
-        array($board_phids))
-      ->setViewer($viewer)
-      ->execute();
-    $objects = mpull($objects, null, 'getPHID');
-
-    $layout_engine = id(new PhabricatorBoardLayoutEngine())
+    return id(new PhabricatorBoardResponseEngine())
       ->setViewer($viewer)
-      ->setBoardPHIDs(array($board_phid))
-      ->setObjectPHIDs(array_keys($objects))
-      ->executeLayout();
-
-    $positions = $layout_engine->getColumnObjectPositions(
-      $board_phid,
-      $column_phid);
-
-    $column_phids = $layout_engine->getColumnObjectPHIDs(
-      $board_phid,
-      $column_phid);
-
-    $column_tasks = array_select_keys($objects, $column_phids);
-
-    if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
-      // TODO: This is a little bit awkward, because PHP and JS use
-      // slightly different sort order parameters to achieve the same
-      // effect. It would be good to unify this a bit at some point.
-      $sort_map = array();
-      foreach ($positions as $position) {
-        $sort_map[$position->getObjectPHID()] = array(
-          -$position->getSequence(),
-          $position->getID(),
-        );
-      }
-    } else {
-      $sort_map = mpull(
-        $column_tasks,
-        'getPrioritySortVector',
-        'getPHID');
-    }
-
-    $data = array(
-      'removeFromBoard' => $remove_card,
-      'sortMap' => $sort_map,
-    );
-
-    $rendering_engine = id(new PhabricatorBoardRenderingEngine())
-      ->setViewer($viewer)
-      ->setObjects(array($task))
-      ->setExcludedProjectPHIDs($board_phids);
-
-    $card = $rendering_engine->renderCard($task->getPHID());
-
-    $item = $card->getItem();
-    $item->addClass('phui-workcard');
-
-    $payload = array(
-      'tasks' => $item,
-      'data' => $data,
-    );
-
-    return id(new AphrontAjaxResponse())
-      ->setContent(
-        array(
-          'tasks' => $item,
-          'data' => $data,
-        ));
+      ->setBoardPHID($board_phid)
+      ->setObjectPHID($object_phid)
+      ->setVisiblePHIDs($visible_phids)
+      ->buildResponse();
   }
 
 
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -194,14 +194,6 @@
     return ManiphestTaskStatus::isClosedStatus($this->getStatus());
   }
 
-  public function getPrioritySortVector() {
-    return array(
-      $this->getPriority(),
-      -$this->getSubpriority(),
-      $this->getID(),
-    );
-  }
-
   public function setProperty($key, $value) {
     $this->properties[$key] = $value;
     return $this;
@@ -219,6 +211,16 @@
     return idx($this->properties, 'cover.thumbnailPHID');
   }
 
+  public function getWorkboardOrderVectors() {
+    return array(
+      PhabricatorProjectColumn::ORDER_PRIORITY => array(
+        (int)-$this->getPriority(),
+        (double)-$this->getSubpriority(),
+        (int)-$this->getID(),
+      ),
+    );
+  }
+
 
 /* -(  PhabricatorSubscribableInterface  )----------------------------------- */
 
diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -228,20 +228,6 @@
           'boardPHID' => $project->getPHID(),
         ));
 
-    $behavior_config = array(
-      'boardID' => $board_id,
-      'projectPHID' => $project->getPHID(),
-      'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
-      'createURI' => $this->getCreateURI(),
-      'uploadURI' => '/file/dropupload/',
-      'coverURI' => $this->getApplicationURI('cover/'),
-      'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
-      'order' => $this->sortKey,
-    );
-    $this->initBehavior(
-      'project-boards',
-      $behavior_config);
-
     $visible_columns = array();
     $column_phids = array();
     $visible_phids = array();
@@ -287,6 +273,9 @@
       ->setEditMap($task_can_edit_map)
       ->setExcludedProjectPHIDs($select_phids);
 
+    $templates = array();
+    $column_maps = array();
+    $all_tasks = array();
     foreach ($visible_columns as $column_phid => $column) {
       $column_tasks = $column_phids[$column_phid];
 
@@ -346,14 +335,35 @@
           ));
 
       foreach ($column_tasks as $task) {
-        $card = $rendering_engine->renderCard($task->getPHID());
-        $cards->addItem($card->getItem());
+        $object_phid = $task->getPHID();
+
+        $card = $rendering_engine->renderCard($object_phid);
+        $templates[$object_phid] = hsprintf('%s', $card->getItem());
+        $column_maps[$column_phid][] = $object_phid;
+
+        $all_tasks[$object_phid] = $task;
       }
 
       $panel->setCards($cards);
       $board->addPanel($panel);
     }
 
+    $behavior_config = array(
+      'boardID' => $board_id,
+      'projectPHID' => $project->getPHID(),
+      'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
+      'createURI' => $this->getCreateURI(),
+      'uploadURI' => '/file/dropupload/',
+      'coverURI' => $this->getApplicationURI('cover/'),
+      'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
+      'order' => $this->sortKey,
+      'templateMap' => $templates,
+      'columnMaps' => $column_maps,
+      'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'),
+    );
+    $this->initBehavior('project-boards', $behavior_config);
+
+
     $sort_menu = $this->buildSortMenu(
       $viewer,
       $this->sortKey);
diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php
--- a/src/applications/project/controller/PhabricatorProjectController.php
+++ b/src/applications/project/controller/PhabricatorProjectController.php
@@ -150,51 +150,18 @@
   protected function newCardResponse($board_phid, $object_phid) {
     $viewer = $this->getViewer();
 
-    $project = id(new PhabricatorProjectQuery())
-      ->setViewer($viewer)
-      ->withPHIDs(array($board_phid))
-      ->executeOne();
-    if (!$project) {
-      return new Aphront404Response();
-    }
-
-    // Reload the object so it reflects edits which have been applied.
-    $object = id(new ManiphestTaskQuery())
-      ->setViewer($viewer)
-      ->withPHIDs(array($object_phid))
-      ->needProjectPHIDs(true)
-      ->executeOne();
-    if (!$object) {
-      return new Aphront404Response();
-    }
-
-    $except_phids = array($board_phid);
-    if ($project->getHasSubprojects() || $project->getHasMilestones()) {
-      $descendants = id(new PhabricatorProjectQuery())
-        ->setViewer($viewer)
-        ->withAncestorProjectPHIDs($except_phids)
-        ->execute();
-      foreach ($descendants as $descendant) {
-        $except_phids[] = $descendant->getPHID();
-      }
+    $request = $this->getRequest();
+    $visible_phids = $request->getStrList('visiblePHIDs');
+    if (!$visible_phids) {
+      $visible_phids = array();
     }
 
-    $rendering_engine = id(new PhabricatorBoardRenderingEngine())
+    return id(new PhabricatorBoardResponseEngine())
       ->setViewer($viewer)
-      ->setObjects(array($object))
-      ->setExcludedProjectPHIDs($except_phids);
-
-    $card = $rendering_engine->renderCard($object->getPHID());
-
-    $item = $card->getItem();
-    $item->addClass('phui-workcard');
-
-    return id(new AphrontAjaxResponse())
-      ->setContent(
-        array(
-          'objectPHID' => $object->getPHID(),
-          'cardHTML' => $item,
-        ));
+      ->setBoardPHID($board_phid)
+      ->setObjectPHID($object_phid)
+      ->setVisiblePHIDs($visible_phids)
+      ->buildResponse();
   }
 
 }
diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php
@@ -0,0 +1,146 @@
+<?php
+
+final class PhabricatorBoardResponseEngine extends Phobject {
+
+  private $viewer;
+  private $boardPHID;
+  private $objectPHID;
+  private $visiblePHIDs;
+
+  public function setViewer(PhabricatorUser $viewer) {
+    $this->viewer = $viewer;
+    return $this;
+  }
+
+  public function getViewer() {
+    return $this->viewer;
+  }
+
+  public function setBoardPHID($board_phid) {
+    $this->boardPHID = $board_phid;
+    return $this;
+  }
+
+  public function getBoardPHID() {
+    return $this->boardPHID;
+  }
+
+  public function setObjectPHID($object_phid) {
+    $this->objectPHID = $object_phid;
+    return $this;
+  }
+
+  public function getObjectPHID() {
+    return $this->objectPHID;
+  }
+
+  public function setVisiblePHIDs(array $visible_phids) {
+    $this->visiblePHIDs = $visible_phids;
+    return $this;
+  }
+
+  public function getVisiblePHIDs() {
+    return $this->visiblePHIDs;
+  }
+
+  public function buildResponse() {
+    $viewer = $this->getViewer();
+    $object_phid = $this->getObjectPHID();
+    $board_phid = $this->getBoardPHID();
+
+    // Load all the other tasks that are visible in the affected columns and
+    // perform layout for them.
+    $visible_phids = $this->getAllVisiblePHIDs();
+
+    $layout_engine = id(new PhabricatorBoardLayoutEngine())
+      ->setViewer($viewer)
+      ->setBoardPHIDs(array($board_phid))
+      ->setObjectPHIDs($visible_phids)
+      ->executeLayout();
+
+    $object_columns = $layout_engine->getObjectColumns(
+      $board_phid,
+      $object_phid);
+
+    $natural = array();
+    foreach ($object_columns as $column_phid => $column) {
+      $column_object_phids = $layout_engine->getColumnObjectPHIDs(
+        $board_phid,
+        $column_phid);
+      $natural[$column_phid] = array_values($column_object_phids);
+    }
+
+    $all_visible = id(new ManiphestTaskQuery())
+      ->setViewer($viewer)
+      ->withPHIDs($visible_phids)
+      ->execute();
+
+    $order_maps = array();
+    foreach ($all_visible as $visible) {
+      $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors();
+    }
+
+    $template = $this->buildTemplate();
+
+    $payload = array(
+      'objectPHID' => $object_phid,
+      'cardHTML' => $template,
+      'columnMaps' => $natural,
+      'orderMaps' => $order_maps,
+    );
+
+    return id(new AphrontAjaxResponse())
+      ->setContent($payload);
+  }
+
+  private function buildTemplate() {
+    $viewer = $this->getViewer();
+    $object_phid = $this->getObjectPHID();
+
+    $excluded_phids = $this->loadExcludedProjectPHIDs();
+
+    $object = id(new ManiphestTaskQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($object_phid))
+      ->needProjectPHIDs(true)
+      ->executeOne();
+    if (!$object) {
+      return new Aphront404Response();
+    }
+
+    $rendering_engine = id(new PhabricatorBoardRenderingEngine())
+      ->setViewer($viewer)
+      ->setObjects(array($object))
+      ->setExcludedProjectPHIDs($excluded_phids);
+
+    $card = $rendering_engine->renderCard($object_phid);
+
+    return hsprintf('%s', $card->getItem());
+  }
+
+  private function loadExcludedProjectPHIDs() {
+    $viewer = $this->getViewer();
+    $board_phid = $this->getBoardPHID();
+
+    $exclude_phids = array($board_phid);
+
+    $descendants = id(new PhabricatorProjectQuery())
+      ->setViewer($viewer)
+      ->withAncestorProjectPHIDs($exclude_phids)
+      ->execute();
+
+    foreach ($descendants as $descendant) {
+      $exclude_phids[] = $descendant->getPHID();
+    }
+
+    return array_fuse($exclude_phids);
+  }
+
+  private function getAllVisiblePHIDs() {
+    $visible_phids = $this->getVisiblePHIDs();
+    $visible_phids[] = $this->getObjectPHID();
+    $visible_phids = array_fuse($visible_phids);
+    return $visible_phids;
+  }
+
+}
diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php
--- a/src/applications/project/view/ProjectBoardTaskCard.php
+++ b/src/applications/project/view/ProjectBoardTaskCard.php
@@ -78,10 +78,6 @@
       ->setHref('/T'.$task->getID())
       ->addSigil('project-card')
       ->setDisabled($task->isClosed())
-      ->setMetadata(
-        array(
-          'objectPHID' => $task->getPHID(),
-        ))
       ->addAction(
         id(new PHUIListItemView())
         ->setName(pht('Edit'))
@@ -115,6 +111,8 @@
       $card->addAttribute($tag_list);
     }
 
+    $card->addClass('phui-workcard');
+
     return $card;
   }
 
diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js
deleted file mode 100644
--- a/webroot/rsrc/js/application/projects/Workboard.js
+++ /dev/null
@@ -1,242 +0,0 @@
-/**
- * @provides javelin-workboard
- * @requires javelin-install
- *           javelin-dom
- *           javelin-util
- *           javelin-vector
- *           javelin-stratcom
- *           javelin-workflow
- *           phabricator-draggable-list
- *           phabricator-drag-and-drop-file-upload
- * @javelin
- */
-
-JX.install('Workboard', {
-
-  construct: function(config) {
-    this._config = config;
-
-    this._boardNodes = {};
-    this._columnMap = {};
-  },
-
-  properties: {
-    uploadURI: null,
-    coverURI: null,
-    moveURI: null,
-    chunkThreshold: null
-  },
-
-  members: {
-    _config: null,
-    _boardNodes: null,
-    _currentBoard: null,
-
-    _panOrigin: null,
-    _panNode: null,
-    _panX: null,
-
-    _columnMap: null,
-
-    start: function() {
-      this._setupCoverImageHandlers();
-      this._setupPanHandlers();
-
-      return this;
-    },
-
-    addBoard: function(board_phid, board_node) {
-      this._currentBoard = board_phid;
-      this._boardNodes[board_phid] = board_node;
-      this._setupDragHandlers(board_node);
-    },
-
-    _getConfig: function() {
-      return this._config;
-    },
-
-    _setupCoverImageHandlers: function() {
-      if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) {
-        return;
-      }
-
-      var drop = new JX.PhabricatorDragAndDropFileUpload('project-card')
-        .setURI(this.getUploadURI())
-        .setChunkThreshold(this.getChunkThreshold());
-
-      drop.listen('didBeginDrag', function(node) {
-        JX.DOM.alterClass(node, 'phui-workcard-upload-target', true);
-      });
-
-      drop.listen('didEndDrag', function(node) {
-        JX.DOM.alterClass(node, 'phui-workcard-upload-target', false);
-      });
-
-      drop.listen('didUpload', JX.bind(this, this._oncoverupload));
-
-      drop.start();
-    },
-
-    _oncoverupload: function(file) {
-      var node = file.getTargetNode();
-      var board = JX.DOM.findAbove(node, 'div', 'jx-workboard');
-
-      var data = {
-        boardPHID: JX.Stratcom.getData(board).boardPHID,
-        objectPHID: JX.Stratcom.getData(node).objectPHID,
-        filePHID: file.getPHID()
-      };
-
-      new JX.Workflow(this.getCoverURI(), data)
-        .setHandler(JX.bind(this, this._queueCardUpdate))
-        .start();
-    },
-
-    _setupPanHandlers: function() {
-      var mousedown = JX.bind(this, this._onpanmousedown);
-      var mousemove = JX.bind(this, this._onpanmousemove);
-      var mouseup = JX.bind(this, this._onpanmouseup);
-
-      JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown);
-      JX.Stratcom.listen('mousemove', null, mousemove);
-      JX.Stratcom.listen('mouseup', null, mouseup);
-    },
-
-    _onpanmousedown: function(e) {
-      if (!JX.Device.isDesktop()) {
-        return;
-      }
-
-      if (e.getNode('workpanel')) {
-        return;
-      }
-
-      if (JX.Stratcom.pass()) {
-        return;
-      }
-
-      e.kill();
-
-      this._panOrigin = JX.$V(e);
-      this._panNode = e.getNode('workboard-shadow');
-      this._panX = this._panNode.scrollLeft;
-    },
-
-    _onpanmousemove: function(e) {
-      if (!this._panOrigin) {
-        return;
-      }
-
-      var cursor = JX.$V(e);
-      this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x);
-    },
-
-    _onpanmouseup: function() {
-      this._panOrigin = null;
-    },
-
-
-    _setupDragHandlers: function(board_node) {
-      var columns = this._findBoardColumns(board_node);
-      var column;
-      var ii;
-      var lists = [];
-
-      for (ii = 0; ii < columns.length; ii++) {
-        column = columns[ii];
-
-        var list = new JX.DraggableList('project-card', column)
-          .setOuterContainer(board_node)
-          .setFindItemsHandler(JX.bind(this, this._findCardsInColumn, column))
-          .setCanDragX(true)
-          .setHasInfiniteHeight(true);
-
-        // TODO: Restore these behaviors.
-        // list.listen('didSend', JX.bind(list, onupdate, cols[ii]));
-        // list.listen('didReceive', JX.bind(list, onupdate, cols[ii]));
-        // onupdate(cols[ii]);
-
-        list.listen('didDrop', JX.bind(this, this._onmovecard, list));
-
-        lists.push(list);
-      }
-
-      for (ii = 0; ii < lists.length; ii++) {
-        lists[ii].setGroup(lists);
-      }
-    },
-
-    _findBoardColumns: function(board_node) {
-      return JX.DOM.scry(board_node, 'ul', 'project-column');
-    },
-
-    _findCardsInColumn: function(column_node) {
-      return JX.DOM.scry(column_node, 'li', 'project-card');
-    },
-
-    _onmovecard: function(list, item, after_node) {
-      list.lock();
-      JX.DOM.alterClass(item, 'drag-sending', true);
-
-      var item_phid = JX.Stratcom.getData(item).objectPHID;
-      var data = {
-        objectPHID: item_phid,
-        columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID
-      };
-
-      if (after_node) {
-        data.afterPHID = JX.Stratcom.getData(after_node).objectPHID;
-      }
-
-      var before_node = item.nextSibling;
-      if (before_node) {
-        var before_phid = JX.Stratcom.getData(before_node).objectPHID;
-        if (before_phid) {
-          data.beforePHID = before_phid;
-        }
-      }
-
-      // TODO: This should be managed per-board.
-      var config = this._getConfig();
-      data.order = config.order;
-
-      new JX.Workflow(this.getMoveURI(), data)
-        .setHandler(JX.bind(this, this._oncardupdate, item, list))
-        .start();
-    },
-
-    _oncardupdate: function(item, list, response) {
-      list.unlock();
-      JX.DOM.alterClass(item, 'drag-sending', false);
-
-      this._queueCardUpdate(response);
-    },
-
-    _queueCardUpdate: function(response) {
-      var board_node = this._boardNodes[this._currentBoard];
-
-      var columns = this._findBoardColumns(board_node);
-      var cards;
-      var ii;
-      var jj;
-      var data;
-
-      for (ii = 0; ii < columns.length; ii++) {
-        cards = this._findCardsInColumn(columns[ii]);
-        for (jj = 0; jj < cards.length; jj++) {
-          data = JX.Stratcom.getData(cards[jj]);
-          if (data.objectPHID == response.objectPHID) {
-            this._replaceCard(cards[jj], JX.$H(response.cardHTML));
-          }
-        }
-      }
-
-    },
-
-    _replaceCard: function(old_node, new_node) {
-      JX.DOM.replace(old_node, new_node);
-    }
-
-  }
-
-});
diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js
@@ -0,0 +1,221 @@
+/**
+ * @provides javelin-workboard-board
+ * @requires javelin-install
+ *           javelin-dom
+ *           javelin-util
+ *           javelin-stratcom
+ *           javelin-workflow
+ *           phabricator-draggable-list
+ *           javelin-workboard-column
+ * @javelin
+ */
+
+JX.install('WorkboardBoard', {
+
+  construct: function(controller, phid, root) {
+    this._controller = controller;
+    this._phid = phid;
+    this._root = root;
+
+    this._templates = {};
+    this._orderMaps = {};
+    this._buildColumns();
+  },
+
+  properties: {
+    order: null,
+  },
+
+  members: {
+    _controller: null,
+    _phid: null,
+    _root: null,
+    _columns: null,
+    _templates: null,
+    _orderMaps: null,
+
+    getRoot: function() {
+      return this._root;
+    },
+
+    getColumns: function() {
+      return this._columns;
+    },
+
+    getColumn: function(k) {
+      return this._columns[k];
+    },
+
+    getPHID: function() {
+      return this._phid;
+    },
+
+    setCardTemplate: function(phid, template)  {
+      this._templates[phid] = template;
+      return this;
+    },
+
+    getCardTemplate: function(phid) {
+      return this._templates[phid];
+    },
+
+    getController: function() {
+      return this._controller;
+    },
+
+    setOrderMap: function(phid, map) {
+      this._orderMaps[phid] = map;
+      return this;
+    },
+
+    getOrderVector: function(phid, key) {
+      return this._orderMaps[phid][key];
+    },
+
+    start: function() {
+      this._setupDragHandlers();
+
+      for (var k in this._columns) {
+        this._columns[k].redraw();
+      }
+    },
+
+    _buildColumns: function() {
+      var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column');
+
+      this._columns = {};
+      for (var ii = 0; ii < nodes.length; ii++) {
+        var node = nodes[ii];
+        var data = JX.Stratcom.getData(node);
+        var phid = data.columnPHID;
+
+        this._columns[phid] = new JX.WorkboardColumn(this, phid, node);
+      }
+    },
+
+    _setupDragHandlers: function() {
+      var columns = this.getColumns();
+
+      var lists = [];
+      for (var k in columns) {
+        var column = columns[k];
+
+        var list = new JX.DraggableList('project-card', column.getRoot())
+          .setOuterContainer(this.getRoot())
+          .setFindItemsHandler(JX.bind(column, column.getCardNodes))
+          .setCanDragX(true)
+          .setHasInfiniteHeight(true);
+
+        list.listen('didDrop', JX.bind(this, this._onmovecard, list));
+
+        lists.push(list);
+      }
+
+      for (var ii = 0; ii < lists.length; ii++) {
+        lists[ii].setGroup(lists);
+      }
+    },
+
+    _findCardsInColumn: function(column_node) {
+      return JX.DOM.scry(column_node, 'li', 'project-card');
+    },
+
+    _onmovecard: function(list, item, after_node, src_list) {
+      list.lock();
+      JX.DOM.alterClass(item, 'drag-sending', true);
+
+      var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;
+      var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID;
+
+      var item_phid = JX.Stratcom.getData(item).objectPHID;
+      var data = {
+        objectPHID: item_phid,
+        columnPHID: dst_phid,
+        order: this.getOrder()
+      };
+
+      if (after_node) {
+        data.afterPHID = JX.Stratcom.getData(after_node).objectPHID;
+      }
+
+      var before_node = item.nextSibling;
+      if (before_node) {
+        var before_phid = JX.Stratcom.getData(before_node).objectPHID;
+        if (before_phid) {
+          data.beforePHID = before_phid;
+        }
+      }
+
+      var visible_phids = [];
+      var column = this.getColumn(dst_phid);
+      for (var object_phid in column.getCards()) {
+        visible_phids.push(object_phid);
+      }
+
+      data.visiblePHIDs = visible_phids.join(',');
+
+      var onupdate = JX.bind(
+        this,
+        this._oncardupdate,
+        list,
+        src_phid,
+        dst_phid,
+        data.afterPHID);
+
+      new JX.Workflow(this.getController().getMoveURI(), data)
+        .setHandler(onupdate)
+        .start();
+    },
+
+    _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) {
+      var src_column = this.getColumn(src_phid);
+      var dst_column = this.getColumn(dst_phid);
+
+      var card = src_column.removeCard(response.objectPHID);
+      dst_column.addCard(card, after_phid);
+
+      this.updateCard(response);
+
+      list.unlock();
+    },
+
+    updateCard: function(response) {
+      var columns = this.getColumns();
+
+      var phid = response.objectPHID;
+
+      if (!this._templates[phid]) {
+        for (var add_phid in response.columnMaps) {
+          this.getColumn(add_phid).newCard(phid);
+        }
+      }
+
+      this.setCardTemplate(phid, response.cardHTML);
+
+      var order_maps = response.orderMaps;
+      for (var order_phid in order_maps) {
+        this.setOrderMap(order_phid, order_maps[order_phid]);
+      }
+
+      var column_maps = response.columnMaps;
+      for (var natural_phid in column_maps) {
+        this.getColumn(natural_phid).setNaturalOrder(column_maps[natural_phid]);
+      }
+
+      for (var column_phid in columns) {
+        var cards = columns[column_phid].getCards();
+        for (var object_phid in cards) {
+          if (object_phid !== phid) {
+            continue;
+          }
+
+          var card = cards[object_phid];
+          card.redraw();
+        }
+        columns[column_phid].redraw();
+      }
+    }
+
+  }
+
+});
diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardCard.js
@@ -0,0 +1,56 @@
+/**
+ * @provides javelin-workboard-card
+ * @requires javelin-install
+ * @javelin
+ */
+
+JX.install('WorkboardCard', {
+
+  construct: function(column, phid) {
+    this._column = column;
+    this._phid = phid;
+  },
+
+  members: {
+    _column: null,
+    _phid: null,
+    _root: null,
+
+    getPHID: function() {
+      return this._phid;
+    },
+
+    getColumn: function() {
+      return this._column;
+    },
+
+    setColumn: function(column) {
+      this._column = column;
+    },
+
+    getNode: function() {
+      if (!this._root) {
+        var phid = this.getPHID();
+        var template = this.getColumn().getBoard().getCardTemplate(phid);
+        this._root = JX.$H(template).getFragment().firstChild;
+
+        JX.Stratcom.getData(this._root).objectPHID = this.getPHID();
+      }
+      return this._root;
+    },
+
+    redraw: function() {
+      var old_node = this._root;
+      this._root = null;
+      var new_node = this.getNode();
+
+      if (old_node && old_node.parentNode) {
+        JX.DOM.replace(old_node, new_node);
+      }
+
+      return this;
+    }
+
+  }
+
+});
diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js
@@ -0,0 +1,177 @@
+/**
+ * @provides javelin-workboard-column
+ * @requires javelin-install
+ *           javelin-workboard-card
+ * @javelin
+ */
+
+JX.install('WorkboardColumn', {
+
+  construct: function(board, phid, root) {
+    this._board = board;
+    this._phid = phid;
+    this._root = root;
+
+    this._cards = {};
+    this._naturalOrder = [];
+  },
+
+  members: {
+    _phid: null,
+    _root: null,
+    _board: null,
+    _cards: null,
+    _naturalOrder: null,
+
+    getPHID: function() {
+      return this._phid;
+    },
+
+    getRoot: function() {
+      return this._root;
+    },
+
+    getCards: function() {
+      return this._cards;
+    },
+
+    getCard: function(phid) {
+      return this._cards[phid];
+    },
+
+    getBoard: function() {
+      return this._board;
+    },
+
+    setNaturalOrder: function(order) {
+      this._naturalOrder = order;
+      return this;
+    },
+
+    newCard: function(phid) {
+      var card = new JX.WorkboardCard(this, phid);
+
+      this._cards[phid] = card;
+      this._naturalOrder.push(phid);
+
+      return card;
+    },
+
+    removeCard: function(phid) {
+      var card = this._cards[phid];
+      delete this._cards[phid];
+
+      for (var ii = 0; ii < this._naturalOrder.length; ii++) {
+        if (this._naturalOrder[ii] == phid) {
+          this._naturalOrder.splice(ii, 1);
+          break;
+        }
+      }
+
+      return card;
+    },
+
+    addCard: function(card, after) {
+      var phid = card.getPHID();
+
+      card.setColumn(this);
+      this._cards[phid] = card;
+
+      var index = 0;
+
+      if (after) {
+        for (var ii = 0; ii < this._naturalOrder.length; ii++) {
+          if (this._naturalOrder[ii] == after) {
+            index = ii + 1;
+            break;
+          }
+        }
+      }
+
+      if (index > this._naturalOrder.length) {
+        this._naturalOrder.push(phid);
+      } else {
+        this._naturalOrder.splice(index, 0, phid);
+      }
+
+      return this;
+    },
+
+    getCardNodes: function() {
+      var cards = this.getCards();
+
+      var nodes = [];
+      for (var k in cards) {
+        nodes.push(cards[k].getNode());
+      }
+
+      return nodes;
+    },
+
+    getCardPHIDs: function() {
+      return JX.keys(this.getCards());
+    },
+
+    redraw: function() {
+      var order = this.getBoard().getOrder();
+
+      var list;
+      if (order == 'natural') {
+        list = this._getCardsSortedNaturally();
+      } else {
+        list = this._getCardsSortedByKey(order);
+      }
+
+      var content = [];
+      for (var ii = 0; ii < list.length; ii++) {
+        var node = list[ii].getNode();
+        content.push(node);
+      }
+
+      JX.DOM.setContent(this.getRoot(), content);
+    },
+
+    _getCardsSortedNaturally: function() {
+      var list = [];
+
+      for (var ii = 0; ii < this._naturalOrder.length; ii++) {
+        var phid = this._naturalOrder[ii];
+        list.push(this.getCard(phid));
+      }
+
+      return list;
+    },
+
+    _getCardsSortedByKey: function(order) {
+      var cards = this.getCards();
+
+      var list = [];
+      for (var k in cards) {
+        list.push(cards[k]);
+      }
+
+      list.sort(JX.bind(this, this._sortCards, order));
+
+      return list;
+    },
+
+    _sortCards: function(order, u, v) {
+      var ud = this.getBoard().getOrderVector(u.getPHID(), order);
+      var vd = this.getBoard().getOrderVector(v.getPHID(), order);
+
+      for (var ii = 0; ii < ud.length; ii++) {
+        if (ud[ii] > vd[ii]) {
+          return 1;
+        }
+
+        if (ud[ii] < vd[ii]) {
+          return -1;
+        }
+      }
+
+      return 0;
+    }
+
+  }
+
+});
diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardController.js
@@ -0,0 +1,201 @@
+/**
+ * @provides javelin-workboard-controller
+ * @requires javelin-install
+ *           javelin-dom
+ *           javelin-util
+ *           javelin-vector
+ *           javelin-stratcom
+ *           javelin-workflow
+ *           phabricator-drag-and-drop-file-upload
+ *           javelin-workboard-board
+ * @javelin
+ */
+
+JX.install('WorkboardController', {
+
+  construct: function() {
+    this._boards = {};
+  },
+
+  properties: {
+    uploadURI: null,
+    coverURI: null,
+    moveURI: null,
+    createURI: null,
+    chunkThreshold: null
+  },
+
+  members: {
+    _boards: null,
+
+    _panOrigin: null,
+    _panNode: null,
+    _panX: null,
+
+    start: function() {
+      this._setupCoverImageHandlers();
+      this._setupPanHandlers();
+      this._setupEditHandlers();
+
+      return this;
+    },
+
+    newBoard: function(phid, node) {
+      var board = new JX.WorkboardBoard(this, phid, node);
+      this._boards[phid] = board;
+      return board;
+    },
+
+    _getBoard: function(board_phid) {
+      return this._boards[board_phid];
+    },
+
+    _setupCoverImageHandlers: function() {
+      if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) {
+        return;
+      }
+
+      var drop = new JX.PhabricatorDragAndDropFileUpload('project-card')
+        .setURI(this.getUploadURI())
+        .setChunkThreshold(this.getChunkThreshold());
+
+      drop.listen('didBeginDrag', function(node) {
+        JX.DOM.alterClass(node, 'phui-workcard-upload-target', true);
+      });
+
+      drop.listen('didEndDrag', function(node) {
+        JX.DOM.alterClass(node, 'phui-workcard-upload-target', false);
+      });
+
+      drop.listen('didUpload', JX.bind(this, this._oncoverupload));
+
+      drop.start();
+    },
+
+    _oncoverupload: function(file) {
+      var node = file.getTargetNode();
+
+      var board = this._getBoardFromNode(node);
+
+      var column_node = JX.DOM.findAbove(node, 'ul', 'project-column');
+      var column_phid = JX.Stratcom.getData(column_node).columnPHID;
+      var column = board.getColumn(column_phid);
+
+      var data = {
+        boardPHID: board.getPHID(),
+        objectPHID: JX.Stratcom.getData(node).objectPHID,
+        filePHID: file.getPHID(),
+        visiblePHIDs: column.getCardPHIDs()
+      };
+
+      new JX.Workflow(this.getCoverURI(), data)
+        .setHandler(JX.bind(board, board.updateCard))
+        .start();
+    },
+
+    _getBoardFromNode: function(node) {
+      var board_node = JX.DOM.findAbove(node, 'div', 'jx-workboard');
+      var board_phid = JX.Stratcom.getData(board_node).boardPHID;
+      return this._getBoard(board_phid);
+    },
+
+    _setupPanHandlers: function() {
+      var mousedown = JX.bind(this, this._onpanmousedown);
+      var mousemove = JX.bind(this, this._onpanmousemove);
+      var mouseup = JX.bind(this, this._onpanmouseup);
+
+      JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown);
+      JX.Stratcom.listen('mousemove', null, mousemove);
+      JX.Stratcom.listen('mouseup', null, mouseup);
+    },
+
+    _onpanmousedown: function(e) {
+      if (!JX.Device.isDesktop()) {
+        return;
+      }
+
+      if (e.getNode('workpanel')) {
+        return;
+      }
+
+      if (JX.Stratcom.pass()) {
+        return;
+      }
+
+      e.kill();
+
+      this._panOrigin = JX.$V(e);
+      this._panNode = e.getNode('workboard-shadow');
+      this._panX = this._panNode.scrollLeft;
+    },
+
+    _onpanmousemove: function(e) {
+      if (!this._panOrigin) {
+        return;
+      }
+
+      var cursor = JX.$V(e);
+      this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x);
+    },
+
+    _onpanmouseup: function() {
+      this._panOrigin = null;
+    },
+
+    _setupEditHandlers: function() {
+      var onadd = JX.bind(this, this._onaddcard);
+      var onedit = JX.bind(this, this._oneditcard);
+
+      JX.Stratcom.listen('click', 'column-add-task', onadd);
+      JX.Stratcom.listen('click', 'edit-project-card', onedit);
+    },
+
+    _onaddcard: function(e) {
+      // We want the 'boards-dropdown-menu' behavior to see this event and
+      // close the dropdown, but don't want to follow the link.
+      e.prevent();
+
+      var column_data = e.getNodeData('column-add-task');
+      var column_phid = column_data.columnPHID;
+
+      var board_phid = column_data.projectPHID;
+      var board = this._getBoard(board_phid);
+      var column = board.getColumn(column_phid);
+
+      var request_data = {
+        responseType: 'card',
+        columnPHID: column.getPHID(),
+        projects: board.getPHID(),
+        visiblePHIDs: column.getCardPHIDs(),
+        order: board.getOrder()
+      };
+
+      new JX.Workflow(this.getCreateURI(), request_data)
+        .setHandler(JX.bind(board, board.updateCard))
+        .start();
+    },
+
+    _oneditcard: function(e) {
+      e.kill();
+
+      var column_node = e.getNode('project-column');
+      var column_phid = JX.Stratcom.getData(column_node).columnPHID;
+
+      var board = this._getBoardFromNode(column_node);
+      var column = board.getColumn(column_phid);
+
+      var request_data = {
+        responseType: 'card',
+        columnPHID: column.getPHID(),
+        visiblePHIDs: column.getCardPHIDs(),
+        order: board.getOrder()
+      };
+
+      new JX.Workflow(e.getNode('tag:a').href, request_data)
+        .setHandler(JX.bind(board, board.updateCard))
+        .start();
+    }
+
+  }
+
+});
diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js
--- a/webroot/rsrc/js/application/projects/behavior-project-boards.js
+++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js
@@ -6,9 +6,7 @@
  *           javelin-vector
  *           javelin-stratcom
  *           javelin-workflow
- *           phabricator-draggable-list
- *           phabricator-drag-and-drop-file-upload
- *           javelin-workboard
+ *           javelin-workboard-controller
  */
 
 JX.behavior('project-boards', function(config, statics) {
@@ -61,66 +59,6 @@
     }
   }
 
-
-  function colsort(u, v) {
-    var ud = JX.Stratcom.getData(u).sort || [];
-    var vd = JX.Stratcom.getData(v).sort || [];
-
-    for (var ii = 0; ii < ud.length; ii++) {
-
-      if (parseInt(ud[ii]) < parseInt(vd[ii])) {
-        return 1;
-      }
-      if (parseInt(ud[ii]) > parseInt(vd[ii])) {
-        return -1;
-      }
-    }
-
-    return 0;
-  }
-
-  function onedit(column, r) {
-    var new_card = JX.$H(r.tasks).getNode();
-    var new_data = JX.Stratcom.getData(new_card);
-    var items = finditems(column);
-    var edited = false;
-    var remove_index = null;
-
-    for (var ii = 0; ii < items.length; ii++) {
-      var item = items[ii];
-
-      var data = JX.Stratcom.getData(item);
-      var phid = data.objectPHID;
-
-      if (phid == new_data.objectPHID) {
-        if (r.data.removeFromBoard) {
-          remove_index = ii;
-        }
-        items[ii] = new_card;
-        data = new_data;
-        edited = true;
-      }
-
-      data.sort = r.data.sortMap[data.objectPHID] || data.sort;
-    }
-
-    // this is an add then...!
-    if (!edited) {
-      items[items.length + 1] = new_card;
-      new_data.sort = r.data.sortMap[new_data.objectPHID] || new_data.sort;
-    }
-
-    if (remove_index !== null) {
-      items.splice(remove_index, 1);
-    }
-
-    items.sort(colsort);
-
-    JX.DOM.setContent(column, items);
-
-    onupdate(column);
-  };
-
   function update_statics(update_config) {
     statics.boardID = update_config.boardID;
     statics.projectPHID = update_config.projectPHID;
@@ -130,56 +68,6 @@
   }
 
   function setup() {
-
-    JX.Stratcom.listen(
-      'click',
-      ['edit-project-card'],
-      function(e) {
-        e.kill();
-        var column = e.getNode('project-column');
-        var request_data = {
-          responseType: 'card',
-          columnPHID: JX.Stratcom.getData(column).columnPHID,
-          order: statics.order
-        };
-        new JX.Workflow(e.getNode('tag:a').href, request_data)
-          .setHandler(JX.bind(null, onedit, column))
-          .start();
-      });
-
-    JX.Stratcom.listen(
-      'click',
-      ['column-add-task'],
-      function (e) {
-
-        // We want the 'boards-dropdown-menu' behavior to see this event and
-        // close the dropdown, but don't want to follow the link.
-        e.prevent();
-
-        var column_data = e.getNodeData('column-add-task');
-        var column_phid = column_data.columnPHID;
-
-        var request_data = {
-          responseType: 'card',
-          columnPHID: column_phid,
-          projects: column_data.projectPHID,
-          order: statics.order
-        };
-
-        var cols = getcolumns();
-        var ii;
-        var column;
-        for (ii = 0; ii < cols.length; ii++) {
-          if (JX.Stratcom.getData(cols[ii]).columnPHID == column_phid) {
-            column = cols[ii];
-            break;
-          }
-        }
-        new JX.Workflow(statics.createURI, request_data)
-          .setHandler(JX.bind(null, onedit, column))
-          .start();
-      });
-
     JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) {
       var data = e.getNodeData('boards-dropdown-menu');
       if (data.menu) {
@@ -234,14 +122,40 @@
   }
 
   if (!statics.workboard) {
-    statics.workboard = new JX.Workboard(config)
+    statics.workboard = new JX.WorkboardController()
       .setUploadURI(config.uploadURI)
       .setCoverURI(config.coverURI)
       .setMoveURI(config.moveURI)
+      .setCreateURI(config.createURI)
       .setChunkThreshold(config.chunkThreshold)
       .start();
   }
 
-  statics.workboard.addBoard(config.projectPHID, JX.$(config.boardID));
+  var board_phid = config.projectPHID;
+  var board_node = JX.$(config.boardID);
+
+  var board = statics.workboard.newBoard(board_phid, board_node)
+    .setOrder(config.order);
+
+  var templates = config.templateMap;
+  for (var k in templates) {
+    board.setCardTemplate(k, templates[k]);
+  }
+
+  var column_maps = config.columnMaps;
+  for (var column_phid in column_maps) {
+    var column = board.getColumn(column_phid);
+    var column_map = column_maps[column_phid];
+    for (var ii = 0; ii < column_map.length; ii++) {
+      column.newCard(column_map[ii]);
+    }
+  }
+
+  var order_maps = config.orderMaps;
+  for (var object_phid in order_maps) {
+    board.setOrderMap(object_phid, order_maps[object_phid]);
+  }
+
+  board.start();
 
 });