diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,12 @@
-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/freckle-env-v0.0.0.0...main)
+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/freckle-env-v0.0.1.1...main)
+
+## [v0.0.1.1](https://github.com/freckle/freckle-app/compare/freckle-env-v0.0.1.0...freckle-env-v0.0.1.1)
+
+Drop `relude` dependency
+
+## [v0.0.1.0](https://github.com/freckle/freckle-app/compare/freckle-env-v0.0.0.0...freckle-env-v0.0.1.0)
+
+Added module `Freckle.App.Dotenv` (moved from `freckle-app-1.19.0.0`).
 
 ## [v0.0.0.0](https://github.com/freckle/freckle-app/tree/freckle-env-v0.0.0.0/freckle-env)
 
diff --git a/freckle-env.cabal b/freckle-env.cabal
--- a/freckle-env.cabal
+++ b/freckle-env.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.18
 name:               freckle-env
-version:            0.0.0.0
+version:            0.0.1.1
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
@@ -20,7 +20,10 @@
     location: https://github.com/freckle/freckle-app
 
 library
-    exposed-modules:    Freckle.App.Env
+    exposed-modules:
+        Freckle.App.Dotenv
+        Freckle.App.Env
+
     hs-source-dirs:     library
     other-modules:      Paths_freckle_env
     default-language:   GHC2021
@@ -39,11 +42,13 @@
 
     build-depends:
         base >=4.16.4.0 && <5,
+        dotenv >=0.10.0.0,
         envparse >=0.5.0,
         errors >=2.3.0,
-        relude >=1.1.0.0,
+        filepath >=1.4.2.2,
         text >=1.2.5.0,
-        time >=1.11.1.1
+        time >=1.11.1.1,
+        unliftio >=0.2.25.0
 
     if impl(ghc >=9.8)
         ghc-options:
diff --git a/library/Freckle/App/Dotenv.hs b/library/Freckle/App/Dotenv.hs
new file mode 100644
--- /dev/null
+++ b/library/Freckle/App/Dotenv.hs
@@ -0,0 +1,65 @@
+-- | Conventional @.env@ handling
+module Freckle.App.Dotenv
+  ( load
+  , loadTest
+  , loadFile
+  ) where
+
+import Prelude
+
+import Configuration.Dotenv qualified as Dotenv
+import Control.Monad ((<=<))
+import Data.Foldable (traverse_)
+import Data.Functor (void)
+import System.FilePath (takeDirectory, (</>))
+import UnliftIO.Directory (doesFileExist, getCurrentDirectory)
+
+-- | Call 'loadFile' with @.env@
+load :: IO ()
+load = loadFile ".env"
+
+-- | Call 'loadFile' with @.env.test@
+loadTest :: IO ()
+loadTest = loadFile ".env.test"
+
+-- | An opinionated 'Configuration.Dotenv.loadFile'
+--
+-- Additional behaviors:
+--
+-- 1. Attempt to locate the file in parent directories too
+--
+--    We, sadly, have a monorepository. So we need to locate a @.env@ file in
+--    parent directories when running tests in sub-directories.
+--
+-- 2. Silently ignore no file found
+--
+--    Since this is used by 'Freckle.App.Test.withApp', which we aim to use in
+--    every non-trivial project, we can't fail in projects that don't need or
+--    have a @.env(.test)@ file (such as this one!).
+--
+-- 3. Use the @.env.example@ feature, but only if one exists alongside
+loadFile :: FilePath -> IO ()
+loadFile = traverse_ go <=< locateInParents
+ where
+  go path = do
+    let examplePath = takeDirectory path </> ".env.example"
+    exampleExists <- doesFileExist examplePath
+
+    void $
+      Dotenv.loadFile $
+        Dotenv.defaultConfig
+          { Dotenv.configPath = [path]
+          , Dotenv.configExamplePath = [examplePath | exampleExists]
+          }
+
+locateInParents :: FilePath -> IO (Maybe FilePath)
+locateInParents path = go =<< getCurrentDirectory
+ where
+  go cwd = do
+    let absPath = cwd </> path
+
+    exists <- doesFileExist absPath
+
+    if exists
+      then pure $ Just absPath
+      else if cwd == "/" then pure Nothing else go $ takeDirectory cwd
diff --git a/library/Freckle/App/Env.hs b/library/Freckle/App/Env.hs
--- a/library/Freckle/App/Env.hs
+++ b/library/Freckle/App/Env.hs
@@ -35,10 +35,12 @@
   , timeout
   ) where
 
-import Relude
+import Prelude
 
 import Control.Error.Util (note)
+import Data.Bifunctor (first, second)
 import Data.Char (isDigit)
+import Data.Text (Text)
 import Data.Text qualified as T
 import Data.Time (UTCTime, defaultTimeLocale, parseTimeM)
 import Env hiding (flag)
@@ -94,9 +96,9 @@
 -- Left [("TIME",UnreadError "unable to parse time as %Y-%m-%d: \"10:00PM\"")]
 time :: String -> Env.Reader Error UTCTime
 time fmt =
-  eitherReader
-    $ note ("unable to parse time as " <> fmt)
-    . parseTimeM True defaultTimeLocale fmt
+  eitherReader $
+    note ("unable to parse time as " <> fmt)
+      . parseTimeM True defaultTimeLocale fmt
 
 -- | Read key-value pairs
 --
@@ -117,11 +119,11 @@
 
 keyValue :: Char -> Env.Reader Error (Text, Text)
 keyValue c =
-  eitherReader $ go . second (T.drop 1) . T.breakOn (T.singleton c) . toText
+  eitherReader $ go . second (T.drop 1) . T.breakOn (T.singleton c) . T.pack
  where
   go = \case
-    (k, v) | T.null v -> Left $ "Key " <> toString k <> " has no value"
-    (k, v) | T.null k -> Left $ "Value " <> toString v <> " has no key"
+    (k, v) | T.null v -> Left $ "Key " <> T.unpack k <> " has no value"
+    (k, v) | T.null k -> Left $ "Value " <> T.unpack v <> " has no key"
     (k, v) -> Right (k, v)
 
 -- | Use 'splitOn' then call the given 'Reader' on each element
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
 name: freckle-env
-version: 0.0.0.0
+version: 0.0.1.1
 maintainer: Freckle Education
 category: Environment, System
 github: freckle/freckle-app
@@ -54,11 +54,13 @@
 library:
   source-dirs: library
   dependencies:
+    - dotenv
     - envparse
     - errors
-    - relude
+    - filepath
     - text
     - time
+    - unliftio
 
 tests:
   doctest:
