diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Marat Khafizov (c) 2018
+
+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 Marat Khafizov 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
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/n2o-nitro.cabal b/n2o-nitro.cabal
new file mode 100644
--- /dev/null
+++ b/n2o-nitro.cabal
@@ -0,0 +1,38 @@
+name:           n2o-nitro
+version:        0.11.0
+description:    Nitro DSL
+homepage:       https://github.com/xafizoff/n2o#readme
+bug-reports:    https://github.com/xafizoff/n2o/issues
+author:         Marat Khafizov
+maintainer:     xafizoff@gmail.com
+copyright:      (c) Marat Khafizov, 2018
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+extra-source-files:  README.md
+category:       Network, N2O, Web
+synopsis:       Nitro Elements, Events and Actions
+
+source-repository head
+  type: git
+  location: https://github.com/xafizoff/n2o
+
+library
+  exposed-modules:
+                  Network.N2O.Nitro
+  other-modules:
+      Paths_n2o_nitro
+  hs-source-dirs:
+      src
+  build-depends: base >= 4.7 && < 5
+               , n2o == 0.11.0
+               , text >= 1.2
+               , bytestring >= 0.10
+               , binary >= 0.5
+               , bert >= 1.2
+               , fmt >= 0.6.1
+               , containers >= 0.5
+               , base64-bytestring >= 1.0
+  default-language: Haskell2010
+
diff --git a/src/Network/N2O/Nitro.hs b/src/Network/N2O/Nitro.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/N2O/Nitro.hs
@@ -0,0 +1,238 @@
+{-# LANGUAGE OverloadedStrings, RecordWildCards, DeriveGeneric,
+  ScopedTypeVariables #-}
+
+{-|
+Module      : Network.N2O.Nitro
+Description : Nitro DSL
+Copyright   : (c) Marat Khafizov, 2018
+License     : BSD-3
+Maintainer  : xafizoff@gmail.com
+Stability   : experimental
+Portability : not portable
+
+Nitro DSL to build interactive user interfaces
+
+-}
+module Network.N2O.Nitro where
+
+import Control.Monad (forM_, void)
+import qualified Data.Binary as B
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Base64.Lazy as B64
+import qualified Data.ByteString.Char8 as C8
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.ByteString.Lazy.Char8 as CL8
+import Data.Char (isAlphaNum, ord, toLower)
+import Data.IORef
+import Data.List (intercalate)
+import Data.Map.Strict ((!?))
+import Data.String
+import qualified Data.Text.Lazy as TL
+import Data.Text.Lazy.Encoding
+import Fmt
+import Fmt.Internal.Core
+import GHC.Generics (Generic)
+import Network.N2O hiding (Event)
+import Numeric (showHex)
+import Prelude hiding (id)
+
+-- | An HTML element
+data Element a
+  = Element { name      :: String
+            , id        :: String
+            , body      :: [Element a]
+            , postback  :: Maybe a
+            , source    :: [String]
+            , noBody    :: Bool
+            , noClosing :: Bool
+            }
+  | Text TL.Text
+  deriving (Show, Generic)
+
+instance (B.Binary a) => B.Binary (Element a)
+
+-- | Action that can be rendered as JavaScript events
+data Action a
+  = AEvent (Event a)
+  | AElement (Element a)
+  | ARaw BL.ByteString
+  deriving (Show, Generic)
+
+instance (B.Binary a) => B.Binary (Action a)
+
+-- | A JavaScript event
+data Event a = Event
+  { eventTarget   :: String
+  , eventPostback :: a
+  , eventType     :: String
+  , eventSource   :: [String]
+  } deriving (Show, Generic)
+
+instance (B.Binary a) => B.Binary (Event a)
+
+-- | Wire an element
+wireEl :: (B.Binary a) => Element a -> N2O f a (Result a)
+wireEl = wire . AElement
+
+-- | Wire action
+wire :: forall f a. (B.Binary a) => Action a -> N2O f a (Result a)
+wire a = do
+  actions <- getActions
+  putActions (a : actions)
+  return Empty
+
+-- | Render list of actions to JavaScript
+renderActions :: (B.Binary a) => [Action a] -> N2O f a BL.ByteString
+renderActions [] = return ""
+renderActions (a:as) = do
+  r <- renderAction a
+  rs <- renderActions as
+  return (r <> ";" <> rs)
+
+-- | Render an action
+renderAction :: (B.Binary a) => Action a -> N2O f a BL.ByteString
+renderAction (ARaw bs) = return bs
+renderAction (AEvent ev) = renderEvent ev
+renderAction (AElement el@Element {..}) = do
+  case postback of
+    Nothing -> return ()
+    Just pb -> void (wire $ AEvent Event
+        {eventType = "click", eventPostback = pb, eventTarget = id, eventSource = source})
+  return ""
+
+-- | Render list of elements to the HTML
+renderElements :: (B.Binary a) => [Element a] -> N2O f a BL.ByteString
+renderElements [] = return ""
+renderElements (e:es) = do
+  r <- renderElement e
+  rs <- renderElements es
+  return (r <> rs)
+
+-- | Render element to the HTML
+renderElement :: (B.Binary a) => Element a -> N2O f a BL.ByteString
+renderElement (Text t) = return $ encodeUtf8 t
+renderElement Element {..} = do
+  case postback of
+    Nothing -> return ()
+    Just pb -> void (wire $ AEvent Event
+      {eventType = "click", eventPostback = pb, eventTarget = id, eventSource = source})
+  case name of
+    "br" -> return "<br>"
+    _ -> do
+      content <- renderElements body
+      return $
+        encodeUtf8 $
+        if noBody
+          then "<" +| name |+ " " +| idProp id |+ "/>"
+          else "<" +| name |+ " " +| idProp id |+ ">" +| decodeUtf8 content |+ "</" +| name |+ ">"
+  where
+    idProp :: String -> String
+    idProp x =
+      if x == ""
+        then ""
+        else "id=\"" +| x |+ "\""
+
+-- | Render event
+renderEvent :: Event a -> N2O f a BL.ByteString
+renderEvent Event {..} = do
+  ref <- ask
+  cx@Context {cxPickle = pickle} <- lift $ readIORef ref
+  case eventSource of
+    [] -> return BL.empty
+    src -> return $ encodeUtf8 $
+            "{ var x=qi('" +| eventTarget |+ "'); x && x.addEventListener('"
+            +| eventType |+ "',function(event){ if (validateSources("
+            +| strJoin (map (\x -> "'" ++ x ++  "'") eventSource) |+
+            ")) { ws.send(enc(tuple(atom('pickle'),bin('" +| eventTarget |+ "'),bin('"
+            +| decodeUtf8 (pickle eventPostback) |+ "'),"
+            +| strJoin (map renderSource src) |+
+            "))); } else console.log('Validation error'); })}"
+  where
+    renderSource s = "tuple(atom('" +| s |+ "'),querySource('" +| s |+ "'))"
+    strJoin [] = "[]"
+    strJoin l = "[" ++ intercalate "," l ++ "]"
+
+-- | Element constructor
+baseElement :: Element a
+baseElement =
+  Element
+    { id = ""
+    , name = undefined
+    , postback = Nothing
+    , body = []
+    , source = []
+    , noBody = False
+    , noClosing = False
+    }
+
+-- | An HTML button
+button :: Element a
+button = baseElement {name = "button", source = []}
+
+-- | A @panel@ widget
+panel = baseElement {name = "div"}
+
+-- | Text node
+text :: TL.Text -> Element a
+text = Text
+
+-- | @<br>@ element
+br = baseElement {name = "br", noBody = True, noClosing = True}
+
+-- | A @textbox@ widget
+textbox :: Element a
+textbox = baseElement {name = "input type=\"text\"", noBody = True}
+
+-- | Update text content of the element with the specified @id@
+updateText :: (B.Binary a) => BL.ByteString -> TL.Text -> N2O f a (Result a)
+updateText target s = wire
+  (ARaw $ encodeUtf8 ("qi('" +| decodeUtf8 target |+ "').innerText='" +| s |+ "'"))
+
+insertBottom :: (B.Binary a) => BL.ByteString -> Element a -> N2O f a (Result a)
+insertBottom target elem = do
+  content <- renderElement elem
+  let tag = "div" :: TL.Text
+      action =
+        "(function(){ var div = qn('" +| tag |+ "'); div.innerHTML = '" +|
+        decodeUtf8 content |+ "';qi('" +|
+        decodeUtf8 target |+ "').appendChild(div.firstChild); })();"
+  wire $ ARaw $ encodeUtf8 action
+
+-- | Escape untrusted text to prevent XSS
+jsEscapeT :: TL.Text -> TL.Text
+jsEscapeT t = TL.pack (escape (TL.unpack t) "")
+  where
+    escape "" acc = acc
+    escape (x:xs) acc =
+      escape xs $
+      if isAlphaNum x
+        then acc ++ [x]
+        else acc <> "\\x" <> (flip showHex "" . ord $ x)
+
+-- | Escape untrusted text to prevent XSS
+jsEscape :: CL8.ByteString -> TL.Text
+jsEscape = jsEscapeT . decodeUtf8
+
+-- | Default pickler
+defPickle :: (Show a) => a -> BL.ByteString
+defPickle = B64.encode . CL8.pack . show
+
+-- | Default depickler
+defDePickle :: (Read a) => BL.ByteString -> Maybe a
+defDePickle bs =
+  case B64.decode bs of
+    Right x -> Just $ read $ CL8.unpack x
+    _ -> Nothing
+
+-- | Get action list from the local mutable state
+getActions :: (B.Binary a) => N2O f a [Action a]
+getActions = do
+  mbActions <- get (C8.pack "actions")
+  return $
+    case mbActions of
+      Just actions -> actions
+      _ -> []
+
+-- | Put actions to the local mutable state
+putActions :: (B.Binary a) => [Action a] -> N2O f a ()
+putActions = put (C8.pack "actions")
