diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# v1.0.1.4
+
+## Fixes
+
+- Fixed missing support for connecting over a Unix-domain socket (#6). A `host` value that looks like an absolute path (e.g. `host=/var/run/postgresql`) names a socket directory rather than a TCP host, and the connection is made to a `.s.PGSQL.<port>` unix-domain socket in that directory, mirroring libpq's rule for the `host` conninfo parameter. A conninfo with no `host` (or an empty one) now defaults the way libpq itself does: the `PGHOST` environment variable if set and non-empty, otherwise a Unix-domain socket in `/tmp` on Unix-like systems (see `Pqi.Native.Connection.defaultUnixSocketDir`'s Haddock for how a distribution that compiles its own `libpq` with a different default, e.g. Fedora's `/run/postgresql`, can match it via `cabal.project` - no source patch needed), or `localhost` on Windows, unchanged. Connect-failure and handshake-failure messages now describe the socket path rather than a host/port pair when connecting this way.
+
+  No privilege-elevation guard is applied to reading `PGHOST` (or the existing `PGUSER` lookup): libpq itself reads these with plain `getenv()`, with no `secure_getenv`/`geteuid`-vs-`getuid` check anywhere in `fe-connect.c` - responsibility for scrubbing the environment before opening a database connection is on any setuid/setgid caller, exactly as it is for libpq.
+
 # v1.0.1.3
 
 ## Fixes
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -53,3 +53,9 @@
 Authentication: **trust**, **MD5**, and **SCRAM-SHA-256** are implemented. SCRAM
 is verified against a password-auth PostgreSQL 17 container (which defaults to
 `scram-sha-256`).
+
+Transport: plain TCP and Unix-domain sockets. A `host` value that's an
+absolute path names a socket directory instead of a TCP host, and omitting
+`host` altogether defaults the way libpq itself does: `PGHOST` if set,
+otherwise a Unix-domain socket in `/tmp` on Unix-like systems, or `localhost`
+on Windows.
diff --git a/pqi-native.cabal b/pqi-native.cabal
--- a/pqi-native.cabal
+++ b/pqi-native.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: pqi-native
-version: 1.0.1.3
+version: 1.0.1.4
 category: Database, PostgreSQL
 synopsis: Native (pure-Haskell) adapter for pqi
 description:
diff --git a/src/library/Pqi/Native/Connection.hs b/src/library/Pqi/Native/Connection.hs
--- a/src/library/Pqi/Native/Connection.hs
+++ b/src/library/Pqi/Native/Connection.hs
@@ -56,16 +56,16 @@
 -- format. Unquoted key=value values only; URI values are percent-decoded.
 -- @.pgpass@ is not supported.
 --
--- The @dfltUser@ argument is the already-resolved default user (see
--- 'resolveDefaultUser'), used whenever the conninfo omits @user@. It is passed
--- in explicitly because resolving it is the one IO effect this otherwise-pure
--- parser needs.
-parseConnInfo :: ByteString -> ByteString -> ConnInfo
-parseConnInfo raw dfltUser =
+-- The @dfltUser@\/@dfltHost@ arguments are the already-resolved defaults (see
+-- 'resolveDefaultUser'\/'resolveDefaultHost'), used whenever the conninfo
+-- omits @user@\/@host@ respectively. They are passed in explicitly because
+-- resolving them is the only IO this otherwise-pure parser needs.
+parseConnInfo :: ByteString -> ByteString -> ByteString -> ConnInfo
+parseConnInfo raw dfltUser dfltHost =
   if
-    | "postgresql://" `ByteString.isPrefixOf` raw -> parseUri dfltUser (ByteString.drop 13 raw)
-    | "postgres://" `ByteString.isPrefixOf` raw -> parseUri dfltUser (ByteString.drop 11 raw)
-    | otherwise -> parseKeyValue dfltUser raw
+    | "postgresql://" `ByteString.isPrefixOf` raw -> parseUri dfltUser dfltHost (ByteString.drop 13 raw)
+    | "postgres://" `ByteString.isPrefixOf` raw -> parseUri dfltUser dfltHost (ByteString.drop 11 raw)
+    | otherwise -> parseKeyValue dfltUser dfltHost raw
 
 -- | Resolve the default @user@ the way libpq does (@conninfo_add_defaults@ /
 -- @pg_fe_getauthname@ in @fe-connect.c@): the @PGUSER@ environment variable if
@@ -104,10 +104,52 @@
 platformUserNameLookupFailureMessage = "could not look up local user name"
 #endif
 
-parseKeyValue :: ByteString -> ByteString -> ConnInfo
-parseKeyValue dfltUser raw =
+-- | The default @host@ used when a conninfo omits it (or gives an empty
+-- value), matching libpq's own @conninfo_add_defaults@ / @PQconnectdbParams@
+-- resolution: @PGHOST@ if set and non-empty, otherwise 'defaultUnixSocketDir'
+-- (or @localhost@ on Windows, where there's no Unix-domain default to fall
+-- back to).
+--
+-- An explicit @host=\/some\/path@ (or its URI equivalent) always connects via
+-- Unix-domain socket regardless of this default - see
+-- 'Transport.isUnixSocketHost'.
+resolveDefaultHost :: IO ByteString
+resolveDefaultHost = do
+  pghost <- lookupEnv "PGHOST"
+  pure $ case pghost of
+    Just h | not (null h) -> ByteString.Char8.pack h
+    _ -> compiledDefaultHost
+
+compiledDefaultHost :: ByteString
+#if defined(mingw32_HOST_OS)
+compiledDefaultHost = "localhost"
+#else
+compiledDefaultHost = defaultUnixSocketDir
+#endif
+
+#ifndef PQI_NATIVE_DEFAULT_UNIX_SOCKET_DIR
+#define PQI_NATIVE_DEFAULT_UNIX_SOCKET_DIR "/tmp"
+#endif
+
+-- | The Unix-domain socket directory used when neither @host@ nor @PGHOST@ is
+-- given (non-Windows only). Defaults to @\/tmp@, the directory the upstream
+-- @postgres.org@ @libpq@ uses. A distribution that compiles its own @libpq@
+-- with a different default can match it here via @cabal.project@. Note this
+-- needs the whole option double-quoted with the inner quotes backslash-escaped
+-- (@cabal.project@'s per-package field parser tokenizes @ghc-options@
+-- shell-style).
+--
+-- > package pqi-native
+-- >   ghc-options: "-DPQI_NATIVE_DEFAULT_UNIX_SOCKET_DIR=\"/run/postgresql\""
+#if !defined(mingw32_HOST_OS)
+defaultUnixSocketDir :: ByteString
+defaultUnixSocketDir = PQI_NATIVE_DEFAULT_UNIX_SOCKET_DIR
+#endif
+
+parseKeyValue :: ByteString -> ByteString -> ByteString -> ConnInfo
+parseKeyValue dfltUser dfltHost raw =
   ConnInfo
-    { host = get "host" "localhost",
+    { host = defaultIfEmpty dfltHost (get "host" ""),
       port = maybe 5432 fst (ByteString.Char8.readInt (get "port" "5432")),
       user = theUser,
       database = get "dbname" theUser,
@@ -124,6 +166,11 @@
         | not (ByteString.null value) -> Just (key, ByteString.drop 1 value)
       _ -> Nothing
 
+defaultIfEmpty :: ByteString -> ByteString -> ByteString
+defaultIfEmpty dflt raw
+  | ByteString.null raw = dflt
+  | otherwise = raw
+
 -- | Conninfo keys already surfaced via their own 'ConnInfo' fields, so they're
 -- excluded from 'extraParams' rather than duplicated there.
 reservedKeys :: Set.Set ByteString
@@ -132,8 +179,8 @@
 -- | Parse the authority+path portion of a @postgresql://@ URI (scheme already
 -- stripped). Handles @[user[:password]@][host[:port]][/dbname]@; ignores query
 -- parameters other than what appears in those components.
-parseUri :: ByteString -> ByteString -> ConnInfo
-parseUri dfltUser withoutScheme =
+parseUri :: ByteString -> ByteString -> ByteString -> ConnInfo
+parseUri dfltUser dfltHost withoutScheme =
   ConnInfo {host, port, user, database, password, extraParams}
   where
     -- Split off optional "userinfo@" prefix. The '@' is unambiguous in this
@@ -189,9 +236,9 @@
           Just c ->
             let h = ByteString.take c hostport
                 p = ByteString.drop (c + 1) hostport
-             in if ByteString.null h then ("localhost", readPort p) else (h, readPort p)
+             in if ByteString.null h then (dfltHost, readPort p) else (h, readPort p)
           Nothing ->
-            (if ByteString.null hostport then "localhost" else hostport, 5432)
+            (defaultIfEmpty dfltHost hostport, 5432)
 
     readPort bs = maybe 5432 fst (ByteString.Char8.readInt bs)
 
@@ -294,6 +341,47 @@
   writeIORef (lastError connection) (Just message)
   writeIORef (connStatus connection) ConnectionBad
 
+-- | Format an initial-connect failure (the socket couldn't even be opened),
+-- matching libpq's distinct phrasing for a Unix-domain socket vs. a TCP host.
+connectFailureMessage :: ConnInfo -> IOException -> ByteString
+connectFailureMessage connInfo err
+  | Transport.isUnixSocketHost (host connInfo) =
+      "could not connect to server: "
+        <> ByteString.Char8.pack (show err)
+        <> " (Unix domain socket '"
+        <> ByteString.Char8.pack (Transport.unixSocketPath (host connInfo) (port connInfo))
+        <> "')"
+  | otherwise = "could not connect to server: " <> ByteString.Char8.pack (show err)
+
+-- | The handshake-failure message ('failWith', inside 'handshake') for a
+-- Unix-domain socket connection: names the socket path rather than a
+-- host\/port pair, matching libpq's phrasing.
+unixSocketFailureMessage :: ConnInfo -> ByteString -> ByteString
+unixSocketFailureMessage connInfo fmtFields =
+  "connection to server on socket '"
+    <> ByteString.Char8.pack (Transport.unixSocketPath (host connInfo) (port connInfo))
+    <> "' failed: "
+    <> fmtFields
+
+-- | The handshake-failure message for a TCP connection: includes the
+-- resolved peer IP when available (it may not be, e.g. if the socket has
+-- already been torn down), matching libpq's phrasing.
+tcpFailureMessage :: Connection -> ConnInfo -> ByteString -> IO ByteString
+tcpFailureMessage connection connInfo fmtFields = do
+  transport <- readIORef (transport connection)
+  mIp <- catch (Just <$> Transport.peerIp transport) (\(_ :: SomeException) -> pure Nothing)
+  pure $ case mIp of
+    Nothing -> fmtFields
+    Just ip ->
+      "connection to server at \""
+        <> host connInfo
+        <> "\" ("
+        <> ip
+        <> "), port "
+        <> ByteString.Char8.pack (show (port connInfo))
+        <> " failed: "
+        <> fmtFields
+
 -- | Open a connection: resolve and connect the socket, send the startup
 -- message, and run the authentication\/startup handshake. Like libpq, a failed
 -- connection (whether due to a network error or a rejected handshake) yields a
@@ -305,21 +393,22 @@
 -- 'ConnectionBad' connection rather than throwing.
 establish :: ByteString -> IO Connection
 establish conninfo = do
+  dfltHost <- resolveDefaultHost
   userResult <- resolveDefaultUser
   case userResult of
     Left message -> do
       transport <- Transport.unconnected
-      connection <- newConnection False transport (parseConnInfo conninfo "")
+      connection <- newConnection False transport (parseConnInfo conninfo "" dfltHost)
       setError connection (ByteString.Char8.pack message)
       pure connection
     Right dfltUser -> do
-      let info = parseConnInfo conninfo dfltUser
+      let info = parseConnInfo conninfo dfltUser dfltHost
       transportResult <- try @IOException (Transport.connect (host info) (port info))
       case transportResult of
         Left err -> do
           transport <- Transport.unconnected
           connection <- newConnection False transport info
-          setError connection ("could not connect to server: " <> ByteString.Char8.pack (show err))
+          setError connection (connectFailureMessage info err)
           pure connection
         Right transport -> do
           connection <- newConnection False transport info
@@ -332,7 +421,7 @@
 nullConnection :: IO Connection
 nullConnection = do
   transport <- Transport.unconnected
-  let info = parseConnInfo "" ""
+  let info = parseConnInfo "" "" "localhost"
   conn <- newConnection True transport info
   writeIORef (lastError conn) (Just "connection pointer is NULL\n")
   pure conn
@@ -418,19 +507,12 @@
         _ -> startingUp
     failWith fields = do
       let fmtFields = formatErrorFields (Map.fromList fields)
-      transport <- readIORef (transport connection)
-      mIp <- catch (Just <$> Transport.peerIp transport) (\(_ :: SomeException) -> pure Nothing)
-      setError connection $ case mIp of
-        Nothing -> fmtFields
-        Just ip ->
-          "connection to server at \""
-            <> host (info connection)
-            <> "\" ("
-            <> ip
-            <> "), port "
-            <> ByteString.Char8.pack (show (port (info connection)))
-            <> " failed: "
-            <> fmtFields
+          connInfo = info connection
+      message <-
+        if Transport.isUnixSocketHost (host connInfo)
+          then pure (unixSocketFailureMessage connInfo fmtFields)
+          else tcpFailureMessage connection connInfo fmtFields
+      setError connection message
 
 -- | The SASL message round-trip used by 'Auth.scram': send a client message and
 -- receive the next server SASL\/auth message, projected to the bytes the SCRAM
diff --git a/src/transport/Pqi/Native/Transport.hs b/src/transport/Pqi/Native/Transport.hs
--- a/src/transport/Pqi/Native/Transport.hs
+++ b/src/transport/Pqi/Native/Transport.hs
@@ -4,6 +4,8 @@
 module Pqi.Native.Transport
   ( Transport,
     connect,
+    isUnixSocketHost,
+    unixSocketPath,
     unconnected,
     close,
     send,
@@ -30,9 +32,31 @@
     readBuffer :: IORef ByteString
   }
 
--- | Open a TCP connection to the given host and port.
+-- | Open a connection to the given host and port. Mirroring libpq: if the
+-- host looks like an absolute path (starts with @\/@ - see 'isUnixSocketHost'),
+-- it names a Unix-domain socket /directory/ rather than a TCP host, and the
+-- connection is made to the socket file @'unixSocketPath' host port@ within
+-- it. Otherwise a TCP connection is made, resolving the host via DNS.
 connect :: ByteString -> Int -> IO Transport
-connect host port = do
+connect host port
+  | isUnixSocketHost host = connectUnix (unixSocketPath host port)
+  | otherwise = connectTcp host port
+
+-- | Whether a conninfo @host@ value names a Unix-domain socket directory
+-- rather than a TCP host - i.e. it looks like an absolute path, per libpq's
+-- rule: "If a host name looks like an absolute path name, it specifies
+-- Unix-domain communication rather than TCP/IP communication".
+isUnixSocketHost :: ByteString -> Bool
+isUnixSocketHost host = not (ByteString.null host) && ByteString.head host == 0x2f -- '/'
+
+-- | The path of the socket file libpq expects within a Unix-domain socket
+-- directory: @\<directory\>\/.s.PGSQL.\<port\>@.
+unixSocketPath :: ByteString -> Int -> FilePath
+unixSocketPath directory port = ByteString.Char8.unpack directory <> "/.s.PGSQL." <> show port
+
+-- | Open a TCP connection to the given host and port.
+connectTcp :: ByteString -> Int -> IO Transport
+connectTcp host port = do
   let hints = Socket.defaultHints {Socket.addrSocketType = Socket.Stream}
   addresses <-
     Socket.getAddrInfo (Just hints) (Just (ByteString.Char8.unpack host)) (Just (show port))
@@ -41,6 +65,18 @@
     address : _ -> do
       sock <- Socket.socket (Socket.addrFamily address) (Socket.addrSocketType address) (Socket.addrProtocol address)
       Socket.connect sock (Socket.addrAddress address)
+      buffer <- newIORef ByteString.empty
+      pure Transport {socket = sock, readBuffer = buffer}
+
+-- | Open a connection to a Unix-domain socket at the given path (the
+-- directory\/@\.s\.PGSQL\.\<port\>@ file, per 'unixSocketPath').
+connectUnix :: FilePath -> IO Transport
+connectUnix path
+  | not Socket.isUnixDomainSocketAvailable =
+      ioError (userError "pqi-native: Unix-domain sockets are not supported on this platform")
+  | otherwise = do
+      sock <- Socket.socket Socket.AF_UNIX Socket.Stream 0
+      Socket.connect sock (Socket.SockAddrUnix path)
       buffer <- newIORef ByteString.empty
       pure Transport {socket = sock, readBuffer = buffer}
 
