diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,1 +1,46 @@
 # websockets-simple
+
+Provides for a slightly more composable structure for websocket apps:
+
+```haskell
+data Input = Increment | Decrement
+  deriving (FromJSON)
+
+data Output = Value Int
+  deriving (ToJSON)
+
+myApp :: MonadBaseControl IO m => m (WebSocketsApp m Input Output)
+myApp = do
+  countRef <- liftIO $ newIORef 0
+  emitter <- liftIO $ newIORef (Nothing :: Async ())
+
+  let killEmitter = do
+        mThread <- liftIO $ readIORef emitter
+        case mThread of
+          Nothing -> pure ()
+          Just thread -> cancel thread
+
+  pure WebSocketsApp
+    { onOpen = \WebSocketsAppParams{send} ->
+        liftBaseWith $ \runInBase -> do
+          thread <- async $ forever $ do
+            count <- readIORef countRef
+            runInBase $ send $ Value count
+            threadDelay 1000000 -- every second, emit the current value
+          writeIORef emitter (Just thread)
+    , onReceive = \WebSocketsAppParams{send,close} x -> do
+        count <- liftIO $
+          ( modifyIORef countRef $ case x of
+              Increment -> (+ 1)
+              Decrement -> (-) 1
+          ) *> readIORef countRef
+        if count >= 10 || count <= -10
+          then close
+          else send (Value count)
+    , onClose = \mReason -> do
+        killEmitter
+        case mReason of
+          Nothing -> liftIO $ writeIORef countRef 0
+          Just _ -> pure ()
+    }
+```
diff --git a/src/Network/WebSockets/Simple.hs b/src/Network/WebSockets/Simple.hs
--- a/src/Network/WebSockets/Simple.hs
+++ b/src/Network/WebSockets/Simple.hs
@@ -5,51 +5,68 @@
   , ScopedTypeVariables
   , NamedFieldPuns
   , FlexibleContexts
+  , InstanceSigs
   #-}
 
-module Network.WebSockets.Simple where
+module Network.WebSockets.Simple
+  ( -- * Types
+    WebSocketsApp (..), WebSocketsAppParams (..), WebSocketsAppThreads (..)
+  , Network.WebSockets.ConnectionException (..), WebSocketsSimpleError (..)
+  , -- * Running
+    toClientAppT, toClientAppT', toServerAppT
+  , -- * Utilities
+    expBackoffStrategy
+  , hoistWebSocketsApp
+  ) where
 
-import Network.WebSockets (DataMessage (..), sendTextData, sendClose, receiveDataMessage, acceptRequest, ConnectionException (CloseRequest))
+import Network.WebSockets (DataMessage (..), sendTextData, sendClose, receiveDataMessage, acceptRequest, ConnectionException (..))
 import Network.Wai.Trans (ServerAppT, ClientAppT)
+import Data.Profunctor (Profunctor (..))
 import Data.Aeson (ToJSON (..), FromJSON (..))
 import qualified Data.Aeson as Aeson
 import Data.ByteString.Lazy (ByteString)
-import Data.IORef (newIORef, writeIORef, readIORef)
-import Data.Word (Word16)
 
 import Control.Monad (void, forever)
+import Control.Monad.IO.Class (MonadIO (..))
 import Control.Monad.Catch (Exception, throwM, MonadThrow, catch, MonadCatch)
 import Control.Monad.Trans.Control (MonadBaseControl (..))
 import Control.Concurrent (threadDelay)
-import Control.Concurrent.Async (Async, async)
-import Control.Concurrent.STM (atomically)
-import Control.Concurrent.STM.TChan (TChan, newTChanIO, writeTChan)
+import Control.Concurrent.Async (Async, async, link)
+import Control.Concurrent.STM (atomically, newTVarIO, readTVarIO, writeTVar)
 
 import GHC.Generics (Generic)
 import Data.Typeable (Typeable)
 
 
 
