diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
index 875390ce3..24a904386 100644
--- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
@@ -1,294 +1,295 @@
 <?php
 
 /**
  * @group aphront
  */
 class AphrontDefaultApplicationConfiguration
   extends AphrontApplicationConfiguration {
 
   public function __construct() {
 
   }
 
   public function getApplicationName() {
     return 'aphront-default';
   }
 
   public function getURIMap() {
     return $this->getResourceURIMapRules() + array(
       '/(?:(?P<filter>(?:jump))/)?' =>
         'PhabricatorDirectoryMainController',
 
       '/typeahead/' => array(
         'common/(?P<type>\w+)/'
           => 'PhabricatorTypeaheadCommonDatasourceController',
       ),
 
       '/oauthserver/' => array(
         'auth/'          => 'PhabricatorOAuthServerAuthController',
         'test/'          => 'PhabricatorOAuthServerTestController',
         'token/'         => 'PhabricatorOAuthServerTokenController',
         'clientauthorization/' => array(
           '' => 'PhabricatorOAuthClientAuthorizationListController',
           'delete/(?P<phid>[^/]+)/' =>
             'PhabricatorOAuthClientAuthorizationDeleteController',
           'edit/(?P<phid>[^/]+)/' =>
             'PhabricatorOAuthClientAuthorizationEditController',
         ),
         'client/' => array(
           ''                        => 'PhabricatorOAuthClientListController',
           'create/'                 => 'PhabricatorOAuthClientEditController',
           'delete/(?P<phid>[^/]+)/' => 'PhabricatorOAuthClientDeleteController',
           'edit/(?P<phid>[^/]+)/'   => 'PhabricatorOAuthClientEditController',
           'view/(?P<phid>[^/]+)/'   => 'PhabricatorOAuthClientViewController',
         ),
       ),
 
       '/~/' => array(
         '' => 'DarkConsoleController',
         'data/(?P<key>[^/]+)/' => 'DarkConsoleDataController',
       ),
 
       '/status/' => 'PhabricatorStatusController',
 
 
       '/help/' => array(
         'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController',
       ),
 
       '/notification/' => array(
         '(?:(?P<filter>all|unread)/)?'
           => 'PhabricatorNotificationListController',
         'panel/' => 'PhabricatorNotificationPanelController',
         'individual/' => 'PhabricatorNotificationIndividualController',
         'status/' => 'PhabricatorNotificationStatusController',
         'clear/' => 'PhabricatorNotificationClearController',
       ),
 
       '/debug/' => 'PhabricatorDebugController',
     );
   }
 
   protected function getResourceURIMapRules() {
     return array(
       '/res/' => array(
         '(?:(?P<mtime>[0-9]+)T/)?'.
+        '(?P<library>[^/]+)/'.
         '(?P<hash>[a-f0-9]{8})/'.
         '(?P<path>.+\.(?:css|js|jpg|png|swf|gif))'
           => 'CelerityPhabricatorResourceController',
       ),
     );
   }
 
   /**
    * @phutil-external-symbol class PhabricatorStartup
    */
   public function buildRequest() {
     $parser = new PhutilQueryStringParser();
     $data   = array();
 
     // If the request has "multipart/form-data" content, we can't use
     // PhutilQueryStringParser to parse it, and the raw data supposedly is not
     // available anyway (according to the PHP documentation, "php://input" is
     // not available for "multipart/form-data" requests). However, it is
     // available at least some of the time (see T3673), so double check that
     // we aren't trying to parse data we won't be able to parse correctly by
     // examining the Content-Type header.
     $content_type = idx($_SERVER, 'CONTENT_TYPE');
     $is_form_data = preg_match('@^multipart/form-data@i', $content_type);
 
     $raw_input = PhabricatorStartup::getRawInput();
     if (strlen($raw_input) && !$is_form_data) {
       $data += $parser->parseQueryString($raw_input);
     } else if ($_POST) {
       $data += $_POST;
     }
 
     $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
 
     $request = new AphrontRequest($this->getHost(), $this->getPath());
     $request->setRequestData($data);
     $request->setApplicationConfiguration($this);
 
     return $request;
   }
 
   public function handleException(Exception $ex) {
     $request = $this->getRequest();
 
     // For Conduit requests, return a Conduit response.
     if ($request->isConduit()) {
       $response = new ConduitAPIResponse();
       $response->setErrorCode(get_class($ex));
       $response->setErrorInfo($ex->getMessage());
 
       return id(new AphrontJSONResponse())
         ->setAddJSONShield(false)
         ->setContent($response->toDictionary());
     }
 
     // For non-workflow requests, return a Ajax response.
     if ($request->isAjax() && !$request->isJavelinWorkflow()) {
       // Log these; they don't get shown on the client and can be difficult
       // to debug.
       phlog($ex);
 
       $response = new AphrontAjaxResponse();
       $response->setError(
         array(
           'code' => get_class($ex),
           'info' => $ex->getMessage(),
         ));
       return $response;
     }
 
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
 
     $user = $request->getUser();
     if (!$user) {
       // If we hit an exception very early, we won't have a user.
       $user = new PhabricatorUser();
     }
 
     if ($ex instanceof PhabricatorPolicyException) {
 
       if (!$user->isLoggedIn()) {
         // If the user isn't logged in, just give them a login form. This is
         // probably a generally more useful response than a policy dialog that
         // they have to click through to get a login form.
         //
         // Possibly we should add a header here like "you need to login to see
         // the thing you are trying to look at".
         $login_controller = new PhabricatorAuthStartController($request);
 
         $auth_app_class = 'PhabricatorApplicationAuth';
         $auth_app = PhabricatorApplication::getByClass($auth_app_class);
         $login_controller->setCurrentApplication($auth_app);
 
         return $login_controller->processRequest();
       }
 
       $list = $ex->getMoreInfo();
       foreach ($list as $key => $item) {
         $list[$key] = phutil_tag('li', array(), $item);
       }
       if ($list) {
         $list = phutil_tag('ul', array(), $list);
       }
 
       $content = array(
         phutil_tag(
           'div',
           array(
             'class' => 'aphront-policy-rejection',
           ),
           $ex->getRejection()),
         phutil_tag(
           'div',
           array(
             'class' => 'aphront-capability-details',
           ),
           pht('Users with the "%s" capability:', $ex->getCapabilityName())),
         $list,
       );
 
       $dialog = new AphrontDialogView();
       $dialog
         ->setTitle($ex->getTitle())
         ->setClass('aphront-access-dialog')
         ->setUser($user)
         ->appendChild($content);
 
       if ($this->getRequest()->isAjax()) {
         $dialog->addCancelButton('/', 'Close');
       } else {
         $dialog->addCancelButton('/', $is_serious ? 'OK' : 'Away With Thee');
       }
 
       $response = new AphrontDialogResponse();
       $response->setDialog($dialog);
       return $response;
     }
 
     if ($ex instanceof AphrontUsageException) {
       $error = new AphrontErrorView();
       $error->setTitle($ex->getTitle());
       $error->appendChild($ex->getMessage());
 
       $view = new PhabricatorStandardPageView();
       $view->setRequest($this->getRequest());
       $view->appendChild($error);
 
       $response = new AphrontWebpageResponse();
       $response->setContent($view->render());
       $response->setHTTPResponseCode(500);
 
       return $response;
     }
 
 
     // Always log the unhandled exception.
     phlog($ex);
 
     $class    = get_class($ex);
     $message  = $ex->getMessage();
 
     if ($ex instanceof AphrontQuerySchemaException) {
       $message .=
         "\n\n".
         "NOTE: This usually indicates that the MySQL schema has not been ".
         "properly upgraded. Run 'bin/storage upgrade' to ensure your ".
         "schema is up to date.";
     }
 
     if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
       $trace = id(new AphrontStackTraceView())
         ->setUser($user)
         ->setTrace($ex->getTrace());
     } else {
       $trace = null;
     }
 
     $content = phutil_tag(
       'div',
       array('class' => 'aphront-unhandled-exception'),
       array(
         phutil_tag('div', array('class' => 'exception-message'), $message),
         $trace,
       ));
 
     $dialog = new AphrontDialogView();
     $dialog
       ->setTitle('Unhandled Exception ("'.$class.'")')
       ->setClass('aphront-exception-dialog')
       ->setUser($user)
       ->appendChild($content);
 
     if ($this->getRequest()->isAjax()) {
       $dialog->addCancelButton('/', 'Close');
     }
 
     $response = new AphrontDialogResponse();
     $response->setDialog($dialog);
     $response->setHTTPResponseCode(500);
 
     return $response;
   }
 
   public function willSendResponse(AphrontResponse $response) {
     return $response;
   }
 
   public function build404Controller() {
     return array(new Phabricator404Controller($this->getRequest()), array());
   }
 
   public function buildRedirectController($uri) {
     return array(
       new PhabricatorRedirectController($this->getRequest()),
       array(
         'uri' => $uri,
       ));
   }
 
 }
