diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1,21 @@
+# Changelog
+
+## 0.5.0.0 — 2026-08-27
+
+### Breaking
+- Require `scotty >= 0.20` (drops `ScottyError` and `ActionT e m`; use `ActionT m`)
+- `ScottyForm e m a` renamed to `ScottyForm m a` in `Web.Scotty.Trans.Form`
+- `Param` values are strict `Text` (scotty >= 0.20); `liftParser` now takes strict `Text` parsers (same as `liftParser'`)
+- `Environment` instance for `ActionT` requires `MonadUnliftIO` (needed for `formParams`)
+
+### Changed
+- Compatible with `ditto` 0.4 and 0.5 (ditto 0.5 is backwards compatible)
+- Replace deprecated `params` with `pathParams`, `queryParams`, and `formParams`
+- Prefer GHC 9.10+ (`GHC2024`) with fallback to `GHC2021` / `Haskell2010`
+- Widen dependency bounds (`base >= 4.14`, `text >= 1.2`)
+- Move language extensions into cabal `default-extensions`
+- Modern cabal format (`cabal-version: 3.8`)
+
+### Added
+- GitHub Actions CI
+- `cabal.project.local.example` for local `ditto` / `ditto-lucid` development
diff --git a/scotty-form.cabal b/scotty-form.cabal
--- a/scotty-form.cabal
+++ b/scotty-form.cabal
@@ -1,35 +1,94 @@
-cabal-version: >=1.10
+cabal-version: 3.8
 name: scotty-form
-version: 0.4.1.0
+version: 0.5.0.0
 synopsis: Html form validation using `ditto`
 description: Formlet library for `scotty` using `lucid` and `ditto`
--- bug-reports:
 license: MIT
 license-file: LICENSE
 author: goolord
 maintainer: zacharyachurchill@gmail.com
--- copyright:
+homepage: https://github.com/goolord/scotty-form
+bug-reports: https://github.com/goolord/scotty-form/issues
 category: Web
 build-type: Simple
-extra-source-files: CHANGELOG.md
+extra-doc-files:
+  CHANGELOG.md
+tested-with:
+  GHC == 9.6.7
+  GHC == 9.8.4
+  GHC == 9.10.3
+  GHC == 9.12.4
+  GHC == 9.14.1
 
 source-repository head
   type: git
   location: https://github.com/goolord/scotty-form.git
 
+common warnings-base
+  ghc-options: -Wall
+
+common warnings-modern
+  if impl(ghc >= 8.10)
+  ghc-options:
+    -Wcompat
+    -Widentities
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wmissing-deriving-strategies
+    -Wunused-packages
+
+common warnings-strict
+  if impl(ghc >= 9.4)
+  ghc-options: -Wredundant-constraints
+
+common extensions-shared
+  default-extensions:
+    BangPatterns
+    FlexibleInstances
+    MultiParamTypeClasses
+    OverloadedStrings
+    RecordWildCards
+    TypeApplications
+    TypeFamilies
+
+common defaults-ghc2024
+  import:
+    warnings-base, warnings-modern, warnings-strict, extensions-shared
+  default-language: GHC2024
+
+common defaults-ghc2021
+  import:
+    warnings-base, warnings-modern, warnings-strict, extensions-shared
+  default-language: GHC2021
+  default-extensions:
+    DataKinds
+    DerivingStrategies
+    LambdaCase
+
+common defaults-legacy
+  import: warnings-base, extensions-shared
+  default-language: Haskell2010
+
+common defaults
+  if impl(ghc >= 9.10)
+    import: defaults-ghc2024
+  elif impl(ghc >= 9.2)
+    import: defaults-ghc2021
+  else
+    import: defaults-legacy
+
 library
+  import: defaults
   exposed-modules:
     Web.Scotty.Form
     Web.Scotty.Trans.Form
-  ghc-options: -Wall
-  -- other-modules:
-  -- other-extensions:
   build-depends:
-      base >=4.12 && <5
-    , text >=1.2.4 && <3.0
-    , lucid >=2.9.12 && <3.0
-    , scotty >=0.12 && <1
-    , ditto >=0.4 && <=0.5
-    , ditto-lucid >= 0.4 && <= 0.5
+      base        >= 4.14 && < 5
+    , text        >= 1.2  && < 3
+    , lucid       >= 2.9  && < 3
+    , scotty      >= 0.20 && < 1
+    , ditto       >= 0.4  && < 0.6
+    , ditto-lucid >= 0.4  && < 0.6
+    , unliftio    >= 0.2  && < 0.3
   hs-source-dirs: src
-  default-language: Haskell2010
+  ghc-options: -fno-warn-orphans -Wno-error=orphans
diff --git a/src/Web/Scotty/Form.hs b/src/Web/Scotty/Form.hs
--- a/src/Web/Scotty/Form.hs
+++ b/src/Web/Scotty/Form.hs
@@ -15,17 +15,16 @@
 import Ditto.Types
 import Web.Scotty
 import qualified Web.Scotty.Trans.Form as Trans
