diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2016, Ben Gamari
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Ben Gamari nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/pipes-s3.cabal b/pipes-s3.cabal
new file mode 100644
--- /dev/null
+++ b/pipes-s3.cabal
@@ -0,0 +1,36 @@
+name:                pipes-s3
+version:             0.1.0.0
+synopsis:            A simple interface for streaming data to and from Amazon S3
+description:
+    This package provides a simple interface for streaming data to and from
+    Amazon's S3 cloud storage service with the @pipes@ package.
+homepage:            http://github.com/bgamari/pipes-s3
+license:             BSD3
+license-file:        LICENSE
+author:              Ben Gamari
+maintainer:          ben@smart-cactus.org
+copyright:           (c) 2016 Ben Gamari
+category:            Network
+build-type:          Simple
+cabal-version:       >=1.10
+
+source-repository head
+  type:      git
+  location:  git://github.com/bgamari/pipes-s3
+
+library
+  exposed-modules:     Pipes.Aws.S3
+  other-extensions:    OverloadedStrings, GeneralizedNewtypeDeriving, ScopedTypeVariables
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  build-depends:       base             >=4.7  && <4.10,
+                       transformers     >=0.4  && <0.6,
+                       bytestring       >=0.10 && <0.11,
+                       text             >=1.2  && <1.3,
+                       pipes-bytestring >=2.1  && <2.2,
+                       pipes-safe       >=2.2  && <2.3,
+                       pipes            >=4.1  && <4.3,
+                       http-client      >=0.4  && <0.5,
+                       http-client-tls  >=0.2  && <0.3,
+                       resourcet        >=1.1  && <1.2,
+                       aws              >=0.13 && <0.14
diff --git a/src/Pipes/Aws/S3.hs b/src/Pipes/Aws/S3.hs
new file mode 100644
--- /dev/null
+++ b/src/Pipes/Aws/S3.hs
@@ -0,0 +1,167 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | A simple streaming interface to the AWS S3 storage service.
+module Pipes.Aws.S3
+   ( Bucket(..)
+   , Object(..)
+     -- * Downloading
+   , fromS3
+   , fromS3'
+     -- ** Convenient re-exports
+   , responseBody
+     -- * Uploading
+   , ChunkSize
+   , toS3
+   , toS3'
+   ) where
+
+import Control.Monad (unless)
+import Data.String (IsString)
+
+import qualified Data.ByteString as BS
+import           Data.ByteString (ByteString)
+import qualified Data.Text as T
+
+import Pipes
+import Pipes.Safe
+import qualified Pipes.Prelude as PP
+import qualified Pipes.ByteString as PBS
+import Control.Monad.Trans.Resource
+import Control.Monad.IO.Class
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS
+import qualified Aws
+import qualified Aws.Core as Aws
+import qualified Aws.S3 as S3
+
+-- | An AWS S3 bucket name
+newtype Bucket = Bucket T.Text
+               deriving (Eq, Ord, Show, Read, IsString)
+
+-- | An AWS S3 object name
+newtype Object = Object T.Text
+               deriving (Eq, Ord, Show, Read, IsString)
+
+-- | Download an object from S3
+--
+-- This initiates an S3 download, requiring that the caller provide a way to
+-- construct a 'Producer' from the initial 'Response' to the request (allowing
+-- the caller to, e.g., handle failure).
+--
+-- For instance to merely produced the content of the response,
+--
+-- @
+-- 'fromS3' bucket object responseBody
+-- @
+--
+fromS3 :: MonadSafe m
+       => Bucket -> Object
+       -> (Response (Producer BS.ByteString m ()) -> Producer BS.ByteString m a)
+       -> Producer BS.ByteString m a
+fromS3 bucket object handler = do
+    cfg <- liftIO Aws.baseConfiguration
+    fromS3' cfg bucket object handler
+
+-- | Download an object from S3 explicitly specifying an @aws@ 'Aws.Configuration',
+-- which provides credentials and logging configuration.
+fromS3' :: MonadSafe m
+        => Aws.Configuration -> Bucket -> Object
+        -> (Response (Producer BS.ByteString m ()) -> Producer BS.ByteString m a)
+        -> Producer BS.ByteString m a
+fromS3' cfg (Bucket bucket) (Object object) handler = do
+    let s3cfg = Aws.defServiceConfig :: S3.S3Configuration Aws.NormalQuery
+    mgr <- liftIO $ newManager tlsManagerSettings
+    req <- liftIO $ buildRequest cfg s3cfg $ S3.getObject bucket object
+    Pipes.Safe.bracket (liftIO $ responseOpen req mgr) (liftIO . responseClose) $ \resp ->
+        handler $ resp { responseBody = from $ brRead $ responseBody resp }
+
+-- Stolen from pipes-http
+withHTTP :: MonadSafe m
+         => Request
+         -> Manager
+         -> (Response (Producer ByteString m ()) -> m a)
+         -> m a
+withHTTP req mgr k =
+    Pipes.Safe.bracket (liftIO $ responseOpen req mgr) (liftIO . responseClose) k'
+  where
+    k' resp = do
+        let p = (from . brRead . responseBody) resp
+        k (resp { responseBody = p})
+
+from :: MonadIO m => IO ByteString -> Producer ByteString m ()
+from io = go
+  where
+    go = do
+        bs <- liftIO io
+        unless (BS.null bs) $ do
+            yield bs
+            go
+
+
+buildRequest :: (MonadIO m, Aws.Transaction r a)
+             => Aws.Configuration
+             -> Aws.ServiceConfiguration r Aws.NormalQuery
+             -> r
+             -> m Request
+buildRequest cfg scfg req = do
+    Just cred <- Aws.loadCredentialsDefault
+    sigData <- liftIO $ Aws.signatureData Aws.Timestamp cred
+    let signed = Aws.signQuery req scfg sigData
+    liftIO $ Aws.queryToHttpRequest signed
+
+-- | To maintain healthy streaming uploads are performed in a chunked manner.
+-- This is the size of the upload chunk size. Due to S3 interface restrictions
+-- this must be at least five megabytes.
+type ChunkSize = Int
+
+type ETag = T.Text
+type PartN = Integer
+
+-- | Upload content to an S3 object explicitly specifying an @aws@
+-- 'Aws.Configuration', which provides credentials and logging configuration.
+toS3 :: forall m a. MonadIO m
+     => ChunkSize -> Bucket -> Object
+     -> Producer BS.ByteString m a
+     -> m a
+toS3 chunkSize bucket object consumer = do
+    cfg <- Aws.baseConfiguration
+    toS3' cfg chunkSize bucket object consumer
+
+-- | Upload content to an S3 object.
+--
+-- This internally uses the S3 multi-part upload interface to achieve streaming
+-- upload behavior.
+toS3' :: forall m a. MonadIO m
+      => Aws.Configuration -> ChunkSize -> Bucket -> Object
+      -> Producer BS.ByteString m a
+      -> m a
+toS3' cfg chunkSize (Bucket bucket) (Object object) consumer = do
+    let s3cfg = Aws.defServiceConfig :: S3.S3Configuration Aws.NormalQuery
+    mgr <- liftIO $ newManager tlsManagerSettings
+
+    resp1 <- liftIO $ runResourceT
+             $ Aws.pureAws cfg s3cfg mgr
+             $ S3.postInitiateMultipartUpload bucket object
+    let uploadId = S3.imurUploadId resp1
+
+    let uploadPart :: (PartN, BS.ByteString) -> m (PartN, ETag)
+        uploadPart (partN, content) = do
+            resp <- liftIO $ runResourceT
+                    $ Aws.pureAws cfg s3cfg mgr
+                    $ S3.uploadPart bucket object partN uploadId (RequestBodyBS content)
+            return (partN, S3.uprETag resp)
+
+    (parts, res) <- PP.toListM' $ consumer
+                              >-> enumFromP 1
+                              >-> PP.mapM uploadPart
+
+    resp2 <- liftIO $ runResourceT
+             $ Aws.pureAws cfg s3cfg mgr
+             $ S3.postCompleteMultipartUpload bucket object uploadId parts
+    return res
+
+enumFromP :: (Monad m, Enum i) => i -> Pipe a (i, a) m r
+enumFromP = go
+  where
+    go i = await >>= \x -> yield (i, x) >> go (succ i)