-data WebSocketsAppParams send m = WebSocketsAppParams
+data WebSocketsAppParams m send = WebSocketsAppParams
   { send  :: send -> m ()
   , close :: m ()
   } deriving (Generic, Typeable)
 
 
-data WebSocketsApp send receive m = WebSocketsApp
-  { onOpen    :: WebSocketsAppParams send m -> m ()
-  , onReceive :: WebSocketsAppParams send m -> receive -> m ()
-  , onClose   :: Maybe (Word16, ByteString) -> m () -- ^ Either was a clean close, with 'Network.WebSockets.CloseRequest' params, or was unclean.
-                                                    --   Note that to implement backoff strategies, you should catch your 'Network.WebSockets.ConnectionException'
-                                                    --   /outside/ this simple app, and only after you've 'Network.WebSockets.runClient' or server, because the
-                                                    --   'Network.WebSockets.Connection' will be different.
+data WebSocketsApp m receive send = WebSocketsApp
+  { onOpen    :: WebSocketsAppParams m send -> m ()
+  , onReceive :: WebSocketsAppParams m send -> receive -> m ()
+  , onClose   :: ConnectionException -> m () -- ^ Should be re-entrant; this exception is caught in all uses of 'send', even if used in a dead 'Network.WebSockets.Connection' in a lingering thread.
   } deriving (Generic, Typeable)
 
+instance Profunctor (WebSocketsApp m) where
+  dimap :: forall a b c d. (a -> b) -> (c -> d) -> WebSocketsApp m b c -> WebSocketsApp m a d
+  dimap receiveF sendF WebSocketsApp{onOpen,onReceive,onClose} = WebSocketsApp
+    { onOpen = \params -> onOpen (getParams params)
+    , onReceive = \params r -> onReceive (getParams params) (receiveF r)
+    , onClose = onClose
+    }
+    where
+      getParams :: WebSocketsAppParams m d -> WebSocketsAppParams m c
+      getParams WebSocketsAppParams{send,close} = WebSocketsAppParams{send = send . sendF,close}
 
+
 hoistWebSocketsApp :: (forall a. m a -> n a)
                    -> (forall a. n a -> m a)
-                   -> WebSocketsApp send receive m
-                   -> WebSocketsApp send receive n
+                   -> WebSocketsApp m receive send
+                   -> WebSocketsApp n receive send
 hoistWebSocketsApp f coF WebSocketsApp{onOpen,onReceive,onClose} = WebSocketsApp
   { onOpen = \WebSocketsAppParams{send,close} -> f $ onOpen WebSocketsAppParams{send = coF . send, close = coF close}
   , onReceive = \WebSocketsAppParams{send,close} r -> f $ onReceive WebSocketsAppParams{send = coF . send, close = coF close} r
@@ -57,67 +74,79 @@
   }
 
 
--- | This can throw a 'WebSocketSimpleError' when json parsing fails. However, do note:
---   the 'onOpen' is called once, but is still forked when called. Likewise, the 'onReceive'
---   function is called /every time/ a (parsable) response is received from the other party,
---   and is forked on every invocation.
+
+instance Applicative m => Monoid (WebSocketsApp m receive send) where
+  mempty = WebSocketsApp
+    { onOpen = \_ -> pure ()
+    , onReceive = \_ _ -> pure ()
+    , onClose = \_ -> pure ()
+    }
+  mappend x y = WebSocketsApp
+    { onOpen = \params -> onOpen x params *> onOpen y params
+    , onReceive = \params r -> onReceive x params r *> onReceive y params r
+    , onClose = \mE -> onClose x mE *> onClose y mE
+    }
+
+
+-- | This can throw a 'WebSocketSimpleError' to the main thread via 'Control.Concurrent.Async.link' when json parsing fails.
 toClientAppT :: forall send receive m
               . ( ToJSON send
                 , FromJSON receive
+                , MonadIO m
                 , MonadBaseControl IO m
                 , MonadThrow m
                 , MonadCatch m
                 )
-             => WebSocketsApp send receive m
-             -> ClientAppT m (Maybe WebSocketsAppThreads)
+             => WebSocketsApp m receive send
+             -> ClientAppT m WebSocketsAppThreads
 toClientAppT WebSocketsApp{onOpen,onReceive,onClose} conn = do
-  let go = do
-        let send :: send -> m ()
-            send x = liftBaseWith $ \_ -> sendTextData conn (Aeson.encode x)
-
-            close :: m ()
-            close = liftBaseWith $ \_ -> sendClose conn (Aeson.encode "requesting close")
+  let send :: send -> m ()
+      send x = liftIO (sendTextData conn (Aeson.encode x)) `catch` onClose
 
-            params :: WebSocketsAppParams send m
-            params = WebSocketsAppParams{send,close}
+      close :: m ()
+      close = liftIO (sendClose conn (Aeson.encode "requesting close")) `catch` onClose
 
-        onOpenThread <- liftBaseWith $ \runToBase ->
-          async $ void $ runToBase $ onOpen params
+      params :: WebSocketsAppParams m send
+      params = WebSocketsAppParams{send,close}
 
-        onReceiveThreads <- liftBaseWith $ \_ -> newTChanIO
+  onOpen params
 
-        void $ forever $ do
-          data' <- liftBaseWith $ \_ -> receiveDataMessage conn
+  receivingThread <- liftBaseWith $ \runInBase -> async $ forever $
+    let go' = do
+          data' <- receiveDataMessage conn
           let data'' = case data' of
                         Text xs _ -> xs
                         Binary xs -> xs
           case Aeson.decode data'' of
             Nothing -> throwM (JSONParseError data'')
-            Just received -> liftBaseWith $ \runToBase -> do
-              thread <- async $ void $ runToBase $ onReceive params received
-              atomically $ writeTChan onReceiveThreads thread
-
-        pure $ Just WebSocketsAppThreads
-          { onOpenThread
-          , onReceiveThreads
-          }
+            Just received -> runInBase (onReceive params received)
+    in  go' `catch` (runInBase . onClose)
 
-      onDisconnect :: ConnectionException -> m (Maybe WebSocketsAppThreads)
-      onDisconnect err = do
-        case err of
-          CloseRequest code reason -> onClose (Just (code,reason))
-          _                        -> onClose Nothing
-        pure Nothing
+  liftIO (link receivingThread)
 
-  go `catch` onDisconnect
+  pure $ WebSocketsAppThreads
+    { wsAppReceivingThread = receivingThread
+    }
 
 
 
-toClientAppT' :: (ToJSON send, FromJSON receive, MonadBaseControl IO m, MonadThrow m, MonadCatch m) => WebSocketsApp send receive m -> ClientAppT m ()
+toClientAppT' :: ( ToJSON send
+                 , FromJSON receive
+                 , MonadIO m
+                 , MonadBaseControl IO m
+                 , MonadThrow m
+                 , MonadCatch m
+                 ) => WebSocketsApp m receive send -> ClientAppT m ()
 toClientAppT' wsApp conn = void (toClientAppT wsApp conn)
 
 
