diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,12 +1,23 @@
 # Changelog for `password-instances`
 
+## 3.1.0.0
+
+-   Added cabal flags (`aeson`, `http-api-data`, `persistent`) to make
+    dependencies optional. All flags default to `True`.
+    [#89](https://github.com/cdepillabout/password/pull/89)
+-   Added compile-time error if all flags are disabled.
+-   Split into dedicated packages.
+    [#87](https://github.com/cdepillabout/password/pull/87)
+
+Thanks to [@blackheaven](https://github.com/blackheaven)
+
 ## 3.0.0.0
 
 -   The `password-instances` library now depends on the new
     `password-types` package, instead of on the `password` package.
     [#40](https://github.com/cdepillabout/password/pull/40)
     Thanks to [@Vlix](https://github.com/Vlix)
--   Small update to the README
+-   Small update to the README.
 -   Changed `PersistField` instance of `PasswordHash` to use
     `decodeUtf8'` for better handling of UTF8 decoding exceptions.
     [#43](https://github.com/cdepillabout/password/pull/43)
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,33 +1,4 @@
-{-# LANGUAGE CPP #-}
-{-# OPTIONS_GHC -Wall #-}
-module Main (main) where
-
-#ifndef MIN_VERSION_cabal_doctest
-#define MIN_VERSION_cabal_doctest(x,y,z) 0
-#endif
-
-#if MIN_VERSION_cabal_doctest(1,0,0)
-
-import Distribution.Extra.Doctest ( defaultMainWithDoctests )
-main :: IO ()
-main = defaultMainWithDoctests "doctests"
-
-#else
-
-#ifdef MIN_VERSION_Cabal
--- If the macro is defined, we have new cabal-install,
--- but for some reason we don't have cabal-doctest in package-db
---
--- Probably we are running cabal sdist, when otherwise using new-build
--- workflow
-#warning You are configuring this package without cabal-doctest installed. \
-         The doctests test-suite will not work as a result. \
-         To fix this, install cabal-doctest before configuring.
-#endif
-
 import Distribution.Simple
 
 main :: IO ()
 main = defaultMain
-
-#endif
diff --git a/password-instances.cabal b/password-instances.cabal
--- a/password-instances.cabal
+++ b/password-instances.cabal
@@ -1,8 +1,8 @@
 cabal-version: 1.12
 
 name:           password-instances
-version:        3.0.0.0
-category:       Data
+version:        3.1.0.0
+category:       Security
 synopsis:       typeclass instances for password package
 description:    A library providing typeclass instances for common libraries for the types from the password package.
 homepage:       https://github.com/cdepillabout/password/tree/master/password-instances#readme
@@ -21,11 +21,25 @@
   type: git
   location: https://github.com/cdepillabout/password
 
+flag aeson
+  description: Enable instances for Aeson
+  default: True
+  manual: True
+
+flag http-api-data
+  description: Enable instances for HTTP API Data
+  default: True
+  manual: True
+
+flag persistent
+  description: Enable instances for Persistent
+  default: True
+  manual: True
+
 custom-setup
   setup-depends:
-      base
-    , Cabal
-    , cabal-doctest  >=1.0.6 && <1.1
+      base          >= 4.9 && < 5
+    , Cabal                   < 4
 
 library
   hs-source-dirs:
@@ -36,57 +50,21 @@
       Paths_password_instances
   build-depends:
       base          >= 4.9 && < 5
-    , aeson         >= 0.2
-    , http-api-data
-    , password-types          < 2
-    , persistent    >= 1.2
-    , text
   ghc-options:
       -Wall
   default-language:
       Haskell2010
-
-test-suite doctests
-  type:
-      exitcode-stdio-1.0
-  hs-source-dirs:
-      test/doctest
-  main-is:
-      doctest.hs
-  ghc-options:
-      -threaded -rtsopts -with-rtsopts=-N
-  build-depends:
-      base >=4.9 && <5
-    , base-compat
-    , doctest
-    , password
-    , password-instances
-    , QuickCheck
-    , quickcheck-instances
-    , template-haskell
-  default-language:
-      Haskell2010
-
-test-suite password-instances-tasty
-  type:
-      exitcode-stdio-1.0
-  hs-source-dirs:
-      test/tasty
-  main-is:
-      Spec.hs
-  ghc-options:
-      -threaded -rtsopts -with-rtsopts=-N
-  build-depends:
-      base >=4.9 && <5
-    , password-instances
-    , password-types
-    , aeson
-    , http-api-data
-    , persistent
-    , quickcheck-instances
-    , tasty
-    , tasty-hunit
-    , tasty-quickcheck
-    , text
-  default-language:
-      Haskell2010
+  default-extensions:
+      CPP
+  if flag(aeson)
+    build-depends:
+        password-aeson
+    cpp-options: -DFLAG_AESON
+  if flag(http-api-data)
+    build-depends:
+        password-http-api-data
+    cpp-options: -DFLAG_HTTP_API_DATA
+  if flag(persistent)
+    build-depends:
+        password-persistent
+    cpp-options: -DFLAG_PERSISTENT
diff --git a/src/Data/Password/Instances.hs b/src/Data/Password/Instances.hs
--- a/src/Data/Password/Instances.hs
+++ b/src/Data/Password/Instances.hs
@@ -1,14 +1,4 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE StandaloneDeriving #-}
-{-# LANGUAGE TypeOperators #-}
-{-# LANGUAGE UndecidableInstances #-}
-{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -Wno-dodgy-exports -Wno-unused-imports #-}
 
 {-|
 Module      : Data.Password.Instances
@@ -24,99 +14,20 @@
 See the "Data.Password.Types" module for more information.
 -}
 
-module Data.Password.Instances () where
+module Data.Password.Instances (module E) where
 
-import Data.Aeson (FromJSON(..), ToJSON(..))
-import Data.Password.Types (Password, PasswordHash(..), mkPassword)
-#if !MIN_VERSION_base(4,13,0)
-import Data.Semigroup ((<>))
+#if !defined(FLAG_AESON) && !defined(FLAG_HTTP_API_DATA) && !defined(FLAG_PERSISTENT)
+#error "At least one of the flags (aeson, http-api-data, persistent) must be enabled"
 #endif
-import Data.Text (pack)
-import Data.Text.Encoding as TE (decodeUtf8')
-import Database.Persist (PersistValue(..))
-import Database.Persist.Class (PersistField(..))
-import Database.Persist.Sql (PersistFieldSql(..))
-import GHC.TypeLits (TypeError, ErrorMessage(..))
-import Web.HttpApiData (FromHttpApiData(..), ToHttpApiData(..))
 
-
--- $setup
--- >>> :set -XOverloadedStrings
--- >>> :set -XDataKinds
---
--- Import needed functions.
---
--- >>> import Data.Aeson (decode)
--- >>> import Data.Password.Bcrypt (Salt(..), hashPasswordWithSalt, unsafeShowPassword)
--- >>> import Database.Persist.Class (PersistField(toPersistValue))
--- >>> import Web.HttpApiData (parseUrlPiece)
-
--- | This instance allows a 'Password' to be created from a JSON blob.
---
--- >>> let maybePassword = decode "\"foobar\"" :: Maybe Password
--- >>> fmap unsafeShowPassword maybePassword
--- Just "foobar"
---
--- There is no instance for 'ToJSON' for 'Password' because we don't want to
--- accidentally encode a plain-text 'Password' to JSON and send it to the end-user.
---
--- Similarly, there is no 'ToJSON' and 'FromJSON' instance for 'PasswordHash'
--- because we don't want to accidentally send the password hash to the end
--- user.
-instance FromJSON Password where
-  parseJSON = fmap mkPassword . parseJSON
-
-type ErrMsg e = 'Text "Warning! Tried to convert plain-text Password to " ':<>: 'Text e ':<>: 'Text "!"
-          ':$$: 'Text "  This is likely a security leak. Please make sure whether this was intended."
-          ':$$: 'Text "  If this is intended, please use 'unsafeShowPassword' before converting to " ':<>: 'Text e
-          ':$$: 'Text ""
-
--- | Type error! Do not use 'toJSON' on a 'Password'!
-instance TypeError (ErrMsg "JSON") => ToJSON Password where
-  toJSON = error "unreachable"
-
--- | This instance allows a 'Password' to be created with functions like
--- 'Web.HttpApiData.parseUrlPiece' or 'Web.HttpApiData.parseQueryParam'.
---
--- >>> let eitherPassword = parseUrlPiece "foobar"
--- >>> fmap unsafeShowPassword eitherPassword
--- Right "foobar"
-instance FromHttpApiData Password where
-  parseUrlPiece = fmap mkPassword . parseUrlPiece
-
--- | Type error! Do not transmit plain-text 'Password's over HTTP!
-instance TypeError (ErrMsg "HttpApiData") => ToHttpApiData Password where
-  toUrlPiece = error "unreachable"
-
--- | This instance allows a 'PasswordHash' to be stored as a field in a database using
--- "Database.Persist".
---
--- >>> let salt = Salt "abcdefghijklmnop"
--- >>> let pass = mkPassword "foobar"
--- >>> let hashedPassword = hashPasswordWithSalt 10 salt pass
--- >>> toPersistValue hashedPassword
--- PersistText "$2b$10$WUHhXETkX0fnYkrqZU3ta.N8Utt4U77kW4RVbchzgvBvBBEEdCD/u"
---
--- In the example above, the long 'PersistText' will be the value you store in
--- the database.
---
--- We don't provide an instance of 'PersistField' for 'Password', because we don't
--- want to make it easy to store a plain-text password in the database.
-instance PersistField (PasswordHash a) where
-  toPersistValue (PasswordHash hpw) = PersistText hpw
-  fromPersistValue = \case
-      PersistText txt -> Right $ PasswordHash txt
-      PersistByteString bs ->
-        either failed (Right . PasswordHash) $ TE.decodeUtf8' bs
-      _ -> Left "did not parse PasswordHash from PersistValue"
-    where
-      failed e = Left $ "Failed decoding PasswordHash to UTF8: " <> pack (show e)
+#ifdef FLAG_AESON
+import Data.Password.Aeson  as E
+#endif
 
--- | This instance allows a 'PasswordHash' to be stored as a field in an SQL
--- database in "Database.Persist.Sql".
-deriving newtype instance PersistFieldSql (PasswordHash a)
+#ifdef FLAG_HTTP_API_DATA
+import Data.Password.HttpApiData as E
+#endif
 
--- | Type error! Do not store plain-text 'Password's in your database!
-instance TypeError (ErrMsg "PersistValue") => PersistField Password where
-  toPersistValue = error "unreachable"
-  fromPersistValue = error "unreachable"
+#ifdef FLAG_PERSISTENT
+import Data.Password.Persistent as E
+#endif
diff --git a/test/doctest/doctest.hs b/test/doctest/doctest.hs
deleted file mode 100644
--- a/test/doctest/doctest.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Main where
-
-import Build_doctests (flags, pkgs, module_sources)
--- import Data.Foldable (traverse_)
-import System.Environment.Compat (unsetEnv)
-import Test.DocTest (doctest)
-
-main :: IO ()
-main = do
-  -- traverse_ putStrLn args
-  unsetEnv "GHC_ENVIRONMENT"
-  doctest args
-  where
-    args = flags ++ pkgs ++ module_sources
diff --git a/test/tasty/Spec.hs b/test/tasty/Spec.hs
deleted file mode 100644
--- a/test/tasty/Spec.hs
+++ /dev/null
@@ -1,55 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-import Data.Aeson
-import Data.Aeson.Types (parseMaybe)
-import Data.Text (Text)
-import Database.Persist.Class (PersistField(..))
-import Test.Tasty
-import Test.Tasty.HUnit
-import Test.Tasty.QuickCheck
-import Test.QuickCheck.Instances.Text ()
-import Web.HttpApiData (FromHttpApiData(..))
-
-import Data.Password.Types (Password, PasswordHash(..), unsafeShowPassword)
-import Data.Password.Instances()
-
-
-main :: IO ()
-main = defaultMain $ testGroup "Password Instances"
-  [ aesonTest
-  , fromHttpApiDataTest
-  , persistTest
-  ]
-
-data TestUser = TestUser {
-  name :: Text,
-  password :: Password
-} deriving (Show)
-
-instance FromJSON TestUser where
-  parseJSON = withObject "TestUser" $ \o ->
-    TestUser <$> o .: "name" <*> o .: "password"
-
-
-aesonTest :: TestTree
-aesonTest = testCase "Password (Aeson)" $
-    assertEqual "password doesn't match" (Just testPassword) $
-      unsafeShowPassword . password <$> parseMaybe parseJSON testUser
-  where
-    testPassword = "testpass"
-    testUser = object
-      [ "name" .= String "testname"
-      , "password" .= String testPassword
-      ]
-
-fromHttpApiDataTest :: TestTree
-fromHttpApiDataTest = testCase "Password (FromHttpApiData)" $
-    assertEqual "password doesn't match" (Right testPassword) $
-      unsafeShowPassword <$> parseUrlPiece testPassword
-  where
-    testPassword = "passtest"
-
-persistTest :: TestTree
-persistTest = testProperty "PasswordHash (PersistField)" $ \pass ->
-    let pwd = PasswordHash pass
-    in fromPersistValue (toPersistValue pwd) === Right pwd
