diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,9 @@
+0.4
+===
+
+  * Listen on UNIX socket (default), TCP allowed on localhost only.
+  * Use command-line options instead of configuration file
+
 0.3.1
 =====
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -22,15 +22,17 @@
 =====
 Type `sproxy-web --help` to see usage summary:
 
-    sproxy-web - Web interface to the sproxy permissions database
+    Usage:
+      sproxy-web [options]
 
-      -h         --help, --usage, --version  Display help and version information.
-                 --undefok                   Whether to fail on unrecognized command line options.
-      -c STRING  --config=STRING             config file (default: sproxy-web.config, from module: Config)
+    Options:
 
+      -c, --connstr=CONNSTR    PostgreSQL connection string [default: dbname=sproxy]
+      -d, --datadir=DIR        Data directory including static files [default: <cabal data dir>]
 
-The config file must have the following simplistic structure.
+      -s, --socket=SOCK        Listen on this UNIX socket [default: /tmp/sproxy-web.sock]
+      -p, --port=PORT          Instead of UNIX socket, listen on this TCP port (localhost)
 
-    db_connection_string = "host=127.0.0.1 port=4534 user=alp dbname=alp password=blah"
-    port = 8003
+      -h, --help               Show this message
+
 
diff --git a/example.config b/example.config
deleted file mode 100644
--- a/example.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-db_connection_string = "hostaddr=127.0.0.1 user=alp dbname=alp"
-
-port = 8001
diff --git a/sproxy-web.cabal b/sproxy-web.cabal
--- a/sproxy-web.cabal
+++ b/sproxy-web.cabal
@@ -1,5 +1,5 @@
 name: sproxy-web
-version: 0.3.1
+version: 0.4
 synopsis: Web interface to sproxy database
 description:
   Web frontend for managing sproxy.
@@ -11,7 +11,7 @@
 copyright: 2014-2016, Zalora South East Asia Pte. Ltd
 category: Web
 build-type: Simple
