diff --git a/resources/sql/patches/064.subporjects.sql b/resources/sql/patches/064.subporjects.sql
new file mode 100644
index 0000000000..379e1bdc77
--- /dev/null
+++ b/resources/sql/patches/064.subporjects.sql
@@ -0,0 +1,11 @@
+ALTER TABLE phabricator_project.project
+  ADD subprojectPHIDs longblob NOT NULL;
+UPDATE phabricator_project.project
+  SET subprojectPHIDs = '[]';
+  
+CREATE TABLE phabricator_project.project_subproject (
+  projectPHID varchar(64) BINARY NOT NULL,
+  subprojectPHID varchar(64) BINARY NOT NULL,
+  PRIMARY KEY (subprojectPHID, projectPHID),
+  UNIQUE KEY (projectPHID, subprojectPHID)
+);
\ No newline at end of file
diff --git a/src/applications/project/controller/list/PhabricatorProjectListController.php b/src/applications/project/controller/list/PhabricatorProjectListController.php
index 05faacf307..25eaf353c7 100644
--- a/src/applications/project/controller/list/PhabricatorProjectListController.php
+++ b/src/applications/project/controller/list/PhabricatorProjectListController.php
@@ -1,136 +1,134 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class PhabricatorProjectListController
   extends PhabricatorProjectController {
 
   public function processRequest() {
 
     $projects = id(new PhabricatorProject())->loadAllWhere(
       '1 = 1 ORDER BY id DESC limit 100');
     $project_phids = mpull($projects, 'getPHID');
 
     $profiles = array();
     if ($projects) {
       $profiles = id(new PhabricatorProjectProfile())->loadAllWhere(
         'projectPHID in (%Ls)',
         $project_phids);
       $profiles = mpull($profiles, null, 'getProjectPHID');
     }
 
     $affil_groups = array();
     if ($projects) {
       $affil_groups = PhabricatorProjectAffiliation::loadAllForProjectPHIDs(
         $project_phids);
     }
 
     $author_phids = mpull($projects, 'getAuthorPHID');
     $handles = id(new PhabricatorObjectHandleData($author_phids))
       ->loadHandles();
 
-    $project_phids = mpull($projects, 'getPHID');
-
     $query = id(new ManiphestTaskQuery())
       ->withProjects($project_phids)
       ->withAnyProject(true)
       ->withStatus(ManiphestTaskQuery::STATUS_OPEN)
       ->setLimit(PHP_INT_MAX);
 
     $tasks = $query->execute();
     $groups = array();
     foreach ($tasks as $task) {
       foreach ($task->getProjectPHIDs() as $phid) {
         $groups[$phid][] = $task;
       }
     }
 
 
     $rows = array();
     foreach ($projects as $project) {
       $phid = $project->getPHID();
 
       $profile = $profiles[$phid];
       $affiliations = $affil_groups[$phid];
 
       $group = idx($groups, $phid, array());
       $task_count = count($group);
 
       $population = count($affiliations);
 
       $status = PhabricatorProjectStatus::getNameForStatus(
         $project->getStatus());
 
       $blurb = $profile->getBlurb();
       $blurb = phutil_utf8_shorten($blurb, $columns = 100);
 
       $rows[] = array(
         phutil_escape_html($project->getName()),
         phutil_escape_html($blurb),
         $handles[$project->getAuthorPHID()]->renderLink(),
         phutil_escape_html($population),
         phutil_escape_html($status),
         phutil_render_tag(
           'a',
           array(
             'href' => '/maniphest/view/all/?projects='.$phid,
           ),
           phutil_escape_html($task_count)),
         phutil_render_tag(
           'a',
           array(
             'class' => 'small grey button',
             'href' => '/project/view/'.$project->getID().'/',
           ),
           'View Project Profile'),
       );
     }
 
     $table = new AphrontTableView($rows);
     $table->setHeaders(
       array(
         'Project',
         'Description',
         'Mastermind',
         'Population',
         'Status',
         'Open Tasks',
         '',
       ));
     $table->setColumnClasses(
       array(
         'pri',
         'wide',
         '',
         'right',
         '',
         'right',
         'action',
       ));
 
     $panel = new AphrontPanelView();
     $panel->appendChild($table);
     $panel->setHeader('Project');
     $panel->setCreateButton('Create New Project', '/project/create/');
 
     return $this->buildStandardPageResponse(
       $panel,
       array(
         'title' => 'Projects',
       ));
   }
 }
