diff --git a/snap-app.cabal b/snap-app.cabal
--- a/snap-app.cabal
+++ b/snap-app.cabal
@@ -1,5 +1,5 @@
 name:                snap-app
-version:             0.1.0.0
+version:             0.1.4
 synopsis:            Simple modules for writing apps with Snap, abstracted from hpaste.
 homepage:            Simple modules for writing apps with Snap, abstracted from hpaste.
 license:             BSD3
@@ -12,11 +12,11 @@
 
 library
   hs-source-dirs:    src
-  exposed-modules:   Snap.App, Snap.App.Types, Snap.App.Controller, Snap.App.Model
+  exposed-modules:   Snap.App, Snap.App.Types, Snap.App.Controller, Snap.App.Model, Snap.App.Migrate
   build-depends:     base >= 4 && <5,
                      snap-core,
                      network,
-                     pgsql-simple >= 0.1.1,
+                     pgsql-simple >= 0.1.2,
                      mtl,
                      blaze-html == 0.4.3.4,
                      safe,
diff --git a/src/Snap/App/Controller.hs b/src/Snap/App/Controller.hs
--- a/src/Snap/App/Controller.hs
+++ b/src/Snap/App/Controller.hs
@@ -28,7 +28,7 @@
 import Data.Maybe
 import Network.URI
 import Data.Text.Lazy             (Text,toStrict)
-import Database.PostgreSQL.Base   (withPoolConnection)
+import Database.PostgreSQL.Base   (withPoolConnection,withTransaction)
 import Database.PostgreSQL.Simple (Pool)
 import Safe                       (readMay)
 import Text.Blaze                 (Html)
@@ -38,10 +38,11 @@
 runHandler :: s -> c -> Pool -> Controller c s () -> Snap ()
 runHandler st conf pool ctrl = do
   withPoolConnection pool $ \conn -> do
-    let state = ControllerState conf conn st
-    -- Default to HTML, can be overridden.
-    modifyResponse $ setContentType "text/html"
-    runReaderT (runController ctrl) state
+    withTransaction conn $ do
+      let state = ControllerState conf conn st
+      -- Default to HTML, can be overridden.
+      modifyResponse $ setContentType "text/html"
+      runReaderT (runController ctrl) state
 
 -- | Strictly renders HTML to Text before outputting it via Snap.
 --   This ensures that any lazy exceptions are caught by the Snap
diff --git a/src/Snap/App/Migrate.hs b/src/Snap/App/Migrate.hs
new file mode 100644
--- /dev/null
+++ b/src/Snap/App/Migrate.hs
@@ -0,0 +1,54 @@
+-- | Migration library
+
+module Snap.App.Migrate where
+
+import Snap.App.Types
+import Snap.App.Model
+import Control.Monad
+import Control.Monad.Trans
+import Database.PostgreSQL.Simple (Only(..))
+import GHC.Int
+
+-- | Migrate the DB to the latest version.
+migrate :: Bool -> [(Int,Model c s Integer)] -> Model c s ()
+migrate create versions = go where
+  go = do
+    when create $ void $ ensureExists
+    rows <- query ["SELECT version FROM version"] ()
+    case rows of
+      [] -> do echo "No database version, initializing to version 0."
+               createVersion
+               setVersion 0
+      [Only v] -> do
+        case lookup (v+1) versions of
+          Just doMigrate -> do echo $ "Migrating to version " ++ show (v+1)
+                               changes <- doMigrate
+                               setVersion (v+1)
+                               echo $ "Rows changed: " ++ show changes
+                               go
+          Nothing -> echo $ "At version " ++ show v ++ "."
+      vs -> error $ "There is more than one database version, fix it: " ++
+                     show vs
+
+-- | Set the current database version.
+setVersion :: Int -> Model c s ()
+setVersion v = do
+  _ <- exec ["UPDATE version SET version = ?"] (Only v)
+  echo $ "Version set to " ++ show v ++ "."
+  return ()
+
+-- | Ensure the version table exists.
+ensureExists :: Model c s ()
+ensureExists = do
+  _ <- exec ["CREATE TABLE version (version int not null default 0)"] ()
+  return ()
+
+-- | Create the version number.
+createVersion :: Model c s ()
+createVersion = do
+  _ <- exec ["INSERT INTO version (version) VALUES (0)"] ()
+  echo $ "Version table created."
+
+-- | Just print to stdout for now.
+echo :: String -> Model c s ()
+echo = liftIO . putStrLn
diff --git a/src/Snap/App/Model.hs b/src/Snap/App/Model.hs
--- a/src/Snap/App/Model.hs
+++ b/src/Snap/App/Model.hs
@@ -6,27 +6,51 @@
 
 module Snap.App.Model
   (model
+  ,runDB
   ,query
   ,single
   ,singleNoParams
   ,queryNoParams
+  ,processQuery
+  ,queryProcessed
   ,exec
   ,DB.Only(..))
   where
 
 import           Control.Monad.Env                       (env)
-
 import           Control.Monad.Reader
 import           Data.String
-import           Database.PostgreSQL.Simple              (Only(..))
+import           Database.PostgreSQL.Base   (withPoolConnection,withTransaction)
+import           Database.PostgreSQL.Simple              (Only(..),ProcessedQuery,Query)
 import qualified Database.PostgreSQL.Simple              as DB
+import           Database.PostgreSQL.Simple (Pool)
 import           Database.PostgreSQL.Simple.QueryParams
 import           Database.PostgreSQL.Simple.QueryResults
 import           Snap.App.Types
 
--- | Run a model action.
+-- | Run a model action at the top-level.
+runDB :: s -> c -> Pool -> Model c s () -> IO ()
+runDB st conf pool mdl = do
+  withPoolConnection pool $ \conn -> do
+    withTransaction conn $ do
+      let state = ModelState conn st conf
+      -- Default to HTML, can be overridden.
+      runReaderT (runModel mdl) state
+
+-- | Run a model action from within a controller.
 model :: AppLiftModel c s => Model c s a -> Controller c s a
 model = liftModel
+
+-- | A version of 'query' that does not perform query substitution.
+queryProcessed :: (QueryResults r) => ProcessedQuery r -> Model c s [r]
+queryProcessed pq = do
+  conn <- env modelStateConn
+  Model $ ReaderT (\_ -> DB.queryProcessed conn pq)
+
+-- | Process a query for later use.
+processQuery :: (QueryParams q,QueryResults r) => Query -> q -> Model c s (ProcessedQuery r)
+processQuery template qs = do
+  Model $ ReaderT (\_ -> DB.processQuery template qs)
 
 -- | Query with some parameters.
 query :: (QueryParams ps,QueryResults r) => [String] -> ps -> Model c s [r]