-extra-source-files: README.md ChangeLog.md example.config
+extra-source-files: README.md ChangeLog.md
 cabal-version: >= 1.20
 data-files:
   static/css/*.css,
@@ -36,11 +36,10 @@
   main-is: Main.hs
   hs-source-dirs: src
   other-modules:
-    Config
+    Application
     DB
     Entities
-    Handlers
-    Paths_sproxy_web
+    Server
     SproxyError
     Views.Common
     Views.DomainList
@@ -50,25 +49,29 @@
     Views.Homepage
     Views.MemberList
     Views.PrivilegeRules
+    Views.Search
   build-depends:
-      base >= 4.8 && < 5
-    , aeson                 >= 0.6
-    , blaze-html            >= 0.7
-    , blaze-markup          >= 0.6
-    , bytestring            >= 0.10
-    , configurator          >= 0.2
+      base                     >= 4.8 && < 5
+    , aeson                    >= 0.6
+    , blaze-html               >= 0.7
+    , blaze-markup             >= 0.6
+    , bytestring               >= 0.10
+    , configurator             >= 0.2
     , data-default-class
     , directory
+    , docopt                   >= 0.7
     , filepath
-    , hflags                >= 0.4
-    , http-types            >= 0.8
-    , mtl                   >= 2.1
-    , postgresql-simple     >= 0.4
-    , resource-pool         >= 0.2
-    , scotty                >= 0.10
-    , text                  >= 0.11
+    , http-types               >= 0.8
+    , interpolatedstring-perl6
+    , mtl                      >= 2.1
+    , network                  >= 2.6
+    , postgresql-simple        >= 0.4
+    , resource-pool            >= 0.2
+    , scotty                   >= 0.10
+    , text                     >= 0.11
+    , unix                     >= 2.7
     , wai
-    , wai-extra             >= 2.0
-    , wai-middleware-static >= 0.4
-    , warp                  >= 3.2
+    , wai-extra                >= 2.0
+    , wai-middleware-static    >= 0.4
+    , warp                     >= 3.2
 
diff --git a/src/Application.hs b/src/Application.hs
new file mode 100644
--- /dev/null
+++ b/src/Application.hs
@@ -0,0 +1,375 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
+
+module Application (
+  app
+) where
+
+import Control.Exception
+import Control.Monad (when)
+import Data.Default.Class
+import Data.Int (Int64)
+import Data.Monoid
+import Data.Pool (Pool)
+import Data.Text.Lazy as Text
+import Database.PostgreSQL.Simple (Connection)
+import Network.HTTP.Types.Status
+import Network.Wai (Application, Middleware)
+import Network.Wai.Middleware.RequestLogger
+import Network.Wai.Middleware.Static
+import System.IO
+import Text.Blaze.Html.Renderer.Text (renderHtml)
+import Text.Blaze.Html5 (Html)
+import Views.DomainList (domainListT)
+import Views.DomainPrivileges (domainPrivilegesT)
+import Views.ErrorPage (errorPageT)
+import Views.GroupList (groupListT)
+import Views.Homepage (homepageT)
+import Views.MemberList (memberListT)
+import Views.PrivilegeRules (privilegeRulesT)
+import Views.Search (searchResultsT)
+import Web.Scotty.Trans
+
+import SproxyError
+import DB
+import Entities
+
+app :: Pool Connection -> FilePath -> IO Application
+app c p = do
+  logger <- mkRequestLogger def{ destination = Handle stderr }
+  scottyAppT id (sproxyWeb c p logger)
+
+sproxyWeb :: Pool Connection -> FilePath -> Middleware -> ScottyT SproxyError IO ()
+sproxyWeb pool staticDirectory logger = do
+  middleware logger
+
+  middleware (staticPolicy (hasPrefix "static" >-> addBase staticDirectory))
+
+  -- error page for uncaught exceptions
+  defaultHandler handleEx
+
+  get "/" $ homepage pool
+
+  post "/search"      $ searchUserH pool
+  post "/delete-user" $ deleteUserH pool
+  post "/rename-user" $ renameUserH pool
+
+  -- groups
+  get "/groups"      $ groupList pool        -- this is the group listing page
+  get "/groups.json" $ jsonGroupList pool    -- json endpoint returning an array of group names
+  post "/groups"     $ jsonUpdateGroup pool  -- endpoint where we POST requests for modifications of groups
+
+  -- group
+  get "/group/:group/members"  $ memberList pool       -- list of members for a group
+  post "/group/:group/members" $ jsonPostMembers pool  -- endpoint for POSTing updates to the member list
+
+  -- domains
+  get "/domains"  $ domainList pool        -- list of domains handled by sproxy
+  post "/domains" $ jsonUpdateDomain pool  -- endpoint for POSTing updates
+
+  -- privileges for a given domain
+  get "/domain/:domain/privileges"      $ domainPrivileges pool          -- listing of privileges available on a domain
+  get "/domain/:domain/privileges.json" $ jsonDomainPrivileges pool      -- json endpoint, array of privilege names
+  post "/domain/:domain/privileges"     $ jsonPostDomainPrivileges pool  -- endpoint for POSTing updates
+
+  -- rules for a given privilege on a given domain
+  get "/domain/:domain/privilege/:privilege/rules"  $ privilegeRules pool  -- listing of paths/methods associated to a privilege
+  post "/domain/:domain/privilege/:privilege/rules" $ jsonPostRule pool    -- endpoint for POSTing updates about these
+
+  -- add/remove group privileges
+  post "/domain/:domain/group_privileges" $ handleGPs pool  -- endpoint for POSTing privilege granting/removal for groups
+
+
+blaze :: Html -> ActionT SproxyError IO ()
+blaze = Web.Scotty.Trans.html . renderHtml
+
+handleEx :: SproxyError -> ActionT SproxyError IO ()
+handleEx = errorPage
+
+errorPage :: SproxyError -> ActionT SproxyError IO ()
+errorPage err = blaze (errorPageT err)
+
+homepage :: Pool Connection -> ActionT SproxyError IO ()
+homepage _ = blaze homepageT
+
+------------------------------------------
+-- handlers related to the group list page
+------------------------------------------
+
+-- POST /groups
+jsonUpdateGroup :: Pool Connection -> ActionT SproxyError IO ()
+jsonUpdateGroup pool = do
+    (operation :: Text) <- param "operation"
+    (t, n) <- case operation of
+             "add" -> do
+                g <- param "group"
+                checked pool (addGroup g)
+
+             "del" -> do
+                g <- param "group"
+                checked pool (removeGroup g)
+
+             "upd" -> do
+                old <- param "old"
+                new <- param "new"
+                checked pool (updateGroup old new)
+
+             _ -> status badRequest400 >> return ("incorrect operation", (-1))
+
+    outputFor t n
+
+-- GET /groups
+groupList :: Pool Connection -> ActionT SproxyError IO ()
+groupList pool = do
+    groups <- Prelude.map Prelude.head `fmap` withDB pool getGroups
+    blaze (groupListT groups)
+
+-- GET /groups.json
+jsonGroupList :: Pool Connection -> ActionT SproxyError IO ()
+jsonGroupList pool = do
+    groups <- Prelude.map Prelude.head `fmap` withDB pool getGroups
+
+    json groups
+
+---------------------------------------------------
+--  handlers related to the group members list page
+---------------------------------------------------
+
+-- GET /group/:group
+memberList :: Pool Connection -> ActionT SproxyError IO ()
+memberList pool = do
+    groupName <- param "group"
+    members   <- Prelude.map Prelude.head `fmap` withDB pool (getMembersFor groupName)
+    blaze (memberListT members groupName)
+
+-- POST /group/:group/members
+jsonPostMembers :: Pool Connection -> ActionT SproxyError IO ()
+jsonPostMembers pool = do
+    groupName <- param "group"
+
+    (operation :: Text) <- param "operation"
+
+    (t, n) <- case operation of
+        "add" -> do
+            m <- param "member"
+            checked pool (addMemberTo m groupName)
+
+        "del" -> do
+            m <- param "member"
+            checked pool (removeMemberOf m groupName)
+
+        "upd" -> do
+            old <- param "old"
+            new <- param "new"
+            checked pool (updateMemberOf old new groupName)
+
+        _     -> status badRequest400 >> return ("incorrect operation", (-1))
+
+    outputFor t n
+
+--------------------------------------
+-- handlers related to the domain list
+--------------------------------------
+
+-- GET /domains
+domainList :: Pool Connection -> ActionT SproxyError IO ()
+domainList pool = do
+    domains <- Prelude.map Prelude.head `fmap` withDB pool getDomains
+    blaze (domainListT domains)
+
+-- POST /domains
+jsonUpdateDomain :: Pool Connection -> ActionT SproxyError IO ()
+jsonUpdateDomain pool = do
+    (operation :: Text) <- param "operation"
+    (t, n) <- case operation of
+             "add" -> do
+                d <- param "domain"
+                checked pool (addDomain d)
+
+             "del" -> do
+                d <- param "domain"
+                checked pool (removeDomain d)
+
+             "upd" -> do
+                old <- param "old"
+                new <- param "new"
+                checked pool (updateDomain old new)
+
+             _ -> status badRequest400 >> return ("incorrect operation", (-1))
+
+    outputFor t n
+
+-------------------------------------------------------------------------
+-- handlers related to the list of possible privileges for a given domain
+-------------------------------------------------------------------------
+
+-- GET /domain/:domain/privileges
+domainPrivileges :: Pool Connection -> ActionT SproxyError IO ()
+domainPrivileges pool = do
+    domain     <- param "domain"
+    privileges <- Prelude.map Prelude.head `fmap` withDB pool (getDomainPrivileges domain)
+    groups     <- Prelude.map Prelude.head `fmap` withDB pool getGroups
+    groupPrivs <- withDB pool (getGroupPrivsFor domain)
+
+    blaze (domainPrivilegesT domain privileges groups groupPrivs)
+
+-- GET /domain/:domain/privileges.json
+jsonDomainPrivileges :: Pool Connection -> ActionT SproxyError IO ()
+jsonDomainPrivileges pool = do
+    domain     <- param "domain"
+    privileges <- Prelude.map Prelude.head `fmap` withDB pool (getDomainPrivileges domain)
+
+    json privileges
+
+-- POST /domain/:domain/group_privileges
+handleGPs :: Pool Connection -> ActionT SproxyError IO ()
+handleGPs pool = do
+    domain <- param "domain"
+
+    (operation :: Text) <- param "operation"
+
+    (t, n) <- case operation of
+        "add" -> do
+            grp <- param "group"
+            priv <- param "privilege"
+            checked pool (addGPFor domain grp priv)
+
+        "del" -> do
+            grp <- param "group"
+            priv <- param "privilege"
+            checked pool (deleteGPOf domain grp priv)
+
+        _ -> status badRequest400 >> return ("incorrect operation", (-1))
+
+    outputFor t n
+
+-- POST /domain/:domain/privileges
+jsonPostDomainPrivileges :: Pool Connection -> ActionT SproxyError IO ()
+jsonPostDomainPrivileges pool = do
+    domain <- param "domain"
+
+    (operation :: Text) <- param "operation"
+
+    (t, n) <- case operation of
+        "add" -> do
+            p <- param "privilege"
+            checked pool (addPrivilegeToDomain p domain)
+
+        "del" -> do
+            p <- param "privilege"
+            checked pool (removePrivilegeOfDomain p domain)
+
+        "upd" -> do
+            old <- param "old"
+            new <- param "new"
+            checked pool (updatePrivilegeOfDomain old new domain)
+
+        _     -> status badRequest400 >> return ("incorrect operation", (-1))
+
+    outputFor t n
+
+-------------------------------------------------------------------------------
+-- handlers related to the rules associated to a given privilege on some domain
+-------------------------------------------------------------------------------
+
+-- GET /domain/:domain/privilege/:privilege/rules
+privilegeRules :: Pool Connection -> ActionT SproxyError IO ()
+privilegeRules pool = do
+    -- TODO: check that the domain and privilege exist
+    domain    <- param "domain"
+    privilege <- param "privilege"
+    rules     <- withDB pool (getRules domain privilege)
+
+    blaze (privilegeRulesT domain privilege rules)
+
+-- POST /domain/:domain/privilege/:privilege/rules
+jsonPostRule :: Pool Connection -> ActionT SproxyError IO ()
+jsonPostRule pool = do
+    -- TODO: check that the domain and privilege exist
+    domain    <- param "domain"
+    privilege <- param "privilege"
+    operation <- param "operation"
+    case operation :: Text of
+        "add" -> addRule domain privilege
+        "del" -> delRule domain privilege
+        "upd" -> updRule domain privilege
+        _     -> status badRequest400 >> text "bad operation"
+
+    where
+      addRule domain privilege = do
+          path   <- param "path"
+          method <- param "method"
+
+          (t, n) <- checked pool
+                            (addRuleToPrivilege domain privilege path method)
+
+          outputFor t n
+
+      delRule domain privilege = do
+          path   <- param "path"
+          method <- param "method"
+
+          (t, n) <- checked pool
+                            (deleteRuleFromPrivilege domain privilege path method)
+
+          outputFor t n
+
+      updRule domain privilege = do
+          what <- param "what"
+
+          when (what /= "path" && what /= "method") $ text "invalid 'what'"
+
+          let updFunc = if what == "path" then updatePathFor else updateMethodFor
+
+          old <- param ("old" <> what)
+          new <- param ("new" <> what)
+          otherField <- param $ if what == "path" then "method" else "path"
+
+          (t, n) <- checked pool
+                            (updFunc domain privilege new old otherField)
+
+          outputFor t n
+
+-- | POST /search, search string in "search_query"
+searchUserH :: Pool Connection -> ActionT SproxyError IO ()
+searchUserH pool = do
+  searchStr <- param "search_query"
+
+  matchingEmails <- withDB pool (searchUser searchStr)
+
+  blaze (searchResultsT searchStr matchingEmails)
+
+-- | POST /delete-user, email to delete in "user_email"
+deleteUserH :: Pool Connection -> ActionT SproxyError IO ()
+deleteUserH pool = do
+  userEmail <- param "user_email"
+  (t, n) <- checked pool (removeUser userEmail)
+  outputFor t n
+
+-- | POST /rename-user:
+--  - old email in "old_email"
+--  - new email in "new_email"
+renameUserH :: Pool Connection -> ActionT SproxyError IO ()
+renameUserH pool = do
+  oldEmail   <- param "old_email"
+  newEmail <- param "new_email"
+  (resp, n) <- checked pool (renameUser oldEmail newEmail)
+  outputFor resp n
+
+-- utility functions
+
+outputFor :: Text -> Int64 -> ActionT SproxyError IO ()
+outputFor t 0    = status badRequest400 >> text ("no: " <> t)
+outputFor t (-1) = status badRequest400 >> text ("error: " <> t)
+outputFor t _    = text t
+
+checked :: Pool Connection
+        -> (Connection -> IO Int64) -- request
+        -> ActionT SproxyError IO (Text, Int64)
+checked pool req =
+    withDB pool req'
+
+    where req' c = flip catch (\(e :: SomeException) -> return (Text.pack (show e), -1))
+                              ( ("",) <$> req c )
+
diff --git a/src/Config.hs b/src/Config.hs
deleted file mode 100644
--- a/src/Config.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
-
-module Config where
-
-import Control.Exception
-import Data.ByteString (ByteString)
-import Data.Configurator as C
-import HFlags
-import System.Directory
-import System.FilePath
-
-import Paths_sproxy_web
-
-defineFlag "c:config" ("sproxy-web.config" :: String) "config file"
-
-data Config = Config {
-    dbConnectionString :: ByteString,
-    port :: Int,
-    staticDir :: FilePath
-  }
-    deriving (Show, Eq)
-
--- | Get the connection string and the port
---   from the config file
-getConfig :: FilePath -> IO Config
-getConfig configFile = do
-    conf    <- C.load [C.Required configFile]
-    Config <$>
-        C.require conf "db_connection_string" <*>
-        C.require conf "port" <*>
-        getStaticDir
-
-getStaticDir :: IO FilePath
-getStaticDir = do
-    currentDir <- getCurrentDirectory
-    staticExists <- doesDirectoryExist (currentDir </> "static")
-    if staticExists then do
-        return currentDir
-    else do
-        cabalDataDir <- getDataDir
-        cabalDataDirExists <- doesDirectoryExist cabalDataDir
-        if cabalDataDirExists
-            then return cabalDataDir
-            else throwIO (ErrorCall "directory for static files not found.")
diff --git a/src/DB.hs b/src/DB.hs
--- a/src/DB.hs
+++ b/src/DB.hs
@@ -1,28 +1,18 @@
-module DB (createDBPool, withDB, DBPool, Connection) where
+module DB (
+  withDB
+) where
 
 import Control.Monad.Trans
 import Data.Pool
 import Database.PostgreSQL.Simple
-import Data.ByteString (ByteString)
 
-type DBPool = Pool Connection
-
--- Initialize our connection pool
-createDBPool :: ByteString -> IO DBPool
-createDBPool connectionString =
-    createPool (connectPostgreSQL connectionString)
-               close
-               1   -- stripe count
-               100 -- amount of secs it stays alive
-               50  -- at most 50 concurrent connections
-
 -- Pick a connection handle from the pool and use it to
 -- execute the given request.
 -- The MonadIO bit lets me avoid caring about where this will run,
 -- although we're always using it in the `ActionT SproxyError IO` monad
 -- but the more general type signature keeps us from doing anything irrelevant
 withDB :: MonadIO m 
-       => DBPool 
+       => Pool Connection
        -> (Connection -> IO a)
        -> m a
 withDB pool act = liftIO $ withResource pool (liftIO . act)
diff --git a/src/Handlers.hs b/src/Handlers.hs
deleted file mode 100644
--- a/src/Handlers.hs
+++ /dev/null
@@ -1,319 +0,0 @@
-{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, TupleSections #-}
-
-module Handlers where
-
-import DB
-import Entities
-import SproxyError
-
-import Control.Exception
-import Control.Monad (when)
-import Data.Int (Int64)
-import Data.Monoid
-import Data.Text.Lazy as Text
-import Network.HTTP.Types.Status
-import Web.Scotty.Trans
-
-import Text.Blaze.Html.Renderer.Text (renderHtml)
-import Text.Blaze.Html5 (Html)
-
-import Views.DomainList (domainListT)
-import Views.DomainPrivileges (domainPrivilegesT)
-import Views.ErrorPage (errorPageT)
-import Views.GroupList (groupListT)
-import Views.Homepage (homepageT)
-import Views.MemberList (memberListT)
-import Views.PrivilegeRules (privilegeRulesT)
-import Views.Search (searchResultsT)
-
-blaze :: Html -> ActionT SproxyError IO ()
-blaze = Web.Scotty.Trans.html . renderHtml
-
-handleEx :: SproxyError -> ActionT SproxyError IO ()
-handleEx = errorPage
-
-errorPage :: SproxyError -> ActionT SproxyError IO ()
-errorPage err = blaze (errorPageT err)
-
-homepage :: DBPool -> ActionT SproxyError IO ()
-homepage _ = blaze homepageT
-
-------------------------------------------
--- handlers related to the group list page
-------------------------------------------
-
--- POST /groups
-jsonUpdateGroup :: DBPool -> ActionT SproxyError IO ()
-jsonUpdateGroup pool = do
-    (operation :: Text) <- param "operation"
-    (t, n) <- case operation of
-             "add" -> do
-                g <- param "group"
-                checked pool (addGroup g)
-
-             "del" -> do
-                g <- param "group"
-                checked pool (removeGroup g)
-
-             "upd" -> do
-                old <- param "old"
-                new <- param "new"
-                checked pool (updateGroup old new)
-
-             _ -> status badRequest400 >> return ("incorrect operation", (-1))
-
-    outputFor t n
-
--- GET /groups
-groupList :: DBPool -> ActionT SproxyError IO ()
-groupList pool = do
-    groups <- Prelude.map Prelude.head `fmap` withDB pool getGroups 
-    blaze (groupListT groups)
-
--- GET /groups.json
-jsonGroupList :: DBPool -> ActionT SproxyError IO ()
-jsonGroupList pool = do
-    groups <- Prelude.map Prelude.head `fmap` withDB pool getGroups
-
-    json groups
-
----------------------------------------------------
---  handlers related to the group members list page
----------------------------------------------------
-
--- GET /group/:group
-memberList :: DBPool -> ActionT SproxyError IO ()
-memberList pool = do
-    groupName <- param "group"
-    members   <- Prelude.map Prelude.head `fmap` withDB pool (getMembersFor groupName)
-    blaze (memberListT members groupName)
-
--- POST /group/:group/members
-jsonPostMembers :: DBPool -> ActionT SproxyError IO ()
-jsonPostMembers pool = do
-    groupName <- param "group"
-
-    (operation :: Text) <- param "operation"
-
-    (t, n) <- case operation of
-        "add" -> do
-            m <- param "member"
-            checked pool (addMemberTo m groupName)
-
-        "del" -> do
-            m <- param "member"
-            checked pool (removeMemberOf m groupName)
-
-        "upd" -> do
-            old <- param "old"
-            new <- param "new"
-            checked pool (updateMemberOf old new groupName)
-
-        _     -> status badRequest400 >> return ("incorrect operation", (-1))
-
-    outputFor t n
-
---------------------------------------
--- handlers related to the domain list
---------------------------------------
-
--- GET /domains
-domainList :: DBPool -> ActionT SproxyError IO ()
-domainList pool = do
-    domains <- Prelude.map Prelude.head `fmap` withDB pool getDomains
-    blaze (domainListT domains)
-
--- POST /domains
-jsonUpdateDomain :: DBPool -> ActionT SproxyError IO ()
-jsonUpdateDomain pool = do
-    (operation :: Text) <- param "operation"
-    (t, n) <- case operation of
-             "add" -> do
-                d <- param "domain"
-                checked pool (addDomain d)
-
-             "del" -> do
-                d <- param "domain"
-                checked pool (removeDomain d)
-
-             "upd" -> do
-                old <- param "old"
-                new <- param "new"
-                checked pool (updateDomain old new)
-
-             _ -> status badRequest400 >> return ("incorrect operation", (-1))
-
-    outputFor t n
-
--------------------------------------------------------------------------
--- handlers related to the list of possible privileges for a given domain
--------------------------------------------------------------------------
-
--- GET /domain/:domain/privileges
-domainPrivileges :: DBPool -> ActionT SproxyError IO ()
-domainPrivileges pool = do
-    domain     <- param "domain"
-    privileges <- Prelude.map Prelude.head `fmap` withDB pool (getDomainPrivileges domain)
-    groups     <- Prelude.map Prelude.head `fmap` withDB pool getGroups
-    groupPrivs <- withDB pool (getGroupPrivsFor domain)
-
-    blaze (domainPrivilegesT domain privileges groups groupPrivs)
-
--- GET /domain/:domain/privileges.json
-jsonDomainPrivileges :: DBPool -> ActionT SproxyError IO ()
-jsonDomainPrivileges pool = do
-    domain     <- param "domain"
-    privileges <- Prelude.map Prelude.head `fmap` withDB pool (getDomainPrivileges domain)
-
-    json privileges
-
--- POST /domain/:domain/group_privileges
-handleGPs :: DBPool -> ActionT SproxyError IO ()
-handleGPs pool = do
-    domain <- param "domain"
-
-    (operation :: Text) <- param "operation"
-
-    (t, n) <- case operation of
-        "add" -> do
-            grp <- param "group"
-            priv <- param "privilege"
-            checked pool (addGPFor domain grp priv)
-
-        "del" -> do
-            grp <- param "group"
-            priv <- param "privilege"
-            checked pool (deleteGPOf domain grp priv)
-
-        _ -> status badRequest400 >> return ("incorrect operation", (-1))
-
-    outputFor t n
-
--- POST /domain/:domain/privileges
-jsonPostDomainPrivileges :: DBPool -> ActionT SproxyError IO ()
-jsonPostDomainPrivileges pool = do
-    domain <- param "domain"
-
-    (operation :: Text) <- param "operation"
-
-    (t, n) <- case operation of
-        "add" -> do
-            p <- param "privilege"
-            checked pool (addPrivilegeToDomain p domain)
-
-        "del" -> do
-            p <- param "privilege"
-            checked pool (removePrivilegeOfDomain p domain)
-
-        "upd" -> do
-            old <- param "old"
-            new <- param "new"
-            checked pool (updatePrivilegeOfDomain old new domain)
-
-        _     -> status badRequest400 >> return ("incorrect operation", (-1))
-
-    outputFor t n
-
--------------------------------------------------------------------------------
--- handlers related to the rules associated to a given privilege on some domain
--------------------------------------------------------------------------------
-
--- GET /domain/:domain/privilege/:privilege/rules
-privilegeRules :: DBPool -> ActionT SproxyError IO ()
-privilegeRules pool = do
-    -- TODO: check that the domain and privilege exist
-    domain    <- param "domain"
-    privilege <- param "privilege"
-    rules     <- withDB pool (getRules domain privilege)
-
-    blaze (privilegeRulesT domain privilege rules)
-
--- POST /domain/:domain/privilege/:privilege/rules
-jsonPostRule :: DBPool -> ActionT SproxyError IO ()
-jsonPostRule pool = do
-    -- TODO: check that the domain and privilege exist
-    domain    <- param "domain"
-    privilege <- param "privilege"
-    operation <- param "operation"
-    case operation :: Text of
-        "add" -> addRule domain privilege
-        "del" -> delRule domain privilege
-        "upd" -> updRule domain privilege
-        _     -> status badRequest400 >> text "bad operation"
-
-    where 
-      addRule domain privilege = do
-          path   <- param "path"
-          method <- param "method"
-
-          (t, n) <- checked pool 
-                            (addRuleToPrivilege domain privilege path method)
-
-          outputFor t n
-
-      delRule domain privilege = do
-          path   <- param "path"
-          method <- param "method"
-
-          (t, n) <- checked pool
-                            (deleteRuleFromPrivilege domain privilege path method)
-
-          outputFor t n
-
-      updRule domain privilege = do
-          what <- param "what"
-          
-          when (what /= "path" && what /= "method") $ text "invalid 'what'"
-
-          let updFunc = if what == "path" then updatePathFor else updateMethodFor
-
-          old <- param ("old" <> what)
-          new <- param ("new" <> what)
-          otherField <- param $ if what == "path" then "method" else "path"
-
-          (t, n) <- checked pool
-                            (updFunc domain privilege new old otherField)
-
-          outputFor t n
-
--- | POST /search, search string in "search_query"
-searchUserH :: DBPool -> ActionT SproxyError IO ()
-searchUserH pool = do
-  searchStr <- param "search_query"
-
-  matchingEmails <- withDB pool (searchUser searchStr)
-
-  blaze (searchResultsT searchStr matchingEmails)
-
--- | POST /delete-user, email to delete in "user_email"
-deleteUserH :: DBPool -> ActionT SproxyError IO ()
-deleteUserH pool = do
-  userEmail <- param "user_email"
-  (t, n) <- checked pool (removeUser userEmail)
-  outputFor t n
-
--- | POST /rename-user:
---  - old email in "old_email"
---  - new email in "new_email"
-renameUserH :: DBPool -> ActionT SproxyError IO ()
-renameUserH pool = do
-  oldEmail   <- param "old_email"
-  newEmail <- param "new_email"
-  (resp, n) <- checked pool (renameUser oldEmail newEmail)
-  outputFor resp n
-
--- utility functions
-
-outputFor :: Text -> Int64 -> ActionT SproxyError IO ()
-outputFor t 0    = status badRequest400 >> text ("no: " <> t)
-outputFor t (-1) = status badRequest400 >> text ("error: " <> t)
-outputFor t _    = text t
-
-checked :: DBPool
-        -> (Connection -> IO Int64) -- request
-        -> ActionT SproxyError IO (Text, Int64)
-checked pool req = 
-    withDB pool req'
-
-    where req' c = flip catch (\(e :: SomeException) -> return (Text.pack (show e), -1))
-                              ( ("",) <$> req c )
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -1,6 +1,55 @@
-module Main where
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
 
-import Run
+module Main (
+  main
+) where
 
+import Data.Maybe (fromJust)
+import Data.Version (showVersion)
+import Paths_sproxy_web (getDataDir, version) -- from cabal
+import System.Environment (getArgs)
+import Text.InterpolatedString.Perl6 (qc)
+import qualified System.Console.Docopt.NoTH as O
+
+import Server (server)
+
+usage :: IO String
+usage = do
+  dataDir <- getDataDir
+  return $
+    "sproxy-web " ++ showVersion version ++
+    " web interface to the sproxy database" ++ [qc|
+
+Usage:
+  sproxy-web [options]
+
+Options:
+
+  -c, --connstr=CONNSTR    PostgreSQL connection string [default: dbname=sproxy]
+  -d, --datadir=DIR        Data directory including static files [default: {dataDir}]
+
+  -s, --socket=SOCK        Listen on this UNIX socket [default: /tmp/sproxy-web.sock]
+  -p, --port=PORT          Instead of UNIX socket, listen on this TCP port (localhost)
+
+  -h, --help               Show this message
+
+|]
+
 main :: IO ()
-main = run
+main = do
+  doco <- O.parseUsageOrExit =<< usage
+  args <- O.parseArgsOrExit doco =<< getArgs
+  if args `O.isPresent` O.longOption "help"
+  then putStrLn $ O.usage doco
+  else do
+    let
+      port = O.getArg args $ O.longOption "port"
+      connstr = fromJust $ O.getArg args $ O.longOption "connstr"
+      socket = fromJust $ O.getArg args $ O.longOption "socket"
+      datadir = fromJust $ O.getArg args $ O.longOption "datadir"
+    let listen = case port of
+          Nothing -> Right socket
+          Just p -> Left $ read p
+    server listen connstr datadir
+
diff --git a/src/Server.hs b/src/Server.hs
new file mode 100644
--- /dev/null
+++ b/src/Server.hs
@@ -0,0 +1,79 @@
+module Server
+(
+  server
+) where
+
+import Control.Exception.Base (throwIO, catch, bracket)
+import Data.Bits ((.|.))
+import Data.ByteString.Char8 (pack)
+import Data.Pool (createPool, destroyAllResources)
+import Network.Socket (socket, bind, listen, close, maxListenQueue,
+                       getSocketName, inet_addr,
+                       Family(AF_UNIX, AF_INET), SocketType(Stream),
+                       Socket, SockAddr(SockAddrUnix, SockAddrInet))
+import Network.Wai.Handler.Warp (Port, defaultSettings, runSettingsSocket)
+import System.IO (hPutStrLn, stderr)
+import System.IO.Error (isDoesNotExistError)
+import System.Posix.Files (removeLink, setFileMode, socketMode,
+                           ownerReadMode, ownerWriteMode, groupReadMode, groupWriteMode)
+import qualified Database.PostgreSQL.Simple as PG
+
+import Application (app)
+
+type Listen = Either Port FilePath
+
+
+server :: Listen -> String -> FilePath -> IO ()
+server socketSpec connectionString staticDir =
+  bracket
+    ( do
+      sock <- createSocket socketSpec
+      pgsql <- createPool
+                (PG.connectPostgreSQL $ pack connectionString)
+                PG.close
+                1 -- stripes
+                60 -- keep alive (seconds)
+                10 -- max connections
+      return (sock, pgsql) )
+    ( \(sock, pgsql) -> do
+      closeSocket sock
+      destroyAllResources pgsql )
+    ( \(sock, pgsql) -> do
+      listen sock maxListenQueue
+      hPutStrLn stderr $ "Static files from `" ++ staticDir ++ "'"
+      runSettingsSocket defaultSettings sock =<< app pgsql staticDir )
+
+
+createSocket :: Listen -> IO Socket
+createSocket (Right path) = do
+  removeIfExists path
+  sock <- socket AF_UNIX Stream 0
+  bind sock $ SockAddrUnix path
+  setFileMode path $ socketMode
+                  .|. ownerWriteMode .|. ownerReadMode
+                  .|. groupWriteMode .|. groupReadMode
+  hPutStrLn stderr $ "Listening on UNIX socket `" ++ path ++ "'"
+  return sock
+createSocket (Left port) = do
+  sock <- socket AF_INET Stream 0
+  addr <- inet_addr "127.0.0.1"
+  bind sock $ SockAddrInet (fromIntegral port) addr
+  hPutStrLn stderr $ "Listening on localhost:" ++ show port
+  return sock
+
+
+closeSocket :: Socket -> IO ()
+closeSocket sock = do
+  name <- getSocketName sock
+  close sock
+  case name of
+    SockAddrUnix path -> removeIfExists path
+    _ -> return ()
+
+
+removeIfExists :: FilePath -> IO ()
+removeIfExists fileName = removeLink fileName `catch` handleExists
+  where handleExists e
+          | isDoesNotExistError e = return ()
+          | otherwise = throwIO e
+
diff --git a/src/Views/Search.hs b/src/Views/Search.hs
new file mode 100644
--- /dev/null
+++ b/src/Views/Search.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Views.Search where
+
+import           Data.Text.Lazy (Text)
+
+import           Text.Blaze.Html5
+import qualified Text.Blaze.Html5 as H
+import qualified Text.Blaze.Html5.Attributes as A
+import qualified Data.Text.Lazy as T
+import           Views.Common
+
+searchResultsT :: Text -> [(Text, [Text])] -> Html
+searchResultsT searchStr matchingMails = 
+    pageT ("Search results") $ do
+        H.p ! A.class_ "lead text-center" $ do
+            "Emails matching "
+            H.i $ "\"" >> toHtml searchStr >> "\""
+
+        H.p ! A.class_ "text-center" $ H.i $
+          "(Click on an email to \"rename\" the user in the sproxy database.)"
+
+        H.div ! A.class_ "text-center" ! A.style "margin: 10px auto;" $ 
+          H.table ! A.id "searchtable" ! A.class_ "table table-condensed" $ do
+            H.thead $ do
+              H.tr $ do
+                H.th ! A.width "30%" ! A.class_ "text-center" $ 
+                  "Email address"
+                H.th ! A.width "40%" ! A.class_ "text-center" $
+                  "Groups"
+                H.th ! A.width "30%" $ mempty
+              H.tbody $
+                emailsHtml
+
+        H.div ! A.class_ "alert alert-success" ! A.id "updatesuccess" $
+            "Successfully updated."
+
+        H.div ! A.class_ "alert alert-error" ! A.id "updateexists" $
+           "This is already used."
+
+        H.div ! A.class_ "alert alert-error" ! A.id "updateerror" $
+            "An error occured when trying to update the email in the DB."
+    
+        H.script ! A.type_ "text/javascript"
+                    ! A.src "/static/js/delete-user.js" $ mempty
+
+        H.script ! A.type_ "text/javascript"
+                    ! A.src "/static/js/rename-user.js" $ mempty
+
+  where emailsHtml = mapM_ emailToHtml matchingMails
+        emailToHtml (mail, groups) = 
+          tr $ do
+            td ! A.class_ "edit email-edit" $ toHtml mail
+            td $ i $ toHtml (T.intercalate ", " groups)
+            td $ a ! A.class_ "delete-btn btn btn-danger btn-xs" $ "Delete from all groups"
