diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,14 @@
 For differences with the original Sproxy scroll down.
 
+1.91.0
+======
+
+  * In addition to good old PostgreSQL data source, made it possible
+    to import permission data from a YAML file. This means that Sproxy2
+    can work without any PostgreSQL database, just using file-only configuration.
+    Useful for development or trivial deployments. Added new `datafile` option
+    in configuration file.
+
 
 1.90.2
 ======
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -55,8 +55,27 @@
 
 Permissions system
 ------------------
+Permissions are stored in internal SQLite3 database and imported
+from data sources, which can be a PostgreSQL database or a file.  See
+[sproxy.sql](./sproxy.sql) and [datafile.yml.example](./datafile.yml.example)
+for details.
 
-Permissions are stored in a PostgreSQL database. See sproxy.sql for details.
+Do note that Sproxy2 fetches only `group_member`, `group_privilege`
+and `privilege_rule` tables, because only these tables are used for
+authorization. The other tables in PostgreSQL schema serve for data
+integrity. Data integrity of the data file is not verfied, though import
+may fail due to primary key restrictions.
+
+Only one data source can be used. The data in internal database, if any,
+is fully overwritten by the data from a data source. If no data source is
+specified, the data in internal database remains unchanged, even between
+restarts.  Broken data source is _not_ fatal. Sproxy will keep using existing
+internal database, or create a new empty one if missed. Broken data source
+means inability to connect to PostgreSQL database, missed datafile, etc.
+
+The data from a PostgreSQL database are periodically fetched into the internal
+database, while the data file is read once at startup.
+
 Here are the main concepts:
 
 - A `group` is identified by a name. Every group has
@@ -72,14 +91,10 @@
   surprising, please see the following example:
 
 
-Do note that Sproxy2 fetches only `group_member`, `group_privilege` and `privilege_rule`
-tables, because only these tables are used for authorization. The other tables
-serve for data integrity.
-
 Keep in mind that:
 
-- Domains are converted into lower case (coming from PostgreSQL or HTTP requests).
-- Emails are converted into lower case (coming from PostgreSQL or OAuth2 providers).
+- Domains are converted into lower case (coming from a data source or HTTP requests).
+- Emails are converted into lower case (coming from a data source or OAuth2 providers).
 - Groups are case-sensitive and treated as is.
 - HTTP methods are *case-sensitive*.
 - HTTP query parameters are ignored when matching a request against the rules.