-toServerAppT :: (ToJSON send, FromJSON receive, MonadBaseControl IO m, MonadThrow m, MonadCatch m) => WebSocketsApp send receive m -> ServerAppT m
+toServerAppT :: ( ToJSON send
+                , FromJSON receive
+                , MonadIO m
+                , MonadBaseControl IO m
+                , MonadThrow m
+                , MonadCatch m
+                ) => WebSocketsApp m receive send -> ServerAppT m
 toServerAppT wsApp pending = do
   conn <- liftBaseWith $ \_ -> acceptRequest pending
   toClientAppT' wsApp conn
@@ -127,41 +156,34 @@
 -- | A simple backoff strategy, which (per second), will increasingly delay at @2^soFar@, until @soFar >= 5minutes@, where it will then routinely poll every
 --   5 minutes.
 expBackoffStrategy :: forall m a
-                    . ( MonadBaseControl IO m
-                      , MonadCatch m
+                    . ( MonadIO m
                       )
-                   => m a -- ^ The run app
-                   -> m a
-expBackoffStrategy app = do
-  soFarVar <- liftBaseWith $ \_ -> newIORef (0 :: Int)
+                   => m a -- ^ Action to call, like pinging a scoped channel to trigger the reconnect
+                   -> m (ConnectionException -> m a)
+expBackoffStrategy action = do
+  soFarVar <- liftIO $ newTVarIO (0 :: Int)
 
   let second = 1000000
 
-  let go = app `catch` backoffStrat
-
-      backoffStrat :: ConnectionException -> m a
-      backoffStrat _ = do
-        liftBaseWith $ \_ -> do
-          soFar <- readIORef soFarVar
-          let delay
-                | soFar >= 5 * 60 = 5 * 60
-                | otherwise       = 2 ^ soFar
-          writeIORef soFarVar (soFar + delay)
-          threadDelay (delay * second)
-        go
-
-  go
+  pure $ \_ -> do
+    liftIO $ do
+      soFar <- readTVarIO soFarVar
+      let delay
+            | soFar >= 5 * 60 = 5 * 60
+            | otherwise       = 2 ^ soFar
+      atomically $ writeTVar soFarVar (soFar + delay)
+      threadDelay (delay * second)
+    action
 
 
 
-data WebSocketSimpleError
+data WebSocketsSimpleError
   = JSONParseError ByteString
   deriving (Generic, Eq, Show)
 
-instance Exception WebSocketSimpleError
+instance Exception WebSocketsSimpleError
 
 
-data WebSocketsAppThreads = WebSocketsAppThreads
-  { onOpenThread     :: Async ()
-  , onReceiveThreads :: TChan (Async ())
+newtype WebSocketsAppThreads = WebSocketsAppThreads
+  { wsAppReceivingThread :: Async ()
   }
diff --git a/src/Network/WebSockets/Simple/PingPong.hs b/src/Network/WebSockets/Simple/PingPong.hs
--- a/src/Network/WebSockets/Simple/PingPong.hs
+++ b/src/Network/WebSockets/Simple/PingPong.hs
@@ -11,10 +11,10 @@
 import Data.Aeson.Types (Value (Array))
 import Control.Monad.Trans.Control (MonadBaseControl (..))
 import Control.Concurrent.Async.Every (every, reset)
-import Control.Concurrent.STM (atomically)
-import Control.Concurrent.STM.TVar (newTVarIO, readTVar, writeTVar)
+import Control.Concurrent.STM (atomically, newTVarIO, readTVar, writeTVar)
 
 
+-- | Uses the JSON literal @[]@ as the ping message
 newtype PingPong a = PingPong {getPingPong :: Maybe a}
 
 -- | Assumes @a@ isn't an 'Data.Aeson.Types.Array' of anything
@@ -24,7 +24,9 @@
 
 -- | Assumes @a@ isn't an 'Data.Aeson.Types.Array' of anything
 instance FromJSON a => FromJSON (PingPong a) where
-  parseJSON (Array _) = pure (PingPong Nothing)
+  parseJSON x@(Array xs)
+    | null xs = pure (PingPong Nothing)
+    | otherwise = (PingPong . Just) <$> parseJSON x
   parseJSON x = (PingPong . Just) <$> parseJSON x
 
 
@@ -32,8 +34,8 @@
 pingPong :: ( MonadBaseControl IO m
             )
          => Int -- ^ Delay in microseconds
-         -> WebSocketsApp send receive m
-         -> m (WebSocketsApp (PingPong send) (PingPong receive) m)
+         -> WebSocketsApp m receive send
+         -> m (WebSocketsApp m (PingPong receive) (PingPong send))
 pingPong delay WebSocketsApp{onOpen,onReceive,onClose} = do
   counterVar <- liftBaseWith $ \_ -> newTVarIO Nothing
 
diff --git a/src/Test/WebSockets/Simple.hs b/src/Test/WebSockets/Simple.hs
--- a/src/Test/WebSockets/Simple.hs
+++ b/src/Test/WebSockets/Simple.hs
@@ -7,15 +7,13 @@
 
 module Test.WebSockets.Simple where
 
-
-import Network.WebSockets.Simple (WebSocketsApp (..), WebSocketsAppParams (..))
+import Network.WebSockets.Simple (WebSocketsApp (..), WebSocketsAppParams (..), ConnectionException (..))
 import Control.Monad (forever, void)
 import Control.Monad.IO.Class (MonadIO (..))
 import Control.Monad.Trans.Control (MonadBaseControl (..))
-import Control.Concurrent (threadDelay)
 import Control.Concurrent.Async (Async, async)
 import Control.Concurrent.STM (atomically)
-import Control.Concurrent.STM.TChan (TChan, newTChan, writeTChan, readTChan, tryReadTChan, isEmptyTChan)
+import Control.Concurrent.STM.TChan (TChan, newTChan, writeTChan, readTChan)
 
 
 
@@ -24,8 +22,8 @@
               . ( MonadIO m
                 , MonadBaseControl IO m
                 )
-             => WebSocketsApp send receive m
-             -> WebSocketsApp receive send m
+             => WebSocketsApp m receive send
+             -> WebSocketsApp m send receive
              -> m (Async (), Async (), TChan send, TChan receive)
 runConnected sendsSreceivesR sendsRreceivesS = do
   (sendChan, receiveChan) <- liftIO $ atomically $ (,) <$> newTChan <*> newTChan
@@ -38,11 +36,10 @@
 
       close :: m ()
       close = do
-        onClose sendsRreceivesS Nothing
-        onClose sendsSreceivesR Nothing
+        onClose sendsRreceivesS ConnectionClosed
+        onClose sendsSreceivesR ConnectionClosed
 
   sToR <- liftBaseWith $ \runInBase -> async $ forever $ do
-    _ <- atomically $ isEmptyTChan sendChan
     s <- atomically $ readTChan sendChan
     void $ runInBase $ onReceive sendsRreceivesS WebSocketsAppParams
       { send = sendToReceive
@@ -50,7 +47,6 @@
       } s
 
   rToS <- liftBaseWith $ \runInBase -> async $ forever $ do
-    _ <- atomically $ isEmptyTChan receiveChan
     r <- atomically $ readTChan receiveChan
     void $ runInBase $ onReceive sendsSreceivesR WebSocketsAppParams
       { send = sendToSend
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -5,6 +5,7 @@
 module Main where
 
 import Network.WebSockets.Simple (WebSocketsApp (..), WebSocketsAppParams (..))
+import Control.Concurrent (threadDelay)
 import Control.Concurrent.STM (atomically)
 import Control.Concurrent.STM.TChan (TChan, writeTChan, newTChanIO, readTChan)
 import Test.Tasty (defaultMain, testGroup)
@@ -13,20 +14,26 @@
 import Test.WebSockets.Simple (runConnected)
 
 
-testReceivingApp :: TChan Int -> WebSocketsApp Int Int IO
+testReceivingApp :: TChan Int -> WebSocketsApp IO Int Int
 testReceivingApp receivedChan = WebSocketsApp
-  { onOpen = \WebSocketsAppParams{send} ->
+  { onOpen = \WebSocketsAppParams{send} -> do
+      putStrLn "sending in 10 seconds..."
+      threadDelay $ 10^6 * 10
       send 0
+      putStrLn "sent."
   , onReceive = \_ x ->
       atomically $ writeTChan receivedChan x
   , onClose = \_ -> pure ()
   }
 
-testSendingApp :: WebSocketsApp Int Int IO
+testSendingApp :: WebSocketsApp IO Int Int
 testSendingApp = WebSocketsApp
   { onOpen = \_ -> pure ()
-  , onReceive = \WebSocketsAppParams{send} x ->
+  , onReceive = \WebSocketsAppParams{send} x -> do
+      putStrLn "received, sending in 10 seconds..."
+      threadDelay $ 10^6 * 10
       send x
+      putStrLn "sent."
   , onClose = \_ -> pure ()
   }
 
diff --git a/websockets-simple.cabal b/websockets-simple.cabal
--- a/websockets-simple.cabal
+++ b/websockets-simple.cabal
@@ -1,50 +1,81 @@
-name:                websockets-simple
-version:             0.0.6.3
-synopsis:            Simpler interface to the websockets api
--- description:
-homepage:            https://github.com/athanclark/websockets-simple#readme
-license:             BSD3
-license-file:        LICENSE
-author:              Athan Clark
-maintainer:          athan.clark@gmail.com
-copyright:           2017 Athan Clark
-category:            Web
-build-type:          Simple
-extra-source-files:  README.md
-cabal-version:       >=1.10
-
-library
-  hs-source-dirs:      src
-  exposed-modules:     Network.WebSockets.Simple
-                       Network.WebSockets.Simple.PingPong
-                       Test.WebSockets.Simple
-  build-depends:       base >= 4.8 && < 5
-                     , aeson
-                     , async
-                     , bytestring
-                     , every >= 0.0.1
-                     , exceptions
-                     , monad-control
-                     , stm
-                     , transformers
-                     , wai-transformers
-                     , websockets >= 0.11
-  default-language:    Haskell2010
+-- This file has been generated from package.yaml by hpack version 0.21.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 0fd7a69684c73e2d69f006be6b9f3028e804149928f115366bc6ef246ebe1cf5
 
+name:           websockets-simple
+version:        0.0.7
+synopsis:       Composable websockets clients
+description:    See README at <https://github.com/athanclark/websockets-simple#readme>
+category:       Web
+homepage:       https://github.com/athanclark/websockets-simple#readme
+bug-reports:    https://github.com/athanclark/websockets-simple/issues
+maintainer:     Athan Clark <athan.clark@gmail.com>
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
 
-test-suite test
-  type:                exitcode-stdio-1.0
-  hs-source-dirs:      test
-  main-is:             Spec.hs
-  build-depends:       base
-                     , websockets-simple
-                     , stm
-                     , tasty
-                     , tasty-hspec
-                     , hspec
-  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
-  default-language:    Haskell2010
+extra-source-files:
+    README.md
 
 source-repository head
-  type:     git
+  type: git
   location: https://github.com/athanclark/websockets-simple
+
+library
+  exposed-modules:
+      Network.WebSockets.Simple
+      Network.WebSockets.Simple.PingPong
+      Test.WebSockets.Simple
+  other-modules:
+      Paths_websockets_simple
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      aeson
+    , async
+    , base >=4.9 && <5
+    , bytestring
+    , every >=0.0.1
+    , exceptions
+    , monad-control
+    , profunctors
+    , stm
+    , transformers
+    , wai-transformers
+    , websockets >=0.11
+  default-language: Haskell2010
+
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Network.WebSockets.Simple
+      Network.WebSockets.Simple.PingPong
+      Test.WebSockets.Simple
+      Paths_websockets_simple
+  hs-source-dirs:
+      test
+      src
+  ghc-options: -Wall
+  build-depends:
+      aeson
+    , async
+    , base >=4.9 && <5
+    , bytestring
+    , every >=0.0.1
+    , exceptions
+    , hspec
+    , monad-control
+    , profunctors
+    , stm
+    , tasty
+    , tasty-hspec
+    , transformers
+    , wai-transformers
+    , websockets >=0.11
+    , websockets-simple
+  default-language: Haskell2010
