diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for streaming-cassava
+
+## 0.1.0.0  -- 2017-06-30
+
+* First version.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Ivan Lazar Miljenovic
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+streaming-cassava
+=================
+
+[![Hackage](https://img.shields.io/hackage/v/streaming-cassava.svg)](https://hackage.haskell.org/package/streaming-cassava) [![Build Status](https://travis-ci.org/ivan-m/streaming-cassava.svg)](https://travis-ci.org/ivan-m/streaming-cassava)
+
+> [cassava] support for the [streaming] ecosystem
+
+[cassava]: http://hackage.haskell.org/package/cassava
+[streaming]: http://hackage.haskell.org/package/streaming
+
+This library allows you to easily stream CSV data in and out.  You can
+do so using both "plain" record-based (with optional header support)
+or name-based (header required to determine ordering)
+encoding/decoding.
+
+All encoding/decoding options are supported, it's possible to
+automatically add on default headers and you can even choose whether
+to fail on the first parse error or handle errors on a row-by-row
+basis.
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/src/Streaming/Cassava.hs b/src/Streaming/Cassava.hs
new file mode 100644
--- /dev/null
+++ b/src/Streaming/Cassava.hs
@@ -0,0 +1,254 @@
+{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses, OverloadedStrings,
+             ScopedTypeVariables #-}
+
+{- |
+   Module      : Streaming.Cassava
+   Description : Cassava support for the streaming library
+   Copyright   : (c) Ivan Lazar Miljenovic
+   License     : MIT
+   Maintainer  : Ivan.Miljenovic@gmail.com
+
+
+
+ -}
+module Streaming.Cassava
+  ( -- * Decoding
+    decode
+  , decodeWith
+  , decodeWithErrors
+  , CsvParseException (..)
+    -- ** Named decoding
+  , decodeByName
+  , decodeByNameWith
+  , decodeByNameWithErrors
+    -- * Encoding
+  , encode
+  , encodeDefault
+  , encodeWith
+    -- ** Named encoding
+  , encodeByName
+  , encodeByNameDefault
+  , encodeByNameWith
+    -- * Re-exports
+  , FromRecord (..)
+  , FromNamedRecord (..)
+  , ToRecord (..)
+  , ToNamedRecord (..)
+  , DefaultOrdered (..)
+  , HasHeader (..)
+  , Header
+  , header
+  , Name
+  , DecodeOptions(..)
+  , defaultDecodeOptions
+  , EncodeOptions(..)
+  , defaultEncodeOptions
+  ) where
+
+import qualified Data.ByteString                    as DB
+import qualified Data.ByteString.Lazy               as DBL
+import           Data.ByteString.Streaming          (ByteString)
+import qualified Data.ByteString.Streaming          as B
+import qualified Data.ByteString.Streaming.Internal as B
+import           Streaming                          (Of, Stream)
+import qualified Streaming.Prelude                  as S
+
+import           Data.Csv             (DecodeOptions(..), DefaultOrdered(..),
+                                       EncodeOptions(..), FromNamedRecord(..),
+                                       FromRecord(..), Header, Name,
+                                       ToNamedRecord(..), ToRecord(..),
+                                       defaultDecodeOptions,
+                                       defaultEncodeOptions, encIncludeHeader,
+                                       header)
+import           Data.Csv.Incremental (HasHeader(..), HeaderParser(..),
+                                       Parser(..))
+import qualified Data.Csv.Incremental as CI
+
+import Control.Exception         (Exception(..))
+import Control.Monad.Error.Class (MonadError, throwError)
+import Control.Monad.Trans.Class (lift)
+import Data.Bifunctor            (first)
+import Data.Maybe                (fromMaybe)
+import Data.String               (IsString(..))
+import Data.Typeable             (Typeable)
+
+--------------------------------------------------------------------------------
+
+-- | Use 'defaultOptions' for decoding the provided CSV.
+decode :: (MonadError CsvParseException m, FromRecord a)
+          => HasHeader -> ByteString m r
+          -> Stream (Of a) m r
+decode = decodeWith defaultDecodeOptions
+
+-- | Return back a stream of values from the provided CSV, stopping at
+--   the first error.
+--
+--   If you wish to instead ignore errors, consider using
+--   'decodeWithErrors' with either 'S.mapMaybe' or @'S.effects'
+--   . 'S.partitionEithers'@.
+--
+--   Unlike 'decodeWithErrors', any remaining input is discarded.
+decodeWith :: (MonadError CsvParseException m, FromRecord a)
+              => DecodeOptions -> HasHeader
+              -> ByteString m r -> Stream (Of a) m r
+decodeWith opts hdr bs = getValues (decodeWithErrors opts hdr bs)
+                         >>= either (throwError . fst) return
+
+-- | Return back a stream with an attempt at type conversion, and
+--   either the previous result or any overall parsing errors with the
+--   remainder of the input.
+--
+--   'S.partitionEithers' may be useful when using this function.
+decodeWithErrors :: (Monad m, FromRecord a) => DecodeOptions -> HasHeader
+                    -> ByteString m r
+                    -> Stream (Of (Either CsvParseException a)) m (Either (CsvParseException, ByteString m r) r)
+decodeWithErrors opts = runParser . CI.decodeWith opts
+
+runParser :: (Monad m) => Parser a -> ByteString m r
+             -> Stream (Of (Either CsvParseException a)) m (Either (CsvParseException, ByteString m r) r)
+runParser = loop
+  where
+    feed f str = (uncurry (loop . f) . fromMaybe (mempty, str))
+                          -- nxt == Nothing, str is just Return
+                 =<< lift (B.unconsChunk str)
+
+    loop p str = case p of
+                   Fail bs err -> return (Left (CsvParseException err, B.consChunk bs str))
+                   Many es get -> withEach es >> feed get str
+                   Done es     -> do withEach es
+                                     -- This is primarily just to
+                                     -- return the @r@ value, but also
+                                     -- acts as a check on the parser.
+                                     nxt <- lift (B.nextChunk str)
+                                     return $ case nxt of
+                                                Left r  -> Right r
+                                                Right _ -> Left ("Unconsumed input", str)
+
+    withEach = S.each . map (first CsvParseException)
+
+--------------------------------------------------------------------------------
+
+-- | Use 'defaultOptions' for decoding the provided CSV.
+decodeByName :: (MonadError CsvParseException m, FromNamedRecord a)
+                => ByteString m r -> Stream (Of a) m r
+decodeByName = decodeByNameWith defaultDecodeOptions
+
+-- | Return back a stream of values from the provided CSV, stopping at
+--   the first error.
+--
+--   A header is required to determine the order of columns, but then
+--   discarded.
+--
+--   If you wish to instead ignore errors, consider using
+--   'decodeByNameWithErrors' with either 'S.mapMaybe' or @'S.effects'
+--   . 'S.partitionEithers'@.
+--
+--   Unlike 'decodeByNameWithErrors', any remaining input is
+--   discarded.
+decodeByNameWith :: (MonadError CsvParseException m, FromNamedRecord a)
+                    => DecodeOptions
+                    -> ByteString m r -> Stream (Of a) m r
+decodeByNameWith opts bs = getValues (decodeByNameWithErrors opts bs)
+                           >>= either (throwError . fst) return
+
+-- | Return back a stream with an attempt at type conversion, but
+--   where the order of columns doesn't have to match the order of
+--   fields of your actual type.
+--
+--   This requires\/assumes a header in the CSV stream, which is
+--   discarded after parsing.
+--
+--   'S.partitionEithers' may be useful when using this function.
+decodeByNameWithErrors :: (Monad m, FromNamedRecord a) => DecodeOptions
+                          -> ByteString m r
+                          -> Stream (Of (Either CsvParseException a)) m (Either (CsvParseException, ByteString m r) r)
+decodeByNameWithErrors = loopH . CI.decodeByNameWith
+  where
+    loopH ph str = case ph of
+                     FailH bs err -> return (Left (CsvParseException err, B.consChunk bs str))
+                     PartialH get -> feedH get str
+                     DoneH _  p   -> runParser p str
+
+    feedH f str = (uncurry (loopH . f) . fromMaybe (mempty, str))
+                           -- nxt == Nothing, str is just Return
+                  =<< lift (B.unconsChunk str)
+
+--------------------------------------------------------------------------------
+
+-- | Encode a stream of values with the default options.
+--
+--   Optionally prefix the stream with headers (the 'header' function
+--   may be useful).
+encode :: (ToRecord a, Monad m) => Maybe Header
+          -> Stream (Of a) m r -> ByteString m r
+encode = encodeWith defaultEncodeOptions
+
+-- | Encode a stream of values with the default options and a derived
+--   header prefixed.
+encodeDefault :: forall a m r. (ToRecord a, DefaultOrdered a, Monad m)
+                 => Stream (Of a) m r -> ByteString m r
+encodeDefault = encode (Just (headerOrder (undefined :: a)))
+
+-- | Encode a stream of values with the provided options.
+--
+--   Optionally prefix the stream with headers (the 'header' function
+--   may be useful).
+encodeWith :: (ToRecord a, Monad m) => EncodeOptions -> Maybe Header
+              -> Stream (Of a) m r -> ByteString m r
+encodeWith opts mhdr = B.fromChunks
+                       . S.concat
+                       . addHeaders
+                       . S.map enc
+  where
+    addHeaders = maybe id (S.cons . enc) mhdr
+
+    enc :: (ToRecord v) => v -> [DB.ByteString]
+    enc = DBL.toChunks . CI.encodeWith opts . CI.encodeRecord
+
+--------------------------------------------------------------------------------
+
+-- | Use the default ordering to encode all fields\/columns.
+encodeByNameDefault :: forall a m r. (DefaultOrdered a, ToNamedRecord a, Monad m)
+                       => Stream (Of a) m r -> ByteString m r
+encodeByNameDefault = encodeByName (headerOrder (undefined :: a))
+
+-- | Select the columns that you wish to encode from your data
+--   structure using default options (which currently includes
+--   printing the header).
+encodeByName :: (ToNamedRecord a, Monad m) => Header
+                -> Stream (Of a) m r -> ByteString m r
+encodeByName = encodeByNameWith defaultEncodeOptions
+
+-- | Select the columns that you wish to encode from your data
+--   structure.
+--
+--   Header printing respects 'encIncludeheader'.
+encodeByNameWith :: (ToNamedRecord a, Monad m) => EncodeOptions -> Header
+                    -> Stream (Of a) m r -> ByteString m r
+encodeByNameWith opts hdr = B.fromChunks
+                            . S.concat
+                            . addHeaders
+                            . S.map enc
+  where
+    opts' = opts { encIncludeHeader = False }
+
+    addHeaders
+      | encIncludeHeader opts = S.cons . DBL.toChunks
+                                . CI.encodeWith opts' . CI.encodeRecord $ hdr
+      | otherwise             = id
+
+    enc = DBL.toChunks . CI.encodeByNameWith opts' hdr . CI.encodeNamedRecord
+
+--------------------------------------------------------------------------------
+
+getValues :: (MonadError e m) => Stream (Of (Either e a)) m r -> Stream (Of a) m r
+getValues = S.mapM (either throwError return)
+
+newtype CsvParseException = CsvParseException String
+  deriving (Eq, Show, Typeable)
+
+instance IsString CsvParseException where
+  fromString = CsvParseException
+
+instance Exception CsvParseException where
+  displayException (CsvParseException e) = "Error parsing csv: " ++ e
diff --git a/streaming-cassava.cabal b/streaming-cassava.cabal
new file mode 100644
--- /dev/null
+++ b/streaming-cassava.cabal
@@ -0,0 +1,53 @@
+name:                streaming-cassava
+version:             0.1.0.0
+synopsis:            Cassava support for the streaming ecosystem
+
+description:
+  Stream values to\/from CSV using Cassava.
+  .
+  Support is available for both named and \"plain\" data types,
+  optional header support and option handling.
+license:             MIT
+license-file:        LICENSE
+author:              Ivan Lazar Miljenovic
+maintainer:          Ivan.Miljenovic@gmail.com
+copyright:           Ivan Lazar Miljenovic
+category:            Data, Streaming
+build-type:          Simple
+extra-source-files:  ChangeLog.md, README.md
+cabal-version:       >=1.10
+tested-with:         GHC == 8.0.2, GHC == 8.1.*
+
+source-repository head
+  type:     git
+  location: https://github.com/ivan-m/streaming-cassava.git
+
+library
+  exposed-modules:     Streaming.Cassava
+  build-depends:       base >=4.8 && <5
+                     , bytestring
+                     , cassava == 0.5.*
+                     , mtl
+                     , streaming >= 0.1.1.0 && < 0.2
+                     , streaming-bytestring < 0.2
+                     , transformers
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+
+
+test-suite roundtrip
+  type:                exitcode-stdio-1.0
+  main-is:             roundtrip.hs
+  build-depends:       streaming-cassava
+                     , base
+                     , hspec == 2.4.*
+                     , mtl >= 2.2.1 && < 2.3
+                     , QuickCheck == 2.*
+                     , quickcheck-instances
+                     , streaming
+                     , text
+                     , vector >= 0.3
+  hs-source-dirs:      test
+  default-language:    Haskell2010
+  ghc-options:         -Wall
diff --git a/test/roundtrip.hs b/test/roundtrip.hs
new file mode 100644
--- /dev/null
+++ b/test/roundtrip.hs
@@ -0,0 +1,88 @@
+{-# LANGUAGE DeriveGeneric, FlexibleContexts, MultiParamTypeClasses, RankNTypes,
+             ScopedTypeVariables #-}
+
+{-# OPTIONS_GHC -Wno-unused-binds #-}
+
+{- |
+   Module      : Main
+   Description : Round-trip property testing
+   Copyright   : (c) Ivan Lazar Miljenovic
+   License     : MIT
+   Maintainer  : Ivan.Miljenovic@gmail.com
+
+
+
+ -}
+module Main (main) where
+
+import Streaming.Cassava
+
+import Streaming.Prelude (Of, Stream, each, toList_)
+
+import Test.Hspec                (describe, hspec)
+import Test.Hspec.QuickCheck     (prop)
+import Test.QuickCheck           (Arbitrary(..))
+import Test.QuickCheck.Instances ()
+
+import           Control.Monad.Except (MonadError, runExcept)
+import           Data.Text            (Text)
+import qualified Data.Vector          as V
+import           GHC.Generics         (Generic)
+
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = hspec $ do
+  describe "Plain records" $ do
+    prop "Just data" (useType encodeDecode)
+    prop "With headers" (useType encodeDecodeHeader)
+  describe "Named records" $ do
+    prop "Default order" (useType encodeDecodeNamed)
+    prop "Reversed order" (useType encodeDecodeNamedReordered)
+
+encodeDecode :: (FromRecord a, ToRecord a, Eq a) => [a] -> Bool
+encodeDecode = encodeDecodeWith (decode NoHeader . encode Nothing)
+
+encodeDecodeHeader :: (DefaultOrdered a, FromRecord a, ToRecord a, Eq a)
+                      => [a] -> Bool
+encodeDecodeHeader = encodeDecodeWith (decode HasHeader . encodeDefault)
+
+encodeDecodeNamed :: (DefaultOrdered a, FromNamedRecord a, ToNamedRecord a, Eq a)
+                     => [a] -> Bool
+encodeDecodeNamed = encodeDecodeWith (decodeByName . encodeByNameDefault)
+
+encodeDecodeNamedReordered :: forall a. (DefaultOrdered a, FromNamedRecord a, ToNamedRecord a, Eq a)
+                              => [a] -> Bool
+encodeDecodeNamedReordered = encodeDecodeWith (decodeByName . encodeByName hdr)
+  where
+    hdr = V.reverse (headerOrder (undefined :: a))
+
+encodeDecodeWith :: (Eq a)
+                    => (forall m r. (MonadError CsvParseException m) => Stream (Of a) m r -> Stream (Of a) m r)
+                    -> [a] -> Bool
+encodeDecodeWith f as = either (const False) (as==)
+                        . runExcept
+                        . toList_
+                        . f
+                        . each
+                        $ as
+
+useType :: ([Test] -> r) -> [Test] -> r
+useType = id
+
+data Test = Test
+  { columnA            :: !Int
+  , longer_column_name :: !Text
+  , mebbe              :: !(Maybe Double)
+  } deriving (Eq, Show, Read, Generic)
+
+-- DeriveAnyClass doesn't work with these types because of the Maybe
+
+instance FromRecord Test
+instance ToRecord Test
+instance DefaultOrdered Test
+instance FromNamedRecord Test
+instance ToNamedRecord Test
+
+instance Arbitrary Test where
+  arbitrary = Test <$> arbitrary <*> arbitrary <*> arbitrary