@@ -177,13 +192,6 @@
 All required Haskell libraries are listed in [sproxy2.cabal](sproxy2.cabal).
 Use [cabal-install](http://www.haskell.org/haskellwiki/Cabal-Install)
 to fetch and build all pre-requisites automatically.
-
-
-Installation
-============
-    $ git clone https://github.com/zalora/sproxy2.git
-    $ cd sproxy2
-    $ cabal install
 
 
 Configuration
diff --git a/datafile.yml.example b/datafile.yml.example
new file mode 100644
--- /dev/null
+++ b/datafile.yml.example
@@ -0,0 +1,34 @@
+--- # Data file. Don't remove this line. This is YAML: https://en.wikipedia.org/wiki/YAML
+
+group_member:
+  - group: "devops"
+    email: "%"
+
+  - group: "foo"
+    email: "%"
+
+
+group_privilege:
+  - group: "foo"
+    domain: "example.com"
+    privilege: "full"
+
+  - group: "devops"
+    domain: "example.com"
+    privilege: "full"
+
+
+privilege_rule:
+  - domain: "example.com"
+    privilege: "full"
+    path: "%"
+    method: "GET"
+
+  - domain: "example.com"
+    privilege: "full"
+    path: "%"
+    method: "POST"
+
+
+... # End of data file. Don't remove this line. This is YAML: https://en.wikipedia.org/wiki/YAML
+
diff --git a/sproxy.yml.example b/sproxy.yml.example
--- a/sproxy.yml.example
+++ b/sproxy.yml.example
@@ -1,8 +1,13 @@
 --- # Sproxy configuration. Don't remove this line. This is YAML: https://en.wikipedia.org/wiki/YAML
 
+# Logging level: debug, info, warn, error.
+# Optional. Default is debug.
+#
+# log_level: debug
+
 # The port Sproxy listens on (HTTPS).
 # Optional. Default is 443.
-# 
+#
 # listen: 443
 
 # Listen on port 80 and redirect HTTP requests to HTTPS.
@@ -24,10 +29,32 @@
 #
 # home: "."
 
+
+# File with SSL certificate. Required.
+# It can be a bundle with the server certificate coming first:
+# cat me-cert.pem CA-cert.pem > cert.pem
+# Once again: most wanted certs go first ;-)
+# Or you can opt in using of `ssl_cert_chain`
+ssl_cert: /path/cert.pem
+
+# File with SSL key (secret!). Required.
+ssl_key: /path/key.pem
+
+# Chain SSL certificate files.
+# Optional. Default is an empty list
+# Example:
+# ssl_cert_chain:
+#   - /path/foo.pem
+#   - /path/bar.pem
+#
+# ssl_cert_chain: []
+
+
 # PostgreSQL database connection string.
 # Optional. If specified, sproxy will periodically pull the data from this
 # database into internal SQLite3 database. Define password in a file
 # referenced by the PGPASSFILE environment variable. Or use the "pgpassfile" option.
+# Cannot be used with the "datafile" option.
 # Example:
 # database: "user=sproxy-readonly dbname=sproxy port=6001"
 #
@@ -40,11 +67,17 @@
 #
 # pgpassfile:
 
-# Logging level: debug, info, warn, error.
-# Optional. Default is debug.
-# 
-# log_level: debug
 
+# YAML file used to fill internal SQLite3 database.
+# Optional. If specified, Sproxy will import it on start overwriting
+# and existing data in the internal database.
+# Useful for development or some simple deployments.
+# Cannot be used with the "database" option.
+# For example see the datafile.yml.example
+#
+# datafile: /path/data.yml
+
+
 # A file with arbitrary content used to sign sproxy cookie and other things (secret!).
 # Optional. If not specified, a random key is generated on startup, and
 # as a consequence, restaring sproxy will invalidate existing user sessions.
@@ -53,25 +86,6 @@
 # This should not be very large, a few random bytes are fine.
 # 
 # key: /run/keys/sproxy.secret
-
-# File with SSL certificate. Required.
-# It can be a bundle with the server certificate coming first:
-# cat me-cert.pem CA-cert.pem > cert.pem
-# Once again: most wanted certs go first ;-)
-# Or you can opt in using of `ssl_cert_chain`
-ssl_cert: /path/cert.pem
-
-# File with SSL key (secret!). Required.
-ssl_key: /path/key.pem
-
-# Chain SSL certificate files.
-# Optional. Default is an empty list
-# Example:
-# ssl_cert_chain:
-#   - /path/foo.pem
-#   - /path/bar.pem
-# 
-# ssl_cert_chain: []
 
 
 # Credentials for supported OAuth2 providers.
diff --git a/sproxy2.cabal b/sproxy2.cabal
--- a/sproxy2.cabal
+++ b/sproxy2.cabal
@@ -1,5 +1,5 @@
 name: sproxy2
-version: 1.90.2
+version: 1.91.0
 synopsis: Secure HTTP proxy for authenticating users via OAuth2
 description:
   Sproxy is secure by default. No requests makes it to the backend
@@ -13,8 +13,13 @@
 copyright: 2016, Zalora South East Asia Pte. Ltd
 category: Databases, Web
 build-type: Simple
-extra-source-files: README.md ChangeLog.md sproxy.yml.example sproxy.sql
 cabal-version: >= 1.20
+extra-source-files:
+  ChangeLog.md
+  README.md
+  datafile.yml.example
+  sproxy.sql
+  sproxy.yml.example
 
 source-repository head
   type: git
@@ -37,6 +42,7 @@
       Sproxy.Logging
       Sproxy.Server
       Sproxy.Server.DB
+      Sproxy.Server.DB.DataFile
     build-depends:
         base >= 4.8 && < 50
       , aeson
diff --git a/src/Sproxy/Config.hs b/src/Sproxy/Config.hs
--- a/src/Sproxy/Config.hs
+++ b/src/Sproxy/Config.hs
@@ -27,6 +27,7 @@
 , cfListen80     :: Maybe Bool
 , cfBackends     :: [BackendConf]
 , cfOAuth2       :: HashMap Text OAuth2Conf
+, cfDataFile     :: Maybe FilePath
 , cfDatabase     :: Maybe String
 , cfPgPassFile   :: Maybe FilePath
 , cfHTTP2        :: Bool
@@ -45,6 +46,7 @@
     <*> m .:? "listen80"
     <*> m .:  "backends"
     <*> m .:  "oauth2"
+    <*> m .:? "datafile"
     <*> m .:? "database"
     <*> m .:? "pgpassfile"
     <*> m .:? "http2"          .!= True
diff --git a/src/Sproxy/Server.hs b/src/Sproxy/Server.hs
--- a/src/Sproxy/Server.hs
+++ b/src/Sproxy/Server.hs
@@ -67,13 +67,8 @@
     setGroupID $ userGroupID u
     setUserID $ userID u
 
-  case cfPgPassFile cf of
-    Nothing -> return ()
-    Just f  -> do
-      Log.info $ "pgpassfile: " ++ show f
-      setEnv "PGPASSFILE" f
-
-  db <- DB.start (cfHome cf) (newDataSource cf)
+  ds <- newDataSource cf
+  db <- DB.start (cfHome cf) ds
 
   key <- maybe
            (Log.info "using new random key" >> getEntropy 32)
@@ -112,11 +107,23 @@
     (sproxy key db oauth2clients backends)
 
 
-newDataSource :: ConfigFile -> Maybe DB.DataSource
+newDataSource :: ConfigFile -> IO (Maybe DB.DataSource)
 newDataSource cf =
-  case cfDatabase cf of
-    Just str -> Just $ DB.PostgreSQL str
-    Nothing -> Nothing
+  case (cfDataFile cf, cfDatabase cf) of
+    (Nothing, Just str) -> do
+      case cfPgPassFile cf of
+        Nothing -> return ()
+        Just f  -> do
+          Log.info $ "pgpassfile: " ++ show f
+          setEnv "PGPASSFILE" f
+      return . Just $ DB.PostgreSQL str
+
+    (Just f, Nothing)   -> return . Just $ DB.File f
+
+    (Nothing, Nothing)  -> return Nothing
+    _ -> do
+      Log.error "only one data source can be used"
+      exitFailure
 
 
 newOAuth2Client :: (Text, OAuth2Conf) -> IO (Text, OAuth2Client)
diff --git a/src/Sproxy/Server/DB.hs b/src/Sproxy/Server/DB.hs
--- a/src/Sproxy/Server/DB.hs
+++ b/src/Sproxy/Server/DB.hs
@@ -14,17 +14,20 @@
 import Data.ByteString.Char8 (pack)
 import Data.Pool (Pool, createPool, withResource)
 import Data.Text (Text, toLower, unpack)
+import Data.Yaml (decodeFileEither)
 import Database.SQLite.Simple (NamedParam((:=)))
 import Text.InterpolatedString.Perl6 (q, qc)
 import qualified Database.PostgreSQL.Simple as PG
 import qualified Database.SQLite.Simple as SQLite
 
+import Sproxy.Server.DB.DataFile ( DataFile(..), GroupMember(..),
+  GroupPrivilege(..), PrivilegeRule(..) )
 import qualified Sproxy.Logging as Log
 
 
 type Database = Pool SQLite.Connection
 
-data DataSource = PostgreSQL String -- | File FilePath
+data DataSource = PostgreSQL String | File FilePath
 
 {- TODO:
  - Hash remote tables and update the local only when the remote change
@@ -87,9 +90,26 @@
     createGroupPrivilege c
     createPrivilegeRule c
 
--- XXX We keep only required minimum of the data, without any integrity check.
--- XXX Integrity check should be done somewhere else, e. g. in the master PostgreSQL database,
--- XXX or during importing the config file.
+populate db (Just (File f)) = do
+  Log.info $ "db: reading " ++ show f
+  r <- decodeFileEither f
+  case r of
+    Left e   -> Log.error $ f ++ ": " ++ show e
+    Right df ->
+      withResource db $ \c -> SQLite.withTransaction c $ do
+        refreshGroupMembers c $ \st ->
+          mapM_ (\gm -> submit st (gmGroup gm, toLower $ gmEmail gm)
+                ) (groupMember df)
+
+        refreshGroupPrivileges c $ \st ->
+          mapM_ (\gp -> submit st (gpGroup gp, toLower $ gpDomain gp, gpPrivilege gp)
+                ) (groupPrivilege df)
+
+        refreshPrivilegeRule c $ \st ->
+          mapM_ (\pr -> submit st (toLower $ prDomain pr, prPrivilege pr, prPath pr, prMethod pr)
+                ) (privilegeRule df)
+
+
 populate db (Just (PostgreSQL connstr)) =
   void . forkIO . forever . flip finally (7 `minutes` threadDelay)
   . logException $ do
@@ -99,39 +119,34 @@
         \pg -> PG.withTransaction pg $ do
 
           Log.info "db: syncing group_member"
-          dropGroupMember c
-          createGroupMember c
-          PG.forEach_ pg
-            [q|SELECT "group", lower(email) FROM group_member|] $ \r ->
-              SQLite.execute c
-                [q|INSERT INTO group_member("group", email) VALUES (?, ?)|]
-                  (r :: (Text, Text))
+          refreshGroupMembers c $ \st ->
+            PG.forEach_ pg
+              [q|SELECT "group", lower(email) FROM group_member|] $ \r ->
+                submit st (r :: (Text, Text))
           count c "group_member"
 
           Log.info "db: syncing group_privilege"
-          dropGroupPrivilege c
-          createGroupPrivilege c
-          PG.forEach_ pg
-            [q|SELECT "group", lower(domain), privilege FROM group_privilege|] $ \r ->
-              SQLite.execute c
-                [q|INSERT INTO group_privilege("group", domain, privilege) VALUES (?, ?, ?)|]
-                  (r :: (Text, Text, Text))
+          refreshGroupPrivileges c $ \st ->
+            PG.forEach_ pg
+              [q|SELECT "group", lower(domain), privilege FROM group_privilege|] $ \r ->
+                submit st (r :: (Text, Text, Text))
           count c "group_privilege"
 
           Log.info "db: syncing privilege_rule"
-          dropPrivilegeRule c
-          createPrivilegeRule c
-          PG.forEach_ pg
-            [q|SELECT lower(domain), privilege, path, method FROM privilege_rule|] $ \r ->
-              SQLite.execute c
-                [q|INSERT INTO privilege_rule(domain, privilege, path, method) VALUES (?, ?, ?, ?)|]
-                  (r :: (Text, Text, Text, Text))
+          refreshPrivilegeRule c $ \st ->
+            PG.forEach_ pg
+              [q|SELECT lower(domain), privilege, path, method FROM privilege_rule|] $ \r ->
+                submit st (r :: (Text, Text, Text, Text))
           count c "privilege_rule"
 
 
-dropGroupMember :: SQLite.Connection -> IO ()
-dropGroupMember c = SQLite.execute_ c "DROP TABLE IF EXISTS group_member"
+-- FIXME short-cut for https://github.com/nurpax/sqlite-simple/issues/50
+-- FIXME nextRow is the only way to execute a prepared statement
+-- FIXME with bound parameters, but we don't expect any results.
+submit :: SQLite.ToRow values => SQLite.Statement -> values -> IO ()
+submit st v = SQLite.withBind st v $ void (SQLite.nextRow st :: IO (Maybe [Int]))
 
+
 createGroupMember :: SQLite.Connection -> IO ()
 createGroupMember c = SQLite.execute_ c [q|
   CREATE TABLE IF NOT EXISTS group_member (
@@ -141,9 +156,14 @@
   )
 |]
 
+refreshGroupMembers :: SQLite.Connection -> (SQLite.Statement -> IO ()) -> IO ()
+refreshGroupMembers c a = do
+  SQLite.execute_ c "DROP TABLE IF EXISTS group_member"
+  createGroupMember c
+  SQLite.withStatement c
+    [q|INSERT INTO group_member("group", email) VALUES (?, ?)|]
+    a
 
-dropGroupPrivilege :: SQLite.Connection -> IO ()
-dropGroupPrivilege c = SQLite.execute_ c "DROP TABLE IF EXISTS group_privilege"
 
 createGroupPrivilege :: SQLite.Connection -> IO ()
 createGroupPrivilege c = SQLite.execute_ c [q|
@@ -155,9 +175,14 @@
   )
 |]
 
+refreshGroupPrivileges :: SQLite.Connection -> (SQLite.Statement -> IO ()) -> IO ()
+refreshGroupPrivileges c a = do
+  SQLite.execute_ c "DROP TABLE IF EXISTS group_privilege"
+  createGroupPrivilege c
+  SQLite.withStatement c
+    [q|INSERT INTO group_privilege("group", domain, privilege) VALUES (?, ?, ?)|]
+    a
 
-dropPrivilegeRule :: SQLite.Connection -> IO ()
-dropPrivilegeRule c = SQLite.execute_ c "DROP TABLE IF EXISTS privilege_rule"
 
 createPrivilegeRule :: SQLite.Connection -> IO ()
 createPrivilegeRule c = SQLite.execute_ c [q|
@@ -169,6 +194,14 @@
     PRIMARY KEY (domain, path, method)
   )
 |]