diff --git a/src/infrastructure/celerity/CelerityPhabricatorResourceController.php b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php
index 1f258cea8..3f42e50c5 100644
--- a/src/infrastructure/celerity/CelerityPhabricatorResourceController.php
+++ b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php
@@ -1,40 +1,50 @@
 <?php
 
 /**
  * Delivers CSS and JS resources to the browser. This controller handles all
  * ##/res/## requests, and manages caching, package construction, and resource
  * preprocessing.
  *
  * @group celerity
  */
 final class CelerityPhabricatorResourceController
   extends CelerityResourceController {
 
   private $path;
   private $hash;
+  private $library;
 
   public function getCelerityResourceMap() {
-    return CelerityResourceMap::getNamedInstance('phabricator');
+    return CelerityResourceMap::getNamedInstance($this->library);
   }
 
   public function willProcessRequest(array $data) {
     $this->path = $data['path'];
     $this->hash = $data['hash'];
+    $this->library = $data['library'];
   }
 
   public function processRequest() {
+    // Check that the resource library exists before trying to serve resources
+    // from it.
+    try {
+      $this->getCelerityResourceMap();
+    } catch (Exception $ex) {
+      return new Aphront400Response();
+    }
+
     return $this->serveResource($this->path);
   }
 
   protected function buildResourceTransformer() {
     $minify_on = PhabricatorEnv::getEnvConfig('celerity.minify');
     $developer_on = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
 
     $should_minify = ($minify_on && !$developer_on);
 
     return id(new CelerityResourceTransformer())
       ->setMinify($should_minify)
       ->setCelerityMap($this->getCelerityResourceMap());
   }
 
 }