-import qualified Data.Text.Lazy as TL
 import Lucid
 
-encQP :: [(a, TL.Text)] -> Text
+encQP :: [(a, Text)] -> Text
 encQP = Trans.encQP
 
 -- | a @ditto@ formlet for @scotty@
 type ScottyForm a = Form ActionM [Param] Trans.ScottyFormError (Html ()) a
 
-ditto :: (Monoid view)
-  => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a <form> tag
+ditto
+  :: ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a <form> tag
   -> Text -- ^ form name prefix
   -> Form ActionM [Param] err view a -- ^ the formlet
   -> ActionM (Result err a, view)
@@ -40,14 +39,14 @@
 dittoSingle = Trans.dittoSingle
 
 -- | create @\<form action=action method=\"GET\" enctype=\"application/xxx-form-urlencoded\"\>@
-simpleDittoGET :: (Applicative f)
+simpleDittoGET :: Applicative f
   => Text -- ^ action
   -> Form ActionM [Param] err (HtmlT f ()) b -- ^ formlet
   -> ActionM (Result err b, HtmlT f ())
 simpleDittoGET = Trans.simpleDittoGET
 
 -- | create @\<form action=action method=\"POST\" enctype=\"application/xxx-form-urlencoded\"\>@
-simpleDittoPOST :: (Applicative f)
+simpleDittoPOST :: Applicative f
   => Text -- ^ action
   -> Form ActionM [Param] err (HtmlT f ()) b -- ^ formlet
   -> ActionM (Result err b, HtmlT f ())
@@ -57,12 +56,6 @@
 liftParser' :: (Text -> Either Text a) -> ([Param] -> Either Trans.ScottyFormError a)
 liftParser' = Trans.liftParser'
 
--- | lift a function which parses lazy @Text@ into a function which parses a @[Param]@
--- e.g.
---
--- @
--- parserRead :: Read a => [Param] -> Either ScottyFormError a
--- parserRead = liftParser readEither
--- @
-liftParser :: (TL.Text -> Either TL.Text a) -> ([Param] -> Either Trans.ScottyFormError a)
+-- | alias for 'liftParser''. Parses strict `Text` from each `Param`.
+liftParser :: (Text -> Either Text a) -> ([Param] -> Either Trans.ScottyFormError a)
 liftParser = Trans.liftParser
