diff --git a/COPYING b/COPYING
new file mode 100644
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,13 @@
+Copyright © 2012, Stephen Paul Weber <singpolyma.net>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Network/Wai/Session/Maybe.hs b/Network/Wai/Session/Maybe.hs
new file mode 100644
--- /dev/null
+++ b/Network/Wai/Session/Maybe.hs
@@ -0,0 +1,101 @@
+{-# LANGUAGE CPP #-}
+module Network.Wai.Session.Maybe (Session, SessionStore, withSession, genSessionId) where
+
+import Data.Monoid (mconcat)
+import Data.String (fromString)
+import Control.Monad.IO.Class (liftIO)
+import Network.HTTP.Types (ResponseHeaders)
+import Network.Wai (Middleware, Request(..))
+#if MIN_VERSION_wai(3,0,0)
+import Network.Wai.Internal (Response(ResponseBuilder,ResponseFile,ResponseStream,ResponseRaw))
+#else
+import Network.Wai.Internal (Response(ResponseBuilder,ResponseFile,ResponseSource))
+#endif
+import Web.Cookie (parseCookies, renderSetCookie, SetCookie(..))
+
+#if MIN_VERSION_vault(0,3,0)
+import Data.Vault.Lazy (Key)
+import qualified Data.Vault.Lazy as Vault
+#else
+import Data.Vault (Key)
+import qualified Data.Vault as Vault
+#endif
+import Data.ByteString (ByteString, foldr')
+import Data.ByteString.Lazy (toStrict)
+import Data.ByteString.Builder (word8Hex, toLazyByteString)
+import qualified Blaze.ByteString.Builder as Builder
+
+import System.Entropy (getEntropy)
+
+-- | Type representing a single session (a lookup, insert pair)
+type Session m k v = ((k -> m (Maybe v)), (k -> v -> m ()))
+
+-- | A 'SessionStore' takes in the contents of the cookie (if there was one)
+-- and returns a ('Session', 'IO' action to get new contents for cookie) pair
+type SessionStore m k v = (Maybe ByteString -> IO (Session m k v, IO (Maybe ByteString)))
+
+-- | Fully parameterised middleware for cookie-based sessions
+withSession ::
+	SessionStore m k v
+	-- ^ The 'SessionStore' to use for sessions
+	-> ByteString
+	-- ^ Name to use for the session cookie (MUST BE ASCII)
+	-> SetCookie
+	-- ^ Settings for the cookie (path, expiry, etc)
+	-> Key (Session m k v)
+	-- ^ 'Data.Vault.Vault' key to use when passing the session through
+	-> Middleware
+#if MIN_VERSION_wai(3,0,0)
+withSession sessions cookieName cookieDefaults vkey app req respond = do
+#else
+withSession sessions cookieName cookieDefaults vkey app req = do
+#endif
+	(session, getNewCookie) <- liftIO $ sessions $ lookup cookieName =<< cookies
+#if MIN_VERSION_wai(3,0,0)
+	app (req {vault = Vault.insert vkey session (vault req)}) (\r -> do
+			mNewCookieVal <- liftIO getNewCookie
+			case mNewCookieVal of
+				Just newCookieVal ->
+					respond $ mapHeader (\hs -> (setCookie, newCookie newCookieVal):hs) r
+				Nothing ->
+					respond r
+		)
+#else
+	resp <- app (req {vault = Vault.insert vkey session (vault req)})
+	mNewCookieVal <- liftIO getNewCookie
+	case mNewCookieVal of
+		Just newCookieVal ->
+			return $ mapHeader (\hs -> (setCookie, newCookie newCookieVal):hs) resp
+		Nothing ->
+			return resp
+#endif
+	where
+	newCookie v = Builder.toByteString $ renderSetCookie $ cookieDefaults {
+			setCookieName = cookieName, setCookieValue = v
+		}
+	cookies = fmap parseCookies $ lookup ciCookie (requestHeaders req)
+	setCookie = fromString "Set-Cookie"
+	ciCookie = fromString "Cookie"
+
+-- | Simple session ID generator using cryptographically strong random IDs
+--
+-- Useful for session stores that use session IDs.
+genSessionId :: IO ByteString
+genSessionId = do
+	randBytes <- getEntropy 32
+	return $ prettyPrint randBytes
+	where
+	prettyPrint :: ByteString -> ByteString
+	prettyPrint = toStrict . toLazyByteString . mconcat . Data.ByteString.foldr'
+		( \ byte acc -> word8Hex byte:acc ) []
+
+-- | Run a function over the headers in a 'Response'
+mapHeader :: (ResponseHeaders -> ResponseHeaders) -> Response -> Response
+mapHeader f (ResponseFile s h b1 b2) = ResponseFile s (f h) b1 b2
+mapHeader f (ResponseBuilder s h b) = ResponseBuilder s (f h) b
+#if MIN_VERSION_wai(3,0,0)
+mapHeader f (ResponseStream s h b) = ResponseStream s (f h) b
+mapHeader f (ResponseRaw io resp) = ResponseRaw io (mapHeader f resp)
+#else
+mapHeader f (ResponseSource s h b) = ResponseSource s (f h) b
+#endif
diff --git a/Network/Wai/Session/Maybe/Map.hs b/Network/Wai/Session/Maybe/Map.hs
new file mode 100644
--- /dev/null
+++ b/Network/Wai/Session/Maybe/Map.hs
@@ -0,0 +1,62 @@
+{-# LANGUAGE CPP, FlexibleContexts #-}
+module Network.Wai.Session.Maybe.Map (mapStore, mapStore_) where
+
+import Control.Monad
+import Data.StateVar
+import Data.ByteString (ByteString)
+import Control.Monad.IO.Class (liftIO, MonadIO)
+import Data.IORef
+import Network.Wai.Session.Maybe (Session, SessionStore, genSessionId)
+
+import Data.Map (Map)
+import qualified Data.Map as Map
+
+-- | Simple session store based on threadsafe 'Data.IORef.IORef's and
+-- 'Data.Map.Map'.  This only works if your application server remains
+-- running (such as with warp).  All data is lost when the server
+-- terminates (bad for CGI).
+--
+-- WARNING: This session is vulnerable to sidejacking,
+-- use with TLS for security.
+mapStore :: (Ord k, MonadIO m) =>
+	IO ByteString
+	-- ^ 'IO' action to generate unique session IDs
+	-> IO (SessionStore m k v)
+mapStore gen =
+	liftM (mapStore' gen) (newThreadSafeStateVar Map.empty)
+	where
+	mapStore' _ ssv (Just k) = do
+		m <- get ssv
+		case Map.lookup k m of
+			Just sv -> return (sessionFromMapStateVar sv, return (Just k))
+			-- Could not find key, so it's as if we were not sent one
+			Nothing -> mapStore' gen ssv Nothing
+	mapStore' genNewKey ssv Nothing = do
+		newKey <- genNewKey
+		sv <- newThreadSafeStateVar Map.empty
+		ssv $~ Map.insert newKey sv
+		return (sessionFromMapStateVar sv, return (Just newKey))
+
+-- | Store using simple session ID generator based on time and 'Data.Unique'
+mapStore_ :: (Ord k, MonadIO m) => IO (SessionStore m k v)
+mapStore_ = mapStore genSessionId
+
+newThreadSafeStateVar :: a -> IO (StateVar a)
+newThreadSafeStateVar a = do
+	ref <- newIORef a
+	return $ makeStateVar
+		(atomicModifyIORef ref (\x -> (x,x)))
+		(\x -> atomicModifyIORef ref (const (x,())))
+
+#if MIN_VERSION_StateVar(1,1,0)
+sessionFromMapStateVar :: (Ord k, MonadIO m) =>
+	StateVar (Map k v) ->
+#else
+sessionFromMapStateVar :: (HasGetter sv, HasSetter sv, Ord k, MonadIO m) =>
+	sv (Map k v) ->
+#endif
+	Session m k v
+sessionFromMapStateVar sv = (
+		(\k -> liftIO (Map.lookup k `liftM` get sv)),
+		(\k v -> liftIO (sv $~ Map.insert k v))
+	)
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,10 @@
+Provides a generic, cookie-based middleware for sessions that is
+parameterised over the session store, the cookie name, and the
+cookie parameters (such as path, expiry, etc).  Passes a pair of
+functions (lookup key, set key) for the current session through the
+'Vault' in the 'Request'.
+
+Also provides a simple example session store based on threadsafe
+'IORef's and 'Data.Map'.
+
+See example/Main.hs in git for example usage.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,3 @@
+import Distribution.Simple
+
+main = defaultMain
diff --git a/wai-session-maybe.cabal b/wai-session-maybe.cabal
new file mode 100644
--- /dev/null
+++ b/wai-session-maybe.cabal
@@ -0,0 +1,54 @@
+name:            wai-session-maybe
+version:         1.0.0
+cabal-version:   >= 1.10
+license:         OtherLicense
+license-file:    COPYING
+category:        Web
+copyright:       © 2012 Stephen Paul Weber
+author:          Stephen Paul Weber <singpolyma@singpolyma.net>
+maintainer:      support@digitallyinduced.com
+stability:       experimental
+tested-with:     GHC == 7.0.3
+synopsis:        Flexible session middleware for WAI
+homepage:        https://github.com/digitallyinduced/wai-session-maybe
+bug-reports:     https://github.com/digitallyinduced/wai-session-maybe/issues
+build-type:      Simple
+description:
+        Provides a generic, cookie-based middleware for sessions that is
+        parameterised over the session store, the cookie name, and the
+        cookie parameters (such as path, expiry, etc).  Passes a pair of
+        functions (lookup key, set key) for the current session through the
+        'Vault' in the 'Request'.
+        .
+        Also provides a simple example session store based on threadsafe
+        'IORef's and 'Data.Map'.
+        .
+        See example/Main.hs in git for example usage.
+
+extra-source-files:
+        README
+
+library
+        default-language: Haskell2010
+        exposed-modules:
+                Network.Wai.Session.Maybe,
+                Network.Wai.Session.Maybe.Map
+
+        build-depends:
+                base == 4.*,
+                containers,
+                bytestring,
+                bytestring-builder,
+                entropy,
+                transformers,
+                time,
+                StateVar,
+                vault,
+                cookie,
+                wai >= 2.0.0,
+                http-types,
+                blaze-builder
+
+source-repository head
+        type:     git
+        location: https://github.com/digitallyinduced/wai-session-maybe.git
