diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for base64
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2019, Emily Pillmore
+
+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 Emily Pillmore 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# Base64
+
+[![Build Status](https://travis-ci.com/emilypi/base64.svg?branch=master)](https://travis-ci.com/emilypi/base64)
+
+Padded and unpadded base64 and base64url encodings for `Text` and `ByteString` values, along with their optics.
+
+
+### Why?
+
+Haskell has two main libraries for Base64: `memory`, and `base64-bytestring`.
+
+Of these, `memory` is geared towards integration with other memory primitives in the library, without much of an eye towards performance, while `base64-bytestring` is built to exclusively address `ByteString` encoding and decoding, and is very performant. Many great strides have been made in the realm of Base64 performance and vectorization just in the past 5 years, which this library attempts to capture. Additionally, we attempt to fix percieved shortcomings with both APIs in the support of unpadded Base64 and Base64-url support (which `memory` provides, but not `base64-bytestring`), as well as supporting `Text` values (neither libraries provide), and also supplying some nice compositional optics for composing structures with Base64-encodable/decodable focii (neither libraries provide).
+
+### Optics
+
+Structs may want to support encoding and decoding their substructures, which is supported with the following prismatic typeclass:
+
+```haskell
+class AsBase64 s where
+    type Base64 s
+    -- | A prism into a base64-encoded focus of
+    -- some type
+    --
+    _Base64 :: Prism' s (Base64 s)
+
+    -- | A prism into the base64url-encoded focus of
+    -- some type
+    --
+    _Base64Url :: Prism' s (Base64 s)
+```
+
+The data of a `Prism` naturally conforms to this "encoding/decoding" dichotomy, where the `Review`, or "builder" half of the `Prism` of type `b -> t` is an encoding, and the "Matcher" half of the prism, of type `s -> Either t a`, represents a decoding of a similar structure. Monomorphizing for `t ~ s` and `a ~ b`, a simple `Prism` is formed:
+
+```haskell
+>>> _Base64 @Text # "<<???>>"
+"PDw/Pz8+Pg=="
+
+>>> "PDw/Pz8+Pg==" ^? _Base64 @Text
+Just "<<???>>"
+```
+
+The two most obvious types for which we have an instance are those that are supported natively by the library: `ByteString` and `Text`. Trivially, their instances consist of the functions provided here in this library.
+
+### Summary
+
+What does this library provide? Here is the summary:
+
+- Better performance over existing Base64 libraries (2x and 3x for most use-cases - see [PERFORMANCE.md](benchmarks/PERFORMANCE.md))
+- Support for unpadded Base64 and Base64-url
+- Support for `Text` encodings and decodings
+- Classy optics for handling more complex structures with Base64 representations
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/base64.cabal b/base64.cabal
new file mode 100644
--- /dev/null
+++ b/base64.cabal
@@ -0,0 +1,102 @@
+cabal-version:       2.4
+
+name:                base64
+version:             0.0.1.0
+synopsis:            RFC 4648-compliant padded and unpadded base64 and base64url encodings
+description:
+  RFC 4648-compliant padded and unpadded base64 and base64url encoding and decoding.
+homepage:            https://github.com/emilypi/base64
+bug-reports:         https://github.com/emilypi/base64/issues
+license:             BSD-3-Clause
+license-file:        LICENSE
+author:              Emily Pillmore
+maintainer:          emilypi@cohomolo.gy
+copyright:           (c) 2019 Emily Pillmore
+category:            Data
+build-type:          Simple
+extra-source-files:
+  CHANGELOG.md
+  README.md
+
+tested-with:         GHC ==8.8.1 || ==8.6.5 || ==8.6.3 || ==8.4.4 || ==8.4.3 || ==8.0.2
+
+source-repository head
+  type:     git
+  location: https://github.com/emilypi/base64.git
+
+
+flag optics
+  description: Enable optics for base64-encoding text and bytestrings
+  manual: True
+  default: True
+
+flag perf-flags
+  description: Performance tuning flags and information
+  manual: True
+  default: False
+
+
+library
+  exposed-modules:     Data.ByteString.Base64
+                     , Data.ByteString.Base64.URL
+
+                     , Data.Text.Encoding.Base64
+                     , Data.Text.Encoding.Base64.URL
+
+  other-modules:       Data.ByteString.Base64.Internal
+
+  build-depends:       base       >=4.9 && <5
+                     , bytestring
+                     , deepseq
+                     , text
+
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+
+  if flag(optics)
+    exposed-modules:   Data.ByteString.Base64.Lens
+                     , Data.Text.Encoding.Base64.Lens
+    build-depends:     lens ^>= 4.18
+
+  if flag(perf-flags)
+    ghc-options:       -ddump-simpl
+                       -ddump-to-file
+                       -ddump-stranal
+
+
+test-suite tasty
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Base64Tests.hs
+  build-depends:       base >=4.9 && <5
+                     , base64
+                     , base64-bytestring
+                     , random-bytestring
+                     , tasty
+                     , tasty-hunit
+                     , text
+
+  ghc-options:       -Wall -threaded -with-rtsopts=-N
+
+benchmark bench
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      benchmarks
+  main-is:             Base64Bench.hs
+  build-depends:       base >=4.9 && <5
+                     , base64
+                     , base64-bytestring
+                     , bytestring
+                     , deepseq
+                     , criterion
+                     , memory
+                     , random-bytestring
+                     , text
+
+  ghc-options:
+    -Wall
+    -threaded
+    -rtsopts
+    -with-rtsopts=-N
diff --git a/benchmarks/Base64Bench.hs b/benchmarks/Base64Bench.hs
new file mode 100644
--- /dev/null
+++ b/benchmarks/Base64Bench.hs
@@ -0,0 +1,97 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+module Main
+( main
+) where
+
+
+import Control.DeepSeq
+
+import Criterion
+import Criterion.Main
+
+import "memory" Data.ByteArray.Encoding as Mem
+import Data.ByteString
+import "base64-bytestring" Data.ByteString.Base64 as Bos
+import "base64" Data.ByteString.Base64 as B64
+import Data.ByteString.Random (random)
+import Data.Kind
+import Data.Text (Text)
+import qualified Data.Text.Encoding.Base64 as B64T
+
+
+main :: IO ()
+main = defaultMain $ fmap benchN [25,100,1000,10000,100000]
+  where
+    benchN n = env (random n) $ bgroup (show n) . bgroup_
+    bgroup_ e =
+      [ bgroup "encode"
+        [ encodeBench @'Mem e
+        , encodeBench @'Bos e
+        , encodeBench @'B64 e
+        ]
+      -- , bgroup "base64 decode"
+      --   [ decodeBench @'Mem e
+      --   , decodeBench @'Bos e
+      --   , decodeBench @'B64 e
+      --   ]
+      ]
+
+encodeBench :: forall a. Harness a => Base64 a -> Benchmark
+encodeBench = bench (label @a) . nf (encoder @a)
+
+decodeBench :: forall a. Harness a => Base64 a -> Benchmark
+decodeBench = bench (label @a) . nf (decoder @a)
+
+
+data Bench where
+  Mem :: Bench
+  Bos :: Bench
+  B64 :: Bench
+  T64 :: Bench
+
+class (NFData (Base64 a), NFData (Err a)) => Harness (a :: Bench) where
+    type Base64 a :: Type
+    type Err a :: Type
+    label :: String
+    encoder :: Base64 a -> Base64 a
+    decoder :: Base64 a -> Either (Err a) (Base64 a)
+
+instance Harness 'Mem where
+    type Base64 'Mem = ByteString
+    type Err 'Mem = String
+    label = "memory"
+    encoder = Mem.convertToBase Mem.Base64
+    decoder = Mem.convertFromBase Mem.Base64
+
+instance Harness 'Bos where
+    type Base64 'Bos = ByteString
+    type Err 'Bos = String
+    label = "base64-bytestring"
+    encoder = Bos.encode
+    decoder = Bos.decode
+
+instance Harness 'B64 where
+    type Base64 'B64 = ByteString
+    type Err 'B64 = Text
+    label = "base64"
+    encoder = B64.encodeBase64
+    decoder = B64.decodeBase64
+
+instance Harness 'T64 where
+    type Base64 'T64 = Text
+    type Err 'T64 = Text
+    label = "base64-text"
+    encoder = B64T.encodeBase64
+    decoder = B64T.decodeBase64
diff --git a/src/Data/ByteString/Base64.hs b/src/Data/ByteString/Base64.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base64.hs
@@ -0,0 +1,66 @@
+-- |
+-- Module       : Data.ByteString.Base64
+-- Copyright 	: (c) 2019 Emily Pillmore
+-- License	: BSD-style
+--
+-- Maintainer	: Emily Pillmore <emilypi@cohomolo.gy>
+-- Stability	: Experimental
+-- Portability	: portable
+--
+-- This module contains the combinators implementing the
+-- RFC 4648 specification for the Base64 encoding including
+-- unpadded and lenient variants
+--
+module Data.ByteString.Base64
+( encodeBase64
+, decodeBase64
+, encodeBase64Unpadded
+, decodeBase64Unpadded
+) where
+
+
+import Data.ByteString (ByteString)
+import Data.ByteString.Base64.Internal
+import Data.Text (Text)
+
+
+-- | Encode a 'ByteString' in base64 with padding.
+--
+-- See: RFC-4648 section 4
+--
+encodeBase64 :: ByteString -> ByteString
+encodeBase64 = encodeB64Padded base64Table
+{-# INLINE encodeBase64 #-}
+
+-- | Decode a padded base64-encoded 'ByteString'
+--
+-- See: RFC-4648 section 4
+--
+decodeBase64 :: ByteString -> Either Text ByteString
+decodeBase64 = decodeB64 decodeB64Table
+{-# INLINE decodeBase64 #-}
+
+-- | Encode a 'ByteString' in base64 without padding.
+--
+-- Note: in some circumstances, the use of padding ("=") in base-encoded data
+-- is not required or used. This is not one of them. If you are absolutely sure
+-- the length of your bytestring is divisible by 3, this function will be the same
+-- as 'encodeBase64' with padding, however, if not, you may see garbage appended to
+-- your bytestring in the form of "\NUL".
+--
+-- Only call unpadded variants when you can make assumptions about the length of
+-- your input data.
+--
+-- See: RFC-4648 section 3.2
+--
+encodeBase64Unpadded :: ByteString -> ByteString
+encodeBase64Unpadded = encodeB64Unpadded base64Table
+{-# INLINE encodeBase64Unpadded #-}
+
+-- | Decode an unpadded base64-encoded 'ByteString'
+--
+-- See: RFC-4648 section 3.2
+--
+decodeBase64Unpadded :: ByteString -> Either Text ByteString
+decodeBase64Unpadded = decodeB64 decodeB64Table
+{-# INLINE decodeBase64Unpadded #-}
diff --git a/src/Data/ByteString/Base64/Internal.hs b/src/Data/ByteString/Base64/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base64/Internal.hs
@@ -0,0 +1,373 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE MagicHash #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications #-}
+-- |
+-- Module       : Data.ByteString.Base64.Internal
+-- Copyright 	: (c) 2019 Emily Pillmore
+-- License	: BSD-style
+--
+-- Maintainer	: Emily Pillmore <emilypi@cohomolo.gy>
+-- Stability	: Experimental
+-- Portability	: portable
+--
+-- Internal module defining the encoding and decoding
+-- processes and tables.
+--
+module Data.ByteString.Base64.Internal
+( -- * Base64 encoding
+  encodeB64Padded
+, encodeB64Unpadded
+
+  -- * Base64 decoding
+, decodeB64
+
+  -- * Decoding Tables
+  -- ** Standard
+, decodeB64Table
+  -- ** Base64-url
+, decodeB64UrlTable
+
+  -- * Encoding Tables
+  -- ** Standard
+, base64Table
+
+  -- ** Base64-url
+, base64UrlTable
+) where
+
+
+import Data.Bits
+import Data.ByteString (ByteString)
+import Data.ByteString.Internal
+import Data.Text (Text)
+import qualified Data.Text as T
+import Foreign.ForeignPtr
+import Foreign.Ptr
+import Foreign.Storable
+
+import GHC.Exts
+import GHC.ForeignPtr
+import GHC.Word
+
+import System.IO.Unsafe
+
+-- -------------------------------------------------------------------------- --
+-- Internal data
+
+-- | Only the lookup table need be a foreignptr,
+-- and then, only so that we can automate some touches to keep it alive
+--
+data EncodingTable = EncodingTable
+  {-# UNPACK #-} !(Ptr Word8)
+  {-# UNPACK #-} !(ForeignPtr Word16)
+
+-- | Allocate and fill @n@ bytes with some data
+--
+writeNPlainForeignPtrBytes
+    :: ( Storable a
+       , Storable b
+       )
+    => Int
+    -> [a]
+    -> ForeignPtr b
+writeNPlainForeignPtrBytes !n as = unsafeDupablePerformIO $ do
+    fp <- mallocPlainForeignPtrBytes n
+    withForeignPtr fp $ \p -> go p as
+    return (castForeignPtr fp)
+  where
+    go !_ [] = return ()
+    go !p (x:xs) = poke p x >> go (plusPtr p 1) xs
+
+packTable :: Addr# -> EncodingTable
+packTable alphabet = etable
+  where
+    ix (I# n) = W8# (indexWord8OffAddr# alphabet n)
+
+    !etable =
+      let bs = concat
+            [ [ ix i, ix j ]
+            | !i <- [0..63]
+            , !j <- [0..63]
+            ]
+      in EncodingTable (Ptr alphabet) (writeNPlainForeignPtrBytes 8192 bs)
+{-# INLINE packTable #-}
+
+base64UrlTable :: EncodingTable
+base64UrlTable = packTable "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"#
+{-# NOINLINE base64UrlTable #-}
+
+base64Table :: EncodingTable
+base64Table = packTable "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"#
+{-# NOINLINE base64Table #-}
+
+
+-- -------------------------------------------------------------------------- --
+-- Unpadded Base64
+
+encodeB64Unpadded :: EncodingTable -> ByteString -> ByteString
+encodeB64Unpadded (EncodingTable _ !efp) (PS sfp !soff !slen) =
+    unsafeCreate dlen $ \dptr ->
+    withForeignPtr sfp $ \sptr ->
+    withForeignPtr efp $ \eptr ->
+      encodeB64UnpaddedInternal
+        eptr
+        (plusPtr sptr soff)
+        (castPtr dptr)
+        (plusPtr sptr (soff + slen))
+  where
+    !dlen = 4 * ((slen + 2) `div` 3)
+{-# INLINE encodeB64Unpadded #-}
+
+-- | Unpadded Base64. The implicit assumption is that the input
+-- data has a length that is a multiple of 3
+--
+encodeB64UnpaddedInternal
+    :: Ptr Word16
+    -> Ptr Word8
+    -> Ptr Word16
+    -> Ptr Word8
+    -> IO ()
+encodeB64UnpaddedInternal etable sptr dptr end = go sptr dptr
+  where
+    w32 :: Word8 -> Word32
+    w32 i = fromIntegral i
+    {-# INLINE w32 #-}
+
+    go !src !dst
+      | src >= end = return ()
+      | otherwise = do
+
+        !i <- w32 <$> peek src
+        !j <- w32 <$> peek (plusPtr src 1)
+        !k <- w32 <$> peek (plusPtr src 2)
+
+        let !w = (shiftL i 16) .|. (shiftL j 8) .|. k
+
+        !x <- peekElemOff etable (fromIntegral (shiftR w 12))
+        !y <- peekElemOff etable (fromIntegral (w .&. 0xfff))
+
+        poke dst x
+        poke (plusPtr dst 2) y
+
+        go (plusPtr src 3) (plusPtr dst 4)
+{-# INLINE encodeB64UnpaddedInternal #-}
+
+-- -------------------------------------------------------------------------- --
+-- Padded Base64
+
+encodeB64Padded :: EncodingTable -> ByteString -> ByteString
+encodeB64Padded (EncodingTable !aptr !efp) (PS !sfp !soff !slen) =
+    unsafeCreate dlen $ \dptr ->
+    withForeignPtr sfp $ \sptr ->
+    withForeignPtr efp $ \eptr ->
+      encodeB64PaddedInternal
+        aptr
+        eptr
+        (plusPtr sptr soff)
+        (castPtr dptr)
+        (plusPtr sptr (soff + slen))
+  where
+    dlen :: Int
+    !dlen = 4 * ((slen + 2) `div` 3)
+{-# INLINE encodeB64Padded #-}
+
+encodeB64PaddedInternal
+    :: Ptr Word8
+    -> Ptr Word16
+    -> Ptr Word8
+    -> Ptr Word16
+    -> Ptr Word8
+    -> IO ()
+encodeB64PaddedInternal (Ptr !alpha) !etable !sptr !dptr !end = go sptr dptr
+  where
+    ix (W8# i) = W8# (indexWord8OffAddr# alpha (word2Int# i))
+    {-# INLINE ix #-}
+
+    w32 :: Word8 -> Word32
+    w32 = fromIntegral
+    {-# INLINE w32 #-}
+
+    go !src !dst
+      | plusPtr src 2 >= end = finalize src (castPtr dst)
+      | otherwise = do
+
+        -- ideally, we want to do single read @uint32_t w = src[0..3]@ and simply
+        -- discard the upper bits. TODO.
+        --
+        !i <- w32 <$> peek src
+        !j <- w32 <$> peek (plusPtr src 1)
+        !k <- w32 <$> peek (plusPtr src 2)
+
+        -- pack 3 'Word8's into a the first 24 bits of a 'Word32'
+        --
+        let !w = (shiftL i 16) .|. (shiftL j 8) .|. k
+
+        -- ideally, we'd want to pack this is in a single read, then
+        -- a single write
+        --
+        !x <- peekElemOff etable (fromIntegral (shiftR w 12))
+        !y <- peekElemOff etable (fromIntegral (w .&. 0xfff))
+
+        poke dst x
+        poke (plusPtr dst 2) y
+
+        go (plusPtr src 3) (plusPtr dst 4)
+
+
+    finalize :: Ptr Word8 -> Ptr Word8 -> IO ()
+    finalize !src !dst
+      | src == end = return ()
+      | otherwise = do
+        !k <- peekByteOff src 0
+
+        let !a = shiftR (k .&. 0xfc) 2
+            !b = shiftL (k .&. 0x03) 4
+
+        pokeByteOff dst 0 (ix a)
+
+        if plusPtr src 2 == end
+        then do
+          !k' <- peekByteOff src 1
+
+          let !b' = shiftR (k' .&. 0xf0) 4 .|. b
+              !c' = shiftL (k' .&. 0x0f) 2
+
+          -- ideally, we'd want to pack these is in a single write
+          --
+          pokeByteOff dst 1 (ix b')
+          pokeByteOff dst 2 (ix c')
+        else do
+          pokeByteOff dst 1 (ix b)
+          pokeByteOff @Word8 dst 2 0x3d
+
+        pokeByteOff @Word8 dst 3 0x3d
+{-# INLINE encodeB64PaddedInternal #-}
+
+-- -------------------------------------------------------------------------- --
+-- Decoding Base64
+
+-- | Non-URLsafe b64 decoding table (naive)
+--
+decodeB64Table :: ForeignPtr Word8
+decodeB64Table = writeNPlainForeignPtrBytes @Word8 256
+      [ 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x3e,0xff,0xff,0xff,0x3f
+      , 0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x3b,0x3c,0x3d,0xff,0xff,0xff,0x63,0xff,0xff
+      , 0xff,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e
+      , 0x0f,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28
+      , 0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f,0x30,0x31,0x32,0x33,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      ]
+{-# NOINLINE decodeB64Table #-}
+
+decodeB64UrlTable :: ForeignPtr Word8
+decodeB64UrlTable = writeNPlainForeignPtrBytes @Word8 256
+      [ 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x3e,0xff,0xff
+      , 0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x3b,0x3c,0x3d,0xff,0xff,0xff,0x63,0xff,0xff
+      , 0xff,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e
+      , 0x0f,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0xff,0xff,0xff,0xff,0x3f
+      , 0xff,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28
+      , 0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f,0x30,0x31,0x32,0x33,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      , 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
+      ]
+{-# NOINLINE decodeB64UrlTable #-}
+
+decodeB64 :: ForeignPtr Word8 -> ByteString -> Either Text ByteString
+decodeB64 !dtfp (PS !sfp !soff !slen)
+    | r /= 0 = Left "invalid padding"
+    | otherwise = unsafeDupablePerformIO $
+      withForeignPtr dtfp $ \dtable ->
+        withForeignPtr sfp $ \sptr -> do
+        dfp <- mallocPlainForeignPtrBytes dlen
+        withForeignPtr dfp $ \dptr ->
+          decodeB64Internal
+            dtable
+            (plusPtr sptr soff)
+            dptr
+            (plusPtr sptr (soff + slen))
+            dfp
+  where
+    (!q, !r) = divMod slen 4
+    !dlen = q * 3
+{-# INLINE decodeB64 #-}
+
+decodeB64Internal
+    :: Ptr Word8
+        -- ^ decode lookup table
+    -> Ptr Word8
+        -- ^ src pointer
+    -> Ptr Word8
+        -- ^ dst pointer
+    -> Ptr Word8
+        -- ^ end of src ptr
+    -> ForeignPtr Word8
+        -- ^ dst foreign ptr (for consing bs)
+    -> IO (Either Text ByteString)
+decodeB64Internal !dtable !sptr !dptr !end !dfp = go dptr sptr 0
+  where
+    err = return . Left . T.pack
+    {-# INLINE err #-}
+
+    finalize !n = return (Right (PS dfp 0 n))
+    {-# INLINE finalize #-}
+
+    look :: Ptr Word8 -> IO Word32
+    look p = do
+      !i <- peekByteOff @Word8 p 0
+      !v <- peekByteOff @Word8 dtable (fromIntegral i)
+      return (fromIntegral v)
+    {-# INLINE look #-}
+
+    go !dst !src !n
+      | src >= end = return (Right (PS dfp 0 n))
+      | otherwise = do
+        a <- look src
+        b <- look (src `plusPtr` 1)
+        c <- look (src `plusPtr` 2)
+        d <- look (src `plusPtr` 3)
+
+        if a == 0x63 || b == 0x63
+        then err
+          $ "invalid padding near offset: "
+          ++ show (src `minusPtr` sptr)
+        else
+          if a .|. b .|. c .|. d == 0xff
+          then err
+            $ "invalid base64 encoding near offset: "
+            ++ show (src `minusPtr` sptr)
+          else do
+            let !w = (a `shiftL` 18)
+                  .|. (b `shiftL` 12)
+                  .|. (c `shiftL` 6)
+                  .|. d
+
+            poke @Word8 dst (fromIntegral (w `shiftR` 16))
+            if c == 0x63
+            then finalize (n + 1)
+            else do
+              poke @Word8 (dst `plusPtr` 1) (fromIntegral (w `shiftR` 8))
+              if d == 0x63
+              then finalize (n + 2)
+              else do
+                poke @Word8 (dst `plusPtr` 2) (fromIntegral w)
+                go (dst `plusPtr` 3) (src `plusPtr` 4) (n + 3)
+{-# INLINE decodeB64Internal #-}
diff --git a/src/Data/ByteString/Base64/Lens.hs b/src/Data/ByteString/Base64/Lens.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base64/Lens.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE TypeFamilies #-}
+-- |
+-- Module       : Data.Text.Encoding.Base64.Lens
+-- Copyright 	: (c) 2019 Emily Pillmore
+-- License	: BSD-style
+--
+-- Maintainer	: Emily Pillmore <emilypi@cohomolo.gy>
+-- Stability	: Experimental
+-- Portability	: TypeFamilies
+--
+-- This module contains the 'AsBase64' instance for @Text@, which is
+-- defined to be the collection of 'Control.Lens.Type.Prism's defining the
+-- RFC 4648 specification for the Base64 encoding format.
+--
+-- In order to expose this file, you must build the package with
+-- '-foptics' enabled.
+--
+module Data.ByteString.Base64.Lens
+( -- * Classy Prisms
+  AsBase64(..)
+, AsBase64Unpadded(..)
+) where
+
+
+import Control.Lens
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Base64 as B64
+import qualified Data.ByteString.Base64.URL as B64U
+
+
+-- | If a particular type @s@ has a base64 representation
+-- for any of its focii, this class provides the optical interface
+-- for satisfying the padded base64 spec in RFC 4648
+--
+class AsBase64 s where
+    type Base64 s
+    -- | A prism into a base64-encoded focus of
+    -- some type
+    --
+    -- Examples:
+    --
+    -- >>> _Base64 @Text # "Sun"
+    -- "UV3u"
+    --    --
+    -- >>> "PDw/Pz8+Pg==" ^? _Base64
+    -- Just "<<???>>"
+    --
+    _Base64 :: Prism' s (Base64 s)
+
+    -- | A prism into the base64url-encoded focus of
+    -- some type
+    --
+    -- Examples:
+    --
+    -- >>> _Base64Url @Text # "Sun"
+    -- "UV3u"
+    --
+    -- >>> "PDw_Pz8-Pg==" ^? _Base64Url
+    -- Just "<<???>>"
+    --
+    _Base64Url :: Prism' s (Base64 s)
+
+-- | If a particular type @a@ has an unpadded base64 representation
+-- for any of its focii, this class provides the optical interface
+-- for satisfying the unpadded base64 spec in RFC 4648
+--
+class AsBase64Unpadded s where
+    type Base64Unpadded s
+    -- | A prism into the unpadded base64-encoded focus of
+    -- some type
+    --
+    _Base64Unpadded :: Prism' s (Base64Unpadded s)
+
+    -- | A prism into the unpadded base64url-encoded focus of
+    -- some type
+    --
+    _Base64UrlUnpadded :: Prism' s (Base64Unpadded s)
+
+
+instance AsBase64 ByteString where
+    type Base64 ByteString = ByteString
+
+    _Base64 = prism' B64.encodeBase64 $ \s -> case B64.decodeBase64 s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64 #-}
+
+    _Base64Url = prism' B64U.encodeBase64 $ \s -> case B64U.decodeBase64 s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64Url #-}
+
+instance AsBase64Unpadded ByteString where
+    type Base64Unpadded ByteString = ByteString
+
+    _Base64Unpadded = prism' B64.encodeBase64 $ \s -> case B64U.decodeBase64 s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64Unpadded #-}
+
+    _Base64UrlUnpadded = prism' B64.encodeBase64 $ \s -> case B64U.decodeBase64Unpadded s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64UrlUnpadded #-}
diff --git a/src/Data/ByteString/Base64/URL.hs b/src/Data/ByteString/Base64/URL.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ByteString/Base64/URL.hs
@@ -0,0 +1,61 @@
+-- |
+-- Module       : Data.ByteString.Base64.URL
+-- Copyright 	: (c) 2019 Emily Pillmore
+-- License	: BSD-style
+--
+-- Maintainer	: Emily Pillmore <emilypi@cohomolo.gy>
+-- Stability	: Experimental
+-- Portability	: portable
+--
+-- This module contains the combinators implementing the
+-- RFC 4648 specification for the Base64-URL encoding including
+-- unpadded and lenient variants
+--
+module Data.ByteString.Base64.URL
+( encodeBase64
+, decodeBase64
+, encodeBase64Unpadded
+, decodeBase64Unpadded
+) where
+
+import Data.ByteString (ByteString)
+import Data.ByteString.Base64.Internal
+import Data.Text (Text)
+
+
+-- | Encode a 'ByteString' in base64-url with padding.
+--
+-- See: RFC-4648 section 5
+--
+encodeBase64 :: ByteString -> ByteString
+encodeBase64 = encodeB64Padded base64UrlTable
+
+-- | Decode a padded base64-url encoded 'ByteString'
+--
+-- See: RFC-4648 section 4
+--
+decodeBase64 :: ByteString -> Either Text ByteString
+decodeBase64 = decodeB64 decodeB64UrlTable
+
+-- | Encode a 'ByteString' in base64-url without padding.
+--
+-- Note: in some circumstances, the use of padding ("=") in base-encoded data
+-- is not required or used. If you are absolutely sure the length of your
+-- input data is divisible by 3, this function will be the same as 'encodeBase64'
+-- with padding. However, if not, you may see garbage appended to output in the
+-- form of "\NUL".
+--
+-- Only call unpadded variants when you can make assumptions about the length of
+-- your input data.
+--
+-- See: RFC-4648 section 3.2
+--
+encodeBase64Unpadded :: ByteString -> ByteString
+encodeBase64Unpadded = encodeB64Unpadded base64UrlTable
+
+-- | Decode an unpadded base64-url encoded 'ByteString'
+--
+-- See: RFC-4648 section 4
+--
+decodeBase64Unpadded :: ByteString -> Either Text ByteString
+decodeBase64Unpadded = decodeB64 decodeB64UrlTable
diff --git a/src/Data/Text/Encoding/Base64.hs b/src/Data/Text/Encoding/Base64.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/Encoding/Base64.hs
@@ -0,0 +1,66 @@
+-- |
+-- Module       : Data.Text.Encoding.Base64
+-- Copyright 	: (c) 2019 Emily Pillmore
+-- License	: BSD-style
+--
+-- Maintainer	: Emily Pillmore <emilypi@cohomolo.gy>
+-- Stability	: Experimental
+-- Portability	: portable
+--
+-- This module contains the combinators implementing the
+-- RFC 4648 specification for the Base64 encoding including
+-- unpadded and lenient variants
+--
+module Data.Text.Encoding.Base64
+( encodeBase64
+, decodeBase64
+, encodeBase64Unpadded
+, decodeBase64Unpadded
+) where
+
+
+import qualified Data.ByteString.Base64 as B64
+
+import Data.Text (Text)
+import qualified Data.Text.Encoding as T
+
+-- | Encode 'Text' in base64 with padding.
+--
+-- See: RFC-4648 section 5
+--
+encodeBase64 :: Text -> Text
+encodeBase64 = T.decodeUtf8 . B64.encodeBase64 . T.encodeUtf8
+
+-- | Decode a padded base64 encoded 'Text' value
+--
+-- See: RFC-4648 section 4
+--
+decodeBase64 :: Text -> Either Text Text
+decodeBase64 = fmap T.decodeUtf8 . B64.decodeBase64 . T.encodeUtf8
+
+-- | Encode a 'Text' in base64 without padding.
+--
+-- Note: in some circumstances, the use of padding ("=") in base-encoded data
+-- is not required or used. If you are absolutely sure the length of your
+-- input data is divisible by 3, this function will be the same as 'encodeBase64'
+-- with padding. However, if not, you may see garbage appended to output in the
+-- form of "\NUL".
+--
+-- Only call unpadded variants when you can make assumptions about the length of
+-- your input data.
+--
+-- See: RFC-4648 section 3.2
+--
+encodeBase64Unpadded :: Text -> Text
+encodeBase64Unpadded = T.decodeUtf8
+    . B64.encodeBase64Unpadded
+    . T.encodeUtf8
+
+-- | Decode an unpadded base64 encoded 'Text'
+--
+-- See: RFC-4648 section 4
+--
+decodeBase64Unpadded :: Text -> Either Text Text
+decodeBase64Unpadded = fmap T.decodeUtf8
+    . B64.decodeBase64Unpadded
+    . T.encodeUtf8
diff --git a/src/Data/Text/Encoding/Base64/Lens.hs b/src/Data/Text/Encoding/Base64/Lens.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/Encoding/Base64/Lens.hs
@@ -0,0 +1,56 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE TypeFamilies #-}
+-- |
+-- Module       : Data.Text.Encoding.Base64.Lens
+-- Copyright 	: (c) 2019 Emily Pillmore
+-- License	: BSD-style
+--
+-- Maintainer	: Emily Pillmore <emilypi@cohomolo.gy>
+-- Stability	: Experimental
+-- Portability	: TypeFamilies
+--
+-- This module contains the 'AsBase64' and 'AsBase64Unpadded' instances
+-- for 'Text', which defined to be the collection of 'Control.Lens.Type.Prism's defining the
+-- RFC 4648 specification for the padded and unpadded Base64 encoding format.
+--
+-- These typeclasses are re-exported for convenience
+--
+module Data.Text.Encoding.Base64.Lens
+( AsBase64(..)
+, AsBase64Unpadded(..)
+) where
+
+
+import Control.Lens
+
+import Data.Text (Text)
+import Data.ByteString.Base64.Lens
+import qualified Data.Text.Encoding.Base64 as B64T
+import qualified Data.Text.Encoding.Base64.URL as B64TU
+
+
+instance AsBase64 Text where
+    type Base64 Text = Text
+
+    _Base64 = prism' B64T.encodeBase64 $ \s -> case B64T.decodeBase64 s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64 #-}
+
+    _Base64Url = prism' B64TU.encodeBase64 $ \s -> case B64TU.decodeBase64 s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64Url #-}
+
+instance AsBase64Unpadded Text where
+    type Base64Unpadded Text = Text
+
+    _Base64Unpadded = prism' B64T.encodeBase64 $ \s -> case B64T.decodeBase64 s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64Unpadded #-}
+
+    _Base64UrlUnpadded = prism' B64TU.encodeBase64 $ \s -> case B64TU.decodeBase64Unpadded s of
+      Left _ -> Nothing
+      Right a -> Just a
+    {-# INLINE _Base64UrlUnpadded #-}
diff --git a/src/Data/Text/Encoding/Base64/URL.hs b/src/Data/Text/Encoding/Base64/URL.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/Encoding/Base64/URL.hs
@@ -0,0 +1,65 @@
+-- |
+-- Module       : Data.Text.Encoding.Base64.URL
+-- Copyright 	: (c) 2019 Emily Pillmore
+-- License	: BSD-style
+--
+-- Maintainer	: Emily Pillmore <emilypi@cohomolo.gy>
+-- Stability	: Experimental
+-- Portability	: portable
+--
+-- This module contains the combinators implementing the
+-- RFC 4648 specification for the Base64-URL encoding including
+-- unpadded and lenient variants
+module Data.Text.Encoding.Base64.URL
+( encodeBase64
+, decodeBase64
+, encodeBase64Unpadded
+, decodeBase64Unpadded
+) where
+
+
+import qualified Data.ByteString.Base64.URL as B64U
+
+import Data.Text (Text)
+import qualified Data.Text.Encoding as T
+
+-- | Encode a 'Text' in base64-url with padding.
+--
+-- See: RFC-4648 section 5
+--
+encodeBase64 :: Text -> Text
+encodeBase64 = T.decodeUtf8 . B64U.encodeBase64 . T.encodeUtf8
+
+-- | Decode a padded base64-url encoded 'Text'
+--
+-- See: RFC-4648 section 4
+--
+decodeBase64 :: Text -> Either Text Text
+decodeBase64 = fmap T.decodeUtf8 . B64U.decodeBase64 . T.encodeUtf8
+
+-- | Encode a 'Text' value in base64-url without padding.
+--
+-- Note: in some circumstances, the use of padding ("=") in base-encoded data
+-- is not required or used. If you are absolutely sure the length of your
+-- input data is divisible by 3, this function will be the same as 'encodeBase64'
+-- with padding. However, if not, you may see garbage appended to output in the
+-- form of "\NUL".
+--
+-- Only call unpadded variants when you can make assumptions about the length of
+-- your input data.
+--
+-- See: RFC-4648 section 3.2
+--
+encodeBase64Unpadded :: Text -> Text
+encodeBase64Unpadded = T.decodeUtf8
+    . B64U.encodeBase64Unpadded
+    . T.encodeUtf8
+
+-- | Decode an unpadded base64-url encoded 'Text' value
+--
+-- See: RFC-4648 section 4
+--
+decodeBase64Unpadded :: Text -> Either Text Text
+decodeBase64Unpadded = fmap T.decodeUtf8
+    . B64U.decodeBase64Unpadded
+    . T.encodeUtf8
diff --git a/test/Base64Tests.hs b/test/Base64Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Base64Tests.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
+module Main
+( main
+, tests
+) where
+
+
+import "base64" Data.ByteString.Base64 as B64
+import "base64" Data.ByteString.Base64.URL as B64U
+import "base64-bytestring" Data.ByteString.Base64 as Bos
+import Data.ByteString.Random (random)
+import Data.Functor (void)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+
+import Test.Tasty
+import Test.Tasty.HUnit
+
+
+main :: IO ()
+main = defaultMain tests
+
+
+tests :: TestTree
+tests = testGroup "Base64 Tests"
+    [ testVectors
+    , sanityTests
+    ]
+
+testVectors :: TestTree
+testVectors = testGroup "RFC 4648 Test Vectors"
+    [ testCaseB64 "" ""
+    , testCaseB64 "f" "Zg=="
+    , testCaseB64 "fo" "Zm8="
+    , testCaseB64 "foo" "Zm9v"
+    , testCaseB64 "foob" "Zm9vYg=="
+    , testCaseB64 "fooba" "Zm9vYmE="
+    , testCaseB64 "foobar" "Zm9vYmFy"
+    ]
+  where
+    testCaseB64 s t =
+      testCase (T.unpack $ if s == "" then "empty" else s) $
+        t @=?  B64.encodeBase64 (T.encodeUtf8 s)
+
+sanityTests :: TestTree
+sanityTests = testGroup "Sanity tests"
+    [ testGroup "very large bytestrings don't segfault"
+        [ chonk
+        ]
+    , testGroup "`base64-bytestring` sanity checks"
+        [ compare64 3
+        , compare64 4
+        , compare64 5
+        , compare64 6
+        , compare64 1000
+        , compare64 100000
+        ]
+    , testGroup "roundtrip encode/decode"
+        [ roundtrip 3
+        , roundtrip 4
+        , roundtrip 5
+        , roundtrip 1000
+        , roundtrip 100000
+        ]
+    ]
+  where
+    chonk = testCase ("Encoding huge bytestrings doesn't result in OOM or segfault") $ do
+      bs <- random 1000000
+      void $ return $ B64.encodeBase64 bs
+      void $ return $ B64U.encodeBase64 bs
+
+    compare64 n = testCase ("Testing " ++ show n ++ "-sized bytestrings") $ do
+      bs <- random n
+      B64.encodeBase64 bs @=? Bos.encode bs
+
+    roundtrip n = testCase ("Roundtrip encode/decode for " ++ show n ++ "-sized bytestrings") $ do
+      bs <- random n
+      B64.decodeBase64 (B64.encodeBase64 bs) @=? Right bs
+      B64U.decodeBase64 (B64U.encodeBase64 bs) @=? Right bs
