diff --git a/app/Desktop/Portal.hs b/app/Desktop/Portal.hs
new file mode 100644
--- /dev/null
+++ b/app/Desktop/Portal.hs
@@ -0,0 +1,183 @@
+module Desktop.Portal
+  ( Request,
+    GetUserInformationResponse (..),
+    OpenFileOptions (..),
+    getUserInformation,
+    openFile,
+    await,
+    cancel,
+  )
+where
+
+import Control.Concurrent (MVar, readMVar, tryPutMVar)
+import Control.Concurrent.MVar (newEmptyMVar)
+import Control.Exception (onException, throwIO)
+import Control.Monad (when)
+import DBus (BusName, InterfaceName, MemberName, MethodCall, ObjectPath)
+import DBus qualified
+import DBus.Client (Client, MatchRule (..))
+import DBus.Client qualified as DBus
+import DBus.Internal.Message (Signal (..))
+import DBus.Internal.Types (Variant)
+import Data.Default.Class (Default (def))
+import Data.Map (Map)
+import Data.Map.Strict qualified as Map
+import Data.Maybe (fromMaybe)
+import Data.Text (Text, pack, unpack)
+import Data.Word (Word32, Word64)
+import System.Random.Stateful qualified as R
+
+data GetUserInformationResponse = GetUserInformationResponse
+  { userId :: Text,
+    userName :: Text,
+    userImage :: Text
+  }
+  deriving (Eq, Show)
+
+data OpenFileOptions = OpenFileOptions
+  {title :: Maybe Text}
+  deriving (Eq, Show)
+
+instance Default OpenFileOptions where
+  def = OpenFileOptions {title = Nothing}
+
+-- | A request that may be in-progress, finished, or cancelled.
+data Request a = Request
+  { client :: Client,
+    methodCall :: MethodCall,
+    result :: MVar (Maybe a)
+  }
+
+instance Eq (Request a) where
+  a == b = a.result == b.result
+
+instance Show (Request a) where
+  show request =
+    "Request{client=<"
+      <> show request.client.clientThreadID
+      <> ">, methodCall="
+      <> show request.methodCall
+      <> ", result=<MVar>}"
+
+-- | Wait for a request to be finished, and return the result if it succeeded.
+await :: Request a -> IO (Maybe a)
+await request = do
+  readMVar request.result
+
+-- | Cancel a request. This will cause any threads blocked on 'await' to receive 'Nothing'.
+cancel :: Request a -> IO ()
+cancel request = do
+  putSucceeded <- tryPutMVar request.result Nothing
+  when putSucceeded $ do
+    -- Otherwise the request was already finished/cancelled. Don't bother calling
+    -- the Close method on the request because disconnection has the same effect.
+    DBus.disconnect request.client
+
+getUserInformation :: Text -> IO (Request GetUserInformationResponse)
+getUserInformation reason =
+  sendRequest "org.freedesktop.portal.Account" "GetUserInformation" [windowHandle] options parseResponse
+  where
+    windowHandle = DBus.toVariant ("" :: Text)
+    options = Map.singleton "reason" (DBus.toVariant reason)
+
+    parseResponse = \case
+      resMap
+        | Just userId <- DBus.fromVariant =<< Map.lookup "id" resMap,
+          Just userName <- DBus.fromVariant =<< Map.lookup "name" resMap,
+          Just userImage <- DBus.fromVariant =<< Map.lookup "image" resMap ->
+            pure GetUserInformationResponse {..}
+      _ ->
+        throwIO (DBus.clientError "getUserInformation: could not parse response")
+
+openFile :: OpenFileOptions -> IO (Request [Text])
+openFile ofOptions =
+  sendRequest "org.freedesktop.portal.FileChooser" "OpenFile" [windowHandle, DBus.toVariant title] mempty parseResponse
+  where
+    windowHandle = DBus.toVariant ("" :: Text)
+    title = fromMaybe "" ofOptions.title
+
+    parseResponse = \case
+      resMap
+        | Just (uris :: [Text]) <- DBus.fromVariant =<< Map.lookup "uris" resMap ->
+            pure uris
+      _ ->
+        throwIO (DBus.clientError "openFile: could not parse response")
+
+sendRequest ::
+  InterfaceName ->
+  MemberName ->
+  [Variant] ->
+  Map Text Variant ->
+  (Map Text Variant -> IO a) ->
+  IO (Request a)
+sendRequest interface memberName parameters options parseResponse = do
+  (client, clientName) <- connect
+  onException (sendRequest' client clientName) (DBus.disconnect client)
+  where
+    sendRequest' client clientName = do
+      (handle, token) <- requestHandle clientName
+      resultVar <- newEmptyMVar
+
+      -- listen before sending the request, to avoid a race condition where the
+      -- response happens before we get a chance to register the listener for it
+      _ <-
+        DBus.addMatch
+          client
+          DBus.matchAny
+            { matchPath = Just handle,
+              matchInterface = Just "org.freedesktop.portal.Request",
+              matchMember = Just "Response"
+            }
+          ( \Signal {signalBody} -> do
+              val <- case signalBody of
+                [code, result]
+                  | Just (0 :: Word32) <- DBus.fromVariant code,
+                    Just (resMap :: Map Text Variant) <- DBus.fromVariant result -> do
+                      Just <$> parseResponse resMap
+                _ -> do
+                  pure Nothing
+              -- need to try because cancel might have been called and populated the mvar with Nothing
+              putSucceeded <- tryPutMVar resultVar val
+              when putSucceeded $
+                DBus.disconnect client -- we only use the connection for a single request, which is now done
+          )
+
+      let methodCall =
+            (DBus.methodCall "/org/freedesktop/portal/desktop" interface memberName)
+              { DBus.methodCallDestination = Just "org.freedesktop.portal.Desktop",
+                DBus.methodCallBody =
+                  parameters <> [DBus.toVariant (Map.insert "handle_token" (DBus.toVariant token) options)]
+              }
+
+      reply <- DBus.call_ client methodCall
+
+      case DBus.methodReturnBody reply of
+        [x]
+          | Just (objX :: ObjectPath) <- DBus.fromVariant x ->
+              if objX == handle
+                then pure (Request client methodCall resultVar)
+                else
+                  let msg = "Unexpected handle: " <> show objX <> " should be " <> show handle <> ". Probably xdg-desktop-portal is too old."
+                   in throwIO (DBus.clientError msg)
+        _ ->
+          throwIO (DBus.clientError ("Request reply in unexpected format: " <> show reply))
+
+connect :: IO (Client, BusName)
+connect = do
+  env <- DBus.getSessionAddress
+  case env of
+    Nothing -> throwIO (DBus.clientError "connect: session address not found.")
+    Just addr -> DBus.connectWithName DBus.defaultClientOptions addr
+
+requestToken :: IO Text
+requestToken = do
+  (rnd :: Word64) <- R.uniformM R.globalStdGen
+  pure ("monomer_flatpak_example_" <> pack (show rnd))
+
+requestHandle :: BusName -> IO (ObjectPath, Text)
+requestHandle clientName = do
+  token <- requestToken
+  pure (DBus.objectPath_ ("/org/freedesktop/portal/desktop/request/" <> escapeClientName clientName <> "/" <> unpack token), token)
+  where
+    escapeClientName =
+      map (\case '.' -> '_'; c -> c) . drop 1 . DBus.formatBusName
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,8 +1,11 @@
 module Main (main) where
 
+import Data.Default.Class (def)
 import Data.Sequence (Seq)
-import qualified Data.Sequence as Seq
-import Data.Text (Text, pack, unpack)
+import Data.Sequence qualified as Seq
+import Data.Text (Text, intercalate, pack, unpack)
+import Desktop.Portal (GetUserInformationResponse (..), Request)
+import Desktop.Portal qualified as Portal
 import Monomer
 import Monomer.Hagrid
 import Paths_monomer_flatpak_example (getDataFileName)
@@ -12,7 +15,8 @@
 
 data AppModel = AppModel
   { fileSystem :: Seq EnvironmentInfo,
-    environmentVariables :: Seq EnvironmentInfo
+    environmentVariables :: Seq EnvironmentInfo,
+    alertContents :: AlertContents
   }
   deriving (Eq, Show)
 
@@ -22,47 +26,119 @@
   }
   deriving (Eq, Show)
 