diff --git a/src/applications/project/controller/profile/PhabricatorProjectProfileController.php b/src/applications/project/controller/profile/PhabricatorProjectProfileController.php
index 01d336326c..ed5ac31534 100644
--- a/src/applications/project/controller/profile/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/profile/PhabricatorProjectProfileController.php
@@ -1,237 +1,297 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class PhabricatorProjectProfileController
   extends PhabricatorProjectController {
 
   private $id;
   private $page;
 
   public function willProcessRequest(array $data) {
     $this->id = idx($data, 'id');
     $this->page = idx($data, 'page');
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
     $uri = $request->getRequestURI();
 
     $project = id(new PhabricatorProject())->load($this->id);
     if (!$project) {
       return new Aphront404Response();
     }
     $profile = $project->loadProfile();
     if (!$profile) {
       $profile = new PhabricatorProjectProfile();
     }
 
     $src_phid = $profile->getProfileImagePHID();
     if (!$src_phid) {
       $src_phid = $user->getProfileImagePHID();
     }
     $picture = PhabricatorFileURI::getViewURIForPHID($src_phid);
 
     $pages = array(
       /*
       '<h2>Active Documents</h2>',
       'tasks'        => 'Maniphest Tasks',
       'revisions'    => 'Differential Revisions',
       '<hr />',
       '<h2>Workflow</h2>',
       'goals'        => 'Goals',
       'statistics'   => 'Statistics',
       '<hr />', */
       '<h2>Information</h2>',
-      'edit'         => 'Edit Profile',
+      'edit'         => 'Edit Project',
       'affiliation'  => 'Edit Affiliation',
     );
 
     if (empty($pages[$this->page])) {
-      $this->page = 'action';   // key($pages);
+      $this->page = 'action';
     }
 
     switch ($this->page) {
       default:
         $content = $this->renderBasicInformation($project, $profile);
         break;
     }
 
     $profile = new PhabricatorProfileView();
     $profile->setProfilePicture($picture);
     $profile->setProfileNames($project->getName());
     foreach ($pages as $page => $name) {
       if (is_integer($page)) {
         $profile->addProfileItem(
           phutil_render_tag(
             'span',
             array(),
             $name));
       } else {
         $uri->setPath('/project/'.$page.'/'.$project->getID().'/');
         $profile->addProfileItem(
           phutil_render_tag(
             'a',
             array(
               'href' => $uri,
               'class' => ($this->page == $page)
                 ? 'phabricator-profile-item-selected'
                 : null,
             ),
             phutil_escape_html($name)));
       }
     }
 
     $profile->appendChild($content);
 
     return $this->buildStandardPageResponse(
       $profile,
       array(
         'title' => $project->getName(),
         ));
   }
 
+  //----------------------------------------------------------------------------
+  // Helper functions
+
   private function renderBasicInformation($project, $profile) {
     $blurb = nonempty(
        $profile->getBlurb(),
        '//Nothing is known about this elusive project.//');
 
     $engine = PhabricatorMarkupEngine::newProfileMarkupEngine();
     $blurb = $engine->markupText($blurb);
 
     $affiliations = $project->loadAffiliations();
 
     $phids = array_merge(
       array($project->getAuthorPHID()),
-      mpull($affiliations, 'getUserPHID'));
+      $project->getSubprojectPHIDs(),
+      mpull($affiliations, 'getUserPHID')
+    );
+    $phids = array_unique($phids);
     $handles = id(new PhabricatorObjectHandleData($phids))
       ->loadHandles();
 
     $affiliated = array();
     foreach ($affiliations as $affiliation) {
       $user = $handles[$affiliation->getUserPHID()]->renderLink();
       $role = phutil_escape_html($affiliation->getRole());
 
       $status = null;
       if ($affiliation->getStatus() == 'former') {
         $role = '<em>Former '.$role.'</em>';
       }
 
       $affiliated[] = '<li>'.$user.' &mdash; '.$role.$status.'</li>';
     }
+
     if ($affiliated) {
       $affiliated = '<ul>'.implode("\n", $affiliated).'</ul>';
     } else {
       $affiliated = '<p><em>No one is affiliated with this project.</em></p>';
     }
 
+    if ($project->getSubprojectPHIDs()) {
+      $table = $this->renderSubprojectTable(
+        $handles,
+        $project->getSubprojectPHIDs());
+      $subproject_list = $table->render();
+    } else {
+      $subproject_list =
+        '<p><em>There are no projects attached for such specie.</em></p>';
+    }
+
+
     $timestamp = phabricator_format_timestamp($project->getDateCreated());
     $status = PhabricatorProjectStatus::getNameForStatus(
       $project->getStatus());
 
     $content =
       '<div class="phabricator-profile-info-group">
         <h1 class="phabricator-profile-info-header">Basic Information</h1>
         <div class="phabricator-profile-info-pane">
           <table class="phabricator-profile-info-table">
             <tr>
               <th>Creator</th>
               <td>'.$handles[$project->getAuthorPHID()]->renderLink().'</td>
             </tr>
             <tr>
               <th>Status</th>
               <td><strong>'.phutil_escape_html($status).'</strong></td>
             </tr>
             <tr>
               <th>Created</th>
               <td>'.$timestamp.'</td>
             </tr>
             <tr>
               <th>PHID</th>
               <td>'.phutil_escape_html($project->getPHID()).'</td>
             </tr>
             <tr>
               <th>Blurb</th>
               <td>'.$blurb.'</td>
             </tr>
           </table>
         </div>
       </div>';
 
     $content .=
-      '<div class="phabricator-profile-info-group">
-        <h1 class="phabricator-profile-info-header">Resources</h1>
-        <div class="phabricator-profile-info-pane">'.
+      '<div class="phabricator-profile-info-group">'.
+        '<h1 class="phabricator-profile-info-header">Resources</h1>'.
+        '<div class="phabricator-profile-info-pane">'.
          $affiliated.
-        '</div>
-      </div>';
+        '</div>'.
+      '</div>';
+
+    $content .= '<div class="phabricator-profile-info-group">'.
+      '<h1 class="phabricator-profile-info-header">Subprojects</h1>'.
+      '<div class="phabricator-profile-info-pane">'.
+        $subproject_list.
+        '</div>'.
+      '</div>';
 
     $query = id(new ManiphestTaskQuery())
       ->withProjects(array($project->getPHID()))
       ->withStatus(ManiphestTaskQuery::STATUS_OPEN)
       ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
       ->setLimit(10)
       ->setCalculateRows(true);
     $tasks = $query->execute();
     $count = $query->getRowCount();
 
     $phids = mpull($tasks, 'getOwnerPHID');
     $phids = array_filter($phids);
     $handles = id(new PhabricatorObjectHandleData($phids))
       ->loadHandles();
 
     $task_views = array();
     foreach ($tasks as $task) {
       $view = id(new ManiphestTaskSummaryView())
         ->setTask($task)
         ->setHandles($handles)
         ->setUser($this->getRequest()->getUser());
       $task_views[] = $view->render();
     }
 
     if (empty($tasks)) {
       $task_views = '<em>No open tasks.</em>';
     } else {
       $task_views = implode('', $task_views);
     }
 
     $open = number_format($count);
 
     $more_link = phutil_render_tag(
       'a',
       array(
         'href' => '/maniphest/view/all/?projects='.$project->getPHID(),
       ),
       "View All Open Tasks \xC2\xBB");
 
     $content .=
       '<div class="phabricator-profile-info-group">
         <h1 class="phabricator-profile-info-header">'.
           "Open Tasks ({$open})".
         '</h1>'.
         '<div class="phabricator-profile-info-pane">'.
           $task_views.
           '<div class="phabricator-profile-info-pane-more-link">'.
             $more_link.
           '</div>'.
         '</div>
       </div>';
 
     return $content;
   }