diff --git a/src/Web/Scotty/Trans/Form.hs b/src/Web/Scotty/Trans/Form.hs
--- a/src/Web/Scotty/Trans/Form.hs
+++ b/src/Web/Scotty/Trans/Form.hs
@@ -1,38 +1,32 @@
-{-# LANGUAGE
-    OverloadedStrings
-  , MultiParamTypeClasses
-  , FlexibleInstances
-  , TypeFamilies
-  , BangPatterns
-#-}
-
-{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fno-warn-orphans -Wno-error=orphans #-}
 
 module Web.Scotty.Trans.Form where
 
+import Data.Bifunctor (first)
+import UnliftIO (MonadUnliftIO)
 import Data.Text (Text)
+import Ditto.Backend
 import Ditto.Core hiding (view)
 import Ditto.Lucid
 import Ditto.Types
 import Lucid (HtmlT, ToHtml (toHtml))
+import Lucid.Base (ToHtml (toHtmlRaw))
 import Web.Scotty.Trans
-import Ditto.Backend
 import qualified Data.Text as T
-import qualified Data.Text.Lazy as TL
-import Data.Bifunctor (first)
-import Lucid.Base (ToHtml (toHtmlRaw))
 
-instance (ScottyError e, Monad m) => Environment (ActionT e m) [Param] where
+instance MonadUnliftIO m => Environment (ActionT m) [Param] where
   environment formId = do
-    qp <- params
-    let !formId' = TL.fromStrict $ encodeFormId formId
-    case filter (\(x,_) -> x == formId') qp of
+    pp <- pathParams
+    qp <- queryParams
+    fp <- formParams
+    let !formId' = encodeFormId formId
+    case filter (\(x, _) -> x == formId') (pp ++ qp ++ fp) of
       [] -> pure Missing
       xs -> pure (Found xs)
 
 instance FormInput [Param] where
   type FileType [Param] = ()
-  getInputStrings xs = fmap (TL.unpack . snd) xs
+  getInputStrings xs = fmap (T.unpack . snd) xs
   getInputFile _ = Left $ commonFormError $ (NoFileFound [("","No support for file uploads")] :: CommonFormError [Param])
 
 instance FormError [Param] ScottyFormError where
@@ -55,18 +49,18 @@
   toHtmlRaw SFEUnexpectedEmpty = "Unexpected empty query param list"
   toHtmlRaw SFEUnexpectedMultiple = "Unexpected multiple query param list"
 
-encQP :: [(a, TL.Text)] -> Text
+encQP :: [(a, Text)] -> Text
 encQP [] = ""
-encQP xs = T.intercalate ", " (fmap (TL.toStrict . snd) xs)
+encQP xs = T.intercalate ", " (fmap snd xs)
 
 -- | a @ditto@ formlet for @scotty@
-type ScottyForm e m a = Form (ActionT e m) [Param] ScottyFormError (HtmlT (ActionT e m) ()) a
+type ScottyForm m a = Form (ActionT m) [Param] ScottyFormError (HtmlT (ActionT m) ()) a
 
-ditto :: (Monoid view, Monad m, ScottyError e)
+ditto :: Monad m
   => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a <form> tag
   -> Text -- ^ form name prefix
-  -> Form (ActionT e m) [Param] err view a -- ^ the formlet
-  -> ActionT e m (Result err a, view)
+  -> Form (ActionT m) [Param] err view a -- ^ the formlet
+  -> ActionT m (Result err a, view)
 ditto toForm prefix formlet = do
   dittoSingle toForm' prefix formlet
   where
@@ -74,11 +68,11 @@
 
 -- | a helpful wrapper around 'runForm'
 dittoSingle
-  :: (Monad m, ScottyError e)
+  :: Monad m
   => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a <form> tag
   -> Text -- ^ form name prefix
-  -> Form (ActionT e m) [Param] err view a -- ^ the formlet
-  -> ActionT e m (Result err a, view)
+  -> Form (ActionT m) [Param] err view a -- ^ the formlet
+  -> ActionT m (Result err a, view)
 dittoSingle toForm prefix formlet = do
   (View viewf, res) <- runForm prefix formlet
   case res of
@@ -86,33 +80,33 @@
     Ok (Proved _ unProved') -> pure (Ok unProved', toForm [] $ viewf [])
 
 -- | create @\<form action=action method=\"GET\" enctype=\"application/xxx-form-urlencoded\"\>@
-simpleDittoGET :: (Applicative f, Monad m, ScottyError e)
+simpleDittoGET
+  :: (Applicative f, Monad m)
   => Text -- ^ action
-  -> Form (ActionT e m) [Param] err (HtmlT f ()) b -- ^ formlet
-  -> ActionT e m (Result err b, HtmlT f ())
+  -> Form (ActionT m) [Param] err (HtmlT f ()) b -- ^ formlet
+  -> ActionT m (Result err b, HtmlT f ())
 simpleDittoGET action form = ditto (formGenGET action) "ditto" form
 
 -- | create @\<form action=action method=\"POST\" enctype=\"application/xxx-form-urlencoded\"\>@
-simpleDittoPOST :: (Applicative f, Monad m, ScottyError e)
+simpleDittoPOST
+  :: (Applicative f, Monad m)
   => Text -- ^ action
-  -> Form (ActionT e m) [Param] err (HtmlT f ()) b -- ^ formlet
-  -> ActionT e m (Result err b, HtmlT f ())
+  -> Form (ActionT m) [Param] err (HtmlT f ()) b -- ^ formlet
+  -> ActionT m (Result err b, HtmlT f ())
 simpleDittoPOST action form = ditto (formGenPOST action) "ditto" form
 
 -- | lift a function which parses strict @Text@ into a function which parses a @[Param]@
 liftParser' :: (Text -> Either Text a) -> ([Param] -> Either ScottyFormError a)
-liftParser' f [(_,x)] = first SFEParseError $ f (TL.toStrict x)
+liftParser' f [(_, x)] = first SFEParseError $ f x
 liftParser' _ [] = Left SFEUnexpectedEmpty
 liftParser' _ _ = Left SFEUnexpectedMultiple
 
--- | lift a function which parses lazy @Text@ into a function which parses a @[Param]@
--- e.g.
+-- | lift a function which parses strict @Text@ into a function which parses a @[Param]@
 --
 -- @
--- parserRead :: Read a => [Param] -> Either ScottyFormError a
--- parserRead = liftParser readEither
+-- parserRead :: [Param] -> Either ScottyFormError Int
+-- parserRead = liftParser' $ \t ->
+--   maybe (Left "not an integer") Right (readMaybe @Int (T.unpack t))
 -- @
-liftParser :: (TL.Text -> Either TL.Text a) -> ([Param] -> Either ScottyFormError a)
-liftParser f [(_,x)] = first (SFEParseError . TL.toStrict) $ f x
-liftParser _ [] = Left SFEUnexpectedEmpty
-liftParser _ _ = Left SFEUnexpectedMultiple
+liftParser :: (Text -> Either Text a) -> ([Param] -> Either ScottyFormError a)
+liftParser = liftParser'
