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/Main.hs b/Main.hs
new file mode 100644
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,10 @@
+import Morph.Config
+import Morph.Migrator
+import Morph.Options
+
+main :: IO ()
+main = do
+  opts   <- getOptions
+  config <- readConfigOrDie opts
+  withConnection config $ \conn -> do
+    migrate conn $ optsMigrationsDirectory opts
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/morph.cabal b/morph.cabal
new file mode 100644
--- /dev/null
+++ b/morph.cabal
@@ -0,0 +1,48 @@
+-- Initial morph.cabal generated by cabal init.  For further documentation,
+--  see http://haskell.org/cabal/users-guide/
+
+name:                  morph
+version:               0.1.0.0
+synopsis:              A simple database migrator for PostgreSQL
+description:           Morph is a tool to migrate your PostgreSQL databases
+                       safely which supports rollbacks.
+license:               BSD3
+license-file:          LICENSE
+author:                Thomas Feron
+maintainer:            tho.feron@gmail.com
+build-type:            Simple
+cabal-version:         >=1.10
+
+source-repository head
+  type:                darcs
+  location:            http://darcs.redspline.com/morph
+  tag:                 0.1.0.0
+
+library
+  hs-source-dirs:      src
+  ghc-options:         -Wall
+  default-language:    Haskell2010
+
+  default-extensions:  OverloadedStrings
+
+  exposed-modules:     Morph.Config
+                       Morph.Migrator
+                       Morph.Options
+
+  build-depends:       base >=4.8 && <4.9
+                     , optparse-applicative ==0.12.*
+                     , aeson
+                     , yaml
+                     , postgresql-simple
+                     , bytestring
+                     , text
+                     , directory
+                     , filepath
+
+executable morph
+  main-is:             Main.hs
+  hs-source-dirs:      .
+  ghc-options:         -Wall
+  default-language:    Haskell2010
+  build-depends:       base >=4.8 && <4.9
+                     , morph
diff --git a/src/Morph/Config.hs b/src/Morph/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Morph/Config.hs
@@ -0,0 +1,88 @@
+module Morph.Config
+  ( readConfigOrDie
+  , withConnection
+  ) where
+
+import           Control.Exception
+
+import           Data.Maybe
+import qualified Data.Aeson                 as J
+import qualified Data.Aeson.Types           as J hiding (Options)
+import qualified Data.Yaml                  as Y
+import qualified Data.ByteString.Lazy.Char8 as BSL
+import qualified Data.Text                  as T
+
+import           System.Exit
+import           System.IO
+
+import           Database.PostgreSQL.Simple
+
+import           Morph.Options
+
+data Config = Config
+  { configUsername :: String
+  , configPassword :: String
+  , configHostname :: String
+  , configPort     :: Integer
+  , configName     :: String
+  }
+
+instance J.FromJSON Config where
+  parseJSON = J.withObject "Config" $ \obj -> Config
+    <$> obj J..: "username"
+    <*> obj J..: "password"
+    <*> obj J..: "hostname"
+    <*> obj J..: "port"
+    <*> obj J..: "name"
+
+parseJSONConfig :: [T.Text] -> J.Value -> J.Parser Config
+parseJSONConfig [] = J.parseJSON
+parseJSONConfig (key : keys) = J.withObject "Parent object" $ \obj -> do
+  val <- obj J..: key
+  parseJSONConfig keys val
+
+parseYAMLConfig :: [T.Text] -> Y.Value -> Y.Parser Config
+parseYAMLConfig [] val = Y.parseJSON val
+parseYAMLConfig (key : keys) (Y.Object obj) = do
+  val <- obj Y..: key
+  parseYAMLConfig keys val
+parseYAMLConfig _ _ = fail "Object expected"
+
+readConfigOrDie :: Options -> IO Config
+readConfigOrDie opts = do
+  contents <- BSL.readFile $
+    fromMaybe (if optsJSONConfig opts then "config.json" else "config.yml")
+              (optsConfigFile opts)
+
+  let eConfig
+        | optsJSONConfig opts = do
+            val <- J.eitherDecode contents
+            J.parseEither (parseJSONConfig (optsKeysPath opts)) val
+        | otherwise = do
+            val <- Y.decodeEither $ BSL.toStrict contents
+            Y.parseEither (parseYAMLConfig (optsKeysPath opts)) val
+
+  case eConfig of
+    Right config -> return config
+    Left err -> do
+      hPutStrLn stderr $ "Can't read the config file: " ++ err
+      exitFailure
+
+createConn :: Config -> IO Connection
+createConn config = connect ConnectInfo
+  { connectHost     = configHostname config
+  , connectUser     = configUsername config
+  , connectPassword = configPassword config
+  , connectDatabase = configName     config
+  , connectPort     = fromInteger $ configPort config
+  }
+
+destroyConn :: Connection -> IO ()
+destroyConn = close
+
+withConnection :: Config -> (Connection -> IO a) -> IO a
+withConnection config f = do
+  conn <- createConn config
+  res <- catch (f conn) $ \e -> destroyConn conn >> throw (e :: SomeException)
+  destroyConn conn
+  return res
diff --git a/src/Morph/Migrator.hs b/src/Morph/Migrator.hs
new file mode 100644
--- /dev/null
+++ b/src/Morph/Migrator.hs
@@ -0,0 +1,122 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE FlexibleInstances #-}
+
+module Morph.Migrator
+  ( migrate
+  ) where
+
+import Control.Monad
+
+import Data.List
+import Data.Maybe
+import Data.Monoid
+import Data.String
+
+import System.Directory
+import System.FilePath
+import System.IO
+
+import Database.PostgreSQL.Simple
+import Database.PostgreSQL.Simple.FromRow
+
+-- | A migration can either be read from file and contain both sides or from the
+-- database and contain only the down side.
+data MigrationType = Full | Rollback
+
+type family MigrationSQL (a :: MigrationType) :: * where
+  MigrationSQL 'Full     = (Query, String)
+  MigrationSQL 'Rollback = Query
+
+data Migration :: MigrationType -> * where
+  Migration ::
+    { migrationIdentifier :: String
+    , migrationSQL        :: MigrationSQL a
+    } -> Migration a
+
+createMigrationTable :: Connection -> IO ()
+createMigrationTable conn = void $ execute_ conn
+  "CREATE TABLE IF NOT EXISTS migrations (\
+  \  id           varchar PRIMARY KEY CHECK (id <> ''),\
+  \  rollback_sql text CHECK (rollback_sql <> '')\
+  \);"
+
+listDone :: Connection -> IO [Migration 'Rollback]
+listDone conn = do
+  pairs <- query_ conn "SELECT id, rollback_sql FROM migrations ORDER BY id ASC"
+  return $ flip map pairs $ \(identifier, mSQL) -> Migration
+    { migrationIdentifier = identifier
+    , migrationSQL        = maybe "" fromString mSQL
+    }
+
+listGoals :: FilePath -> IO [Migration 'Full]
+listGoals dir = do
+    allNames <- sort <$> getDirectoryContents dir
+    let upNames     = filter (".up.sql"   `isSuffixOf`) allNames
+        downNames   = filter (".down.sql" `isSuffixOf`) allNames
+
+    forM upNames $ \upName -> do
+      let identifier = extractIdentifier upName
+      up   <- readMigrationFile upName
+      down <- readDownMigrationFile downNames identifier
+      return Migration
+        { migrationIdentifier = identifier
+        , migrationSQL        = (up, down)
+        }
+
+  where
+    extractIdentifier :: FilePath -> String
+    extractIdentifier = takeWhile (`elem` ("0123456789" :: String))
+
+    readMigrationFile :: FilePath -> IO Query
+    readMigrationFile path = do
+      contents <- readFile $ dir </> path
+      return $ fromString contents
+
+    readDownMigrationFile :: [FilePath] -> String -> IO String
+    readDownMigrationFile paths identifier =
+      case find ((==identifier) . extractIdentifier) paths of
+        Nothing -> return $
+          "RAISE EXCEPTION 'No rollback migration found for "
+          <> fromString identifier <> "';"
+        Just path -> readFile $ dir </> path
+
+rollbackMigration :: Connection -> Migration 'Rollback -> IO ()
+rollbackMigration conn migration = do
+  hPutStrLn stderr $
+    "Rollbacking migration " ++ migrationIdentifier migration ++ " ..."
+  void $ execute_ conn $ migrationSQL migration
+  void $ execute conn "DELETE FROM migrations WHERE id = ?" $
+    Only $ migrationIdentifier migration
+
+doMigration :: Connection -> Migration 'Full -> IO ()
+doMigration conn migration = do
+  hPutStrLn stderr $
+    "Running migration " ++ migrationIdentifier migration ++ " ..."
+  let (up, down) = migrationSQL migration
+  void $ execute_ conn up
+  void $ execute conn "INSERT INTO migrations (id, rollback_sql) VALUES (?, ?)"
+                 (migrationIdentifier migration, down)
+
+migrate :: Connection -> FilePath -> IO ()
+migrate conn dir = do
+  createMigrationTable conn
+
+  doneMigrations <- listDone  conn
+  goalMigrations <- listGoals dir
+
+  let doneIdentifiers = map migrationIdentifier doneMigrations
+      goalIdentifiers = map migrationIdentifier goalMigrations
+
+      toRollbackIdentifiers = doneIdentifiers \\ goalIdentifiers
+      toDoIdentifiers       = goalIdentifiers \\ doneIdentifiers
+
+      toRollback = filter ((`elem` toRollbackIdentifiers) . migrationIdentifier)
+                          doneMigrations
+      toDo       = filter ((`elem` toDoIdentifiers)       . migrationIdentifier)
+                          goalMigrations
+
+  withTransaction conn $ do
+    forM_ toRollback $ rollbackMigration conn
+    forM_ toDo       $ doMigration       conn
diff --git a/src/Morph/Options.hs b/src/Morph/Options.hs
new file mode 100644
--- /dev/null
+++ b/src/Morph/Options.hs
@@ -0,0 +1,36 @@
+module Morph.Options
+  ( Options(..)
+  , getOptions
+  ) where
+
+import qualified Data.Text as T
+
+import           Options.Applicative
+
+data Options = Options
+  { optsConfigFile          :: Maybe FilePath
+  , optsKeysPath            :: [T.Text]
+  , optsMigrationsDirectory :: FilePath
+  , optsJSONConfig          :: Bool
+  }
+
+optionsParser :: Parser Options
+optionsParser = Options
+  <$> option (Just <$> str)
+             (short 'c' <> long "config" <> metavar "PATH"
+              <> value Nothing <> help "Path to the config file.")
+  <*> option ((T.splitOn "." . T.pack) <$> str)
+             (short 'p' <> long "path" <> metavar "KEY1[.KEY2[...]]"
+              <> value [] <> help "The keys to traverse in the JSON to find\
+                                  \ the database connection info.")
+  <*> strOption (short 'd' <> long "dir" <> metavar "PATH"
+                 <> showDefault <> value "migrations"
+                 <> help "Path to the directory containing migrations.")
+  <*> flag False True (short 'j' <> long "json"
+                       <> help "Read config file as JSON.")
+
+getOptions :: IO Options
+getOptions = execParser $ info (helper <*> optionsParser) $
+  fullDesc
+  <> progDesc "Migrator for PostgreSQL databases with support for rollbacks"
+  <> footer "This program is licensed under the BSD-3 license."
