diff --git a/docs/framework/asset-builder.md b/docs/framework/asset-builder.md
new file mode 100644
index 0000000000000000000000000000000000000000..78ee6b31fcf52fe4930bc6345a07e0388a222253
--- /dev/null
+++ b/docs/framework/asset-builder.md
@@ -0,0 +1,137 @@
+# Asset Builder
+
+The `AssetBuilder` manages semi-dynamic assets, such as generated JS/CSS
+files. Semi-dynamic assets are simultaneously:
+
+ * __Dynamic__: They vary depending on the current system configuration.
+   You cannot lock-in a correct version of the asset because each
+   installation may need a slightly different version.
+ * __Static__: Within a given site, the asset is not likely to change.
+   It could even be served directly by the web-server (without the overhead
+   of PHP/CMS/Civi bootstrap)
+
+!!! note "Example: Batch loading Angular HTML"
+    When visiting the AngularJS base page `civicrm/a`, one needs to load a
+    mix of many small HTML files.  It's ideal to aggregate them into one
+    bigger file and reduce the number of round-trip HTTP requests.
+
+    This asset is _dynamic_ because extensions can add or modify HTML files.
+    Two different sites would have different HTML files (depending on the
+    mix of extensions).  Yet, within a particular site/configuration, the
+    content is _static_ because the HTML doesn't actually change at runtime.
+
+The `AssetBuilder` addresses this use-case with *lazy* building.  Assets are not
+distributed as part of `git` or `tar.gz`.  Rather, the first time you try to
+use an asset, it fires off a hook to build the asset.  The content is stored
+in a cache file.
+
+!!! tip "Tip: Caching and development"
+    If you are developing or patching assets, then the caching behavior may
+    get distracting. To bypass the cache, enable **debug mode**.
+
+## Usage: Simple asset
+
+There are generally two aspects to using `AssetBuilder` -- creating a URL
+for the asset, and defining the content of the asset.
+
+For example, suppose we wanted to define a static file named
+`api-fields.json` which lists all the fields of all the API entities.
+
+```php
+// Get the URL to `api-fields.json`.
+$url = \Civi::service('asset_builder')->getUrl('api-fields.json');
+
+// Define the content of `api-fields.json` using `hook_civicrm_buildAsset`.
+function mymodule_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
+  if ($asset !== 'api-fields.json') return;
+
+  $entities = civicrm_api3('Entity', 'get', array());
+  $fields = array();
+  foreach ($entities['values'] as $entity) {
+    $fields[$entity] = civicrm_api3($entity, 'getfields');
+  }
+
+  $mimeType = 'application/json';
+  $content = json_encode($fields);
+}
+```
+
+!!! note "What does `getUrl(...)` do?"
+
+    In normal/production mode, `getUrl(...)` checks to see if the asset
+    already exists.  If necessary, it fires the hook and saves the asset to
+    disk.  Finally, it returns the direct URL for the asset -- which allows
+    the user to fetch it quickly (without extra PHP/CMS/Civi bootstrapping).
+
+    In debug mode, `getUrl(...)` returns the URL of a PHP script.  The PHP
+    script will build the asset every time it's requested.
+
+## Usage: Parameterized asset
+
+What should you do if you need to create a series of similar assets, based on slightly
+different permutations or configurations? Add parameters (aka `$params`).
+
+For example, we might want a copy of `api-fields.json` which only includes a
+handful of chosen entities.  Simply pass the chosen entities into
+`getUrl()`, then update the definition to use `$params['entities']`, as in:
+
+```php
+// Get the URL to `api-fields.json`. This variant only includes
+// a few contact-related entities.
+$contactEntitiesUrl = \Civi::service('asset_builder')
+  ->getUrl('api-fields.json', array(
+    'entities' => array('Contact', 'Phone', 'Email', 'Address'),
+  )
+);
+
+// Get the URL to `api-fields.json`. This variant only includes
+// a few case-related entities.
+$caseEntitiesUrl = \Civi::service('asset_builder')
+  ->getUrl('api-fields.json', array(
+    'entities' => array('Case', 'Activity', 'Relationship'),
+  )
+);
+
+// Define the content of `api-fields.json` using `hook_civicrm_buildAsset`.
+function mymodule_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
+  if ($asset !== 'api-fields.json') return;
+
+  $fields = array();
+  foreach ($params['entities'] as $entity) {
+    $fields[$entity] = civicrm_api3($entity, 'getfields');
+  }
+
+  $mimeType = 'application/json';
+  $content = json_encode($fields);
+}
+```
+
+!!! note "Note: Parmaters and caching"
+    Each combination of ($asset,$params) will be cached separately.
+
+!!! tip "Tip: Economize parameter size"
+    In debug mode, all parameters are passed as part of the URL. `AssetBuilder`
+    will try to compress them, but fundamentally: long `$params` will produce
+    long URLs.
+
+## Other considerations
+
+!!! note "Compare: How does AssetBuilder differ from [Assetic](https://github.com/kriswallsmith/assetic)?"
+    Both are written in PHP, but they address differet parts of the process:
+
+     * `AssetBuilder` provides URL-routing, caching, and parameterization.
+       Its strength is defining the *lifecycle* of a dynamic asset.
+     * `Assetic` provides a library of generators and filters.  Its strength
+       is defining the *content* of an asset.
+
+    You could use them together -- e.g.  in `hook_civicrm_buildAsset`,
+    declare a new asset and use `Assetic` to build its content.
+
+!!! caution "Caution: Assimilate non-confidential data"
+    The current implementation does not take aggressive measures to keep
+    assets confidential. For example, an asset built from public JS files
+    is fine, but an asset built from permissioned data (contact-records
+    or activity-records) could be problematic.
+
+    It may be possible to fix this by computing URL digests differently, but
+    (at time of writing) we don't have a need/use-case.
diff --git a/docs/hooks/hook_civicrm_buildAsset.md b/docs/hooks/hook_civicrm_buildAsset.md
new file mode 100644
index 0000000000000000000000000000000000000000..18c5ffa026d889a3098d71ddc2a95f2694aaab67
--- /dev/null
+++ b/docs/hooks/hook_civicrm_buildAsset.md
@@ -0,0 +1,32 @@
+# hook_civicrm_buildAsset
+
+## Description
+
+This hook fires whenever the system builds a semi-dynamic asset.  For more
+discussion, see [AssetBuilder](/framework/asset-builder.md).
+
+## Definition
+
+    hook_civicrm_buildAsset($asset, $params, &$mimeType, &$content)
+
+## Parameters
+
+ * `$asset` (string): the logical file name of an asset (ex: `hello-world.json`)
+ * `$params` (array): an optional set of parameters describing how to build the asset
+ * `$mimeType` (string, output): the MIME type of the asset (ex: `application/json`)
+ * `$content` (string, output): the full content of the asset
+
+## Returns
+
+ * null
+
+## Example
+
+```php
+function mymodule_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
+  if ($asset === 'hello-world.json') {
+    $mimeType = 'application/json';
+    $content = json_encode(array('hello', 'world'));
+  }
+}
+```
\ No newline at end of file