diff --git a/src/infrastructure/celerity/resources/CelerityPhysicalResources.php b/src/infrastructure/celerity/resources/CelerityPhysicalResources.php
index e6d37dc27..995b9f1a5 100644
--- a/src/infrastructure/celerity/resources/CelerityPhysicalResources.php
+++ b/src/infrastructure/celerity/resources/CelerityPhysicalResources.php
@@ -1,52 +1,61 @@
 <?php
 
 /**
  * Defines the location of physical static resources which exist at build time
  * and are precomputed into a resource map.
  */
 abstract class CelerityPhysicalResources extends CelerityResources {
 
   private $map;
 
   abstract public function getPathToMap();
   abstract public function findBinaryResources();
   abstract public function findTextResources();
 
   public function loadMap() {
     if ($this->map === null) {
       $this->map = include $this->getPathToMap();
     }
     return $this->map;
   }
 
   public static function getAll() {
     static $resources_map;
     if ($resources_map === null) {
       $resources_map = array();
 
       $resources_list = id(new PhutilSymbolLoader())
         ->setAncestorClass('CelerityPhysicalResources')
         ->loadObjects();
 
       foreach ($resources_list as $resources) {
         $name = $resources->getName();
+
+        if (!preg_match('/^[a-z0-9]+/', $name)) {
+          throw new Exception(
+            pht(
+              'Resources name "%s" is not valid; it must contain only '.
+              'lowercase latin letters and digits.',
+              $name));
+        }
+
         if (empty($resources_map[$name])) {
           $resources_map[$name] = $resources;
         } else {
           $old = get_class($resources_map[$name]);
           $new = get_class($resources);
           throw new Exception(
             pht(
               'Celerity resource maps must have unique names, but maps %s and '.
               '%s share the same name, "%s".',
               $old,
               $new,
               $name));
         }
       }
     }
 
     return $resources_map;
   }
 
 }
diff --git a/src/infrastructure/celerity/resources/CelerityResources.php b/src/infrastructure/celerity/resources/CelerityResources.php
index f3d8a93b7..a9604d944 100644
--- a/src/infrastructure/celerity/resources/CelerityResources.php
+++ b/src/infrastructure/celerity/resources/CelerityResources.php
@@ -1,39 +1,40 @@
 <?php
 
 /**
  * Defines the location of static resources.
  */
 abstract class CelerityResources {
 
   private $map;
 
   abstract public function getName();
   abstract public function getResourceData($name);
 
   public function getResourceModifiedTime($name) {
     return 0;
   }
 
   public function getCelerityHash($data) {
     $tail = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
     $hash = PhabricatorHash::digest($data, $tail);
     return substr($hash, 0, 8);
   }
 
   public function getResourceType($path) {
     return CelerityResourceTransformer::getResourceType($path);
   }
 
   public function getResourceURI($hash, $name) {
-    return "/res/{$hash}/{$name}";
+    $resources = $this->getName();
+    return "/res/{$resources}/{$hash}/{$name}";
   }
 
   public function getResourcePackages() {
     return array();
   }
 
   public function loadMap() {
     return array();
   }
 
 }