diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# CHANGELOG
+
+## v0.0.1.0 –
+* Release
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,20 @@
+Copyright (c) 2021 Hécate Moonlight
+
+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,38 @@
+# cache-effectful
+
+A `Cache` effect for the [`effectful`][effectful] ecosystem.
+
+## How to use
+
+This library exposes the following elements:
+
+* `Cache` — The type-level effect that you can declare in your type signatures.
+
+```
+populateIntCache :: (Cache Int Int :> es) => Eff es ()
+```
+
+* `insert`, `lookup`, `keys`, `delete`, `filterWithKey` – Operations on `Cache`. They should always be used with Type Applications when using literals:
+
+```Haskell
+insertAndLookup :: (Cache Int Int :> es) => Eff es (Maybe Int)
+insertAndLookup = do
+  insert @Int @Int 3 12
+  lookup @Int 3
+
+listKeys :: (Cache Int Int :> es) => Eff es [Int]
+listKeys = do
+  populateIntCache
+  keys @Int @Int
+```
+
+* An IO Runner
+
+```
+runCacheIO (cache :: Data.Cache Int Int)
+```
+
+See the [tests][tests] to see an example use.
+
+[effectful]: https://haskell-effectful.github.io
+[tests]: https://github.com/haskell-effectful/cache-effectful/blob/main/cache-effectful/test/Main.hs
diff --git a/cache-effectful.cabal b/cache-effectful.cabal
new file mode 100644
--- /dev/null
+++ b/cache-effectful.cabal
@@ -0,0 +1,84 @@
+cabal-version: 3.0
+name: cache-effectful
+version: 0.0.1.0
+synopsis: A Cache effect for the effectful ecosystem.
+category: cache
+homepage:
+  https://github.com/haskell-effectful/cache-effectful/tree/main/cache-effectful#readme
+
+bug-reports: https://github.com/haskell-effectful/cache-effectful/issues
+author: Hécate Moonlight
+maintainer: Hécate Moonlight
+license: MIT
+build-type: Simple
+tested-with: ghc ==9.10.3 || ==9.12.4 || ==9.14.1
+extra-source-files:
+  LICENSE.md
+  README.md
+
+extra-doc-files:
+  CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/haskell-effectful/cache-effectful
+
+common common-extensions
+  default-extensions:
+    ConstraintKinds
+    DataKinds
+    FlexibleContexts
+    FlexibleInstances
+    GADTs
+    KindSignatures
+    ScopedTypeVariables
+    TypeApplications
+    TypeOperators
+
+  default-language: Haskell2010
+
+common common-ghc-options
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Widentities
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wpartial-fields
+    -Wredundant-constraints
+    -fhide-source-paths
+    -Wno-unused-do-bind
+
+common common-rts-options
+  ghc-options:
+    -rtsopts
+    -threaded
+    -with-rtsopts=-N
+
+library
+  import: common-extensions
+  import: common-ghc-options
+  hs-source-dirs: src
+  exposed-modules: Effectful.Cache
+  build-depends:
+    base <4.23,
+    cache >=0.1.3 && <0.2,
+    effectful-core >=2.6 && <2.7,
+    hashable >=1.5.1 && <1.6,
+
+test-suite cache-effectful-test
+  import: common-extensions
+  import: common-ghc-options
+  import: common-rts-options
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules: Utils
+  hs-source-dirs: test
+  build-depends:
+    base,
+    cache,
+    cache-effectful,
+    effectful-core,
+    hashable,
+    tasty >=1.5.4 && <1.6,
+    tasty-hunit >=0.10.2 && <0.11,
diff --git a/src/Effectful/Cache.hs b/src/Effectful/Cache.hs
new file mode 100644
--- /dev/null
+++ b/src/Effectful/Cache.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE Strict #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+--   Module      : Effectful.Cache
+--   Copyright   : © Hécate Moonlight, 2021
+--   License     : MIT
+--   Maintainer  : hecate@glitchbra.in
+--   Stability   : stable
+--
+--   An effect wrapper around 'Data.Cache' for the Effectful ecosystem
+module Effectful.Cache
+  ( -- * The /Cache/ effect
+    Cache (..)
+
+    -- * Handlers
+  , runCacheIO
+
+    -- * Cache operations
+  , insert
+  , lookup
+  , keys
+  , delete
+  , filterWithKey
+  ) where
+
+import Control.Monad.IO.Class
+import qualified Data.Cache as C
+import Data.Hashable
+import Data.Kind
+import Effectful
+import Effectful.Dispatch.Dynamic (interpret, send)
+import Prelude hiding (lookup)
+
+-- | Operations on a cache
+--
+-- Since it is an effect with type variables, you will have the duty of making unambiguous calls to the provided
+-- functions.
+-- This means that with numerical literals ('3', '4', etc),
+-- [visible type applications](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/type_applications.html)
+-- will be necessary. See each function's documentation for examples.
+--
+-- @since 0.0.1.0
+data Cache k v :: Effect where
+  Insert :: (Eq k, Hashable k) => k -> v -> Cache k v m ()
+  Lookup :: (Eq k, Hashable k) => k -> Cache k v m (Maybe v)
+  Keys :: Cache k v m [k]
+  Delete :: (Eq k, Hashable k) => k -> Cache k v m ()
+  FilterWithKey :: (Eq k, Hashable k) => (k -> v -> Bool) -> Cache k v m ()
+
+-- |
+-- @since 0.0.1.0
+type instance DispatchOf (Cache k v) = 'Dynamic
+
+-- | The default IO handler
+--
+-- @since 0.0.1.0
+runCacheIO
+  :: forall (k :: Type) (v :: Type) (es :: [Effect]) (a :: Type)
+   . (Hashable k, IOE :> es)
+  => C.Cache k v
+  -> Eff (Cache k v : es) a
+  -> Eff es a
+runCacheIO cache = interpret $ \_ -> \case
+  Insert key value -> liftIO $ C.insert cache key value
+  Lookup key -> liftIO $ C.lookup cache key
+  Keys -> liftIO $ C.keys cache
+  Delete key -> liftIO $ C.delete cache key
+  FilterWithKey fun -> liftIO $ C.filterWithKey fun cache
+
+-- | Insert an item in the cache, using the default expiration value of the cache.
+--
+-- @since 0.0.1.0
+insert
+  :: forall (k :: Type) (v :: Type) (es :: [Effect])
+   . (Hashable k, Cache k v :> es)
+  => k
+  -> v
+  -> Eff es ()
+insert key value = send $ Insert key value
+
+-- | Lookup an item with the given key, and delete it if it is expired.
+--
+-- The function will only return a value if it is present in the cache and if the item is not expired.
+-- The function will eagerly delete the item from the cache if it is expired.
+--
+-- @since 0.0.1.0
+lookup
+  :: forall (k :: Type) (v :: Type) (es :: [Effect])
+   . (Hashable k, Cache k v :> es)
+  => k
+  -> Eff es (Maybe v)
+lookup key = send $ Lookup key
+
+-- | List all the keys of the cache.
+--
+-- Since 'Cache' has type variables, you will need to use visible type applications or explicitly typed
+-- arguments for the key *and* value parameters to distinguish this 'Cache' from potentially other ones.
+--
+-- === __Example__
+--
+-- > listKeys :: (Cache Int Int :> es) => Eff es [Int]
+-- > listKeys = do
+-- >   mapM_ (\(k,v) -> insert @Int @Int k v) [(2,4),(3,6),(4,8),(5,10)]
+-- >   keys @Int @Int -- [2,3,4,5]
+--
+-- @since 0.0.1.0
+keys
+  :: forall (k :: Type) (v :: Type) (es :: [Effect])
+   . Cache k v :> es
+  => Eff es [k]
+keys = send @(Cache k v) Keys
+
+-- | Delete the provided key from the cache it is present.
+--
+-- Since 'Cache' has type variables, you will need to use visible type applications or explicitly typed
+-- arguments for the key *and* value parameters to distinguish this 'Cache' from potentially other ones.
+--
+-- === __Example__
+--
+-- > deleteKeys :: (Cache Int Int :> es) => Eff es [Int]
+-- > deleteKeys = do
+-- >   mapM_ (\(k,v) -> insert @Int @Int k v) [(2,4),(3,6),(4,8),(5,10)]
+-- >   delete @Int @Int 3
+-- >   delete @Int @Int 5
+-- >   keys @Int @Int -- [2,4]
+--
+-- @since 0.0.1.0
+delete
+  :: forall (k :: Type) (v :: Type) (es :: [Effect])
+   . (Hashable k, Cache k v :> es)
+  => k
+  -> Eff es ()
+delete key = send @(Cache k v) $ Delete key
+
+-- | Keeps elements that satisfy the predicate (used for cache invalidation).
+--
+-- Note that the predicate might be called for expired items.
+--
+-- Since 'Cache' has type variables, you will need to use visible type applications or explicitly typed
+-- arguments for the key *and* value parameters to distinguish this 'Cache' from potentially other ones.
+--
+-- === __Example__
+--
+-- > filterKeys :: (Cache Int Int :> es) => Eff es [Int]
+-- > filterKeys = do
+-- >   mapM_ (\(k,v) -> insert @Int @Int k v) [(2,4),(3,6),(4,8),(5,10)]
+-- >   filterWithKey @Int @Int (\k _ -> k /= 3)
+-- >   keys @Int @Int -- [2,4,5]
+--
+-- @since 0.0.1.0
+filterWithKey
+  :: forall (k :: Type) (v :: Type) (es :: [Effect])
+   . (Hashable k, Cache k v :> es)
+  => (k -> v -> Bool)
+  -> Eff es ()
+filterWithKey fun = send $ FilterWithKey fun
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,78 @@
+{-# LANGUAGE NoOverloadedStrings #-}
+
+module Main where
+
+import qualified Data.Cache as C
+import Effectful
+import Test.Tasty
+import Test.Tasty.HUnit
+import Prelude hiding (lookup)
+
+import Effectful.Cache
+import qualified Utils as U
+
+main :: IO ()
+main =
+  defaultMain $
+    testGroup
+      "cache-effectful"
+      [ testCase "Insert & Lookup" $ testInsertAndLookup =<< initIntCache
+      , testCase "Listing keys" $ testListKeys =<< initIntCache
+      , testCase "Deleting keys" $ testDeleteKeys =<< initIntCache
+      , testCase "Filter with key" $ testFilterWithKey =<< initIntCache
+      ]
+
+initStringCache :: IO (C.Cache String String)
+initStringCache = C.newCache Nothing
+
+initIntCache :: IO (C.Cache Int Int)
+initIntCache = C.newCache Nothing
+
+populateIntCache :: Cache Int Int :> es => Eff es ()
+populateIntCache =
+  mapM_ (\(k, v) -> insert @Int @Int k v) [(2, 4), (3, 6), (4, 8), (5, 10)]
+
+---
+
+testInsertAndLookup :: C.Cache Int Int -> Assertion
+testInsertAndLookup cache = runEff $ do
+  result <- runCacheIO cache insertAndLookup
+  U.assertEqual "Looking up key 3 yields the value 12" (Just 12) result
+  where
+    insertAndLookup :: Cache Int Int :> es => Eff es (Maybe Int)
+    insertAndLookup = do
+      insert @Int @Int 3 12
+      lookup @Int 3
+
+testListKeys :: C.Cache Int Int -> Assertion
+testListKeys cache = runEff $ do
+  result <- runCacheIO cache listKeys
+  U.assertEqual "Correct list of key in the cache" [2, 3, 4, 5] result
+  where
+    listKeys :: Cache Int Int :> es => Eff es [Int]
+    listKeys = do
+      populateIntCache
+      keys @Int @Int
+
+testDeleteKeys :: C.Cache Int Int -> Assertion
+testDeleteKeys cache = runEff $ do
+  result <- runCacheIO cache deleteKeys
+  U.assertEqual "Keys are deleted" [2, 4] result
+  where
+    deleteKeys :: Cache Int Int :> es => Eff es [Int]
+    deleteKeys = do
+      populateIntCache
+      delete @Int @Int 3
+      delete @Int @Int 5
+      keys @Int @Int
+
+testFilterWithKey :: C.Cache Int Int -> Assertion
+testFilterWithKey cache = runEff $ do
+  result <- runCacheIO cache filterKeys
+  U.assertEqual "Keys are properly filtered" [2, 4, 5] result
+  where
+    filterKeys :: Cache Int Int :> es => Eff es [Int]
+    filterKeys = do
+      populateIntCache
+      filterWithKey @Int @Int (\k _ -> k /= 3)
+      keys @Int @Int -- [2,4,5]
diff --git a/test/Utils.hs b/test/Utils.hs
new file mode 100644
--- /dev/null
+++ b/test/Utils.hs
@@ -0,0 +1,23 @@
+module Utils
+  ( assertBool
+  , assertEqual
+  , assertFailure
+  ) where
+
+import Effectful
+import GHC.Stack
+import qualified Test.Tasty.HUnit as T
+
+assertBool :: (HasCallStack, IOE :> es) => String -> Bool -> Eff es ()
+assertBool msg p = liftIO $ T.assertBool msg p
+
+assertEqual
+  :: (HasCallStack, Eq a, Show a, IOE :> es)
+  => String
+  -> a
+  -> a
+  -> Eff es ()
+assertEqual msg expected given = liftIO $ T.assertEqual msg expected given
+
+assertFailure :: (HasCallStack, IOE :> es) => String -> Eff es a
+assertFailure msg = liftIO $ T.assertFailure msg
