diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md
index a00d36fa87..e7cd61d8d2 100644
--- a/docs/overview/architecture-overview.md
+++ b/docs/overview/architecture-overview.md
@@ -247,6 +247,13 @@ development postgresql user.
`tools/provision` also invokes `tools/do-destroy-rebuild-database`
to create the actual database with its schema.
+### Thumbor and thumbnailing
+
+We use Thumbor, a popular open source thumbnailing server, to serve
+images (both for inline URL previews and serving uploaded image
+files). See [our thumbnailing docs](../subsystems/thumbnailing.html)
+for more details on how this works.
+
### Nagios
Nagios is an optional component used for notifications to the system
diff --git a/docs/subsystems/index.rst b/docs/subsystems/index.rst
index 6a00ec3e1b..ff96980cdf 100644
--- a/docs/subsystems/index.rst
+++ b/docs/subsystems/index.rst
@@ -37,6 +37,7 @@ Subsystems Documentation
documentation
conversion
input-pills
+ thumbnailing
presence
unread_messages
billing
diff --git a/docs/subsystems/thumbnailing.md b/docs/subsystems/thumbnailing.md
new file mode 100644
index 0000000000..34cd59b9fe
--- /dev/null
+++ b/docs/subsystems/thumbnailing.md
@@ -0,0 +1,80 @@
+# Thumbnailing
+
+There are two key places one would naturally want to thumbnail images
+in a team chat application like Zulip:
+
+* On the server-side, when serving inline image and URL previews in
+ the bodies of messages. This is very important for Zulip's network
+ performance of low-bandwidth networks.
+* In mobile apps, to avoid uploading full-size images on a mobile
+ network (which Zulip does not yet implement),
+
+Our server-side thumbnailing system is powered by [thumbor][], a
+popular open source server for serving images and thumbnailing them.
+
+Thumbor is responsible for a few things in Zulip:
+
+* Serving all image content over HTTPS, even if the original/upstream
+ image was hosted on HTTP (this was previously done by `camo` in
+ older versions of Zulip). This is important to avoid mixed-content
+ warnings from browsers (which look very bad), and does have some
+ real security benefit in protecting our users from malicious
+ content.
+* Minimizing potentially unnecessary bandwidth that might be used in
+ communication between the Zulip server and clients. Before we
+ introduced this feature,
+
+Thumbor handles a lot of details for us, varying from signing of
+thumbnailing URLs, to caching for DoS prevention.
+
+It is configured via the `THUMBOR_URL` setting in
+`/etc/zulip/settings.py`; you can host Thumbor on the same machine as
+the Zulip server, or a remote server (which is better for isolation,
+since security bugs in image-processing libraries have in the past
+been a common attack vector).
+
+In order to avoid putting the raw Thumbor URLs (which have a complex
+encoding that we might want to change over time) into Zulip messages,
+we instead encoding in markdown URLs of the form
+`/thumbnail/?url=https://example.com/image.png&size=thumbnail` as the
+`src` in our image tags, and that URL serves a
+(configuration-dependent) redirect to the actual image hosted on
+thumbor.
+
+The thumbnailing system is used for any images that appear in the
+bodies of Zulip messages (i.e. both images linked to by users, as well
+as uploaded image files.). We exclude a few special image sources
+(e.g. youtube stills) only because they are already thumbnailed.
+
+For uploaded image files, we enforce the same security policy on
+thumbnail URLs that we do for the uploaded files themselves.
+
+A correct client implementation interacting with the thumbnailing
+system should do the following:
+
+* For serving the thumbnailed to 100px height version of images,
+ nothing special is required; the client just needs to display the
+ `src=` value in the `
` tag in the rendered message HTML.
+* For displaying a "full-size" version of an image (e.g. to use in a
+ lightbox), the client can access the `data-fullsize-src` attribute
+ on the `
` tag; this will contain the URL for a full-size
+ version.
+* Ideally, when clicking on an image to switch from the thumbnail to
+ the full-size / lightbox size, the client should immediately display
+ the thumbnailed (low resolution) version and in parallel fetch the
+ full-size version in the background, transparently swapping it into
+ place once the full size version is available. This provides a
+ slick user experience where the user doesn't see a loading state,
+ and instead just sees the image focus a few hundred milliseconds
+ after clicking the image.
+
+
+## Avatars, realm icons, and custom emoji
+
+Currently, these user-uploaded content are thumbnailed by Zulip's
+internal file-upload code, in part because they change rarely and
+don't have the same throughput/performance requirements as
+user-uploaded files. We may later convert them to use thumbor as
+well.
+
+[thumbor]: https://github.com/thumbor/thumbor
diff --git a/zerver/lib/bugdown/__init__.py b/zerver/lib/bugdown/__init__.py
index aa5bd6836a..47d39f7a68 100644
--- a/zerver/lib/bugdown/__init__.py
+++ b/zerver/lib/bugdown/__init__.py
@@ -227,6 +227,8 @@ def add_a(
a.set("data-id", data_id)
img = markdown.util.etree.SubElement(a, "img")
if is_thumbor_enabled() and use_thumbnails:
+ # See docs/thumbnailing.md for some high-level documentation.
+ #
# We strip leading '/' from relative URLs here to ensure
# consistency in what gets passed to /thumbnail
url = url.lstrip('/')
diff --git a/zerver/lib/thumbnail.py b/zerver/lib/thumbnail.py
index c83fc2919b..44d95ae629 100644
--- a/zerver/lib/thumbnail.py
+++ b/zerver/lib/thumbnail.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+# See https://zulip.readthedocs.io/en/latest/subsystems/thumbnailing.html
import base64
import os
import sys
diff --git a/zerver/views/thumbnail.py b/zerver/views/thumbnail.py
index cc75e12401..2a676721ff 100644
--- a/zerver/views/thumbnail.py
+++ b/zerver/views/thumbnail.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+# See https://zulip.readthedocs.io/en/latest/subsystems/thumbnailing.html
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
diff --git a/zthumbor/loaders/zloader.py b/zthumbor/loaders/zloader.py
index afdfa43372..7387242333 100644
--- a/zthumbor/loaders/zloader.py
+++ b/zthumbor/loaders/zloader.py
@@ -1,3 +1,4 @@
+# See https://zulip.readthedocs.io/en/latest/subsystems/thumbnailing.html
from __future__ import absolute_import
from six.moves import urllib