+
+  private function renderSubprojectTable(
+    PhabricatorObjectHandleData $handles,
+    $subprojects_phids) {
+
+    $rows = array();
+    foreach ($subprojects_phids as $subproject_phid) {
+      $phid = $handles[$subproject_phid]->getPHID();
+
+      $rows[] = array(
+        phutil_escape_html($handles[$phid]->getFullName()),
+        phutil_render_tag(
+          'a',
+          array(
+            'class' => 'small grey button',
+            'href' => $handles[$phid]->getURI(),
+          ),
+          'View Project Profile'),
+      );
+    }
+
+    $table = new AphrontTableView($rows);
+     $table->setHeaders(
+       array(
+         'Name',
+         '',
+       ));
+     $table->setColumnClasses(
+       array(
+         'pri',
+         'action right',
+       ));
+
+    return $table;
+  }
 }
diff --git a/src/applications/project/controller/profile/__init__.php b/src/applications/project/controller/profile/__init__.php
index 0a31f17672..b9241382bc 100644
--- a/src/applications/project/controller/profile/__init__.php
+++ b/src/applications/project/controller/profile/__init__.php
@@ -1,26 +1,27 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'aphront/response/404');
 phutil_require_module('phabricator', 'applications/files/uri');
 phutil_require_module('phabricator', 'applications/maniphest/query');
 phutil_require_module('phabricator', 'applications/maniphest/view/tasksummary');
 phutil_require_module('phabricator', 'applications/markup/engine');
 phutil_require_module('phabricator', 'applications/phid/handle/data');
 phutil_require_module('phabricator', 'applications/project/constants/status');
 phutil_require_module('phabricator', 'applications/project/controller/base');
 phutil_require_module('phabricator', 'applications/project/storage/profile');
 phutil_require_module('phabricator', 'applications/project/storage/project');
