diff --git a/CHANGES.md b/CHANGES.md
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -21,6 +21,37 @@
 User-visible changes in hledger-web.
 See also the hledger changelog.
 
+# 1.32 2023-12-01
+
+Features
+
+- The hledger-web app on the Sandstorm cloud platform has been updated to
+  a recent version (Jacob Weisz, #2102), and now uses Sandstorm's access
+  control. (Jakub Zárybnický, #821)
+
+Improvements
+
+- The --capabilities and --capabilities-header options have been replaced
+  with an easier `--allow=view|add|edit|sandstorm` option.
+  `add` is the default access level, while `sandstorm` is for use on Sandstorm.
+  UI and docs now speak of "permissions" rather than "capabilities".
+  (#834)
+
+- The Sandstorm app's permissions and roles have been renamed for clarity. (#834)
+
+- Permissions are now checked earlier, before the web app is started,
+  producing clearer command line errors when appropriate.
+
+- Account's `adeclarationinfo` field is now included in JSON output. (#2097) (S. Zeid)
+
+Fixes
+
+- The app can now serve on address 0.0.0.0 (exposing it on all interfaces),
+  which previously didn't work.
+  (#2099) (Philipp Klocke)
+
+- The broken "File format help" link in the edit form has been fixed. (#2103)
+
 # 1.31 2023-09-03
 
 Improvements
diff --git a/Hledger/Web.hs b/Hledger/Web.hs
--- a/Hledger/Web.hs
+++ b/Hledger/Web.hs
@@ -1,11 +1,22 @@
 {-|
-Re-export the modules of the hledger-web program.
+
+This is the root module of the @hledger-web@ package,
+providing hledger's web user interface and JSON API.
+The main function and command-line options are exported.
+
+== See also:
+
+- hledger-lib:Hledger
+- hledger:Hledger.Cli
+- [The README files](https://github.com/search?q=repo%3Asimonmichael%2Fhledger+path%3A**%2FREADME*&type=code&ref=advsearch)
+- [The high-level developer docs](https://hledger.org/dev.html)
+
 -}
 
-module Hledger.Web
-  ( module Hledger.Web.WebOptions
-  , module Hledger.Web.Main
-  ) where
+module Hledger.Web (
+  module Hledger.Web.Main,
+  module Hledger.Web.WebOptions
+) where
 
 import Hledger.Web.WebOptions
 import Hledger.Web.Main
diff --git a/Hledger/Web/Foundation.hs b/Hledger/Web/Foundation.hs
--- a/Hledger/Web/Foundation.hs
+++ b/Hledger/Web/Foundation.hs
@@ -16,7 +16,7 @@
 module Hledger.Web.Foundation where
 
 import Control.Applicative ((<|>))
-import Control.Monad (join, when)
+import Control.Monad (join, when, unless)
 -- import Control.Monad.Except (runExceptT)  -- now re-exported by Hledger
 import qualified Data.ByteString.Char8 as BC
 import Data.Traversable (for)
@@ -96,7 +96,7 @@
 -- Please see the documentation for the Yesod typeclass. There are a number
 -- of settings which can be configured by overriding methods here.
 instance Yesod App where
-  approot = ApprootMaster $ appRoot . settings
+  approot = guessApprootOr (ApprootMaster $ appRoot . settings)
 
   makeSessionBackend _ = do
     hledgerdata <- getXdgDirectory XdgCache "hledger"
@@ -114,7 +114,7 @@
 
     master <- getYesod
     here <- fromMaybe RootR <$> getCurrentRoute
-    VD{opts, j, qparam, q, qopts, caps} <- getViewData
+    VD{opts, j, qparam, q, qopts, perms} <- getViewData
     msg <- getMessage
     showSidebar <- shouldShowSidebar
 
@@ -132,6 +132,7 @@
 
     let accounts =
           balanceReportAsHtml (JournalR, RegisterR) here hideEmptyAccts j qparam qopts $
+          styleAmounts (journalCommodityStylesWith HardRounding j) $
           balanceReport rspec' j
 
         topShowmd = if showSidebar then "col-md-4" else "col-any-0" :: Text
@@ -198,7 +199,7 @@
   , qparam :: Text       -- ^ the current "q" request parameter
   , q     :: Query      -- ^ a query parsed from the q parameter
   , qopts :: [QueryOpt] -- ^ query options parsed from the q parameter
-  , caps  :: [Capability] -- ^ capabilities enabled for this request
+  , perms :: [Permission]  -- ^ permissions enabled for this request (by --allow and/or X-Sandstorm-Permissions)
   } deriving (Show)
 
 instance Show Text.Blaze.Markup where show _ = "<blaze markup>"
@@ -233,16 +234,19 @@
   -- if either of the above gave an error, display it
   maybe (pure ()) (setMessage . toHtml) $ mjerr <|> mqerr
 
-  -- do some permissions checking
-  caps <- case capabilitiesHeader_ opts of
-    Nothing -> return (capabilities_ opts)
-    Just h -> do
+  -- find out which permissions are enabled
+  perms <- case allow_ opts of
+    -- if started with --allow=sandstorm, take permissions from X-Sandstorm-Permissions header
+    SandstormAccess -> do
+      let h = "X-Sandstorm-Permissions"
       hs <- fmap (BC.split ',' . snd) . filter ((== h) . fst) . requestHeaders <$> waiRequest
-      fmap join . for (join hs) $ \x -> case capabilityFromBS x of
-        Left e -> [] <$ addMessage "" ("Unknown permission: " <> toHtml (BC.unpack e))
-        Right c -> pure [c]
+      fmap join . for (join hs) $ \x -> case parsePermission x of
+        Left  e -> [] <$ addMessage "" ("Unknown permission: " <> toHtml e)
+        Right p -> pure [p]
+    -- otherwise take them from the access level specified by --allow's access level
+    cliaccess -> pure $ accessLevelToPermissions cliaccess
 
-  return VD{opts, today, j, qparam, q, qopts, caps}
+  return VD{opts, today, j, qparam, q, qopts, perms}
 
 checkServerSideUiEnabled :: Handler ()
 checkServerSideUiEnabled = do
@@ -280,3 +284,11 @@
       liftIO . writeIORef jref $ filterJournalTransactions depthlessinitialq j'
       return (j',Nothing)
     Right (_, False) -> return (j, Nothing)
+
+-- | In a request handler, check for the given permission
+-- and fail with a message if it's not present.
+require :: Permission -> Handler ()
+require p = do
+  VD{perms} <- getViewData
+  unless (p `elem` perms) $ permissionDenied $
+    "Missing the '" <> T.pack (showPermission p) <> "' permission"
diff --git a/Hledger/Web/Handler/AddR.hs b/Hledger/Web/Handler/AddR.hs
--- a/Hledger/Web/Handler/AddR.hs
+++ b/Hledger/Web/Handler/AddR.hs
@@ -30,8 +30,8 @@
 postAddR :: Handler ()
 postAddR = do
   checkServerSideUiEnabled
-  VD{caps, j, today} <- getViewData
-  when (CapAdd `notElem` caps) (permissionDenied "Missing the 'add' capability")
+  VD{j, today} <- getViewData
+  require AddPermission
 
   ((res, view), enctype) <- runFormPost $ addForm j today
   case res of
@@ -59,8 +59,8 @@
 -- The web form handler above should probably use PUT as well.
 putAddR :: Handler RepJson
 putAddR = do
-  VD{caps, j, opts} <- getViewData
-  when (CapAdd `notElem` caps) (permissionDenied "Missing the 'add' capability")
+  VD{j, opts} <- getViewData
+  require AddPermission
 
   (r :: Result Transaction) <- parseCheckJsonBody
   case r of
diff --git a/Hledger/Web/Handler/EditR.hs b/Hledger/Web/Handler/EditR.hs
--- a/Hledger/Web/Handler/EditR.hs
+++ b/Hledger/Web/Handler/EditR.hs
@@ -31,8 +31,8 @@
 postEditR :: FilePath -> Handler ()
 postEditR f = do
   checkServerSideUiEnabled
-  VD {caps, j} <- getViewData
-  when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")
+  VD {j} <- getViewData
+  require EditPermission
 
   (f', txt) <- journalFile404 f j
   ((res, view), enctype) <- runFormPost (editForm f' txt)
diff --git a/Hledger/Web/Handler/JournalR.hs b/Hledger/Web/Handler/JournalR.hs
--- a/Hledger/Web/Handler/JournalR.hs
+++ b/Hledger/Web/Handler/JournalR.hs
@@ -20,15 +20,17 @@
 getJournalR :: Handler Html
 getJournalR = do
   checkServerSideUiEnabled
-  VD{caps, j, q, opts, qparam, qopts, today} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
+  VD{perms, j, q, opts, qparam, qopts, today} <- getViewData
+  require ViewPermission
   let title = case inAccount qopts of
         Nothing -> "General Journal"
         Just (a, inclsubs) -> "Transactions in " <> a <> if inclsubs then "" else " (excluding subaccounts)"
       title' = title <> if q /= Any then ", filtered" else ""
       acctlink a = (RegisterR, [("q", replaceInacct qparam $ accountQuery a)])
       rspec = (reportspec_ $ cliopts_ opts){_rsQuery = filterQuery (not . queryIsDepth) q}
-      items = reverse $ entriesReport rspec j
+      items = reverse $
+        styleAmounts (journalCommodityStylesWith HardRounding j) $
+        entriesReport rspec j
       transactionFrag = transactionFragment j
 
   defaultLayout $ do
diff --git a/Hledger/Web/Handler/MiscR.hs b/Hledger/Web/Handler/MiscR.hs
--- a/Hledger/Web/Handler/MiscR.hs
+++ b/Hledger/Web/Handler/MiscR.hs
@@ -38,17 +38,17 @@
 getManageR :: Handler Html
 getManageR = do
   checkServerSideUiEnabled
-  VD{caps, j} <- getViewData
-  when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")
+  VD{j} <- getViewData
+  require EditPermission
   defaultLayout $ do
-    setTitle "Manage journal"
+    setTitle "Edit journal"
     $(widgetFile "manage")
 
 getDownloadR :: FilePath -> Handler TypedContent
 getDownloadR f = do
   checkServerSideUiEnabled
-  VD{caps, j} <- getViewData
-  when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")
+  VD{j} <- getViewData
+  require EditPermission
   (f', txt) <- journalFile404 f j
   addHeader "Content-Disposition" ("attachment; filename=\"" <> T.pack f' <> "\"")
   sendResponse ("text/plain" :: ByteString, toContent txt)
@@ -57,53 +57,53 @@
 
 getVersionR :: Handler TypedContent
 getVersionR = do
-  VD{caps} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
-  selectRep $ do
-    provideJson $ packageversion
+  require ViewPermission
+  selectRep $ provideJson $ packageversion
 
 getAccountnamesR :: Handler TypedContent
 getAccountnamesR = do
-  VD{caps, j} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
-  selectRep $ do
-    provideJson $ journalAccountNames j
+  VD{j} <- getViewData
+  require ViewPermission
+  selectRep $ provideJson $ journalAccountNames j
 
 getTransactionsR :: Handler TypedContent
 getTransactionsR = do
-  VD{caps, j} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
-  selectRep $ do
-    provideJson $ jtxns j
+  VD{j} <- getViewData
+  require ViewPermission
+  selectRep $ provideJson $ jtxns j
 
 getPricesR :: Handler TypedContent
 getPricesR = do
-  VD{caps, j} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
-  selectRep $ do
+  VD{j} <- getViewData
+  require ViewPermission
+  selectRep $
     provideJson $ map priceDirectiveToMarketPrice $ jpricedirectives j
 
 getCommoditiesR :: Handler TypedContent
 getCommoditiesR = do
-  VD{caps, j} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
+  VD{j} <- getViewData
+  require ViewPermission
   selectRep $ do
     provideJson $ (M.keys . jinferredcommodities) j
 
 getAccountsR :: Handler TypedContent
 getAccountsR = do
-  VD{caps, j} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
+  VD{j} <- getViewData
+  require ViewPermission
   selectRep $ do
-    provideJson $ laccounts $ ledgerFromJournal Any j
+    provideJson $
+      styleAmounts (journalCommodityStylesWith HardRounding j) $
+      flattenAccounts $ mapAccounts (accountSetDeclarationInfo j) $ ledgerRootAccount $ ledgerFromJournal Any j
 
 getAccounttransactionsR :: Text -> Handler TypedContent
 getAccounttransactionsR a = do
-  VD{caps, j} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
+  VD{j} <- getViewData
+  require ViewPermission
   let
     rspec = defreportspec
     thisacctq = Acct $ accountNameToAccountRegex a -- includes subs
   selectRep $ do
-    provideJson $ accountTransactionsReport rspec{_rsQuery=Any} j thisacctq
+    provideJson $
+      styleAmounts (journalCommodityStylesWith HardRounding j) $
+      accountTransactionsReport rspec{_rsQuery=Any} j thisacctq
 
diff --git a/Hledger/Web/Handler/RegisterR.hs b/Hledger/Web/Handler/RegisterR.hs
--- a/Hledger/Web/Handler/RegisterR.hs
+++ b/Hledger/Web/Handler/RegisterR.hs
@@ -26,8 +26,8 @@
 getRegisterR :: Handler Html
 getRegisterR = do
   checkServerSideUiEnabled
-  VD{caps, j, q, opts, qparam, qopts, today} <- getViewData
-  when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")
+  VD{perms, j, q, opts, qparam, qopts, today} <- getViewData
+  require ViewPermission
 
   let (a,inclsubs) = fromMaybe ("all accounts",True) $ inAccount qopts
       s1 = if inclsubs then "" else " (excluding subaccounts)"
@@ -45,7 +45,9 @@
           zip xs $
           zip (map (T.unpack . accountSummarisedName . paccount) xs) $
           tail $ (", "<$xs) ++ [""]
-      items = accountTransactionsReport rspec{_rsQuery=q} j acctQuery
+      items =
+        styleAmounts (journalCommodityStylesWith HardRounding j) $
+        accountTransactionsReport rspec{_rsQuery=q} j acctQuery
       balancelabel
         | isJust (inAccount qopts), balanceaccum_ (_rsReportOpts rspec) == Historical = "Historical Total"
         | isJust (inAccount qopts) = "Period Total"
diff --git a/Hledger/Web/Handler/UploadR.hs b/Hledger/Web/Handler/UploadR.hs
--- a/Hledger/Web/Handler/UploadR.hs
+++ b/Hledger/Web/Handler/UploadR.hs
@@ -35,8 +35,8 @@
 postUploadR :: FilePath -> Handler ()
 postUploadR f = do
   checkServerSideUiEnabled
-  VD {caps, j} <- getViewData
-  when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")
+  VD {j} <- getViewData
+  require EditPermission
 
   (f', _) <- journalFile404 f j
   ((res, view), enctype) <- runFormPost (uploadForm f')
diff --git a/Hledger/Web/Import.hs b/Hledger/Web/Import.hs
--- a/Hledger/Web/Import.hs
+++ b/Hledger/Web/Import.hs
@@ -23,4 +23,4 @@
 import           Hledger.Web.Foundation           as Import
 import           Hledger.Web.Settings             as Import
 import           Hledger.Web.Settings.StaticFiles as Import
-import           Hledger.Web.WebOptions           as Import (Capability(..))
+import           Hledger.Web.WebOptions           as Import (Permission(..))
diff --git a/Hledger/Web/Settings.hs b/Hledger/Web/Settings.hs
--- a/Hledger/Web/Settings.hs
+++ b/Hledger/Web/Settings.hs
@@ -12,6 +12,7 @@
 import Data.Default (def)
 import Data.Maybe (fromMaybe)
 import Data.Text (Text)
+import qualified Data.Text as T
 import Data.Yaml
 import Language.Haskell.TH.Syntax (Q, Exp)
 import Text.Hamlet
@@ -19,6 +20,8 @@
 import Yesod.Default.Config
 import Yesod.Default.Util
 
+import Hledger.Cli.Version (packagemajorversion)
+
 development :: Bool
 development =
 #if DEVELOPMENT
@@ -31,10 +34,10 @@
 production = not development
 
 hledgerorgurl :: Text
-hledgerorgurl = "http://hledger.org"
+hledgerorgurl = "https://hledger.org"
 
 manualurl :: Text
-manualurl = hledgerorgurl <> "hledger.html"
+manualurl = hledgerorgurl <> "/" <> T.pack packagemajorversion <> "/hledger.html"
 
 -- | The default IP address to listen on. May be overridden with --host.
 defhost :: String
diff --git a/Hledger/Web/WebOptions.hs b/Hledger/Web/WebOptions.hs
--- a/Hledger/Web/WebOptions.hs
+++ b/Hledger/Web/WebOptions.hs
@@ -6,17 +6,18 @@
 import Data.ByteString (ByteString)
 import qualified Data.ByteString.Char8 as BC
 import Data.ByteString.UTF8 (fromString)
-import Data.CaseInsensitive (CI, mk)
 import Data.Default (Default(def))
 import Data.Maybe (fromMaybe)
-import qualified Data.Text as T
 import Data.Text (Text)
 import System.Environment (getArgs)
 import Network.Wai as WAI
 import Network.Wai.Middleware.Cors
+import Safe (lastMay)
 
 import Hledger.Cli hiding (packageversion, progname, prognameandversion)
 import Hledger.Web.Settings (defhost, defport, defbaseurl)
+import qualified Data.Text as T
+import Data.Char (toLower)
 
 -- cf Hledger.Cli.Version
 
@@ -46,6 +47,11 @@
       (setboolopt "serve-api")
       "like --serve, but serve only the JSON web API, without the server-side web UI"
   , flagReq
+      ["allow"]
+      (\s opts -> Right $ setopt "allow" s opts)
+      "view|add|edit"
+      "set the user's access level for changing data (default: `add`). It also accepts `sandstorm` for use on that platform (reads permissions from the `X-Sandstorm-Permissions` request header)."
+  , flagReq
       ["cors"]
       (\s opts -> Right $ setopt "cors" s opts)
       "ORIGIN"
@@ -75,16 +81,6 @@
       (\s opts -> Right $ setopt "file-url" s opts)
       "FILEURL"
       "set the static files url (default: BASEURL/static)"
-  , flagReq
-      ["capabilities"]
-      (\s opts -> Right $ setopt "capabilities" s opts)
-      "CAP[,CAP..]"
-      "enable the view, add, and/or manage capabilities (default: view,add)"
-  , flagReq
-      ["capabilities-header"]
-      (\s opts -> Right $ setopt "capabilities-header" s opts)
-      "HTTPHEADER"
-      "read capabilities to enable from a HTTP header, like X-Sandstorm-Permissions (default: disabled)"
   , flagNone
       ["test"]
       (setboolopt "test")
@@ -117,17 +113,16 @@
 
 -- hledger-web options, used in hledger-web and above
 data WebOpts = WebOpts
-  { serve_ :: Bool
-  , serve_api_ :: Bool
-  , cors_ :: Maybe String
-  , host_ :: String
-  , port_ :: Int
-  , base_url_ :: String
-  , file_url_ :: Maybe String
-  , capabilities_ :: [Capability]
-  , capabilitiesHeader_ :: Maybe (CI ByteString)
-  , cliopts_ :: CliOpts
-  , socket_ :: Maybe String
+  { serve_              :: !Bool
+  , serve_api_          :: !Bool
+  , cors_               :: !(Maybe String)
+  , host_               :: !String
+  , port_               :: !Int
+  , base_url_           :: !String
+  , file_url_           :: !(Maybe String)
+  , allow_              :: !AccessLevel
+  , cliopts_            :: !CliOpts
+  , socket_             :: !(Maybe String)
   } deriving (Show)
 
 defwebopts :: WebOpts
@@ -139,8 +134,7 @@
   , port_               = def
   , base_url_           = ""
   , file_url_           = Nothing
-  , capabilities_       = [CapView, CapAdd]
-  , capabilitiesHeader_ = Nothing
+  , allow_              = AddAccess
   , cliopts_            = def
   , socket_             = Nothing
   }
@@ -153,15 +147,15 @@
     cliopts <- rawOptsToCliOpts rawopts
     let h = fromMaybe defhost $ maybestringopt "host" rawopts
         p = fromMaybe defport $ maybeposintopt "port" rawopts
-        b =
-          maybe (defbaseurl h p) stripTrailingSlash $
-          maybestringopt "base-url" rawopts
-        caps' = T.splitOn "," . T.pack =<< listofstringopt "capabilities" rawopts
-        caps = case traverse capabilityFromText caps' of
-          Left e -> error' ("Unknown capability: " ++ T.unpack e)  -- PARTIAL:
-          Right [] -> [CapView, CapAdd]
-          Right xs -> xs
+        b = maybe (defbaseurl h p) stripTrailingSlash $ maybestringopt "base-url" rawopts
         sock = stripTrailingSlash <$> maybestringopt "socket" rawopts
+        access =
+          case lastMay $ listofstringopt "allow" rawopts of
+            Nothing -> AddAccess
+            Just t ->
+              case parseAccessLevel t of
+                Right al -> al
+                Left err -> error' ("Unknown access level: " ++ err)  -- PARTIAL:
     return
       defwebopts
       { serve_ = case sock of
@@ -173,8 +167,7 @@
       , port_ = p
       , base_url_ = b
       , file_url_ = stripTrailingSlash <$> maybestringopt "file-url" rawopts
-      , capabilities_ = caps
-      , capabilitiesHeader_ = mk . BC.pack <$> maybestringopt "capabilities-header" rawopts
+      , allow_ = access
       , cliopts_ = cliopts
       , socket_ = sock
       }
@@ -189,28 +182,48 @@
   args <- fmap (replaceNumericFlags . ensureDebugHasArg) . expandArgsAt =<< getArgs
   rawOptsToWebOpts . either usageError id $ process webmode args
 
-data Capability
-  = CapView
-  | CapAdd
-  | CapManage
+data Permission
+  = ViewPermission  -- ^ allow viewing things (read only)
+  | AddPermission   -- ^ allow adding transactions, or more generally allow appending text to input files 
+  | EditPermission  -- ^ allow editing input files
   deriving (Eq, Ord, Bounded, Enum, Show)
 
-capabilityFromText :: Text -> Either Text Capability
-capabilityFromText "view" = Right CapView
-capabilityFromText "add" = Right CapAdd
-capabilityFromText "manage" = Right CapManage
-capabilityFromText x = Left x
+parsePermission :: ByteString -> Either Text Permission
+parsePermission "view" = Right ViewPermission
+parsePermission "add"  = Right AddPermission
+parsePermission "edit" = Right EditPermission
+parsePermission x = Left $ T.pack $ BC.unpack x
 
-capabilityFromBS :: ByteString -> Either ByteString Capability
-capabilityFromBS "view" = Right CapView
-capabilityFromBS "add" = Right CapAdd
-capabilityFromBS "manage" = Right CapManage
-capabilityFromBS x = Left x
+-- | Convert to the lower case permission name.
+showPermission :: Permission -> String
+showPermission p = map toLower $ reverse $ drop 10 $ reverse $ show p
 
+-- | For the --allow option: how much access to allow to hledger-web users ?
+data AccessLevel =
+    ViewAccess       -- ^ view permission only
+  | AddAccess        -- ^ view and add permissions
+  | EditAccess       -- ^ view, add and edit permissions
+  | SandstormAccess  -- ^ the permissions specified by the X-Sandstorm-Permissions HTTP request header
+  deriving (Eq, Ord, Bounded, Enum, Show)
+
+parseAccessLevel :: String -> Either String AccessLevel
+parseAccessLevel "view"      = Right ViewAccess
+parseAccessLevel "add"       = Right AddAccess
+parseAccessLevel "edit"      = Right EditAccess
+parseAccessLevel "sandstorm" = Right SandstormAccess
+parseAccessLevel s = Left $ s <> ", should be one of: view, add, edit, sandstorm"
+
+-- | Convert an --allow access level to the permissions used internally.
+-- SandstormAccess generates an empty list, to be filled in later.
+accessLevelToPermissions :: AccessLevel -> [Permission]
+accessLevelToPermissions ViewAccess      = [ViewPermission]
+accessLevelToPermissions AddAccess       = [ViewPermission, AddPermission]
+accessLevelToPermissions EditAccess      = [ViewPermission, AddPermission, EditPermission]
+accessLevelToPermissions SandstormAccess = []  -- detected from request header
+
 simplePolicyWithOrigin :: Origin -> CorsResourcePolicy
 simplePolicyWithOrigin origin =
     simpleCorsResourcePolicy { corsOrigins = Just ([origin], False) }
-
 
 corsPolicyFromString :: String -> WAI.Middleware
 corsPolicyFromString origin =
diff --git a/hledger-web.1 b/hledger-web.1
--- a/hledger-web.1
+++ b/hledger-web.1
@@ -1,21 +1,18 @@
 
-.TH "HLEDGER-WEB" "1" "September 2023" "hledger-web-1.31 " "hledger User Manuals"
+.TH "HLEDGER-WEB" "1" "December 2023" "hledger-web-1.32 " "hledger User Manuals"
 
 
 
 .SH NAME
-.PP
 hledger-web - robust, friendly plain text accounting (Web version)
 .SH SYNOPSIS
-.PP
-\f[V]hledger-web    [--serve|--serve-api] [OPTS] [ARGS]\f[R]
+\f[CR]hledger-web    [--serve|--serve-api] [OPTS] [ARGS]\f[R]
 .PD 0
 .P
 .PD
-\f[V]hledger web -- [--serve|--serve-api] [OPTS] [ARGS]\f[R]
+\f[CR]hledger web -- [--serve|--serve-api] [OPTS] [ARGS]\f[R]
 .SH DESCRIPTION
-.PP
-This manual is for hledger\[aq]s web interface, version 1.31.
+This manual is for hledger\[aq]s web interface, version 1.32.
 See also the hledger manual for common concepts and file formats.
 .PP
 hledger is a robust, user-friendly, cross-platform set of programs for
@@ -40,9 +37,9 @@
 every edit.
 .PP
 Like hledger, it reads from (and appends to) a journal file specified by
-the \f[V]LEDGER_FILE\f[R] environment variable (defaulting to
-\f[V]$HOME/.hledger.journal\f[R]); or you can specify files with
-\f[V]-f\f[R] options.
+the \f[CR]LEDGER_FILE\f[R] environment variable (defaulting to
+\f[CR]$HOME/.hledger.journal\f[R]); or you can specify files with
+\f[CR]-f\f[R] options.
 It can also read timeclock files, timedot files, or any CSV/SSV/TSV file
 with a date field.
 (See hledger(1) -> Input for details.)
@@ -54,15 +51,14 @@
 minutes of inactivity (no requests received and no open browser windows
 viewing it).
 .IP \[bu] 2
-With \f[V]--serve\f[R]: the app runs without stopping, and without
+With \f[CR]--serve\f[R]: the app runs without stopping, and without
 opening a browser.
 .IP \[bu] 2
-With \f[V]--serve-api\f[R]: only the JSON API is served.
+With \f[CR]--serve-api\f[R]: only the JSON API is served.
 .PP
 In all cases hledger-web runs as a foreground process, logging requests
 to stdout.
 .SH OPTIONS
-.PP
 Command-line options and arguments may be used to set an initial filter
 on the data.
 These filter options are not shown in the web UI, but it will be applied
@@ -70,85 +66,82 @@
 .PP
 hledger-web provides the following options:
 .TP
-\f[V]--serve\f[R]
+\f[CR]--serve\f[R]
 serve and log requests, don\[aq]t browse or auto-exit after timeout
 .TP
-\f[V]--serve-api\f[R]
+\f[CR]--serve-api\f[R]
 like --serve, but serve only the JSON web API, without the server-side
 web UI
 .TP
-\f[V]--host=IPADDR\f[R]
+\f[CR]--host=IPADDR\f[R]
 listen on this IP address (default: 127.0.0.1)
 .TP
-\f[V]--port=PORT\f[R]
+\f[CR]--port=PORT\f[R]
 listen on this TCP port (default: 5000)
 .TP
-\f[V]--socket=SOCKETFILE\f[R]
+\f[CR]--socket=SOCKETFILE\f[R]
 use a unix domain socket file to listen for requests instead of a TCP
 socket.
-Implies \f[V]--serve\f[R].
+Implies \f[CR]--serve\f[R].
 It can only be used if the operating system can provide this type of
 socket.
 .TP
-\f[V]--base-url=URL\f[R]
+\f[CR]--base-url=URL\f[R]
 set the base url (default: http://IPADDR:PORT).
 Note: affects url generation but not route parsing.
 Can be useful if running behind a reverse web proxy that does path
 rewriting.
 .TP
-\f[V]--file-url=URL\f[R]
+\f[CR]--file-url=URL\f[R]
 set the static files url (default: BASEURL/static).
 hledger-web normally serves static files itself, but if you wanted to
 serve them from another server for efficiency, you would set the url
 with this.
 .TP
-\f[V]--capabilities=CAP[,CAP..]\f[R]
-enable the view, add, and/or manage capabilities (default: view,add)
-.TP
-\f[V]--capabilities-header=HTTPHEADER\f[R]
-read capabilities to enable from a HTTP header, like
-X-Sandstorm-Permissions (default: disabled)
+\f[CR]--allow=view|add|edit\f[R]
+set the user\[aq]s access level for changing data (default:
+\f[CR]add\f[R]).
+It also accepts \f[CR]sandstorm\f[R] for use on that platform (reads
+permissions from the \f[CR]X-Sandstorm-Permissions\f[R] request header).
 .TP
-\f[V]--test\f[R]
+\f[CR]--test\f[R]
 run hledger-web\[aq]s tests and exit.
 hspec test runner args may follow a --, eg: hledger-web --test -- --help
 .PP
 By default the server listens on IP address 127.0.0.1, accessible only
 to local requests.
-You can use \f[V]--host\f[R] to change this, eg \f[V]--host 0.0.0.0\f[R]
-to listen on all configured addresses.
+You can use \f[CR]--host\f[R] to change this, eg
+\f[CR]--host 0.0.0.0\f[R] to listen on all configured addresses.
 .PP
-Similarly, use \f[V]--port\f[R] to set a TCP port other than 5000, eg if
-you are running multiple hledger-web instances.
+Similarly, use \f[CR]--port\f[R] to set a TCP port other than 5000, eg
+if you are running multiple hledger-web instances.
 .PP
-Both of these options are ignored when \f[V]--socket\f[R] is used.
-In this case, it creates an \f[V]AF_UNIX\f[R] socket file at the
+Both of these options are ignored when \f[CR]--socket\f[R] is used.
+In this case, it creates an \f[CR]AF_UNIX\f[R] socket file at the
 supplied path and uses that for communication.
 This is an alternative way of running multiple hledger-web instances
 behind a reverse proxy that handles authentication for different users.
 The path can be derived in a predictable way, eg by using the username
 within the path.
-As an example, \f[V]nginx\f[R] as reverse proxy can use the variable
-\f[V]$remote_user\f[R] to derive a path from the username used in a HTTP
-basic authentication.
-The following \f[V]proxy_pass\f[R] directive allows access to all
-\f[V]hledger-web\f[R] instances that created a socket in
-\f[V]/tmp/hledger/\f[R]:
+As an example, \f[CR]nginx\f[R] as reverse proxy can use the variable
+\f[CR]$remote_user\f[R] to derive a path from the username used in a
+HTTP basic authentication.
+The following \f[CR]proxy_pass\f[R] directive allows access to all
+\f[CR]hledger-web\f[R] instances that created a socket in
+\f[CR]/tmp/hledger/\f[R]:
 .IP
-.nf
-\f[C]
+.EX
   proxy_pass http://unix:/tmp/hledger/${remote_user}.socket;
-\f[R]
-.fi
+.EE
 .PP
-You can use \f[V]--base-url\f[R] to change the protocol, hostname, port
+You can use \f[CR]--base-url\f[R] to change the protocol, hostname, port
 and path that appear in hyperlinks, useful eg for integrating
 hledger-web within a larger website.
-The default is \f[V]http://HOST:PORT/\f[R] using the server\[aq]s
-configured host address and TCP port (or \f[V]http://HOST\f[R] if PORT
+The default is \f[CR]http://HOST:PORT/\f[R] using the server\[aq]s
+configured host address and TCP port (or \f[CR]http://HOST\f[R] if PORT
 is 80).
 .PP
-With \f[V]--file-url\f[R] you can set a different base url for static
+With \f[CR]--file-url\f[R] you can set a different base url for static
 files, eg for better caching or cookie-less serving on high performance
 websites.
 .PP
@@ -156,144 +149,144 @@
 hledger manual\[aq]s command line tips also apply here):
 .SS General help options
 .TP
-\f[V]-h --help\f[R]
+\f[CR]-h --help\f[R]
 show general or COMMAND help
 .TP
-\f[V]--man\f[R]
+\f[CR]--man\f[R]
 show general or COMMAND user manual with man
 .TP
-\f[V]--info\f[R]
+\f[CR]--info\f[R]
 show general or COMMAND user manual with info
 .TP
-\f[V]--version\f[R]
+\f[CR]--version\f[R]
 show general or ADDONCMD version
 .TP
-\f[V]--debug[=N]\f[R]
+\f[CR]--debug[=N]\f[R]
 show debug output (levels 1-9, default: 1)
 .SS General input options
 .TP
-\f[V]-f FILE --file=FILE\f[R]
+\f[CR]-f FILE --file=FILE\f[R]
 use a different input file.
-For stdin, use - (default: \f[V]$LEDGER_FILE\f[R] or
-\f[V]$HOME/.hledger.journal\f[R])
+For stdin, use - (default: \f[CR]$LEDGER_FILE\f[R] or
+\f[CR]$HOME/.hledger.journal\f[R])
 .TP
-\f[V]--rules-file=RULESFILE\f[R]
+\f[CR]--rules-file=RULESFILE\f[R]
 Conversion rules file to use when reading CSV (default: FILE.rules)
 .TP
-\f[V]--separator=CHAR\f[R]
+\f[CR]--separator=CHAR\f[R]
 Field separator to expect when reading CSV (default: \[aq],\[aq])
 .TP
-\f[V]--alias=OLD=NEW\f[R]
+\f[CR]--alias=OLD=NEW\f[R]
 rename accounts named OLD to NEW
 .TP
-\f[V]--anon\f[R]
+\f[CR]--anon\f[R]
 anonymize accounts and payees
 .TP
-\f[V]--pivot FIELDNAME\f[R]
+\f[CR]--pivot FIELDNAME\f[R]
 use some other field or tag for the account name
 .TP
-\f[V]-I --ignore-assertions\f[R]
+\f[CR]-I --ignore-assertions\f[R]
 disable balance assertion checks (note: does not disable balance
 assignments)
 .TP
-\f[V]-s --strict\f[R]
+\f[CR]-s --strict\f[R]
 do extra error checking (check that all posted accounts are declared)
 .SS General reporting options
 .TP
-\f[V]-b --begin=DATE\f[R]
+\f[CR]-b --begin=DATE\f[R]
 include postings/txns on or after this date (will be adjusted to
 preceding subperiod start when using a report interval)
 .TP
-\f[V]-e --end=DATE\f[R]
+\f[CR]-e --end=DATE\f[R]
 include postings/txns before this date (will be adjusted to following
 subperiod end when using a report interval)
 .TP
-\f[V]-D --daily\f[R]
+\f[CR]-D --daily\f[R]
 multiperiod/multicolumn report by day
 .TP
-\f[V]-W --weekly\f[R]
+\f[CR]-W --weekly\f[R]
 multiperiod/multicolumn report by week
 .TP
-\f[V]-M --monthly\f[R]
+\f[CR]-M --monthly\f[R]
 multiperiod/multicolumn report by month
 .TP
-\f[V]-Q --quarterly\f[R]
+\f[CR]-Q --quarterly\f[R]
 multiperiod/multicolumn report by quarter
 .TP
-\f[V]-Y --yearly\f[R]
+\f[CR]-Y --yearly\f[R]
 multiperiod/multicolumn report by year
 .TP
-\f[V]-p --period=PERIODEXP\f[R]
+\f[CR]-p --period=PERIODEXP\f[R]
 set start date, end date, and/or reporting interval all at once using
 period expressions syntax
 .TP
-\f[V]--date2\f[R]
+\f[CR]--date2\f[R]
 match the secondary date instead (see command help for other effects)
 .TP
-\f[V]--today=DATE\f[R]
+\f[CR]--today=DATE\f[R]
 override today\[aq]s date (affects relative smart dates, for
 tests/examples)
 .TP
-\f[V]-U --unmarked\f[R]
+\f[CR]-U --unmarked\f[R]
 include only unmarked postings/txns (can combine with -P or -C)
 .TP
-\f[V]-P --pending\f[R]
+\f[CR]-P --pending\f[R]
 include only pending postings/txns
 .TP
-\f[V]-C --cleared\f[R]
+\f[CR]-C --cleared\f[R]
 include only cleared postings/txns
 .TP
-\f[V]-R --real\f[R]
+\f[CR]-R --real\f[R]
 include only non-virtual postings
 .TP
-\f[V]-NUM --depth=NUM\f[R]
+\f[CR]-NUM --depth=NUM\f[R]
 hide/aggregate accounts or postings more than NUM levels deep
 .TP
-\f[V]-E --empty\f[R]
+\f[CR]-E --empty\f[R]
 show items with zero amount, normally hidden (and vice-versa in
 hledger-ui/hledger-web)
 .TP
-\f[V]-B --cost\f[R]
+\f[CR]-B --cost\f[R]
 convert amounts to their cost/selling amount at transaction time
 .TP
-\f[V]-V --market\f[R]
+\f[CR]-V --market\f[R]
 convert amounts to their market value in default valuation commodities
 .TP
-\f[V]-X --exchange=COMM\f[R]
+\f[CR]-X --exchange=COMM\f[R]
 convert amounts to their market value in commodity COMM
 .TP
-\f[V]--value\f[R]
+\f[CR]--value\f[R]
 convert amounts to cost or market value, more flexibly than -B/-V/-X
 .TP
-\f[V]--infer-equity\f[R]
+\f[CR]--infer-equity\f[R]
 infer conversion equity postings from costs
 .TP
-\f[V]--infer-costs\f[R]
+\f[CR]--infer-costs\f[R]
 infer costs from conversion equity postings
 .TP
-\f[V]--infer-market-prices\f[R]
+\f[CR]--infer-market-prices\f[R]
 use costs as additional market prices, as if they were P directives
 .TP
-\f[V]--forecast\f[R]
+\f[CR]--forecast\f[R]
 generate transactions from periodic rules,
 between the latest recorded txn and 6 months from today,
 or during the specified PERIOD (= is required).
 Auto posting rules will be applied to these transactions as well.
 Also, in hledger-ui make future-dated transactions visible.
 .TP
-\f[V]--auto\f[R]
+\f[CR]--auto\f[R]
 generate extra postings by applying auto posting rules to all txns (not
 just forecast txns)
 .TP
-\f[V]--verbose-tags\f[R]
+\f[CR]--verbose-tags\f[R]
 add visible tags indicating transactions or postings which have been
 generated/modified
 .TP
-\f[V]--commodity-style\f[R]
+\f[CR]--commodity-style\f[R]
 Override the commodity style in the output for the specified commodity.
 For example \[aq]EUR1.000,00\[aq].
 .TP
-\f[V]--color=WHEN (or --colour=WHEN)\f[R]
+\f[CR]--color=WHEN (or --colour=WHEN)\f[R]
 Should color-supporting commands use ANSI color codes in text output.
 \[aq]auto\[aq] (default): whenever stdout seems to be a color-supporting
 terminal.
@@ -302,7 +295,7 @@
 \[aq]never\[aq] or \[aq]no\[aq]: never.
 A NO_COLOR environment variable overrides this.
 .TP
-\f[V]--pretty[=WHEN]\f[R]
+\f[CR]--pretty[=WHEN]\f[R]
 Show prettier output, e.g.
 using unicode box-drawing characters.
 Accepts \[aq]yes\[aq] (the default) or \[aq]no\[aq] (\[aq]y\[aq],
@@ -315,13 +308,12 @@
 .PP
 Some reporting options can also be written as query arguments.
 .SH PERMISSIONS
-.PP
 By default, hledger-web allows anyone who can reach it to view the
 journal and to add new transactions, but not to change existing data.
 .PP
 You can restrict who can reach it by
 .IP \[bu] 2
-setting the IP address it listens on (see \f[V]--host\f[R] above).
+setting the IP address it listens on (see \f[CR]--host\f[R] above).
 By default it listens on 127.0.0.1, accessible to all users on the local
 machine.
 .IP \[bu] 2
@@ -331,27 +323,26 @@
 .PP
 You can restrict what the users who reach it can do, by
 .IP \[bu] 2
-using the \f[V]--capabilities=CAP[,CAP..]\f[R] flag when you start it,
+using the \f[CR]--capabilities=CAP[,CAP..]\f[R] flag when you start it,
 enabling one or more of the following capabilities.
-The default value is \f[V]view,add\f[R]:
+The default value is \f[CR]view,add\f[R]:
 .RS 2
 .IP \[bu] 2
-\f[V]view\f[R] - allows viewing the journal file and all included files
+\f[CR]view\f[R] - allows viewing the journal file and all included files
 .IP \[bu] 2
-\f[V]add\f[R] - allows adding new transactions to the main journal file
+\f[CR]add\f[R] - allows adding new transactions to the main journal file
 .IP \[bu] 2
-\f[V]manage\f[R] - allows editing, uploading or downloading the main or
+\f[CR]manage\f[R] - allows editing, uploading or downloading the main or
 included files
 .RE
 .IP \[bu] 2
-using the \f[V]--capabilities-header=HTTPHEADER\f[R] flag to specify a
+using the \f[CR]--capabilities-header=HTTPHEADER\f[R] flag to specify a
 HTTP header from which it will read capabilities to enable.
 hledger-web on Sandstorm uses the X-Sandstorm-Permissions header to
 integrate with Sandstorm\[aq]s permissions.
 This is disabled by default.
 .SH EDITING, UPLOADING, DOWNLOADING
-.PP
-If you enable the \f[V]manage\f[R] capability mentioned above,
+If you enable the \f[CR]manage\f[R] capability mentioned above,
 you\[aq]ll see a new \[dq]spanner\[dq] button to the right of the search
 form.
 Clicking this will let you edit, upload, or download the journal file or
@@ -372,7 +363,6 @@
 (Probably.
 This needs re-testing.)
 .SH RELOADING
-.PP
 hledger-web detects changes made to the files by other means (eg if you
 edit it directly, outside of hledger-web), and it will show the new data
 when you reload the page or navigate to a new page.
@@ -382,24 +372,20 @@
 (Note: if you are viewing files mounted from another machine, make sure
 that both machine clocks are roughly in step.)
 .SH JSON API
-.PP
 In addition to the web UI, hledger-web also serves a JSON API that can
 be used to get data or add new transactions.
-If you want the JSON API only, you can use the \f[V]--serve-api\f[R]
+If you want the JSON API only, you can use the \f[CR]--serve-api\f[R]
 flag.
 Eg:
 .IP
-.nf
-\f[C]
+.EX
 $ hledger-web -f examples/sample.journal --serve-api
 \&...
-\f[R]
-.fi
+.EE
 .PP
 You can get JSON data from these routes:
 .IP
-.nf
-\f[C]
+.EX
 /version
 /accountnames
 /transactions
@@ -407,15 +393,13 @@
 /commodities
 /accounts
 /accounttransactions/ACCOUNTNAME
-\f[R]
-.fi
+.EE
 .PP
 Eg, all account names in the journal (similar to the accounts command).
 (hledger-web\[aq]s JSON does not include newlines, here we use python to
 prettify it):
 .IP
-.nf
-\f[C]
+.EX
 $ curl -s http://127.0.0.1:5000/accountnames | python -m json.tool
 [
     \[dq]assets\[dq],
@@ -432,13 +416,11 @@
     \[dq]liabilities\[dq],
     \[dq]liabilities:debts\[dq]
 ]
-\f[R]
-.fi
+.EE
 .PP
 Or all transactions:
 .IP
-.nf
-\f[C]
+.EX
 $ curl -s http://127.0.0.1:5000/transactions | python -m json.tool
 [
     {
@@ -457,8 +439,7 @@
                         \[dq]aismultiplier\[dq]: false,
                         \[dq]aprice\[dq]: null,
 \&...
-\f[R]
-.fi
+.EE
 .PP
 Most of the JSON corresponds to hledger\[aq]s data types; for details of
 what the fields mean, see the Hledger.Data.Json haddock docs and click
@@ -469,34 +450,31 @@
 type.
 To understand that, go to the Hledger.Web.Handler.MiscR haddock and look
 at the source for the appropriate handler to see what it returns.
-Eg for \f[V]/accounttransactions\f[R] it\[aq]s getAccounttransactionsR,
-returning a \[dq]\f[V]accountTransactionsReport ...\f[R]\[dq].
+Eg for \f[CR]/accounttransactions\f[R] it\[aq]s getAccounttransactionsR,
+returning a \[dq]\f[CR]accountTransactionsReport ...\f[R]\[dq].
 Looking up the haddock for that we can see that /accounttransactions
 returns an AccountTransactionsReport, which consists of a report title
 and a list of AccountTransactionsReportItem (etc).
 .PP
 You can add a new transaction to the journal with a PUT request to
-\f[V]/add\f[R], if hledger-web was started with the \f[V]add\f[R]
+\f[CR]/add\f[R], if hledger-web was started with the \f[CR]add\f[R]
 capability (enabled by default).
 The payload must be the full, exact JSON representation of a hledger
 transaction (partial data won\[aq]t do).
-You can get sample JSON from hledger-web\[aq]s \f[V]/transactions\f[R]
-or \f[V]/accounttransactions\f[R], or you can export it with
+You can get sample JSON from hledger-web\[aq]s \f[CR]/transactions\f[R]
+or \f[CR]/accounttransactions\f[R], or you can export it with
 hledger-lib, eg like so:
 .IP
-.nf
-\f[C]
+.EX
 \&.../hledger$ stack ghci hledger-lib
 >>> writeJsonFile \[dq]txn.json\[dq] (head $ jtxns samplejournal)
 >>> :q
-\f[R]
-.fi
+.EE
 .PP
 Here\[aq]s how it looks as of hledger-1.17 (remember, this JSON
 corresponds to hledger\[aq]s Transaction and related data types):
 .IP
-.nf
-\f[C]
+.EX
 {
     \[dq]tcomment\[dq]: \[dq]\[dq],
     \[dq]tpostings\[dq]: [
@@ -582,21 +560,17 @@
     \[dq]tdescription\[dq]: \[dq]income\[dq],
     \[dq]tstatus\[dq]: \[dq]Unmarked\[dq]
 }
-\f[R]
-.fi
+.EE
 .PP
 And here\[aq]s how to test adding it with curl.
 This should add a new entry to your journal:
 .IP
-.nf
-\f[C]
+.EX
 $ curl http://127.0.0.1:5000/add -X PUT -H \[aq]Content-Type: application/json\[aq] --data-binary \[at]txn.json
-\f[R]
-.fi
+.EE
 .SH DEBUG OUTPUT
 .SS Debug output
-.PP
-You can add \f[V]--debug[=N]\f[R] to the command line to log debug
+You can add \f[CR]--debug[=N]\f[R] to the command line to log debug
 output.
 N ranges from 1 (least output, the default) to 9 (maximum output).
 Typically you would start with 1 and increase until you are seeing
@@ -608,14 +582,12 @@
 .PD 0
 .P
 .PD
-\f[V]hledger-web --debug=3 2>hledger-web.log\f[R].
+\f[CR]hledger-web --debug=3 2>hledger-web.log\f[R].
 .SH ENVIRONMENT
-.PP
 \f[B]LEDGER_FILE\f[R] The main journal file to use when not specified
-with \f[V]-f/--file\f[R].
-Default: \f[V]$HOME/.hledger.journal\f[R].
+with \f[CR]-f/--file\f[R].
+Default: \f[CR]$HOME/.hledger.journal\f[R].
 .SH BUGS
-.PP
 We welcome bug reports in the hledger issue tracker (shortcut:
 http://bugs.hledger.org), or on the #hledger chat or hledger mail list
 (https://hledger.org/support).
diff --git a/hledger-web.cabal b/hledger-web.cabal
--- a/hledger-web.cabal
+++ b/hledger-web.cabal
@@ -1,13 +1,13 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.35.1.
+-- This file has been generated from package.yaml by hpack version 0.36.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:           hledger-web
-version:        1.31
-synopsis:       Web-based user interface for the hledger accounting system
-description:    A simple web-based user interface for the hledger accounting system,
+version:        1.32
+synopsis:       Web user interface for the hledger accounting system
+description:    A simple web user interface for the hledger accounting system,
                 providing a more modern UI than the command-line or terminal interfaces.
                 It can be used as a local single-user UI, or as a multi-user UI for
                 viewing\/adding\/editing on the web.
@@ -130,7 +130,12 @@
 library
   exposed-modules:
       Hledger.Web
+      Hledger.Web.Main
+      Hledger.Web.WebOptions
       Hledger.Web.Application
+      Hledger.Web.Import
+      Hledger.Web.Test
+  other-modules:
       Hledger.Web.Foundation
       Hledger.Web.Handler.AddR
       Hledger.Web.Handler.EditR
@@ -138,20 +143,15 @@
       Hledger.Web.Handler.MiscR
       Hledger.Web.Handler.RegisterR
       Hledger.Web.Handler.UploadR
-      Hledger.Web.Import
-      Hledger.Web.Main
       Hledger.Web.Settings
       Hledger.Web.Settings.StaticFiles
-      Hledger.Web.Test
-      Hledger.Web.WebOptions
       Hledger.Web.Widget.AddForm
       Hledger.Web.Widget.Common
-  other-modules:
       Paths_hledger_web
   hs-source-dirs:
       ./
   ghc-options: -Wall -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns
-  cpp-options: -DVERSION="1.31"
+  cpp-options: -DVERSION="1.32"
   build-depends:
       Decimal >=0.5.1
     , aeson >=1 && <2.3
@@ -171,8 +171,8 @@
     , extra >=1.6.3
     , filepath
     , hjsmin
-    , hledger ==1.31.*
-    , hledger-lib ==1.31.*
+    , hledger ==1.32.*
+    , hledger-lib ==1.32.*
     , hspec
     , http-client
     , http-conduit
@@ -180,9 +180,10 @@
     , megaparsec >=7.0.0 && <9.6
     , mtl >=2.2.1
     , network
+    , safe >=0.3.19
     , shakespeare >=2.0.2.2
     , template-haskell
-    , text >=1.2
+    , text >=1.2.4.1
     , time >=1.5
     , transformers
     , unix-compat
@@ -212,7 +213,7 @@
   hs-source-dirs:
       app
   ghc-options: -Wall -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns
-  cpp-options: -DVERSION="1.31"
+  cpp-options: -DVERSION="1.32"
   build-depends:
       base >=4.14 && <4.19
     , hledger-web
@@ -232,16 +233,10 @@
   hs-source-dirs:
       test
   ghc-options: -Wall -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns
-  cpp-options: -DVERSION="1.31"
+  cpp-options: -DVERSION="1.32"
   build-depends:
       base >=4.14 && <4.19
-    , hledger
-    , hledger-lib
     , hledger-web
-    , hspec
-    , text
-    , yesod
-    , yesod-test
   default-language: Haskell2010
   if (flag(dev)) || (flag(library-only))
     cpp-options: -DDEVELOPMENT
diff --git a/hledger-web.info b/hledger-web.info
--- a/hledger-web.info
+++ b/hledger-web.info
@@ -16,7 +16,7 @@
    'hledger-web [--serve|--serve-api] [OPTS] [ARGS]'
 'hledger web -- [--serve|--serve-api] [OPTS] [ARGS]'
 
-   This manual is for hledger's web interface, version 1.31.  See also
+   This manual is for hledger's web interface, version 1.32.  See also
 the hledger manual for common concepts and file formats.
 
    hledger is a robust, user-friendly, cross-platform set of programs
@@ -110,14 +110,11 @@
      normally serves static files itself, but if you wanted to serve
      them from another server for efficiency, you would set the url with
      this.
-'--capabilities=CAP[,CAP..]'
-
-     enable the view, add, and/or manage capabilities (default:
-     view,add)
-'--capabilities-header=HTTPHEADER'
+'--allow=view|add|edit'
 
-     read capabilities to enable from a HTTP header, like
-     X-Sandstorm-Permissions (default: disabled)
+     set the user's access level for changing data (default: 'add').  It
+     also accepts 'sandstorm' for use on that platform (reads
+     permissions from the 'X-Sandstorm-Permissions' request header).
 '--test'
 
      run hledger-web's tests and exit.  hspec test runner args may
@@ -648,28 +645,28 @@
 Node: Top225
 Node: OPTIONS2577
 Ref: #options2682
-Node: General help options5993
-Ref: #general-help-options6143
-Node: General input options6425
-Ref: #general-input-options6611
-Node: General reporting options7313
-Ref: #general-reporting-options7478
-Node: PERMISSIONS10868
-Ref: #permissions11007
-Node: EDITING UPLOADING DOWNLOADING12219
-Ref: #editing-uploading-downloading12400
-Node: RELOADING13234
-Ref: #reloading13368
-Node: JSON API13801
-Ref: #json-api13916
-Node: DEBUG OUTPUT19404
-Ref: #debug-output19529
-Node: Debug output19556
-Ref: #debug-output-119657
-Node: ENVIRONMENT20074
-Ref: #environment20193
-Node: BUGS20310
-Ref: #bugs20394
+Node: General help options5970
+Ref: #general-help-options6120
+Node: General input options6402
+Ref: #general-input-options6588
+Node: General reporting options7290
+Ref: #general-reporting-options7455
+Node: PERMISSIONS10845
+Ref: #permissions10984
+Node: EDITING UPLOADING DOWNLOADING12196
+Ref: #editing-uploading-downloading12377
+Node: RELOADING13211
+Ref: #reloading13345
+Node: JSON API13778
+Ref: #json-api13893
+Node: DEBUG OUTPUT19381
+Ref: #debug-output19506
+Node: Debug output19533
+Ref: #debug-output-119634
+Node: ENVIRONMENT20051
+Ref: #environment20170
+Node: BUGS20287
+Ref: #bugs20371
 
 End Tag Table
 
diff --git a/hledger-web.txt b/hledger-web.txt
--- a/hledger-web.txt
+++ b/hledger-web.txt
@@ -9,7 +9,7 @@
        hledger web -- [--serve|--serve-api] [OPTS] [ARGS]
 
 DESCRIPTION
-       This manual is for hledger's web interface, version 1.31.  See also the
+       This manual is for hledger's web interface, version 1.32.  See also the
        hledger manual for common concepts and file formats.
 
        hledger  is a robust, user-friendly, cross-platform set of programs for
@@ -87,13 +87,10 @@
               them from another server for efficiency, you would set  the  url
               with this.
 
-       --capabilities=CAP[,CAP..]
-              enable  the  view,  add,  and/or  manage  capabilities (default:
-              view,add)
-
-       --capabilities-header=HTTPHEADER
-              read capabilities to enable from a  HTTP  header,  like  X-Sand-
-              storm-Permissions (default: disabled)
+       --allow=view|add|edit
+              set  the  user's  access level for changing data (default: add).
+              It also accepts sandstorm for use on that platform  (reads  per-
+              missions from the X-Sandstorm-Permissions request header).
 
        --test run  hledger-web's  tests  and exit.  hspec test runner args may
               follow a --, eg: hledger-web --test -- --help
@@ -567,4 +564,4 @@
 SEE ALSO
        hledger(1), hledger-ui(1), hledger-web(1), ledger(1)
 
-hledger-web-1.31                September 2023                  HLEDGER-WEB(1)
+hledger-web-1.32                 December 2023                  HLEDGER-WEB(1)
diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet
--- a/templates/default-layout.hamlet
+++ b/templates/default-layout.hamlet
@@ -7,7 +7,7 @@
 <div#topbar .col-md-8 .col-sm-8 .col-xs-10>
   <h1>#{takeFileName (journalFilePath j)}
 
-$if elem CapView caps
+$if elem ViewPermission perms
   <div#sidebar-menu .sidebar-offcanvas.#{sideShowmd}.#{sideShowsm}>
     <table .main-menu .table>
       ^{accounts}
@@ -15,7 +15,7 @@
 <div#main-content .col-xs-12.#{mainShowmd}.#{mainShowsm}>
   $maybe m <- msg
     <div #message .alert.alert-info>#{m}
-  $if elem CapView caps
+  $if elem ViewPermission perms
     <form#searchform.input-group method=GET>
       <input .form-control name=q value=#{qparam} placeholder="Search"
         title="Enter hledger search patterns to filter the data below">
@@ -25,7 +25,7 @@
             <span .glyphicon .glyphicon-remove-circle>
         <button .btn .btn-default type=submit title="Apply search terms">
           <span .glyphicon .glyphicon-search>
-        $if elem CapManage caps
+        $if elem EditPermission perms
           <a href="@{ManageR}" .btn.btn-default title="Manage journal files">
             <span .glyphicon .glyphicon-wrench>
         <button .btn .btn-default type=button data-toggle="modal" data-target="#helpmodal"
diff --git a/templates/edit-form.hamlet b/templates/edit-form.hamlet
--- a/templates/edit-form.hamlet
+++ b/templates/edit-form.hamlet
@@ -11,7 +11,7 @@
   <tr>
     <td style="border:0">
       <span.help>
-        ^{helplink "file-format" "File format help"}
+        ^{helplink "data-formats" "File format help"}
     <td .text-right style="border:0">
       <a.btn.btn-default href="@{ManageR}">Go back
       <input.btn.btn-default type=submit value="Save">
diff --git a/templates/journal.hamlet b/templates/journal.hamlet
--- a/templates/journal.hamlet
+++ b/templates/journal.hamlet
@@ -1,7 +1,7 @@
 <h2>
   #{title'}
 
-$if elem CapAdd caps
+$if elem AddPermission perms
   <a #addformlink href="#" role="button" style="cursor:pointer; margin-top:1em;"
      data-toggle="modal" data-target="#addmodal" title="Add a new transaction to the journal">
     Add a transaction
@@ -33,5 +33,5 @@
           <td .amount style="text-align:right;">
             ^{mixedAmountAsHtml amt}
 
-$if elem CapAdd caps
+$if elem AddPermission perms
   ^{addModal AddR j today}
diff --git a/templates/register.hamlet b/templates/register.hamlet
--- a/templates/register.hamlet
+++ b/templates/register.hamlet
@@ -35,5 +35,5 @@
           <td style="text-align:right;">
             ^{mixedAmountAsHtml bal}
 
-$if elem CapAdd caps
+$if elem AddPermission perms
   ^{addModal AddR j today}