+data AlertContents
+  = AlertNotShown
+  | AlertRequestingUserInformation (Request GetUserInformationResponse)
+  | AlertUserInformation GetUserInformationResponse
+  | AlertRequestingOpenFile (Request [Text])
+  | AlertOpenFile [Text]
+  deriving (Eq, Show)
+
 data AppEvent
   = AppInit
   | AppInitFinish (Seq EnvironmentInfo) (Seq EnvironmentInfo)
+  | GetUserInformation
+  | GetUserInformationStart (Request GetUserInformationResponse)
+  | GetUserInformationFinish GetUserInformationResponse
+  | OpenFile
+  | OpenFileStart (Request [Text])
+  | OpenFileFinish [Text]
+  | CancelRequest
+  | CloseAlert
 
 main :: IO ()
 main = do
   regularFontPath <- pack <$> getDataFileName "/fonts/Cantarell/Cantarell-Regular.ttf"
+  boldFontPath <- pack <$> getDataFileName "/fonts/Cantarell/Cantarell-Bold.ttf"
   iconPath <- pack <$> getDataFileName "/io.github.Dretch.MonomerFlatpakExample.png"
-  startApp initialModel handleEvent buildUI (config regularFontPath iconPath)
+  startApp initialModel handleEvent buildUI (config regularFontPath boldFontPath iconPath)
   where
