diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1 @@
+# htmx-servant
diff --git a/htmx-servant.cabal b/htmx-servant.cabal
new file mode 100644
--- /dev/null
+++ b/htmx-servant.cabal
@@ -0,0 +1,43 @@
+cabal-version:      3.6
+name:               htmx-servant
+version:            0.1.0.0
+synopsis:           Use htmx with servant
+description:
+  Please see the README on GitHub at <https://github.com/JonathanLorimer/htmx#readme>
+
+license:            MIT
+category:           Web, HTML
+author:             Jonathan Lorimer
+maintainer:         jonathanlorimer@pm.me
+build-type:         Simple
+extra-source-files: README.md
+
+source-repository head
+  type:     git
+  location: https://github.com/JonathanLorimer/htmx
+
+common def-exts
+  default-extensions:
+    DataKinds
+    LambdaCase
+    OverloadedStrings
+
+library
+  import:           def-exts
+
+  -- cabal-fmt: expand src
+  exposed-modules:
+    Htmx.Servant.Lucid
+    Htmx.Servant.Request
+    Htmx.Servant.Response
+
+  hs-source-dirs:   src
+  build-depends:
+    , base        >=4.7      && <5
+    , htmx
+    , htmx-lucid
+    , lucid       >=2.9.12.1 && <2.11.20230408.0
+    , servant     >=0.19     && <0.30
+    , text        >=2        && <3
+
+  default-language: Haskell2010
diff --git a/src/Htmx/Servant/Lucid.hs b/src/Htmx/Servant/Lucid.hs
new file mode 100644
--- /dev/null
+++ b/src/Htmx/Servant/Lucid.hs
@@ -0,0 +1,55 @@
+{- |
+Module      : Htmx.Servant.Lucid
+Description : Typesafe versions of HTMX request tags
+
+This module exports Lucid combinators that leverage the Servant 'Link'
+type to guarantee that they are live URLs, therefore making the requests
+"safe".
+-}
+module Htmx.Servant.Lucid (
+    hxDeleteSafe_,
+    hxGetSafe_,
+    hxPatchSafe_,
+    hxPostSafe_,
+    hxPushUrlSafe_,
+    hxPutSafe_,
+)
+where
+
+import Data.Text (Text)
+import Htmx.Lucid.Core (
+    hxGet_,
+    hxPost_,
+    hxPushUrl_,
+ )
+import Htmx.Lucid.Extra (
+    hxDelete_,
+    hxPatch_,
+    hxPut_,
+ )
+import Lucid.Base (Attribute)
+import Servant.API (ToHttpApiData (..), toUrlPiece)
+import Servant.Links (Link)
+
+hxDeleteSafe_ :: Link -> Attribute
+hxDeleteSafe_ = hxDelete_ . toUrl
+
+hxGetSafe_ :: Link -> Attribute
+hxGetSafe_ = hxGet_ . toUrl
+
+hxPatchSafe_ :: Link -> Attribute
+hxPatchSafe_ = hxPatch_ . toUrl
+
+hxPostSafe_ :: Link -> Attribute
+hxPostSafe_ = hxPost_ . toUrl
+
+hxPushUrlSafe_ :: Either Bool Link -> Attribute
+hxPushUrlSafe_ boolOrUrl = hxPushUrl_ $ case boolOrUrl of
+    Left bool -> if bool then "true" else "false"
+    Right url -> toUrl url
+
+hxPutSafe_ :: Link -> Attribute
+hxPutSafe_ = hxPut_ . toUrl
+
+toUrl :: (ToHttpApiData a) => a -> Text
+toUrl = ("/" <>) . toUrlPiece
diff --git a/src/Htmx/Servant/Request.hs b/src/Htmx/Servant/Request.hs
new file mode 100644
--- /dev/null
+++ b/src/Htmx/Servant/Request.hs
@@ -0,0 +1,34 @@
+{- |
+Module      : Htmx.Servant.RequestHeaders
+Description : Helper types for HTMX request headers
+
+<https://htmx.org/reference/#request_headers>
+-}
+module Htmx.Servant.Request where
+
+import Data.Text (Text)
+import Servant.API.Header (Header)
+
+-- | indicates that the request is via an element using hx-boost
+type HXBoosted = Header "HX-Boosted" Bool
+
+-- | the current URL of the browser
+type HXCurrentURL = Header "HX-Current-URL" Text
+
+-- | “true” if the request is for history restoration after a miss in the local history cache
+type HXHistoryRestoreRequest = Header "HX-History-Restore-Request" Bool
+
+-- | the user response to an hx-prompt
+type HXPrompt a = Header "HX-Prompt" a
+
+-- | always “true”
+type HXRequest = Header "HX-Prompt" Bool
+
+-- | the id of the target element if it exists
+type HXTarget = Header "HX-Target" Text
+
+-- | the name of the triggered element if it exists
+type HXTriggerName = Header "HX-Trigger-Name" Text
+
+-- | the id of the triggered element if it exists
+type HXTrigger = Header "HX-Trigger" Text
diff --git a/src/Htmx/Servant/Response.hs b/src/Htmx/Servant/Response.hs
new file mode 100644
--- /dev/null
+++ b/src/Htmx/Servant/Response.hs
@@ -0,0 +1,47 @@
+{- |
+Module      : Htmx.Servant.ResponseHeaders
+Description : Helper types for HTMX response headers
+
+<https://htmx.org/reference/#response_headers>
+-}
+module Htmx.Servant.Response where
+
+import Data.Text (Text)
+import Htmx.Swap (Swap)
+import Servant.API.Header (Header)
+
+-- | allows you to do a client-side redirect that does not do a full page reload
+type HXLocation = Header "HX-Location" Text
+
+-- | pushes a new url into the history stack
+type HXPushURL = Header "HX-Push-Url" Text
+
+-- | can be used to do a client-side redirect to a new location
+type HXRedirect = Header "HX-Redirect" Text
+
+-- | if set to “true” the client-side will do a full refresh of the page
+type HXRefresh = Header "HX-Refresh" Bool
+
+-- | replaces the current URL in the location bar
+type HXReplaceUrl = Header "HX-Replace-Url" Bool
+
+-- | replaces the current URL in the location bar
+type HXReswap = Header "HX-Reswap" Swap
+
+-- | a CSS selector that updates the target of the
+-- content update to a different element on the page
+type HXRetarget = Header "HX-Retarget" Text
+
+-- | a CSS selector that allows you to choose which part of the response is used
+-- to be swapped in. Overrides an existing hx-select on the triggering element
+type HXReselect = Header "HX-Reselect" Text
+
+-- | allows you to trigger client-side events
+type HXTrigger = Header "HX-Trigger" Text
+
+-- | allows you to trigger client-side events after the settle step
+type HXTriggerAfterSettle = Header "HX-Trigger-After-Settle" Text
+
+-- | allows you to trigger client-side events after the swap stepallows you to
+-- trigger client-side events after the settle step
+type HXTriggerAfterSwap = Header "HX-Trigger-After-Swap" Text
