diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for seakale-postgresql
+
+## 0.1.0.0 -- 2017-01-31
+
+* First version.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2016, Thomas Feron
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Thomas Feron nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/seakale-postgresql.cabal b/seakale-postgresql.cabal
new file mode 100644
--- /dev/null
+++ b/seakale-postgresql.cabal
@@ -0,0 +1,45 @@
+name:                  seakale-postgresql
+version:               0.1.0.0
+synopsis:              PostgreSQL backend for Seakale
+description:           This package provides a way to run code written with Seakale with a PostgreSQL database.
+license:               BSD3
+license-file:          LICENSE
+author:                Thomas Feron
+maintainer:            thomas.feron@redspline.com
+category:              Database
+build-type:            Simple
+extra-source-files:    ChangeLog.md
+cabal-version:         >=1.10
+
+source-repository head
+  type:                darcs
+  location:            http://darcs.redspline.com/seakale
+  tag:                 0.1.0.0
+
+library
+  ghc-options:         -Wall
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+  default-extensions:  TypeFamilies
+                       OverloadedStrings
+                       FlexibleInstances
+                       MultiParamTypeClasses
+                       FunctionalDependencies
+                       ConstraintKinds
+                       LambdaCase
+                       RecordWildCards
+                       OverloadedStrings
+                       FlexibleContexts
+
+  exposed-modules:     Database.Seakale.PostgreSQL
+                       Database.Seakale.PostgreSQL.FromRow
+                       Database.Seakale.PostgreSQL.ToRow
+
+  build-depends:       base >=4.8 && <4.10
+                     , seakale
+                     , postgresql-libpq
+                     , bytestring
+                     , mtl
+                     , free
+                     , time
diff --git a/src/Database/Seakale/PostgreSQL.hs b/src/Database/Seakale/PostgreSQL.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Seakale/PostgreSQL.hs
@@ -0,0 +1,249 @@
+module Database.Seakale.PostgreSQL
+  ( Connection
+  , ConnectInfo(..)
+  , connect
+  , connectString
+  , disconnect
+  , Request
+  , RequestT
+  , runRequest
+  , runRequestT
+  , Select
+  , SelectT
+  , runSelect
+  , runSelectT
+  , Store
+  , StoreT
+  , runStore
+  , runStoreT
+  , HasConnection(..)
+  , PSQL(..)
+  , SeakaleError(..)
+  , T.Query(..) -- prefixed to export EmptyQuery
+  , Field(..)
+  , Row
+  , ColumnInfo(..)
+  , QueryData
+  , Vector(..)
+  , cons, (<:>)
+  , nil, (<:|)
+  , Zero
+  , One
+  , Two
+  , Three
+  , Four
+  , Five
+  , Six
+  , Seven
+  , Eight
+  , Nine
+  , Ten
+  ) where
+
+import           Control.Monad.Except
+import           Control.Monad.Identity
+import           Control.Monad.Reader
+import           Control.Monad.State
+import           Control.Monad.Trans.Free
+
+import           Data.Maybe
+import           Data.Monoid
+import           Data.Word
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy as BSL
+
+import           Database.PostgreSQL.LibPQ hiding (Row, status)
+
+import           Database.Seakale.Types
+                   hiding (runQuery, runExecute, EmptyQuery)
+import qualified Database.Seakale.Request.Internal as I
+import qualified Database.Seakale.Store.Internal as I
+import qualified Database.Seakale.Types as T
+
+data ConnectInfo = ConnectInfo
+  { ciHostname :: String
+  , ciPort     :: Word16
+  , ciUsername :: String
+  , ciPassword :: String
+  , ciDatabase :: String
+  }
+
+toConnectionString :: ConnectInfo -> BS.ByteString
+toConnectionString ConnectInfo{..} =
+    "host=" <> quote ciHostname
+    <> " port=" <> BS.pack (show ciPort)
+    <> " user=" <> quote ciUsername
+    <> " password=" <> quote ciPassword
+    <> " dbname=" <> quote ciDatabase
+
+  where
+    quote :: String -> BS.ByteString
+    quote = ("'" <>) . (<> "'") . escapeQuotes "" . BS.pack
+
+    escapeQuotes :: BS.ByteString -> BS.ByteString -> BS.ByteString
+    escapeQuotes _ "" = ""
+    escapeQuotes prefix s =
+      let (start, end) = fmap (BS.drop 1) $ BS.break (=='\'') s
+      in prefix <> start <> escapeQuotes "''" end
+
+connect :: ConnectInfo -> IO Connection
+connect = connectString . toConnectionString
+
+connectString :: BS.ByteString -> IO Connection
+connectString = connectdb
+
+disconnect :: Connection -> IO ()
+disconnect = finish
+
+class Monad m => HasConnection m where
+  withConn :: (Connection -> m a) -> m a
+
+instance Monad m => HasConnection (ReaderT Connection m) where
+  withConn f = f =<< ask
+
+instance HasConnection m => HasConnection (ExceptT e m) where
+  withConn f = do
+    eRes <- lift $ withConn $ runExceptT . f
+    either throwError return eRes
+
+instance HasConnection m => HasConnection (StateT s m) where
+  withConn f = do
+    s <- get
+    (x, s') <- lift $ withConn $ flip runStateT s . f
+    put s'
+    return x
+
+type TypeCache = [(Oid, BS.ByteString)]
+
+data PSQL = PSQL
+
+instance Backend PSQL where
+  type ColumnType PSQL = BS.ByteString
+
+  type MonadBackend PSQL m = ( HasConnection m
+                             , MonadState TypeCache m
+                             , MonadIO m
+                             )
+
+  runQuery   _ = runExceptT . runQuery
+  runExecute _ = runExceptT . runExecute
+
+type RequestT = I.RequestT PSQL
+type Request  = I.Request  PSQL
+
+runRequestT :: (HasConnection m, MonadIO m) => RequestT m a
+            -> m (Either SeakaleError a)
+runRequestT =
+  fmap fst . flip runStateT [] . I.runRequestT PSQL . hoistFreeT lift
+
+runRequest :: (HasConnection m, MonadIO m) => Request a
+           -> m (Either SeakaleError a)
+runRequest = runRequestT . hoistFreeT (return . runIdentity)
+
+type SelectT = I.SelectT PSQL
+type Select  = I.Select  PSQL
+
+runSelectT :: Monad m => SelectT m a -> RequestT m a
+runSelectT = I.runSelectT
+
+runSelect :: Select a -> Request a
+runSelect = I.runSelect
+
+type StoreT m = I.StoreT PSQL m
+type Store    = I.Store  PSQL
+
+runStoreT :: Monad m => StoreT m a -> RequestT m a
+runStoreT = I.runStoreT
+
+runStore :: Store a -> Request a
+runStore = I.runStore
+
+runQuery :: MonadBackend PSQL m => BSL.ByteString
+         -> ExceptT BS.ByteString m ([ColumnInfo PSQL], [Row PSQL])
+runQuery lazyReq = do
+  let req = mconcat $ BSL.toChunks lazyReq
+  res <- exec' req
+
+  let _until i = takeWhile (/= i) $ iterate (+1) 0
+  ncols <- liftIO $ nfields res
+  nrows <- liftIO $ ntuples res
+
+  let colIndices = _until ncols
+  cols <- forM colIndices $ \col -> do
+    name <- liftIO $ fname res col
+    oid  <- liftIO $ ftype res col
+    typ  <- resolveType oid
+    return ColumnInfo
+      { colInfoName = name
+      , colInfoType = typ
+      }
+
+  rows <- liftIO $ forM (_until nrows) $ \row -> do
+    forM colIndices $ \col -> do
+      mValue <- getvalue' res row col
+      return Field { fieldValue = mValue }
+
+  return (cols, rows)
+
+runExecute :: MonadBackend PSQL m => BSL.ByteString
+           -> ExceptT BS.ByteString m Integer
+runExecute lazyReq = do
+  let req = mconcat $ BSL.toChunks lazyReq
+  res <- exec' req
+
+  mBS <- liftIO $ cmdTuples res
+  case fmap (reads . BS.unpack) mBS of
+    Just ((n,"") : _) -> return n
+    _ -> throwError $ "Can't get number of rows affected for " <> req
+
+resolveType :: MonadBackend PSQL m => Oid
+            -> ExceptT BS.ByteString m BS.ByteString
+resolveType oid = do
+  cache <- get
+  case lookup oid cache of
+    Just n  -> return n
+    Nothing -> do
+      cache' <- getTypes
+      put cache'
+      case lookup oid cache' of
+        Just n  -> return n
+        Nothing -> throwError $ BS.pack $ "Can't resolve type for " ++ show oid
+
+getTypes :: MonadBackend PSQL m => ExceptT BS.ByteString m TypeCache
+getTypes = do
+  res <- exec' "SELECT oid, typname FROM pg_type"
+  let _until i = takeWhile (/= i) $ iterate (+1) 0
+  nrows <- liftIO $ ntuples res
+
+  forM (_until nrows) $ \row -> do
+    oidBS  <- liftIO $ getvalue' res row 0
+    nameBS <- liftIO $ getvalue' res row 1
+    case (fmap (reads . BS.unpack) oidBS, nameBS) of
+      (Just ((i,"") : _), Just name) ->
+        return (Oid (fromInteger i), name)
+      _ -> throwError "Can't read types from pg_type"
+
+exec' :: MonadBackend PSQL m => BS.ByteString
+      -> ExceptT BS.ByteString m Result
+exec' req = do
+  res <- withConn $ \conn -> do
+    mRes <- liftIO $ exec conn req
+    case mRes of
+      Nothing -> do
+        err <- liftIO $ fromMaybe "Fatal error" <$> errorMessage conn
+        throwError err
+      Just r -> return r
+
+  let err bs = do
+        msg <- liftIO $ fromMaybe bs <$> resultErrorMessage res
+        throwError msg
+  status <- liftIO $ resultStatus res
+  case status of
+    EmptyQuery  -> err "Empty query"
+    CopyIn      -> err "COPY not supported"
+    CopyOut     -> err "COPY not supported"
+    BadResponse -> err "Bad response"
+    FatalError  -> err "Fatal error"
+    _ -> return ()
+
+  return res
diff --git a/src/Database/Seakale/PostgreSQL/FromRow.hs b/src/Database/Seakale/PostgreSQL/FromRow.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Seakale/PostgreSQL/FromRow.hs
@@ -0,0 +1,81 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Database.Seakale.PostgreSQL.FromRow
+  ( module Database.Seakale.FromRow
+  ) where
+
+import           Data.Monoid
+import           Data.Time
+import qualified Data.ByteString.Char8 as BS
+
+import           Database.Seakale.FromRow
+import           Database.Seakale.PostgreSQL
+
+instance FromRow PSQL One Bool where
+  fromRow = pconsume `pbind` \(_, f) -> case fieldValue f of
+    Nothing  -> pfail "unexpected NULL"
+    Just "t" -> preturn True
+    Just "f" -> preturn False
+    Just bs  -> pfail $ "unreadable boolean: " ++ BS.unpack bs
+
+instance FromRow PSQL One UTCTime where
+  fromRow = pconsume `pbind` \(ColumnInfo{..}, Field{..}) ->
+    case (colInfoType, fieldValue) of
+      ("timestamp", Just bs) ->
+        case parseTimeM True defaultTimeLocale "%F %T%Q" (BS.unpack bs) of
+          Just t  -> preturn t
+          Nothing -> pfail $ "invalid time: " ++ BS.unpack bs
+      ("timestamptz", Just bs) ->
+        case parseTimeM True defaultTimeLocale "%F %T%Q%z"
+               (BS.unpack bs ++ "00") of
+          Just t  -> preturn t
+          Nothing -> pfail $ "invalid time: " ++ BS.unpack bs
+      (bs, Just _) -> pfail $ "invalid type for time: " ++ BS.unpack bs
+      (_, Nothing) -> pfail "unexpected NULL for time"
+
+instance FromRow PSQL One String where
+  fromRow = pmap BS.unpack fromRow
+
+instance {-# OVERLAPPABLE #-} FromRow PSQL One a => FromRow PSQL One [a] where
+  fromRow = pconsume `pbind` \(col@ColumnInfo{..}, Field{..}) ->
+    case (BS.splitAt 1 colInfoType, fieldValue) of
+      (("_", typ), Just bs) -> arrayParser (col { colInfoType = typ }) bs
+      (_, Just _) -> pfail $ "invalid type for list: " ++ BS.unpack colInfoType
+      (_, Nothing) -> pfail "unexpected NULL for list"
+
+-- FIXME: What about \n for example?
+arrayParser :: FromRow PSQL One a => ColumnInfo PSQL -> BS.ByteString
+            -> RowParser PSQL Zero [a]
+arrayParser col = either pfail preturn . go
+  where
+    go :: FromRow PSQL One a => BS.ByteString -> Either String [a]
+    go bs = case BS.splitAt 1 bs of
+      ("{", "}") -> return []
+      ("{", bs') -> readValues id bs'
+      _ -> Left $ "invalid array starting with " ++ show (BS.take 30 bs)
+
+    readValues :: FromRow PSQL One a => ([a] -> [a]) -> BS.ByteString
+               -> Either String [a]
+    readValues f bs = do
+      (valBS, bs') <- readByteString bs
+      let mValBS = if valBS == "NULL" then Nothing else Just valBS
+      val <- parseRow fromRow PSQL [col] [Field mValBS]
+      case BS.splitAt 1 bs' of
+        (",", bs'') -> readValues (f . (val :)) bs''
+        ("}", "")   -> return $! f [val]
+        _ -> Left $ "invalid array around " ++ show (BS.take 30 bs')
+
+    readByteString :: BS.ByteString
+                   -> Either String (BS.ByteString, BS.ByteString)
+    readByteString bs = case BS.splitAt 1 bs of
+      ("\"", bs') -> readByteString' "" bs'
+      _ -> return $ BS.span (\c -> c /= ',' && c /= '}') bs
+
+    readByteString' :: BS.ByteString -> BS.ByteString
+                    -> Either String (BS.ByteString, BS.ByteString)
+    readByteString' acc bs =
+      case fmap (BS.splitAt 1) (BS.span (\c -> c /= '"' && c /= '\\') bs) of
+        (bs', ("\"", bs'')) -> return (acc <> bs', bs'')
+        (bs', ("\\", bs'')) -> let (c, bs''') = BS.splitAt 1 bs''
+                               in readByteString' (acc <> bs' <> c) bs'''
+        (bs', _) -> Left $ "unreadable value around " ++ show (BS.take 30 bs')
diff --git a/src/Database/Seakale/PostgreSQL/ToRow.hs b/src/Database/Seakale/PostgreSQL/ToRow.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Seakale/PostgreSQL/ToRow.hs
@@ -0,0 +1,40 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Database.Seakale.PostgreSQL.ToRow
+  ( module Database.Seakale.ToRow
+  ) where
+
+import           Data.List
+import           Data.Monoid
+import           Data.Time
+import qualified Data.ByteString.Char8 as BS
+
+import           Database.Seakale.ToRow
+import           Database.Seakale.Types
+
+import           Database.Seakale.PostgreSQL
+
+instance ToRow PSQL One Bool where
+  toRow _ = \case
+    True  -> Cons "'t'" Nil
+    False -> Cons "'f'" Nil
+
+instance ToRow PSQL One UTCTime where
+  toRow backend = toRow backend . formatTime defaultTimeLocale "%F %T%QZ"
+
+instance ToRow PSQL One String where
+  toRow backend = toRow backend . BS.pack
+
+instance {-# OVERLAPPABLE #-} ToRow PSQL One a => ToRow PSQL One [a] where
+  toRow backend =
+    singleton . ("'{" <>) . (<> "}'") . mconcat . intersperse ","
+    . map (("\"" <>) . (<> "\"") . escapeByteString)
+    . (>>= vectorToList . toRow backend)
+
+escapeByteString :: BS.ByteString -> BS.ByteString
+escapeByteString bs =
+  case fmap (BS.splitAt 1) (BS.span (\c -> c /= '\\' && c /= '"') bs) of
+    (bs', ("\\", bs'')) -> bs' <> "\\\\" <> escapeByteString bs''
+    (bs', ("\"", bs'')) -> bs' <> "\\\"" <> escapeByteString bs''
+    (bs', _) -> bs'