-    initialModel = AppModel {fileSystem = mempty, environmentVariables = mempty}
-    config regularFontPath iconPath =
+    initialModel =
+      AppModel
+        { fileSystem = mempty,
+          environmentVariables = mempty,
+          alertContents = AlertNotShown
+        }
+    config regularFontPath boldFontPath iconPath =
       [ appTheme darkTheme,
         appWindowTitle "Monomer Flatpak Example",
         appWindowIcon iconPath,
         appWindowState (MainWindowNormal (1000, 800)),
         appFontDef "Regular" regularFontPath,
+        appFontDef "Bold" boldFontPath,
         appDisableAutoScale True,
         appInitEvent AppInit
       ]
 
 buildUI :: UIBuilder AppModel AppEvent
-buildUI _wenv model =
-  vstack_
-    [childSpacing_ 5]
-    [ label "Monomer Flatpak Example"
-        `styleBasic` [textSize 40, paddingL 5, paddingT 10, paddingR 10],
-      label "This is a demo of the monomer framework running inside the Flatpak sandbox."
-        `styleBasic` [padding 5],
-      hagrid
-        [ (textColumn "File System Info" (.key)) {initialWidth = 300},
-          (widgetColumn "" valueColumn) {initialWidth = 700}
+buildUI _wenv model = tree
+  where
+    tree =
+      zstack
+        [ vstack_
+            [childSpacing_ 5]
+            [ label "Monomer Flatpak Example"
+                `styleBasic` [textSize 40, paddingL 5, paddingT 10, paddingR 10],
+              label "This is a demo of the monomer framework running inside the Flatpak sandbox."
+                `styleBasic` [padding 5],
+              hstack_
+                [childSpacing]
+                [ label "Portals:",
+                  button "Get User Information" GetUserInformation,
+                  button "Open File" OpenFile
+                ]
+                `styleBasic` [padding 5],
+              hagrid
+                [ (textColumn "File System Info" (.key)) {initialWidth = 300},
+                  (widgetColumn "" valueColumn) {initialWidth = 700}
+                ]
+                model.fileSystem,
+              hagrid
+                [ (textColumn "Environment Variable" (.key)) {initialWidth = 300},
+                  (widgetColumn "" valueColumn) {initialWidth = 700}
+                ]
+                model.environmentVariables
+            ],
+          -- nodeKey to workaround https://github.com/fjvallarino/monomer/issues/265
+          maybeAlert `nodeKey` pack (show model.alertContents)
         ]
-        model.fileSystem,
-      hagrid
-        [ (textColumn "Environment Variable" (.key)) {initialWidth = 300},
-          (widgetColumn "" valueColumn) {initialWidth = 700}
+
+    maybeAlert = case model.alertContents of
+      AlertNotShown ->
+        spacer `nodeVisible` False
+      AlertRequestingUserInformation _ ->
+        alert_
+          CancelRequest
+          [titleCaption "Getting User Information"]
+          (label "Wait or cancel by closing this alert..." `styleBasic` [padding 20])
+      AlertRequestingOpenFile _ ->
+        alert_
+          CancelRequest
+          [titleCaption "Opening File..."]
+          (label "Wait or cancel by closing this alert..." `styleBasic` [padding 20])
+      AlertUserInformation info ->
+        alert_
+          CloseAlert
+          [titleCaption "Get User Information Response"]
+          (userInfoAlertContents info `styleBasic` [padding 20])
+      AlertOpenFile files ->
+        alert_
+          CloseAlert
+          [titleCaption "Open File Response"]
+          (openFileAlertContents files `styleBasic` [padding 20])
+
+    userInfoAlertContents response =
+      vstack_
+        [childSpacing]
+        [ label "Request successful.",
+          label ("User Id: " <> response.userId),
+          label ("User Name: " <> response.userName),
+          label ("User Image: " <> response.userImage)
         ]
-        model.environmentVariables
-    ]
 
+    openFileAlertContents files =
+      label ("Selected files: " <> intercalate ", " files)
+
 valueColumn :: Int -> EnvironmentInfo -> WidgetNode s e
 valueColumn _ix model =
   label_ model.value [multiline, ellipsis]
@@ -91,3 +167,37 @@
     ]
   AppInitFinish fileSystem environmentVariables ->
     [Model model {fileSystem, environmentVariables}]
+  GetUserInformation ->
+    [ Producer $ \emit -> do
+        res <- Portal.getUserInformation "Allows FlatpakMonomerExample to show user information."
+        emit (GetUserInformationStart res)
+        Portal.await res >>= \case
+          Nothing -> emit CloseAlert
+          Just result -> emit (GetUserInformationFinish result)
+    ]
+  GetUserInformationStart res ->
+    [Model model {alertContents = AlertRequestingUserInformation res}]
+  GetUserInformationFinish info ->
+    [Model model {alertContents = AlertUserInformation info}]
+  OpenFile ->
+    [ Producer $ \emit -> do
+        res <- Portal.openFile def
+        emit (OpenFileStart res)
+        Portal.await res >>= \case
+          Nothing -> emit CloseAlert
+          Just result -> emit (OpenFileFinish result)
+    ]
+  OpenFileStart res ->
+    [Model model {alertContents = AlertRequestingOpenFile res}]
+  OpenFileFinish files ->
+    [Model model {alertContents = AlertOpenFile files}]
+  CancelRequest ->
+    let cancel = case model.alertContents of
+          AlertRequestingOpenFile res ->
+            [Producer (const (Portal.cancel res))]
+          AlertRequestingUserInformation res ->
+            [Producer (const (Portal.cancel res))]
+          _ -> []
+     in cancel <> [Model model {alertContents = AlertNotShown}]
+  CloseAlert ->
+    [Model model {alertContents = AlertNotShown}]
diff --git a/assets/fonts/Cantarell/Cantarell-Bold.ttf b/assets/fonts/Cantarell/Cantarell-Bold.ttf
new file mode 100644
Binary files /dev/null and b/assets/fonts/Cantarell/Cantarell-Bold.ttf differ
diff --git a/assets/io.github.Dretch.MonomerFlatpakExample.metainfo.xml b/assets/io.github.Dretch.MonomerFlatpakExample.metainfo.xml
--- a/assets/io.github.Dretch.MonomerFlatpakExample.metainfo.xml
+++ b/assets/io.github.Dretch.MonomerFlatpakExample.metainfo.xml
@@ -23,6 +23,7 @@
   </screenshots>
 
   <releases>
+    <release version="0.0.5.0" date="2023-04-09"/>
     <release version="0.0.4.0" date="2023-04-07"/>
     <release version="0.0.3.1" date="2023-03-11"/>
     <release version="0.0.3.0" date="2023-03-10"/>
diff --git a/monomer-flatpak-example.cabal b/monomer-flatpak-example.cabal
--- a/monomer-flatpak-example.cabal
+++ b/monomer-flatpak-example.cabal
@@ -1,6 +1,6 @@
 cabal-version: 1.12
 name:          monomer-flatpak-example
-version:       0.0.4.0
+version:       0.0.5.0
 license:       MIT
 license-file:  LICENSE
 maintainer:    garethdanielsmith@gmail.com
@@ -11,6 +11,7 @@
 category:      GUI
 build-type:    Simple
 data-files:
+    fonts/Cantarell/Cantarell-Bold.ttf
     fonts/Cantarell/Cantarell-Regular.ttf
     fonts/Cantarell/OFL.txt
     io.github.Dretch.MonomerFlatpakExample.desktop
@@ -26,11 +27,15 @@
 executable monomer-flatpak-example
     main-is:            Main.hs
     hs-source-dirs:     app
-    other-modules:      Paths_monomer_flatpak_example
+    other-modules:
+        Desktop.Portal
+        Paths_monomer_flatpak_example
+
     default-language:   Haskell2010
     default-extensions:
         DisambiguateRecordFields DuplicateRecordFields FlexibleContexts
-        LambdaCase NamedFieldPuns OverloadedRecordDot OverloadedStrings
+        ImportQualifiedPost LambdaCase NamedFieldPuns OverloadedRecordDot
+        OverloadedStrings RecordWildCards ScopedTypeVariables
 
     ghc-options:
         -threaded -rtsopts -with-rtsopts=-N -Wall -Wcompat
@@ -40,7 +45,10 @@
     build-depends:
         base >=4.7 && <5,
         containers <0.7,
+        data-default-class <0.2,
+        dbus <1.3,
         directory <1.4,
         monomer <1.6,
         monomer-hagrid <0.3,
+        random <1.3,
         text <1.3
