diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,29 @@
+## [2.0.1](https://github.com/haskell-github-trust/effectful-data-cache/compare/2.0.0...2.0.1) (2026-10-25)
+
+## [2.0.0](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.4...2.0.0) (2026-10-09)
+
+### ⚠ BREAKING CHANGES
+
+* split into base & otel packages
+
+### Code Refactoring
+
+* split into base & otel packages ([782b664](https://github.com/haskell-github-trust/effectful-data-cache/commit/782b664762aa56f2c9515ec3cff7cc4d612dd630))
+
+## [1.0.4](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.3...1.0.4) (2026-10-02)
+
+## [1.0.3](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.2...1.0.3) (2026-10-01)
+
+## [1.0.2](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.1...1.0.2) (2026-09-29)
+
+## [1.0.1](https://github.com/haskell-github-trust/effectful-data-cache/compare/1.0.0...1.0.1) (2026-09-28)
+
+### Features
+
+* add otel telemetry ([9c0cdf2](https://github.com/haskell-github-trust/effectful-data-cache/commit/9c0cdf2a8e001644bde24cdcd0524243ee1651f7))
+
+## 1.0.0 (2026-09-28)
+
+### Features
+
+* initial commit ([fefe10b](https://github.com/haskell-github-trust/effectful-data-cache/commit/fefe10b9003d85f70cb0348c7f715f3a1125bd4e))
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -17,16 +17,5 @@
 
 ## Tracing
 
-`traceCache` wraps every operation in an OpenTelemetry span
-(`cache.insert`, `cache.lookup`, …), with `cache.hit` on lookups and
-`cache.size` on `size`/`keys`. Keys and values are never recorded.
-
-```haskell
-import Effectful.Cache.OpenTelemetry (traceCache)
-
-main = runEff . runCache Nothing . traceCache tracer $ do
-  insert "key" (42 :: Int)
-```
-
-It is an interposer, so it layers over any interpreter and can be omitted
-without touching call sites.
+See [effectful-data-cache-opentelemetry](effectful-data-cache-opentelemetry)
+for OpenTelemetry instrumentation.
diff --git a/effectful-data-cache.cabal b/effectful-data-cache.cabal
--- a/effectful-data-cache.cabal
+++ b/effectful-data-cache.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           effectful-data-cache
-version:        0.1.0.2
+version:        0.2.0.1
 synopsis:       Data cache effect for the `effectful` system
 description:    Please see the README on GitHub at <https://github.com/haskell-github-trust/effectful-data-cache#readme>
 category:       cache
@@ -19,6 +19,8 @@
 build-type:     Simple
 extra-source-files:
     README.md
+extra-doc-files:
+    CHANGELOG.md
 
 source-repository head
   type: git
@@ -27,7 +29,6 @@
 library
   exposed-modules:
       Effectful.Cache
-      Effectful.Cache.OpenTelemetry
   other-modules:
       Paths_effectful_data_cache
   autogen-modules:
@@ -41,15 +42,12 @@
     , clock ==0.8.*
     , effectful-core >=2.3 && <3
     , hashable >=1.5 && <2
-    , hs-opentelemetry-api ==0.3.*
-    , text >=2.1 && <3
   default-language: Haskell2010
 
 test-suite effectful-data-cache-test
   type: exitcode-stdio-1.0
   main-is: Spec.hs
   other-modules:
-      Effectful.Cache.OpenTelemetrySpec
       Effectful.CacheSpec
       Paths_effectful_data_cache
   autogen-modules:
@@ -66,10 +64,5 @@
     , effectful-core
     , effectful-data-cache
     , hashable >=1.5 && <2
-    , hs-opentelemetry-api
-    , hs-opentelemetry-exporter-in-memory
-    , hs-opentelemetry-sdk
     , hspec
-    , text
-    , unordered-containers
   default-language: Haskell2010
diff --git a/src/Effectful/Cache.hs b/src/Effectful/Cache.hs
--- a/src/Effectful/Cache.hs
+++ b/src/Effectful/Cache.hs
@@ -7,6 +7,12 @@
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE TypeOperators #-}
 
+-- | Dynamic effect over "Data.Cache", an in-memory key/value store with
+-- optional per-item expiration.
+--
+-- > runEff . runCache @Text @Int Nothing $ do
+-- >   insert "a" 1
+-- >   lookup @Text @Int "a"
 module Effectful.Cache (
   Cache (..),
   insert,
@@ -41,49 +47,63 @@
 import System.Clock (TimeSpec)
 import Prelude hiding (lookup)
 
--- | @since 0.1.0.0
+-- | Cache of values @v@ indexed by keys @k@.
+--
+-- @since 0.1.0.0
 data Cache k v :: Effect where
+  -- | See 'insert'.
   Insert
     :: Hashable k
     => k
     -> v
     -> Cache k v m ()
+  -- | See 'insert''.
   Insert'
     :: Hashable k
     => Maybe TimeSpec
     -> k
     -> v
     -> Cache k v m ()
+  -- | See 'lookup'.
   Lookup
     :: Hashable k
     => k
     -> Cache k v m (Maybe v)
+  -- | See 'lookup''.
   Lookup'
     :: Hashable k
     => k
     -> Cache k v m (Maybe v)
+  -- | See 'keys'.
   Keys
     :: Hashable k => Cache k v m [k]
+  -- | See 'delete'.
   Delete
     :: Hashable k
     => k
     -> Cache k v m ()
+  -- | See 'filterWithKey'.
   FilterWithKey
     :: Hashable k
     => (k -> v -> Bool)
     -> Cache k v m ()
+  -- | See 'purge'.
   Purge
     :: Hashable k
     => Cache k v m ()
+  -- | See 'purgeExpired'.
   PurgeExpired
     :: Hashable k
     => Cache k v m ()
+  -- | See 'size'.
   Size
     :: Hashable k
     => Cache k v m Int
+  -- | See 'defaultExpiration'.
   DefaultExpiration
     :: Hashable k
     => Cache k v m (Maybe TimeSpec)
+  -- | See 'setDefaultExpiration'.
   SetDefaultExpiration
     :: Hashable k
     => Maybe TimeSpec
@@ -91,7 +111,9 @@
 
 type instance DispatchOf (Cache k v) = Dynamic
 
--- | @since 0.1.0.0
+-- | Insert an item with the cache's default expiration.
+--
+-- @since 0.1.0.0
 insert
   :: forall k v es
    . Cache k v :> es
@@ -101,7 +123,10 @@
   -> Eff es ()
 insert k v = send (Insert k v :: Cache k v (Eff es) ())
 
--- | @since 0.1.0.0
+-- | Insert an item with an explicit expiration, relative to now;
+-- 'Nothing' never expires. The default expiration is ignored.
+--
+-- @since 0.1.0.0
 insert'
   :: forall k v es
    . Cache k v :> es
@@ -112,7 +137,9 @@
   -> Eff es ()
 insert' ts k v = send (Insert' ts k v :: Cache k v (Eff es) ())
 
--- | @since 0.1.0.0
+-- | Look up a non-expired item, evicting it if expired.
+--
+-- @since 0.1.0.0
 lookup
   :: forall k v es
    . Cache k v :> es
@@ -122,6 +149,7 @@
 lookup k = send (Lookup k :: Cache k v (Eff es) (Maybe v))
 
 -- | Like 'lookup' but never evicts the expired entry it read.
+--
 -- @since 0.1.0.0
 lookup'
   :: forall k v es
@@ -131,7 +159,9 @@
   -> Eff es (Maybe v)
 lookup' k = send (Lookup' k :: Cache k v (Eff es) (Maybe v))
 
--- | @since 0.1.0.0
+-- | All keys in the store, including expired ones.
+--
+-- @since 0.1.0.0
 keys
   :: forall k v es
    . Cache k v :> es
@@ -139,7 +169,9 @@
   => Eff es [k]
 keys = send (Keys :: Cache k v (Eff es) [k])
 
--- | @since 0.1.0.0
+-- | Delete an item; no-op if absent.
+--
+-- @since 0.1.0.0
 delete
   :: forall k v es
    . Cache k v :> es
@@ -148,7 +180,10 @@
   -> Eff es ()
 delete k = send (Delete k :: Cache k v (Eff es) ())
 
--- | @since 0.1.0.0
+-- | Keep only items satisfying the predicate, which may be called
+-- on expired items.
+--
+-- @since 0.1.0.0
 filterWithKey
   :: forall k v es
    . Cache k v :> es
@@ -157,7 +192,9 @@
   -> Eff es ()
 filterWithKey p = send (FilterWithKey p :: Cache k v (Eff es) ())
 
--- | @since 0.1.0.0
+-- | Delete all items.
+--
+-- @since 0.1.0.0
 purge
   :: forall k v es
    . Cache k v :> es
@@ -165,7 +202,9 @@
   => Eff es ()
 purge = send (Purge :: Cache k v (Eff es) ())
 
--- | @since 0.1.0.0
+-- | Delete all expired items, atomically.
+--
+-- @since 0.1.0.0
 purgeExpired
   :: forall k v es
    . Cache k v :> es
@@ -173,7 +212,9 @@
   => Eff es ()
 purgeExpired = send (PurgeExpired :: Cache k v (Eff es) ())
 
--- | @since 0.1.0.0
+-- | Number of items, including expired ones.
+--
+-- @since 0.1.0.0
 size
   :: forall k v es
    . Cache k v :> es
@@ -181,7 +222,9 @@
   => Eff es Int
 size = send (Size :: Cache k v (Eff es) Int)
 
--- | @since 0.1.0.0
+-- | Expiration applied by 'insert'.
+--
+-- @since 0.1.0.0
 defaultExpiration
   :: forall k v es
    . Cache k v :> es
@@ -189,7 +232,9 @@
   => Eff es (Maybe TimeSpec)
 defaultExpiration = send (DefaultExpiration :: Cache k v (Eff es) (Maybe TimeSpec))
 
--- | @since 0.1.0.0
+-- | Change the expiration applied by subsequent 'insert's.
+--
+-- @since 0.1.0.0
 setDefaultExpiration
   :: forall k v es
    . Cache k v :> es
@@ -199,6 +244,7 @@
 setDefaultExpiration ts = send (SetDefaultExpiration ts :: Cache k v (Eff es) ())
 
 -- | Run against a fresh store with the given default expiration.
+--
 -- @since 0.1.0.0
 runCache
   :: forall k v es a
@@ -211,6 +257,7 @@
   runCacheWith c eff
 
 -- | Run against an existing 'C.Cache', e.g. one shared with non-effectful code.
+--
 -- @since 0.1.0.0
 runCacheWith
   :: forall k v es a
diff --git a/src/Effectful/Cache/OpenTelemetry.hs b/src/Effectful/Cache/OpenTelemetry.hs
deleted file mode 100644
--- a/src/Effectful/Cache/OpenTelemetry.hs
+++ /dev/null
@@ -1,67 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE GADTs #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE TypeOperators #-}
-
--- | OpenTelemetry instrumentation for 'Cache', layered over any interpreter.
---
--- > runEff . runCache Nothing . traceCache tracer $ do ...
-module Effectful.Cache.OpenTelemetry (
-  traceCache,
-) where
-
-import Data.Maybe (isJust)
-import Data.Text (Text)
-import Effectful
-import Effectful.Cache (Cache (..))
-import Effectful.Dispatch.Dynamic (interpose, passthrough)
-import OpenTelemetry.Trace.Core (
-  Tracer,
-  addAttribute,
-  defaultSpanArguments,
-  inSpan',
- )
-
--- | Wrap every cache operation in a span. Keys and values are never recorded:
--- unbounded cardinality, and likely user data.
---
--- @since 0.1.0.1
-traceCache
-  :: forall k v es a
-   . Cache k v :> es
-  => IOE :> es
-  => Tracer
-  -> Eff es a
-  -> Eff es a
-traceCache tracer = interpose @(Cache k v) $ \env op ->
-  inSpan' tracer (opName op) defaultSpanArguments $ \sp -> do
-    r <- passthrough env op
-    case op of
-      Lookup _ -> addAttribute sp "cache.hit" (isJust r)
-      Lookup' _ -> addAttribute sp "cache.hit" (isJust r)
-      Size -> addAttribute sp "cache.size" r
-      Keys -> addAttribute sp "cache.size" (length r)
-      _ -> pure ()
-    pure r
-
-opName
-  :: Cache k v m a
-  -> Text
-opName = \case
-  Insert{} -> "cache.insert"
-  Insert'{} -> "cache.insert"
-  Lookup{} -> "cache.lookup"
-  Lookup'{} -> "cache.lookup"
-  Keys{} -> "cache.keys"
-  Delete{} -> "cache.delete"
-  FilterWithKey{} -> "cache.filterWithKey"
-  Purge{} -> "cache.purge"
-  PurgeExpired{} -> "cache.purgeExpired"
-  Size{} -> "cache.size"
-  DefaultExpiration{} -> "cache.defaultExpiration"
-  SetDefaultExpiration{} -> "cache.setDefaultExpiration"
diff --git a/test/Effectful/Cache/OpenTelemetrySpec.hs b/test/Effectful/Cache/OpenTelemetrySpec.hs
deleted file mode 100644
--- a/test/Effectful/Cache/OpenTelemetrySpec.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications #-}
-
-module Effectful.Cache.OpenTelemetrySpec (spec) where
-
-import Data.IORef (readIORef)
-import Data.Text (Text)
-import Effectful
-import Effectful.Cache
-import Effectful.Cache.OpenTelemetry (traceCache)
-import OpenTelemetry.Attributes (Attribute (..), PrimitiveAttribute (..), lookupAttribute)
-import OpenTelemetry.Exporter.InMemory.Span (inMemoryListExporter)
-import OpenTelemetry.Trace (
-  ImmutableSpan (..),
-  createTracerProvider,
-  emptyTracerProviderOptions,
-  makeTracer,
-  shutdownTracerProvider,
-  tracerOptions,
- )
-import Test.Hspec
-import Prelude hiding (lookup)
-
--- | Run the given cache program under a traced interpreter, returning its
--- result plus every span that was exported.
-traced
-  :: Eff '[Cache String Int, IOE] a
-  -> IO (a, [ImmutableSpan])
-traced prog = do
-  (processor, ref) <- inMemoryListExporter
-  provider <- createTracerProvider [processor] emptyTracerProviderOptions
-  let tracer = makeTracer provider "effectful-data-cache-test" tracerOptions
-  r <- runEff . runCache @String @Int Nothing $ traceCache @String @Int tracer prog
-  shutdownTracerProvider provider
-  spans <- readIORef ref
-  pure (r, reverse spans)
-
-attr
-  :: ImmutableSpan
-  -> Text
-  -> Maybe Attribute
-attr s k = lookupAttribute (spanAttributes s) k
-
-spec :: Spec
-spec = do
-  it "emits a span per operation" $ do
-    (_, spans) <- traced $ do
-      insert @String @Int "a" (1 :: Int)
-      _ <- lookup @String @Int "a"
-      purge @String @Int
-    map spanName spans `shouldBe` ["cache.insert", "cache.lookup", "cache.purge"]
-
-  it "records hits and misses" $ do
-    (_, spans) <- traced $ do
-      insert @String @Int "a" (1 :: Int)
-      _ <- lookup @String @Int "a"
-      _ <- lookup @String @Int "nope"
-      pure ()
-    let hits = [attr s "cache.hit" | s <- spans, spanName s == "cache.lookup"]
-    hits
-      `shouldBe` [ Just (AttributeValue (BoolAttribute True))
-                 , Just (AttributeValue (BoolAttribute False))
-                 ]
-
-  it "records the size" $ do
-    (_, spans) <- traced $ do
-      insert @String @Int "a" (1 :: Int)
-      insert @String @Int "b" (2 :: Int)
-      _ <- size @String @Int
-      pure ()
-    [attr s "cache.size" | s <- spans, spanName s == "cache.size"]
-      `shouldBe` [Just (AttributeValue (IntAttribute 2))]
-
-  it "still returns the underlying result" $ do
-    (r, _) <- traced $ do
-      insert @String @Int "a" (1 :: Int)
-      lookup @String @Int "a"
-    r `shouldBe` Just 1
