diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# hex-text
+
+`hex-text` is a small library for converting between `ByteString`s and their representations as hexidecimal numbers encoded as `Text`.
+
+A `ByteString` is a list of bytes. A byte is a number between 0 and 255, represented by the `Word8` type. In a fixed-width hexidecimal representation, the lowest byte 0 is represented by the hex string `00`, and the greatest byte 255 is represented by the hex string `ff`. So, for example, the `ByteString` consisting of bytes \[ 1, 2, 3, 253, 254, 255 \] is represented as `010203fdfeff`.
+
+```haskell
+λ> import Text.Hex (encodeHex)
+λ> import Data.ByteString (pack)
+
+λ> (encodeHex . pack) [1, 2, 3, 253, 254, 255]
+"010203fdfeff"
+```
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,3 @@
+import Distribution.Simple
+
+main = defaultMain
diff --git a/hex-text.cabal b/hex-text.cabal
new file mode 100644
--- /dev/null
+++ b/hex-text.cabal
@@ -0,0 +1,49 @@
+name: hex-text
+version: 0.1.0.0
+category: Text
+
+synopsis: ByteString-Text hexidecimal conversions
+
+description: Encode a ByteString as a hexidecimal Text value,
+             or decode hexidecimal Text as a ByteString.
+
+homepage:    https://github.com/typeclasses/hex-text
+bug-reports: https://github.com/typeclasses/hex-text/issues
+
+author:     Chris Martin
+maintainer: Chris Martin, Julie Moronuki
+
+copyright: 2018 Typeclass Consulting, LLC
+license: Apache-2.0
+license-file: license.txt
+
+build-type: Simple
+cabal-version: >= 1.10
+
+extra-source-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/typeclasses/hex-text
+
+library
+  default-language: Haskell2010
+  hs-source-dirs: src
+  build-depends:
+      base >=4.7 && <5
+    , base16-bytestring
+    , bytestring
+    , text
+  exposed-modules:
+      Text.Hex
+
+test-suite doctest
+  default-language: Haskell2010
+  hs-source-dirs: test
+  type: exitcode-stdio-1.0
+  main-is: doctest.hs
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , doctest
diff --git a/license.txt b/license.txt
new file mode 100644
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,13 @@
+Copyright 2018 Typeclass Consulting, LLC
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/src/Text/Hex.hs b/src/Text/Hex.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Hex.hs
@@ -0,0 +1,134 @@
+module Text.Hex
+    (
+    -- * Encoding and decoding
+      encodeHex
+    , decodeHex
+    , lazilyEncodeHex
+
+    -- * Types
+    , Text
+    , LazyText
+    , ByteString
+    , LazyByteString
+
+    -- * Type conversions
+    , lazyText
+    , strictText
+    , lazyByteString
+    , strictByteString
+
+    ) where
+
+-- base16-bytestring
+import qualified Data.ByteString.Base16 as Base16
+import qualified Data.ByteString.Base16.Lazy as LazyBase16
+
+-- bytestring
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
+
+-- text
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Text.Lazy as LazyText
+import qualified Data.Text.Lazy.Encoding as LazyText
+
+-- | Strict byte string
+
+type ByteString =
+    ByteString.ByteString
+
+-- | Lazy byte string
+
+type LazyByteString =
+    LazyByteString.ByteString
+
+-- | Strict text
+
+type Text =
+    Text.Text
+
+-- | Lazy text
+
+type LazyText =
+    LazyText.Text
+
+-- |
+-- Encodes a byte string as hexidecimal number represented in text.
+-- Each byte of the input is converted into two characters in the
+-- resulting text.
+--
+-- >>> (encodeHex . ByteString.singleton) 192
+-- "c0"
+--
+-- >>> (encodeHex . ByteString.singleton) 168
+-- "a8"
+--
+-- >>> (encodeHex . ByteString.pack) [192, 168, 1, 2]
+-- "c0a80102"
+--
+-- 'Text' produced by @encodeHex@ can be converted back to a
+-- 'ByteString' using 'decodeHex'.
+--
+-- The lazy variant of @encodeHex@ is 'lazilyEncodeHex'.
+
+encodeHex :: ByteString -> Text
+encodeHex bs =
+    Text.decodeUtf8 (Base16.encode bs)
+
+-- |
+-- Decodes hexidecimal text as a byte string. If the text contains
+-- an even number of characters and consists only of the digits @0@
+-- through @9@ and letters @a@ through @f@, then the result is a
+-- 'Just' value.
+--
+-- >>> (fmap ByteString.unpack . decodeHex . Text.pack) "c0a80102"
+-- Just [192,168,1,2]
+--
+-- If the text contains an odd number of characters, decoding fails
+-- and produces 'Nothing'.
+--
+-- >>> (fmap ByteString.unpack . decodeHex . Text.pack) "c0a8010"
+-- Nothing
+--
+-- If the text contains non-hexidecimal characters, decoding fails
+-- and produces 'Nothing'.
+--
+-- >>> (fmap ByteString.unpack . decodeHex . Text.pack) "x0a80102"
+-- Nothing
+--
+-- The letters may be in either upper or lower case. This next
+-- example therefore gives the same result as the first one above:
+--
+-- >>> (fmap ByteString.unpack . decodeHex . Text.pack) "C0A80102"
+-- Just [192,168,1,2]
+
+decodeHex :: Text -> Maybe ByteString
+decodeHex txt =
+    let (x, remainder) = Base16.decode (Text.encodeUtf8 txt)
+    in  if ByteString.null remainder then Just x else Nothing
+
+-- |
+-- @lazilyEncodeHex@ is the lazy variant of 'encodeHex'.
+--
+-- With laziness, it is possible to encode byte strings of
+-- infinite length:
+--
+-- >>> (LazyText.take 8 . lazilyEncodeHex . LazyByteString.pack . cycle) [1, 2, 3]
+-- "01020301"
+
+lazilyEncodeHex :: LazyByteString -> LazyText
+lazilyEncodeHex bs =
+    LazyText.decodeUtf8 (LazyBase16.encode bs)
+
+lazyText :: Text -> LazyText
+lazyText = LazyText.fromStrict
+
+strictText :: LazyText -> Text
+strictText = LazyText.toStrict
+
+lazyByteString :: ByteString -> LazyByteString
+lazyByteString = LazyByteString.fromStrict
+
+strictByteString :: LazyByteString -> ByteString
+strictByteString = LazyByteString.toStrict
diff --git a/test/doctest.hs b/test/doctest.hs
new file mode 100644
--- /dev/null
+++ b/test/doctest.hs
@@ -0,0 +1,5 @@
+import Test.DocTest
+
+main :: IO ()
+main =
+  doctest [ "-isrc", "src/Text/Hex.hs" ]