+phutil_require_module('phabricator', 'view/control/table');
 phutil_require_module('phabricator', 'view/layout/profile');
 phutil_require_module('phabricator', 'view/utils');
 
 phutil_require_module('phutil', 'markup');
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('PhabricatorProjectProfileController.php');
diff --git a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php
index 55b0c1c218..ae9710eca2 100644
--- a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php
+++ b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php
@@ -1,147 +1,163 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class PhabricatorProjectProfileEditController
   extends PhabricatorProjectController {
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
   }
 
   public function processRequest() {
 
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $project = id(new PhabricatorProject())->load($this->id);
     if (!$project) {
       return new Aphront404Response();
     }
     $profile = $project->loadProfile();
 
     if (empty($profile)) {
       $profile = new PhabricatorProjectProfile();
     }
 
+    if ($project->getSubprojectPHIDs()) {
+      $phids = $project->getSubprojectPHIDs();
+      $handles = id(new PhabricatorObjectHandleData($phids))
+        ->loadHandles();
+      $subprojects = mpull($handles, 'getFullName', 'getPHID');
+    } else {
+      $subprojects = array();
+    }
+
     $options = PhabricatorProjectStatus::getStatusMap();
 
     $e_name = true;
     $errors = array();
     if ($request->isFormPost()) {
       $project->setName($request->getStr('name'));
       $project->setStatus($request->getStr('status'));
+      $project->setSubprojectPHIDs($request->getArr('set_subprojects'));
       $profile->setBlurb($request->getStr('blurb'));
 
       if (!strlen($project->getName())) {
         $e_name = 'Required';
         $errors[] = 'Project name is required.';
       } else {
         $e_name = null;
       }
 
       if (!empty($_FILES['image'])) {
         $err = idx($_FILES['image'], 'error');
         if ($err != UPLOAD_ERR_NO_FILE) {
           $file = PhabricatorFile::newFromPHPUpload(
             $_FILES['image'],
             array(
               'authorPHID' => $user->getPHID(),
             ));
           $okay = $file->isTransformableImage();
           if ($okay) {
             $xformer = new PhabricatorImageTransformer();
             $xformed = $xformer->executeProfileTransform(
               $file,
               $width = 280,
               $min_height = 140,
               $max_height = 420);
             $profile->setProfileImagePHID($xformed->getPHID());
           } else {
             $errors[] =
               'Only valid image files (jpg, jpeg, png or gif) '.
               'will be accepted.';
           }
         }
       }
 
       if (!$errors) {
         $project->save();
         $profile->setProjectPHID($project->getPHID());
         $profile->save();
         return id(new AphrontRedirectResponse())
           ->setURI('/project/view/'.$project->getID().'/');
       }
     }
 
     $error_view = null;
     if ($errors) {
       $error_view = new AphrontErrorView();
       $error_view->setTitle('Form Errors');
       $error_view->setErrors($errors);
     }
 
     $header_name = 'Edit Project';
     $title = 'Edit Project';
     $action = '/project/edit/'.$project->getID().'/';
 
     $form = new AphrontFormView();
     $form
       ->setUser($user)
       ->setAction($action)
       ->setEncType('multipart/form-data')
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel('Name')
           ->setName('name')
           ->setValue($project->getName())
           ->setError($e_name))
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel('Project Status')
           ->setName('status')
           ->setOptions($options)
           ->setValue($project->getStatus()))
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setLabel('Blurb')
           ->setName('blurb')
           ->setValue($profile->getBlurb()))
