diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2018 Richard Cook
+
+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,41 @@
+# AWS Easy
+
+A simplified, high-level [AWS][aws] API built on top of [amazonka][amazonka]
+
+This is a collection of helper functions and some Template Haskell that I use regularly and has streamlined by use of the [amazonka][amazonka] framework. It was extracted from the code I wrote as part of my [AWS via Haskell][aws-via-haskell] series of blog posts.
+
+
+## Setup
+
+### Clone repository
+
+```
+git clone https://github.com/rcook/aws-easy.git
+```
+
+### Install compiler
+
+```
+stack setup
+```
+
+### Build
+
+```
+stack build --fast
+```
+
+### Test
+
+```
+stack test
+```
+
+## Licence
+
+Released under [MIT License][licence]
+
+[amazonka]: https://hackage.haskell.org/package/amazonka
+[aws]: https://aws.amazon.com/
+[aws-via-haskell]: http://blog.rcook.org/blog/2017/aws-via-haskell/
+[licence]: LICENSE
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,16 @@
+{-|
+Module      : Main
+Description : Setup entrypoint for @aws-easy@
+Copyright   : (C) Richard Cook, 2018
+Licence     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : stable
+Portability : portable
+-}
+
+module Main (main) where
+
+import           Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,4 @@
+module Main (main) where
+
+main :: IO ()
+main = putStrLn "Hello world"
diff --git a/aws-easy.cabal b/aws-easy.cabal
new file mode 100644
--- /dev/null
+++ b/aws-easy.cabal
@@ -0,0 +1,46 @@
+name:                                       aws-easy
+version:                                    0.1.0.0
+synopsis:                                   AWS Easy: Helper functions for working with amazonka
+description:                                This package provides assorted functions and some Template Haskell to simplify working the amazonka family of packages for interacting with Amazon Web Services.
+homepage:                                   https://github.com/rcook/aws-easy#readme
+license:                                    MIT
+license-file:                               LICENSE
+author:                                     Richard Cook
+maintainer:                                 rcook@rcook.org
+copyright:                                  2018 Richard Cook
+category:                                   Network
+build-type:                                 Simple
+cabal-version:                              >= 1.10
+extra-source-files:                         README.md
+
+source-repository head
+  type:                                     git
+  location:                                 https://github.com/rcook/aws-easy.git
+
+library
+  default-language:                         Haskell2010
+  hs-source-dirs:                           lib
+  ghc-options:                              -W -Wall -fwarn-incomplete-patterns -fwarn-unused-imports
+  build-depends:                            amazonka
+                                          , amazonka-core
+                                          , base >= 4.7 && < 5
+                                          , bytestring
+                                          , lens
+                                          , resourcet
+                                          , template-haskell
+                                          , text
+  exposed-modules:                          Network.AWS.Easy
+                                          , Network.AWS.Easy.Classes
+                                          , Network.AWS.Easy.Prelude
+                                          , Network.AWS.Easy.Service
+                                          , Network.AWS.Easy.TH
+                                          , Network.AWS.Easy.Types
+                                          , Network.AWS.Easy.Util
+
+executable aws-easy-app
+  default-language:                         Haskell2010
+  hs-source-dirs:                           app
+  main-is:                                  Main.hs
+  ghc-options:                              -threaded -rtsopts -with-rtsopts=-N -W -Wall -fwarn-incomplete-patterns -fwarn-unused-imports
+  build-depends:                            aws-easy
+                                          , base >= 4.7 && < 5
diff --git a/lib/Network/AWS/Easy.hs b/lib/Network/AWS/Easy.hs
new file mode 100644
--- /dev/null
+++ b/lib/Network/AWS/Easy.hs
@@ -0,0 +1,25 @@
+{-|
+Module      : Network.AWS.Easy
+Description : Umbrella module
+Copyright   : (C) Richard Cook, 2018
+License     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : experimental
+Portability : portable
+-}
+
+module Network.AWS.Easy
+    ( module Network.AWS.Easy.Classes
+    , module Network.AWS.Easy.Prelude
+    , module Network.AWS.Easy.Service
+    , module Network.AWS.Easy.TH
+    , module Network.AWS.Easy.Types
+    , module Network.AWS.Easy.Util
+    ) where
+
+import           Network.AWS.Easy.Classes
+import           Network.AWS.Easy.Prelude
+import           Network.AWS.Easy.Service
+import           Network.AWS.Easy.TH
+import           Network.AWS.Easy.Types
+import           Network.AWS.Easy.Util
diff --git a/lib/Network/AWS/Easy/Classes.hs b/lib/Network/AWS/Easy/Classes.hs
new file mode 100644
--- /dev/null
+++ b/lib/Network/AWS/Easy/Classes.hs
@@ -0,0 +1,30 @@
+{-|
+Module      : Network.AWS.Easy.Classes
+Description : Service and session type classes
+Copyright   : (C) Richard Cook, 2018
+License     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : experimental
+Portability : portable
+
+This modules provides service and session type classes for the "AWS via Haskell" project.
+-}
+
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Network.AWS.Easy.Classes
+    ( ServiceClass(..)
+    , SessionClass(..)
+    ) where
+
+import           Network.AWS (Service)
+import           Network.AWS.Easy.Types
+
+class ServiceClass a where
+    type TypedSession a :: *
+    rawService :: a -> Service
+    wrappedSession :: Session -> TypedSession a
+
+class SessionClass a where
+    rawSession :: a -> Session
diff --git a/lib/Network/AWS/Easy/Prelude.hs b/lib/Network/AWS/Easy/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/lib/Network/AWS/Easy/Prelude.hs
@@ -0,0 +1,45 @@
+{-|
+Module      : Network.AWS.Easy.Prelude
+Description : Re-exports of most commonly used Amazonka functions
+Copyright   : (C) Richard Cook, 2018
+License     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : experimental
+Portability : portable
+
+This module provides re-exports of most commonly used Amazonka functions as well as lens and error-handling functions.
+-}
+
+module Network.AWS.Easy.Prelude
+    ( (^.)
+    , (&)
+    , (.~)
+    , _ServiceError
+    , AsError
+    , Credentials(..)
+    , Region(..)
+    , ServiceError
+    , await
+    , hasCode
+    , hasStatus
+    , send
+    , sinkBody
+    , toText
+    ) where
+
+import           Control.Lens ((^.), (&), (.~))
+import           Network.AWS
+                    ( _ServiceError
+                    , AsError
+                    , Credentials(..)
+                    , Region(..)
+                    , ServiceError
+                    , await
+                    , send
+                    , sinkBody
+                    )
+import           Network.AWS.Data (toText)
+import           Network.AWS.Error
+                    ( hasCode
+                    , hasStatus
+                    )
diff --git a/lib/Network/AWS/Easy/Service.hs b/lib/Network/AWS/Easy/Service.hs
new file mode 100644
--- /dev/null
+++ b/lib/Network/AWS/Easy/Service.hs
@@ -0,0 +1,113 @@
+{-|
+Module      : Network.AWS.Easy.Service
+Description : Configuring and making client connections to AWS services
+Copyright   : (C) Richard Cook, 2018
+License     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : experimental
+Portability : portable
+
+This modules provides support for configuring and making client connections to AWS services for the "AWS via Haskell" project.
+-}
+
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Network.AWS.Easy.Service
+    ( AWSConfig
+    , Endpoint(..)
+    , HostName
+    , Logging(..)
+    , Port
+    , awscCredentials
+    , awscEndpoint
+    , awscLogging
+    , awsConfig
+    , connect
+    , withAWS
+    ) where
+
+import           Control.Lens ((<&>), makeLenses, set)
+import           Control.Monad.Trans.AWS
+                    ( AWST'
+                    , reconfigure
+                    , runAWST
+                    , within
+                    )
+import           Control.Monad.Trans.Resource
+                    ( MonadBaseControl
+                    , ResourceT
+                    )
+import           Data.ByteString (ByteString)
+import           Network.AWS
+                    ( Credentials(..)
+                    , Env
+                    , LogLevel(..)
+                    , Region(..)
+                    , Service
+                    , envLogger
+                    , newEnv
+                    , newLogger
+                    , runResourceT
+                    , setEndpoint
+                    )
+import           Network.AWS.Easy.Classes
+import           Network.AWS.Easy.Types
+import           System.IO (stdout)
+
+type HostName = ByteString
+
+type Port = Int
+
+data Logging = LoggingEnabled | LoggingDisabled
+
+data Endpoint = AWSRegion Region | Local HostName Port
+
+data AWSConfig = AWSConfig
+    { _awscEndpoint :: Endpoint
+    , _awscLogging :: Logging
+    , _awscCredentials :: Credentials
+    }
+makeLenses ''AWSConfig
+
+awsConfig :: Endpoint -> AWSConfig
+awsConfig endpoint = AWSConfig endpoint LoggingDisabled Discover
+
+connect :: forall a . ServiceClass a => AWSConfig -> a -> IO (TypedSession a)
+connect (AWSConfig endpoint logging credentials) service = do
+    let serviceRaw = rawService service
+    e <- mkEnv logging credentials
+    let (r, s) = regionService endpoint serviceRaw
+    session' <- return $ Session e r s
+    let session = wrappedSession @a session'
+    return session
+
+mkEnv :: Logging -> Credentials -> IO Env
+-- Standard discovery mechanism for credentials, log to standard output
+mkEnv LoggingEnabled c = do
+    logger <- newLogger Debug stdout
+    newEnv c <&> set envLogger logger
+-- Standard discovery mechanism for credentials, no logging
+mkEnv LoggingDisabled c = newEnv c
+
+regionService :: Endpoint -> Service -> (Region, Service)
+-- Run against a DynamoDB instance running on AWS in specified region
+regionService (AWSRegion region) s = (region, s)
+-- Run against a local DynamoDB instance on a given host and port
+regionService (Local hostName port) s = (NorthVirginia, setEndpoint False hostName port s)
+
+withAWS :: (MonadBaseControl IO m, SessionClass b) =>
+    AWST' Env (ResourceT m) a
+    -> b
+    -> m a
+withAWS action session =
+    let Session{..} = rawSession session
+    in
+        runResourceT . runAWST _sEnv . within _sRegion $ do
+            reconfigure _sService action
diff --git a/lib/Network/AWS/Easy/TH.hs b/lib/Network/AWS/Easy/TH.hs
new file mode 100644
--- /dev/null
+++ b/lib/Network/AWS/Easy/TH.hs
@@ -0,0 +1,87 @@
+{-|
+Module      : Network.AWS.Easy.TH
+Description : Template Haskell helpers for 'Network.AWS.Easy'
+Copyright   : (C) Richard Cook, 2018
+License     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : experimental
+Portability : portable
+
+This modules provides Template Haskell helper functions for eliminating boilerplate
+-}
+
+{-# LANGUAGE TemplateHaskell #-}
+
+module Network.AWS.Easy.TH
+    ( wrapAWSService
+    ) where
+
+import           Language.Haskell.TH
+import           Network.AWS (Service)
+import           Network.AWS.Easy.Classes
+import           Network.AWS.Easy.Types
+
+-- |Generates type-safe AWS service and session wrappers types for use with
+-- 'AWSViaHaskell.AWSService.connect' and 'AWSViaHaskell.AWSService.withAWS' functions
+--
+-- Example top-level invocation:
+--
+-- @
+-- wrapAWSService \'dynamoDB \"DDBService\" \"DDBSession\"
+-- @
+--
+-- This will generate boilerplate like the following:
+--
+-- @
+-- data DDBService = DDBService Service
+--
+-- data DDBSession = DDBSession Session
+--
+-- instance ServiceClass DDBService where
+--     type TypedSession DDBService = DDBSession
+--     rawService (DDBService x) = x
+--     wrappedSession = DDBSession
+--
+-- instance SessionClass DDBSession where
+--     rawSession (DDBSession x) = x
+--
+-- dynamoDBService :: DDBService
+-- dynamoDBService = DDBService dynamoDB
+-- @
+wrapAWSService ::
+    Name        -- ^ Name of the amazonka 'Network.AWS.Types.Service' value to wrap
+    -> String   -- ^ Name of the service type to generate
+    -> String   -- ^ Name of the session type to generate
+    -> Q [Dec]  -- ^ Declarations for splicing into source file
+wrapAWSService varN serviceTypeName sessionTypeName = do
+    serviceVarN <- newName "x"
+    sessionVarN <- newName "x"
+    let serviceN = mkName serviceTypeName
+        sessionN = mkName sessionTypeName
+        wrappedVarN = mkName $ nameBase varN ++ "Service"
+        serviceD = DataD [] serviceN [] Nothing [NormalC serviceN [(Bang NoSourceUnpackedness NoSourceStrictness, ConT ''Service)]] []
+        sessionD = DataD [] sessionN [] Nothing [NormalC sessionN [(Bang NoSourceUnpackedness NoSourceStrictness, ConT ''Session)]] []
+        serviceInst = InstanceD
+                        Nothing
+                        []
+                        (AppT (ConT ''ServiceClass) (ConT serviceN))
+                        [ TySynInstD ''TypedSession (TySynEqn [ConT serviceN] (ConT sessionN))
+                        , FunD 'rawService [Clause [ConP serviceN [VarP serviceVarN]] (NormalB (VarE serviceVarN)) []]
+                        , ValD (VarP 'wrappedSession) (NormalB (ConE $ mkName sessionTypeName)) []
+                        ]
+        sessionInst = InstanceD
+                        Nothing
+                        []
+                        (AppT (ConT ''SessionClass) (ConT sessionN))
+                        [ FunD 'rawSession [Clause [ConP sessionN [VarP sessionVarN]] (NormalB (VarE sessionVarN)) []]
+                        ]
+        sig = SigD wrappedVarN (ConT serviceN)
+        var = ValD (VarP wrappedVarN) (NormalB (AppE (ConE serviceN) (VarE $ varN))) []
+    pure
+        [ serviceD
+        , sessionD
+        , serviceInst
+        , sessionInst
+        , sig
+        , var
+        ]
diff --git a/lib/Network/AWS/Easy/Types.hs b/lib/Network/AWS/Easy/Types.hs
new file mode 100644
--- /dev/null
+++ b/lib/Network/AWS/Easy/Types.hs
@@ -0,0 +1,30 @@
+{-|
+Module      : Network.AWS.Easy.Types
+Description : Support types
+Copyright   : (C) Richard Cook, 2018
+License     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : experimental
+Portability : portable
+
+This modules provides support types for the "AWS via Haskell" project.
+-}
+
+{-# LANGUAGE TemplateHaskell #-}
+
+module Network.AWS.Easy.Types
+    ( Session(..)
+    , sEnv
+    , sRegion
+    , sService
+    ) where
+
+import           Control.Lens (makeLenses)
+import           Network.AWS (Env, Region, Service)
+
+data Session = Session
+    { _sEnv :: Env
+    , _sRegion :: Region
+    , _sService :: Service
+    }
+makeLenses ''Session
diff --git a/lib/Network/AWS/Easy/Util.hs b/lib/Network/AWS/Easy/Util.hs
new file mode 100644
--- /dev/null
+++ b/lib/Network/AWS/Easy/Util.hs
@@ -0,0 +1,31 @@
+{-|
+Module      : Network.AWS.Easy.Util
+Description : General-purpose helper functions
+Copyright   : (C) Richard Cook, 2018
+License     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : experimental
+Portability : portable
+
+This modules provides general-purpose helper functions for the "AWS via Haskell" project.
+-}
+
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Network.AWS.Easy.Util
+    ( intToText
+    , parseInt
+    ) where
+
+import           Data.Text (Text)
+import qualified Data.Text as Text (null, pack)
+import qualified Data.Text.Read as Text (decimal)
+
+intToText :: Int -> Text
+intToText = Text.pack . show
+
+parseInt :: Text -> Maybe Int
+parseInt s = case Text.decimal s of
+    Left _ -> Nothing
+    Right (result, s') -> if Text.null s' then Just result else Nothing
