diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2024 goosedb
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# Minion
+
+Minion is Haskell library for developing web applications. It stands between [Scotty](https://hackage.haskell.org/package/scotty) and [Servant](https://hackage.haskell.org/package/servant-server)  
+
+|                  | Scotty | Minion | Servant |
+| ---------------- | ------ | ------ | ------- |
+| As simple as ABC | Yes    | No     | No      |
+| At term level    | Yes    | Yes    | No      |
+| Typesafe         | No     | Yes    | Yes     |
+| Introspectable   | No     | Yes    | Yes     |
+| Generated client | No     | No     | Yes     |
+
+  
+Since Minion defines servers at the term level, it's easier to start and without excess verbosity.
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
+module Main where
+
+import Web.Minion
+import Network.Wai.Handler.Warp qualified as Warp
+
+main :: IO ()
+main = Warp.run 9001 app
+
+app :: ApplicationM IO
+app = serve api 
+
+api :: Router Void IO
+api = "api" /> 
+    [ "about" /> handlePlainText @String GET (pure "Hello-World Minion server")
+    , "hello" /> capture @String "name" 
+              .> handlePlainText @String GET (\name -> pure $ "Hello, " <> name <> "!")
+    ]
+```
+
+Documentation and examples can be found on [Hackage](https://hackage.haskell.org/package/minion)  
+
+Minion ecosystem also contains following libraries:
+* [minion-conduit](https://hackage.haskell.org/package/minion-conduit) 
+* [minion-htmx](https://hackage.haskell.org/package/minion-htmx) 
+* [minion-jwt](https://hackage.haskell.org/package/minion-jwt) 
+* [minion-wai-extra](https://hackage.haskell.org/package/minion-wai-extra) 
+* [minion-openapi3](https://hackage.haskell.org/package/minion-openapi3) 
diff --git a/app/Jwt.hs b/app/Jwt.hs
new file mode 100644
--- /dev/null
+++ b/app/Jwt.hs
@@ -0,0 +1,9 @@
+module Main where
+
+import Network.Wai.Handler.Warp qualified as Warp
+import Web.Minion.Examples.Jwt qualified
+
+main :: IO ()
+main = do
+  app <- Web.Minion.Examples.Jwt.app
+  Warp.run 9001 app
diff --git a/minion-jwt.cabal b/minion-jwt.cabal
new file mode 100644
--- /dev/null
+++ b/minion-jwt.cabal
@@ -0,0 +1,83 @@
+cabal-version:      3.0
+name:               minion-jwt
+version:            0.1.0.0
+license:            MIT
+license-file:       LICENSE
+author:             goosedb
+maintainer:         goosedb@yandex.ru
+synopsis:           Minion JWT support
+category:           Web
+build-type:         Simple
+extra-source-files: README.md
+
+common common
+  ghc-options:        -Wall
+  default-extensions:
+    AllowAmbiguousTypes
+    BlockArguments
+    ConstraintKinds
+    DataKinds
+    DefaultSignatures
+    DeriveAnyClass
+    DeriveGeneric
+    DerivingStrategies
+    DerivingStrategies
+    DuplicateRecordFields
+    DuplicateRecordFields
+    FlexibleContexts
+    FlexibleInstances
+    FunctionalDependencies
+    GADTs
+    GeneralizedNewtypeDeriving
+    ImportQualifiedPost
+    LambdaCase
+    MultiParamTypeClasses
+    NamedFieldPuns
+    OverloadedLists
+    OverloadedRecordDot
+    OverloadedRecordDot
+    OverloadedStrings
+    PolyKinds
+    RankNTypes
+    RecordWildCards
+    RoleAnnotations
+    ScopedTypeVariables
+    TypeApplications
+    TypeFamilies
+    TypeOperators
+    UndecidableInstances
+    ViewPatterns
+
+library
+  import:           common
+  exposed-modules:
+    Web.Minion.Auth.Jwt
+    Web.Minion.Examples.Jwt
+
+  build-depends:
+    , aeson
+    , base >= 4.16 && < 5
+    , bytestring
+    , http-types
+    , jose
+    , minion
+    , mtl
+    , text
+    , time
+    , transformers
+    , wai
+
+  hs-source-dirs:   src
+  default-language: Haskell2010
+
+executable minion-jwt-example
+  import:           common
+  main-is:          Jwt.hs
+  build-depends:
+    , base
+    , minion-jwt
+    , warp
+
+  hs-source-dirs:   app
+  default-language: Haskell2010
+  ghc-options:      -threaded
diff --git a/src/Web/Minion/Auth/Jwt.hs b/src/Web/Minion/Auth/Jwt.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Minion/Auth/Jwt.hs
@@ -0,0 +1,74 @@
+module Web.Minion.Auth.Jwt where
+
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Trans.Maybe (MaybeT (..))
+import Crypto.JOSE qualified as Jose
+import Crypto.JWT (JWTError)
+import Crypto.JWT qualified as Jose
+import Data.Aeson (FromJSON (..))
+import Data.ByteString qualified as Bytes
+import Data.ByteString.Lazy qualified as Bytes.Lazy
+import Data.Function ((&))
+import Data.Functor ((<&>))
+import Data.Time qualified as Time
+import Network.HTTP.Types.Header qualified as Http
+import Network.Wai qualified as Wai
+import Web.Minion
+
+data JwtAuthSettings m payload a = JwtAuthSettings
+  { getNow :: m Time.UTCTime
+  , jwk :: m Jose.JWK
+  , validationSettings :: m Jose.JWTValidationSettings
+  , check :: MakeError -> Either JWTError (JwtPayload payload) -> m (AuthResult a)
+  }
+
+defaultJwtAuthSettings ::
+  (MonadIO m) =>
+  m Jose.JWK ->
+  -- | Audience predicate
+  (Jose.StringOrURI -> Bool) ->
+  (MakeError -> Either JWTError (JwtPayload payload) -> m (AuthResult a)) ->
+  JwtAuthSettings m payload a
+defaultJwtAuthSettings jwk audCheck check =
+  JwtAuthSettings
+    { getNow = liftIO Time.getCurrentTime
+    , jwk = jwk
+    , validationSettings = pure (Jose.defaultJWTValidationSettings audCheck)
+    , check = check
+    }
+
+data JwtPayload a = JwtPayload
+  { claims :: Jose.ClaimsSet
+  , payload :: a
+  }
+
+instance Jose.HasClaimsSet (JwtPayload a) where
+  claimsSet f JwtPayload{..} = f claims <&> \c -> JwtPayload{claims = c, ..}
+
+instance (FromJSON a) => FromJSON (JwtPayload a) where
+  parseJSON v =
+    JwtPayload
+      <$> parseJSON v
+      <*> parseJSON v
+
+data Bearer payload
+
+instance (MonadIO m, FromJSON payload) => IsAuth (Bearer payload) m a where
+  type Settings (Bearer payload) m a = JwtAuthSettings m payload a
+  toAuth JwtAuthSettings{..} buildError req = do
+    jwk_ <- jwk
+    now <- getNow
+    settings <- validationSettings
+    payload <- Jose.runJOSE $ runMaybeT do
+      authHeader <- Wai.requestHeaders req & lookup Http.hAuthorization & hoistMaybe
+      compact <- hoistMaybe $ Bytes.stripPrefix prefix authHeader
+      jwt <- Jose.decodeCompact $ Bytes.Lazy.fromStrict compact
+      Jose.verifyJWTAt settings jwk_ now jwt
+    case payload of
+      Left e -> check (buildError req) (Left e)
+      Right Nothing -> pure Indefinite
+      Right (Just (v :: JwtPayload payload)) -> check (buildError req) (Right v)
+   where
+    prefix = "Bearer "
+
+    hoistMaybe = MaybeT . pure
diff --git a/src/Web/Minion/Examples/Jwt.hs b/src/Web/Minion/Examples/Jwt.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Minion/Examples/Jwt.hs
@@ -0,0 +1,62 @@
+module Web.Minion.Examples.Jwt (app) where
+
+import Control.Monad (forM_)
+import Control.Monad.Reader (MonadIO (liftIO), ReaderT (runReaderT), asks)
+import Crypto.JOSE (JWK, bestJWSAlg, fromOctets, newJWSHeader, runJOSE)
+import Crypto.JWT (JWTError, encodeCompact, signJWT)
+import Data.Aeson (FromJSON, ToJSON)
+import Data.ByteString.Lazy qualified as Bytes.Lazy
+import Data.Functor (($>))
+import Data.Text.Encoding qualified
+import Data.Text.IO qualified
+import GHC.Generics (Generic)
+import Network.HTTP.Types.Status qualified as Http
+import System.Environment (getArgs)
+import Web.Minion
+
+import Web.Minion.Auth.Jwt
+
+type M = ReaderT Env IO
+
+newtype JwtUserInfo = JwtUserInfo {userId :: UserId}
+  deriving (Generic)
+  deriving anyclass (FromJSON, ToJSON)
+
+newtype UserId = UserId Int
+  deriving newtype (FromJSON, Show, Read, ToJSON)
+
+newtype Env = Env
+  {authCtx :: HList '[JwtAuthSettings M JwtUserInfo UserId]}
+
+app :: IO (ApplicationM IO)
+app = do
+  showJwts
+  pure $ \req resp ->
+    runReaderT (serve api req resp) (Env $ jwtSettings :# HNil)
+
+api :: Router Void M
+api = "api" /> "auth" /> myAuth .> handle GET authEndpoint
+
+authEndpoint :: UserId -> ReaderT Env IO NoBody
+authEndpoint userId = liftIO (putStrLn $ "User " <> show userId) $> NoBody
+
+myAuth :: ValueCombinator Void (WithReq M (Auth '[Bearer JwtUserInfo] UserId)) ts M
+myAuth = auth @'[Bearer JwtUserInfo] @UserId (asks authCtx) \makeError -> \case
+  _ -> throwM $ makeError Http.status401 mempty
+
+jwtSettings :: JwtAuthSettings M JwtUserInfo UserId
+jwtSettings = defaultJwtAuthSettings (pure myJwk) (const True) do
+  const (pure . either (const BadAuth) (\JwtPayload{payload = JwtUserInfo{..}} -> Authenticated userId))
+
+myJwk :: JWK
+myJwk = fromOctets @Bytes.Lazy.ByteString "really secret and long enough key"
+
+showJwts :: IO ()
+showJwts = do
+  userIds <- map (read @UserId) <$> getArgs
+  forM_ userIds \userId -> do
+    Right jwt <- runJOSE @JWTError do
+      alg <- bestJWSAlg myJwk
+      signJWT myJwk (newJWSHeader ((), alg)) (JwtUserInfo userId)
+    let jwtTxt = Data.Text.Encoding.decodeUtf8 . Bytes.Lazy.toStrict $ encodeCompact jwt
+    putStr (show userId <> ": ") >> Data.Text.IO.putStrLn jwtTxt