+      ->appendChild(
+        id(new AphrontFormTokenizerControl())
+          ->setDatasource('/typeahead/common/projects/')
+          ->setLabel('Subprojects')
+          ->setName('set_subprojects')
+          ->setValue($subprojects))
       ->appendChild(
         id(new AphrontFormFileControl())
           ->setLabel('Change Image')
           ->setName('image'))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->addCancelButton('/project/view/'.$project->getID().'/')
           ->setValue('Save'));
 
     $panel = new AphrontPanelView();
     $panel->setHeader($header_name);
-    $panel->setWidth(AphrontPanelView::WIDTH_FORM);
+    $panel->setWidth(AphrontPanelView::WIDTH_WIDE);
     $panel->appendChild($form);
 
     return $this->buildStandardPageResponse(
       array(
         $error_view,
         $panel,
       ),
       array(
         'title' => $title,
       ));
   }
 }
diff --git a/src/applications/project/controller/profileedit/__init__.php b/src/applications/project/controller/profileedit/__init__.php
index a03f8883d2..46a80365db 100644
--- a/src/applications/project/controller/profileedit/__init__.php
+++ b/src/applications/project/controller/profileedit/__init__.php
@@ -1,29 +1,31 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'aphront/response/404');
 phutil_require_module('phabricator', 'aphront/response/redirect');
 phutil_require_module('phabricator', 'applications/files/storage/file');
 phutil_require_module('phabricator', 'applications/files/transform');
+phutil_require_module('phabricator', 'applications/phid/handle/data');
 phutil_require_module('phabricator', 'applications/project/constants/status');
 phutil_require_module('phabricator', 'applications/project/controller/base');
 phutil_require_module('phabricator', 'applications/project/storage/profile');
 phutil_require_module('phabricator', 'applications/project/storage/project');
 phutil_require_module('phabricator', 'view/form/base');
 phutil_require_module('phabricator', 'view/form/control/file');
 phutil_require_module('phabricator', 'view/form/control/select');
 phutil_require_module('phabricator', 'view/form/control/submit');
 phutil_require_module('phabricator', 'view/form/control/text');
 phutil_require_module('phabricator', 'view/form/control/textarea');
+phutil_require_module('phabricator', 'view/form/control/tokenizer');
 phutil_require_module('phabricator', 'view/form/error');
 phutil_require_module('phabricator', 'view/layout/panel');
 
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('PhabricatorProjectProfileEditController.php');
diff --git a/src/applications/project/storage/project/PhabricatorProject.php b/src/applications/project/storage/project/PhabricatorProject.php
index d9da01e716..4558eb5723 100644
--- a/src/applications/project/storage/project/PhabricatorProject.php
+++ b/src/applications/project/storage/project/PhabricatorProject.php
@@ -1,49 +1,75 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class PhabricatorProject extends PhabricatorProjectDAO {
 
   protected $name;
   protected $phid;
   protected $status = PhabricatorProjectStatus::UNKNOWN;
   protected $authorPHID;
+  protected $subprojectPHIDs = array();
+
+  private $subprojectsNeedUpdate;
 
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
+      self::CONFIG_SERIALIZATION => array(
+        'subprojectPHIDs' => self::SERIALIZATION_JSON,
+      ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPHIDConstants::PHID_TYPE_PROJ);
   }
 
