diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# from-env
+
+Haskell package to construct datatypes from environment variables.
+
+In many applications you'll have an `.env` file or set environment variables
+some way. These environment variables usually contain configuration data such
+as database connection urls, secrets; etc.
+
+Next, you make a configuration data type to hold this variables so your
+application can access them. Normally you'd have a bunch of calls to `lookupEnv`
+in order to build your data type. This is tedious and error-prone. Thankfully,
+in Haskell we can do better!
+
+```haskell
+import GHC.Generics
+import System.Environment.FromEnv
+
+data Config = Config
+    { configDbUrl     :: !String
+    , configApiSecret :: !String
+    }
+    deriving Generic
+
+instance FromEnv Config
+
+main = do
+    config <- fromEnv
+    -- do something with config
+```
+
+And that's it! By deriving `Generic` you can now create an instance of `FromEnv`
+for free. Check out the haddocks for more.
+
+## License
+
+MIT
diff --git a/from-env.cabal b/from-env.cabal
new file mode 100644
--- /dev/null
+++ b/from-env.cabal
@@ -0,0 +1,68 @@
+cabal-version:      2.4
+name:               from-env
+version:            0.1.0.0
+synopsis:
+  Provides a generic way to construct values from environment variables.
+
+description:
+  .
+  This package exposes a class `FromEnv` that works with `GHC.Generics` to provide a generic way
+  to construct values from environment variables.
+  .
+  In many applications you'll have a configuration object holding a connection string to your
+  database, the url of an S3 bucket, you name it. It can be tedious to have to construct this
+  configuration object manually. With this package, you just derive `Generic` and then create an
+  instance of `FromEnv` for your configuration type and you're done. Just call `fromEnv` and you got
+  your configuration.
+  .
+  > import System.Environment.FromEnv
+  > import GHC.Generics
+  > newtype Config = Config { configS3BucketUrl :: String } deriving Generic
+  > instance FromEnv Config
+  > config <- fromEnv
+  .
+  The default behaviour is to convert field names like `configS3BucketUrl` into 
+  environment variables like `CONFIG_S3_BUCKET_URL`, but it can be overriden by providing a
+  custom instance of `FromEnv`.
+
+bug-reports:        https://github.com/aloussase/from-env/issues
+license:            MIT
+author:             Alexander Goussas
+maintainer:         goussasalexander@gmail.com
+copyright:          Alexander Goussas 2023
+category:           Configuration
+extra-source-files: README.md
+
+source-repository head
+  type:     git
+  location: https://github.com/aloussase/from-env.git
+
+library
+  exposed-modules:
+    System.Environment.FromEnv
+    System.Environment.FromEnv.TryParse
+
+  ghc-options:      -Wall
+
+  -- other-modules:
+  build-depends:
+    , base    ^>=4.16.4.0
+    , casing  >=0.1.4     && <0.2
+    , text    >=1.2.5     && <1.3
+
+  hs-source-dirs:   src
+  default-language: Haskell2010
+
+test-suite from-env-test-suite
+  type:               exitcode-stdio-1.0
+  ghc-options:        -Wall
+  hs-source-dirs:     test
+  main-is:            Spec.hs
+  other-modules:      System.Environment.FromEnvSpec
+  build-depends:
+    , base      ^>=4.16.4.0
+    , from-env
+    , hspec
+
+  build-tool-depends: hspec-discover:hspec-discover
+  default-language:   Haskell2010
diff --git a/src/System/Environment/FromEnv.hs b/src/System/Environment/FromEnv.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Environment/FromEnv.hs
@@ -0,0 +1,114 @@
+{-# LANGUAGE DataKinds            #-}
+{-# LANGUAGE DefaultSignatures    #-}
+{-# LANGUAGE DerivingStrategies   #-}
+{-# LANGUAGE FlexibleContexts     #-}
+{-# LANGUAGE FlexibleInstances    #-}
+{-# LANGUAGE KindSignatures       #-}
+{-# LANGUAGE ScopedTypeVariables  #-}
+{-# LANGUAGE TypeApplications     #-}
+{-# LANGUAGE TypeOperators        #-}
+{-# LANGUAGE UndecidableInstances #-}
+module System.Environment.FromEnv
+(
+  -- * Core class
+    FromEnv (..)
+  -- * Options
+  , defaultEnvOpts
+  , FromEnvOptions ( optsFieldLabelModifier )
+  -- * Generic parsing class
+  , GFromEnv (..)
+  -- * Errors
+  , FromEnvError (..)
+) where
+
+import           Control.Applicative                 (liftA2)
+import           Control.Monad.IO.Class              (MonadIO, liftIO)
+import           GHC.Generics
+import           System.Environment                  (lookupEnv)
+
+import           Text.Casing                         (screamingSnake)
+
+import           System.Environment.FromEnv.TryParse
+
+
+-- | Class for things that can be created from environment variables.
+class FromEnv a where
+  fromEnv :: (MonadIO m) => m (Either FromEnvError a)
+  default fromEnv :: (MonadIO m, Generic a, GFromEnv' (Rep a)) => m (Either FromEnvError a)
+  fromEnv = gFromEnv defaultEnvOpts
+
+-- | Try to convert a field name into an environment variable name.
+type FieldLabelModifier = String -> String
+
+-- | Options to specify how to construct your datatype from environment variables.
+-- Options can be set using record update syntax and 'defaultEnvOpts'.
+newtype FromEnvOptions = FromEnvOptions
+  { optsFieldLabelModifier :: FieldLabelModifier
+  -- ^ Function to map from a field name to an environment variable name.
+  }
+
+-- | Default 'FromEnvOptions':
+--
+-- The default options will try to read a field name fieldName from an
+-- environment variables FIELD_NAME, as this is the most common naming
+-- convention for environment variables.
+--
+-- If you want different behavior, see 'gFromEnv'.
+--
+-- @
+-- 'FromEnvOptions'
+-- { 'optsFieldLabelModifier' = Just . 'Text.Casing.screamingSnake'
+-- }
+-- @
+defaultEnvOpts :: FromEnvOptions
+defaultEnvOpts = FromEnvOptions
+  { optsFieldLabelModifier =  screamingSnake
+  }
+
+class GFromEnv a where
+  -- | Try to construct a value from environment variables.
+  gFromEnv :: (MonadIO m) => FromEnvOptions -> m (Either FromEnvError a)
+  default gFromEnv :: (MonadIO m, Generic a, GFromEnv' (Rep a)) => FromEnvOptions -> m (Either FromEnvError a)
+  gFromEnv opts = fmap to <$> gFromEnv' opts
+
+instance (Generic a, GFromEnv' (Rep a)) => GFromEnv a
+
+class GFromEnv' f where
+  gFromEnv' :: (MonadIO m) => FromEnvOptions -> m (Either FromEnvError (f a))
+
+instance {-# OVERLAPPING #-} GFromEnv' f => GFromEnv' (M1 i c f) where
+  gFromEnv' converter = fmap M1 <$> gFromEnv' converter
+
+instance (GFromEnv' f, GFromEnv' g) => GFromEnv' (f :*: g)  where
+  gFromEnv' opts = do
+    f' <- gFromEnv' @f opts
+    g' <- gFromEnv' @g opts
+    return $ liftA2 (:*:) f' g'
+
+instance {-# OVERLAPPING #-} (Selector s, TryParse a) => GFromEnv' (M1 S s (K1 i a)) where
+  gFromEnv' opts = do
+    let m :: M1 i s f a
+        m = undefined
+        name = optsFieldLabelModifier opts $ selName m
+    envValue <- liftIO $ lookupEnv name
+    return $ do
+        v <- maybeToEither (UnsetVariable name) envValue
+        r <- maybeToEither (FailedToParse name v) (tryParse v)
+        return . M1 . K1 $ r
+
+maybeToEither :: b -> Maybe a -> Either b a
+maybeToEither _ (Just a) = Right a
+maybeToEither b Nothing  = Left b
+
+data FromEnvError
+    = UnsetVariable String
+    -- ^ A field was unset in the environment
+    | FailedToParse String String
+    -- ^ Failed to parse a given field from an environment variable
+    deriving Eq
+
+instance Show FromEnvError where
+    show (UnsetVariable fieldName) =
+        "The field " <> fieldName <> " was unset in the environment"
+    show (FailedToParse fieldName envValue) =
+        "Failed to parse the field " <> fieldName <> " from the value " <> envValue
diff --git a/src/System/Environment/FromEnv/TryParse.hs b/src/System/Environment/FromEnv/TryParse.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Environment/FromEnv/TryParse.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE FlexibleInstances #-}
+module System.Environment.FromEnv.TryParse
+(
+    TryParse (..)
+)
+where
+
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Text.Read (readMaybe)
+
+-- | Class for things that may be parsed from strings.
+class TryParse a where
+  tryParse :: String -> Maybe a
+
+instance TryParse Int where
+  tryParse = readMaybe
+
+instance TryParse [Char] where
+   tryParse = Just
+
+instance TryParse Char where
+   tryParse [c] = Just c
+   tryParse _   = Nothing
+
+instance TryParse Text where
+  tryParse = Just . T.pack
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/System/Environment/FromEnvSpec.hs b/test/System/Environment/FromEnvSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/System/Environment/FromEnvSpec.hs
@@ -0,0 +1,61 @@
+{-# LANGUAGE DataKinds        #-}
+{-# LANGUAGE DeriveAnyClass   #-}
+{-# LANGUAGE DeriveGeneric    #-}
+{-# LANGUAGE TypeApplications #-}
+module System.Environment.FromEnvSpec (main, spec) where
+
+import           Data.Either                (isLeft, isRight)
+import           GHC.Generics
+import           System.Environment         (getEnvironment, setEnv, unsetEnv)
+import           Test.Hspec                 (Spec, after_, describe, hspec, it,
+                                             shouldBe, shouldSatisfy)
+
+import           System.Environment.FromEnv
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = fromEnvSpec >> gFromEnvSpec
+
+fromEnvSpec :: Spec
+fromEnvSpec =
+  describe "fromEnv" $ after_ clearEnvs $ do
+    it "returns Nothing when the necessary environment variables are not set" $ do
+      config <- fromEnv @Config
+      config `shouldSatisfy` isLeft
+
+    it "returns Nothing when only one environment variable is missing" $ do
+      setEnv "CONFIG_DB_URL" "hello"
+      config <- fromEnv @Config
+      config `shouldBe` Left (UnsetVariable "CONFIG_API_KEY")
+
+    it "returns the configuration object when all necessary variables are set" $ do
+      setEnv "CONFIG_DB_URL" "hello"
+      setEnv "CONFIG_API_KEY" "world"
+      config <- fromEnv
+      config `shouldSatisfy` isRight
+      unwrapEither config `shouldBe` Config "hello" "world"
+
+gFromEnvSpec :: Spec
+gFromEnvSpec =
+    describe "gFromEnv" $ after_ clearEnvs $ do
+        it "returns the configuration object when using a custom field label modifier" $ do
+            setEnv "configDbURL" "hello"
+            setEnv "configApiKey" "world"
+            config <- gFromEnv (defaultEnvOpts { optsFieldLabelModifier = id })
+            config `shouldSatisfy` isRight
+            unwrapEither config `shouldBe` Config "hello" "world"
+
+data Config = Config
+  { configDbURL  :: !String
+  , configApiKey :: !String
+  }
+  deriving (Eq, Show, Generic, FromEnv)
+
+clearEnvs :: IO ()
+clearEnvs = getEnvironment >>= mapM_ unsetEnv . fmap fst
+
+unwrapEither :: Either a b -> b
+unwrapEither (Left _)  = error "tried to unwrap left"
+unwrapEither (Right b) = b
