diff --git a/app/migration/Main.hs b/app/migration/Main.hs
--- a/app/migration/Main.hs
+++ b/app/migration/Main.hs
@@ -51,8 +51,12 @@
                , privateDefault :: Maybe Bool
                , archiveDefault :: Maybe Bool
                , privacyLock :: Maybe Bool }
+  | CreateApiKey { conn :: Text
+                 , userName :: Text }
   | DeleteUser { conn :: Text
                , userName :: Text }
+  | DeleteApiKey { conn :: Text
+                 , userName :: Text }
   | ImportBookmarks { conn :: Text
                     , userName :: Text
                     , bookmarkFile :: FilePath }
@@ -92,12 +96,32 @@
           (UniqueUserName userName)
           (User userName hash' Nothing False False False)
           [ UserPasswordHash P.=. hash'
-          , UserApiToken P.=. Nothing
           , UserPrivateDefault P.=. fromMaybe False privateDefault
           , UserArchiveDefault P.=. fromMaybe False archiveDefault
           , UserPrivacyLock P.=. fromMaybe False privacyLock
           ]
         pure () :: DB ()
+
+    CreateApiKey {..} ->
+      P.runSqlite conn $ do
+        apiKey@(ApiKey plainKey) <- liftIO generateApiKey
+        muser <- P.getBy (UniqueUserName userName)
+        case muser of
+          Nothing -> liftIO (print (userName ++ " not found"))
+          Just (P.Entity uid _) -> do
+            -- API key is only displayed once after creation,
+            -- since it is stored in hashed form.
+            let hashedKey = hashApiKey apiKey
+            P.update uid  [ UserApiToken P.=. Just hashedKey ]
+            liftIO $ print plainKey
+
+    DeleteApiKey {..} ->
+      P.runSqlite conn $ do
+        muser <- P.getBy (UniqueUserName userName)
+        case muser of
+          Nothing -> liftIO (print (userName ++ " not found"))
+          Just (P.Entity uid _) -> do
+            P.update uid  [ UserApiToken P.=. Nothing ]
 
     DeleteUser {..} ->
       P.runSqlite conn $ do
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,6 @@
+__v0.0.11__
+add api key auth. 
+add CreateApiKey/DeleteApiKey commands to executable 'migration' 
 
 __v0.0.10__
 update purescript&package versions
diff --git a/espial.cabal b/espial.cabal
--- a/espial.cabal
+++ b/espial.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           espial
-version:        0.0.10
+version:        0.0.11
 synopsis:       Espial is an open-source, web-based bookmarking server.
 description:    .
                 Espial is an open-source, web-based bookmarking server.
@@ -163,6 +163,7 @@
       aeson >=1.4
     , attoparsec
     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
+    , base64
     , bcrypt >=0.0.8
     , blaze-html >=0.9 && <1.0
     , bytestring >=0.9 && <0.11
@@ -173,6 +174,7 @@
     , conduit >=1.0 && <2.0
     , connection
     , containers
+    , cryptohash-sha256
     , data-default
     , directory >=1.1 && <1.4
     , entropy
@@ -273,6 +275,7 @@
       aeson >=1.4
     , attoparsec
     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
+    , base64
     , bcrypt >=0.0.8
     , blaze-html >=0.9 && <1.0
     , bytestring >=0.9 && <0.11
@@ -283,6 +286,7 @@
     , conduit >=1.0 && <2.0
     , connection
     , containers
+    , cryptohash-sha256
     , data-default
     , directory >=1.1 && <1.4
     , entropy
@@ -380,6 +384,7 @@
       aeson >=1.4
     , attoparsec
     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
+    , base64
     , bcrypt >=0.0.8
     , blaze-html >=0.9 && <1.0
     , bytestring >=0.9 && <0.11
@@ -390,6 +395,7 @@
     , conduit >=1.0 && <2.0
     , connection
     , containers
+    , cryptohash-sha256
     , data-default
     , directory >=1.1 && <1.4
     , entropy
@@ -493,6 +499,7 @@
       aeson >=1.4
     , attoparsec
     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
+    , base64
     , bcrypt >=0.0.8
     , blaze-html >=0.9 && <1.0
     , bytestring >=0.9 && <0.11
@@ -503,6 +510,7 @@
     , conduit >=1.0 && <2.0
     , connection
     , containers
+    , cryptohash-sha256
     , data-default
     , directory >=1.1 && <1.4
     , entropy
diff --git a/src/Foundation.hs b/src/Foundation.hs
--- a/src/Foundation.hs
+++ b/src/Foundation.hs
@@ -17,6 +17,7 @@
 import qualified Data.CaseInsensitive as CI
 import qualified Data.Text.Encoding as TE
 import qualified Yesod.Core.Unsafe as Unsafe
+import qualified Network.Wai as Wai
 
 data App = App
     { appSettings    :: AppSettings
@@ -67,8 +68,20 @@
             else id
 
     yesodMiddleware :: HandlerFor App res -> HandlerFor App res
-    yesodMiddleware = customMiddleware . defaultYesodMiddleware . defaultCsrfMiddleware
+    yesodMiddleware = customMiddleware . defaultYesodMiddleware . customCsrfMiddleware
       where
+        customCsrfMiddleware handler = do
+          maybeRoute <- getCurrentRoute
+          dontCheckCsrf <- case maybeRoute of
+            -- `maybeAuthId` checks for the validity of the Authorization
+            -- header anyway, but it is still a good idea to limit this
+            -- flexibility to designated routes.
+            -- For the time being, `AddR` is the only route that accepts an
+            -- authentication token.
+            Just AddR -> isJust <$> lookupHeader "Authorization"
+            _ -> pure False
+          (if dontCheckCsrf then id else defaultCsrfMiddleware) handler
+
         customMiddleware handler = do
           addHeader "X-Frame-Options" "DENY"
           yesod <- getYesod
@@ -167,6 +180,24 @@
   onLogout =
     deleteSession userNameKey
   redirectToReferer = const True
+  maybeAuthId = do
+    req <- waiRequest
+    let mAuthHeader = lookup "Authorization" (Wai.requestHeaders req)
+        extractKey = stripPrefix "ApiKey " . TE.decodeUtf8
+    case mAuthHeader of
+      Just authHeader ->
+        case extractKey authHeader of
+          Just apiKey -> do
+            user <- liftHandler $ runDB $ getApiKeyUser (ApiKey apiKey)
+            let userId = entityKey <$> user
+            pure userId
+          -- Since we disable CSRF middleware in the presence of Authorization
+          -- header, we need to explicitly check for the validity of the header
+          -- content. Otherwise, a dummy Authorization header with garbage input
+          -- could be provided to circumvent CSRF token requirement, making the app
+          -- vulnerable to CSRF attacks.
+          Nothing -> pure Nothing
+      _ -> defaultMaybeAuthId
 
 instance YesodAuthPersist App
 
diff --git a/src/Model.hs b/src/Model.hs
--- a/src/Model.hs
+++ b/src/Model.hs
@@ -34,7 +34,7 @@
   Id Int64
   name Text
   passwordHash BCrypt
-  apiToken Text Maybe
+  apiToken HashedApiKey Maybe
   privateDefault Bool
   archiveDefault Bool
   privacyLock Bool
@@ -158,6 +158,10 @@
 getUserByName :: UserNameP -> DB (Maybe (Entity User))
 getUserByName (UserNameP uname) =
   selectFirst [UserName CP.==. uname] []
+
+getApiKeyUser :: ApiKey -> DB (Maybe (Entity User))
+getApiKeyUser apiKey =
+  selectFirst [UserApiToken CP.==. Just (hashApiKey apiKey)] []
 
 -- returns a list of pair of bookmark with tags merged into a string
 bookmarksTagsQuery
diff --git a/src/ModelCustom.hs b/src/ModelCustom.hs
--- a/src/ModelCustom.hs
+++ b/src/ModelCustom.hs
@@ -12,6 +12,8 @@
 import System.Entropy (getEntropy)
 import qualified Data.ByteString.Builder as BB
 import qualified Data.ByteString.Lazy as LBS
+import qualified Data.ByteString.Base64.URL as Base64Url
+import qualified Crypto.Hash.SHA256 as SHA256
 
 mkSlug :: Int -> IO T.Text
 mkSlug size =
@@ -58,3 +60,18 @@
 validatePasswordHash :: BCrypt -> T.Text -> Bool
 validatePasswordHash hash' pass = do
   validatePassword (TE.encodeUtf8 (unBCrypt hash')) (TE.encodeUtf8 pass)
+
+newtype ApiKey = ApiKey { unApiKey :: T.Text }
+
+newtype HashedApiKey
+  = HashedApiKey T.Text
+  deriving stock (Eq, Ord, Show)
+  deriving newtype (PersistField, PersistFieldSql, A.FromJSON, A.ToJSON)
+
+generateApiKey :: IO ApiKey
+generateApiKey = do
+  bytes <- getEntropy 32
+  pure $ ApiKey $ Base64Url.encodeBase64 bytes
+
+hashApiKey :: ApiKey -> HashedApiKey
+hashApiKey = HashedApiKey . TE.decodeUtf8 . Base64Url.encodeBase64' . SHA256.hash . TE.encodeUtf8 . unApiKey