+  public function setSubprojectPHIDs(array $phids) {
+    $this->subprojectPHIDs = $phids;
+    $this->subprojectsNeedUpdate = true;
+    return $this;
+  }
+
   public function loadProfile() {
     $profile = id(new PhabricatorProjectProfile())->loadOneWhere(
       'projectPHID = %s',
       $this->getPHID());
     return $profile;
   }
 
   public function loadAffiliations() {
     $affils = PhabricatorProjectAffiliation::loadAllForProjectPHIDs(
       array($this->getPHID()));
     return $affils[$this->getPHID()];
   }
+
+  public function save() {
+    $result = parent::save();
+
+    if ($this->subprojectsNeedUpdate) {
+      // If we've changed the project PHIDs for this task, update the link
+      // table.
+      PhabricatorProjectSubproject::updateProjectSubproject($this);
+      $this->subprojectsNeedUpdate = false;
+    }
+
+    return $result;
+  }
+
 }
diff --git a/src/applications/project/storage/project/__init__.php b/src/applications/project/storage/project/__init__.php
index bc7599691d..12477e8dc7 100644
--- a/src/applications/project/storage/project/__init__.php
+++ b/src/applications/project/storage/project/__init__.php
@@ -1,19 +1,20 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'applications/phid/constants');
 phutil_require_module('phabricator', 'applications/phid/storage/phid');
 phutil_require_module('phabricator', 'applications/project/constants/status');
 phutil_require_module('phabricator', 'applications/project/storage/affiliation');
 phutil_require_module('phabricator', 'applications/project/storage/base');
 phutil_require_module('phabricator', 'applications/project/storage/profile');
+phutil_require_module('phabricator', 'applications/project/storage/subproject');
 
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('PhabricatorProject.php');
diff --git a/src/applications/project/storage/subproject/PhabricatorProjectSubproject.php b/src/applications/project/storage/subproject/PhabricatorProjectSubproject.php
new file mode 100644
index 0000000000..43832e7275
--- /dev/null
+++ b/src/applications/project/storage/subproject/PhabricatorProjectSubproject.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This is a DAO for the Task -> Project table, which denormalizes the
+ * relationship between tasks and projects into a link table so it can be
+ * efficiently queried. This table is not authoritative; the projectPHIDs field
+ * of ManiphestTask is. The rows in this table are regenerated when transactions
+ * are applied to tasks which affected their associated projects.
+ *
+ * @group maniphest
+ */
+final class PhabricatorProjectSubproject extends PhabricatorProjectDAO {
+
+  protected $projectPHID;
+  protected $subprojectPHID;
+
+  public function getConfiguration() {
+    return array(
+      self::CONFIG_IDS          => self::IDS_MANUAL,
+      self::CONFIG_TIMESTAMPS   => false,
+    );
+  }
+
+  public static function updateProjectSubproject(PhabricatorProject $project) {
+    $dao = new PhabricatorProjectSubproject();
+    $conn = $dao->establishConnection('w');
+
+    $sql = array();
+    foreach ($project->getSubprojectPHIDs() as $subproject_phid) {
+      $sql[] = qsprintf(
+        $conn,
+        '(%s, %s)',
+        $project->getPHID(),
+        $subproject_phid);
+    }
+
+    queryfx(
+      $conn,
+      'DELETE FROM %T WHERE projectPHID = %s',
+      $dao->getTableName(),
+      $project->getPHID());
+    if ($sql) {
+      queryfx(
+        $conn,
+        'INSERT INTO %T (projectPHID, subprojectPHID) VALUES %Q',
+        $dao->getTableName(),
+        implode(', ', $sql));
+    }
+  }
+
+}
diff --git a/src/applications/project/storage/subproject/__init__.php b/src/applications/project/storage/subproject/__init__.php
new file mode 100644
index 0000000000..cffb2495f9
--- /dev/null
+++ b/src/applications/project/storage/subproject/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/project/storage/base');
+phutil_require_module('phabricator', 'storage/qsprintf');
+phutil_require_module('phabricator', 'storage/queryfx');
+
+
+phutil_require_source('PhabricatorProjectSubproject.php');
\ No newline at end of file