diff --git a/obelisk/frontend/frontend.cabal b/obelisk/frontend/frontend.cabal
index 7c899e8f045771ae8fd309eeddd0a7c3cb2872b9..229453e46512bcb711c51efa1792f2e50ab6ef4e 100644
--- a/obelisk/frontend/frontend.cabal
+++ b/obelisk/frontend/frontend.cabal
@@ -45,6 +45,7 @@ library
     FrontendPaths
     Pages.Folder
     Pages.TechDemo
+    Pages.DownloadOneCap
   ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -fno-show-valid-hole-fits
 
 executable frontend
diff --git a/obelisk/frontend/src/Controller.hs b/obelisk/frontend/src/Controller.hs
index a91a28560345390cfe56801d4372d5ba54d517b0..092aa64ab4fb8c34057a6df3bd192f1c92db9b24 100644
--- a/obelisk/frontend/src/Controller.hs
+++ b/obelisk/frontend/src/Controller.hs
@@ -11,6 +11,7 @@ import Reflex.Dom.Core
 
 import Pages.Folder (folderPage)
 import Pages.TechDemo (techDemoPage)
+import Pages.DownloadOneCap (downloadOneCapPage)
 
 -- dyn :: (Adjustable t m, NotReady t m, PostBuild t m) => Dynamic t (m a) -> m (Event t a)
 -- constDyn :: Reflex t => a -> Dynamic t a
@@ -25,7 +26,7 @@ import Pages.TechDemo (techDemoPage)
 -- button :: DomBuilder t m => Text -> m (Event t ())
 
 -- | An enumeration of the views to which it is possible to switch.
-data WhichPage = TechDemo | FolderPage
+data WhichPage = TechDemo | FolderPage | DownloadOneCap
 
 -- | A view onto whichever page is currently active, as determined by user
 -- inputs to the app.
@@ -40,7 +41,7 @@ activePage =
       let activePageEv = pickPage <$> switchEv
 
       -- Dynamic (Event t WhichPage)
-      widgetDyn <- widgetHold (pickPage FolderPage) activePageEv
+      widgetDyn <- widgetHold (pickPage DownloadOneCap) activePageEv
 
       -- Behavior t (Event t WhichPage)
       let widgetBhr = current widgetDyn
@@ -57,3 +58,4 @@ activePage =
 pickPage :: ObeliskWidget t r m => WhichPage -> m (Event t WhichPage)
 pickPage TechDemo = fmap (FolderPage <$) techDemoPage
 pickPage FolderPage = fmap (TechDemo <$) folderPage
+pickPage DownloadOneCap = fmap (FolderPage <$) downloadOneCapPage
diff --git a/obelisk/frontend/src/Pages/DownloadOneCap.hs b/obelisk/frontend/src/Pages/DownloadOneCap.hs
new file mode 100644
index 0000000000000000000000000000000000000000..42cbb881b00c99980cd22588649d5c5aa5b44b8a
--- /dev/null
+++ b/obelisk/frontend/src/Pages/DownloadOneCap.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Pages.DownloadOneCap (downloadOneCapPage) where
+
+import Control.Monad.IO.Class (liftIO)
+import Data.List.NonEmpty (NonEmpty(..))
+
+import Obelisk.Frontend
+import Reflex.Dom.Core
+
+import Tahoe.CHK.Types (StorageServer)
+import Tahoe.CHK.Capability (pCapability)
+import Text.Megaparsec (parse)
+
+import TahoeDownloader
+
+data TahoeConfiguration =
+    TahoeConfiguration
+    { storageServers :: NonEmpty StorageServer
+    } deriving (Eq, Ord, Show)
+
+downloadOneCapPage :: ObeliskWidget t r m => m (Event t ())
+downloadOneCapPage TahoeConfiguration { storageServers } = do
+  -- Place an input field into the page and retain a reference to it.
+  el "span" $ text "Download: "
+  capInputWidget <- inputElement def
+
+  -- `Dynamic t T.Text` holding the value of the text input.
+  let capTextDyn = _inputElement_value capInputWidget
+
+  -- `Dynamic t CHK` holding parsed capabilities.
+  let parsedCap = filterRight $ parse pCapability "" <$> capTextDyn
+
+  sharesEv <- performEvent $ ffor parsedCap $ liftIO . downloadShareBytes $ \case ->
+              (CHKVerifier v) ->  v
+              (CHKReader (Reader { verifier })) -> verifier
+
+  plaintextEv <- decodeFull <$> sharesEv
+  plaintextDyn <- holdDyn "" plaintextEv
+
+  el "div" $ do
+    el "div" $ text "Plaintext: "
+    el "div" $ dynText plaintextDyn
diff --git a/obelisk/frontend/src/TahoeDownloader.hs b/obelisk/frontend/src/TahoeDownloader.hs
new file mode 100644
index 0000000000000000000000000000000000000000..b857a238fb5c0542b0c3527ae9e506ebcbaede04
--- /dev/null
+++ b/obelisk/frontend/src/TahoeDownloader.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE NamedFieldPuns #-}
+
+module TahoeDownloader (downloadShares) where
+
+import Tahoe.CHK.Decode (Share)
+import Tahoe.CHK.Capability (Verifier(..))
+import Tahoe.CHK.Types (StorageServer)
+import qualified Data.ByteString as BS
+
+type ShareNumber = Int
+
+type ShareMap = Map ShareNumber [StorageServer]
+
+-- | Identify which servers claim to have some data at some index.
+locateShares ::
+    -- | All of the servers which could possibly have the data.  These "could
+    -- possibly" have the data because local configuration suggests the data
+    -- might have been uploaded to them in the past.
+    [StorageServer] ->
+    -- | The storage index at which to look for data.
+    StorageIndex ->
+    -- | The share numbers to look for.
+    Set ShareNumber ->
+    -- | A mapping from share number to servers which claim to have the data.
+    IO ()
+
+-- | Download the bytes for one share number at some index from some server.
+downloadShare ::
+    -- | All of the servers which likely have a certain share number at the
+    -- index.  These "likely" have the data because a recent ``locateShares``
+    -- call found it there.
+    [StorageServer] ->
+    -- | The storage index at which to look for data.
+    StorageIndex ->
+    -- | The share number to look for.
+    ShareNumber ->
+    -- | Either an error encountered trying to get the data or the data itself.
+    IO (Either DownloadError ByteString)
+
+type Required = Int
+
+-- | Find and download enough shares to decode the data identified by the given capability.
+fetchShares ::
+    [StorageServer] ->
+    StorageIndex ->
+    Required ->
+    IO [(ShareNumber, ByteString)]
+
+-- | Maybe something like the number of currently valid shares that can be lost
+-- before the object is lost.
+type Health = Int
+
+-- from tahoe-chk
+-- verifyFull :: Verifier -> [(ShareNumber, ByteString)] -> Either VerifyError Health
+-- decodeFull :: Reader -> [(ShareNumber, ByteString)] -> Either VerifyError ByteString