+
+refreshPrivilegeRule :: SQLite.Connection -> (SQLite.Statement -> IO ()) -> IO ()
+refreshPrivilegeRule c a = do
+  SQLite.execute_ c "DROP TABLE IF EXISTS privilege_rule"
+  createPrivilegeRule c
+  SQLite.withStatement c
+    [q|INSERT INTO privilege_rule(domain, privilege, path, method) VALUES (?, ?, ?, ?)|]
+    a
 
 
 count :: SQLite.Connection -> String -> IO ()
diff --git a/src/Sproxy/Server/DB/DataFile.hs b/src/Sproxy/Server/DB/DataFile.hs
new file mode 100644
--- /dev/null
+++ b/src/Sproxy/Server/DB/DataFile.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Sproxy.Server.DB.DataFile (
+  DataFile(..)
+, GroupMember(..)
+, GroupPrivilege(..)
+, PrivilegeRule(..)
+) where
+
+import Control.Applicative (empty)
+import Data.Aeson (FromJSON, parseJSON)
+import Data.Text (Text)
+import Data.Yaml (Value(Object), (.:))
+
+
+data DataFile = DataFile {
+  groupMember    :: [GroupMember]
+, groupPrivilege :: [GroupPrivilege]
+, privilegeRule  :: [PrivilegeRule]
+} deriving (Show)
+
+instance FromJSON DataFile where
+  parseJSON (Object m) = DataFile <$>
+        m .: "group_member"
+    <*> m .: "group_privilege"
+    <*> m .: "privilege_rule"
+  parseJSON _ = empty
+
+
+data GroupMember = GroupMember {
+  gmGroup :: Text
+, gmEmail :: Text
+} deriving (Show)
+
+instance FromJSON GroupMember where
+  parseJSON (Object m) = GroupMember <$>
+        m .: "group"
+    <*> m .: "email"
+  parseJSON _ = empty
+
+
+data GroupPrivilege = GroupPrivilege {
+  gpGroup     :: Text
+, gpDomain    :: Text
+, gpPrivilege :: Text
+} deriving (Show)
+
+instance FromJSON GroupPrivilege where
+  parseJSON (Object m) = GroupPrivilege <$>
+        m .: "group"
+    <*> m .: "domain"
+    <*> m .: "privilege"
+  parseJSON _ = empty
+
+
+data PrivilegeRule = PrivilegeRule {
+  prDomain    :: Text
+, prPrivilege :: Text
+, prPath      :: Text
+, prMethod    :: Text
+} deriving (Show)
+
+instance FromJSON PrivilegeRule where
+  parseJSON (Object m) = PrivilegeRule <$>
+        m .: "domain"
+    <*> m .: "privilege"
+    <*> m .: "path"
+    <*> m .: "method"
+  parseJSON _ = empty
+
