diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for aws-arn
 
+## 0.3.1.0 -- 2023-03-01
+
+* Add `Network.AWS.ARN.S3` with support for S3 bucket and object ARNs.
+
 ## 0.3.0.0 -- 2022-10-14
 
 * Reinstate the improper `Iso'`s from version 0.1.x; the 0.2.0.0
diff --git a/aws-arn.cabal b/aws-arn.cabal
--- a/aws-arn.cabal
+++ b/aws-arn.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               aws-arn
-version:            0.3.0.0
+version:            0.3.1.0
 synopsis:
   Types and optics for manipulating Amazon Resource Names (ARNs)
 
@@ -53,7 +53,7 @@
 
 common deps
   build-depends:
-    , base             >=4.12   && <4.17
+    , base             >=4.12   && <4.18
     , deriving-compat  >=0.5.10 && <0.7
     , profunctors      >=5.0    && <5.7
     , tagged           ^>=0.8
@@ -65,6 +65,7 @@
     Network.AWS.ARN
     Network.AWS.ARN.Internal.Lens
     Network.AWS.ARN.Lambda
+    Network.AWS.ARN.S3
 
   build-depends:   hashable >=1.3.0.0 && <1.5
   hs-source-dirs:  src
@@ -75,6 +76,7 @@
   main-is:            Main.hs
   other-modules:
     Network.AWS.ARN.Lambda.Test
+    Network.AWS.ARN.S3.Test
     Network.AWS.ARN.Test
 
   hs-source-dirs:     test
diff --git a/src/Network/AWS/ARN/S3.hs b/src/Network/AWS/ARN/S3.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/AWS/ARN/S3.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+--
+-- Module      : Network.AWS.ARN.S3
+-- Copyright   : (C) 2020-2023 Bellroy Pty Ltd
+-- License     : BSD-3-Clause
+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
+-- Stability   : experimental
+module Network.AWS.ARN.S3
+  ( -- * S3 Object
+    Object (..),
+    parseObject,
+    renderObject,
+
+    -- * S3 Bucket
+    Bucket (..),
+    parseBucket,
+    renderBucket,
+
+    -- ** Prisms
+    _Object,
+    _Bucket,
+  )
+where
+
+import Data.Hashable (Hashable)
+import Data.Text (Text)
+import qualified Data.Text as T
+import GHC.Generics (Generic)
+import Network.AWS.ARN.Internal.Lens (Prism', prism')
+
+-- $setup
+-- >>> :set -XOverloadedStrings
+-- >>> import Network.AWS.ARN.Internal.Lens ((^?))
+
+-- | An AWS S3 object, made of a bucket and an object key.
+--
+-- >>> "bucket-name/my/object" ^? _Object
+-- Just (Object {bucket = Bucket {bucketName = "bucket-name"}, objectKey = "my/object"})
+--
+-- >>> "bucket-name" ^? _Object
+-- Nothing
+--
+-- >>> bucket <$> "bucket-name/my/object" ^? _Object
+-- Just (Bucket {bucketName = "bucket-name"})
+--
+-- @since 0.3.1.0
+data Object = Object
+  { bucket :: Bucket,
+    objectKey :: Text
+  }
+  deriving (Eq, Ord, Hashable, Show, Generic)
+
+-- | @since 0.3.1.0
+parseObject :: Text -> Maybe Object
+parseObject t = case T.breakOn "/" t of
+  ("", _) -> Nothing
+  (_, "") -> Nothing
+  (bucketName, object) -> Just $ Object (Bucket bucketName) (T.drop 1 object)
+
+-- | @since 0.3.1.0
+renderObject :: Object -> Text
+renderObject Object {bucket, objectKey} =
+  renderBucket bucket <> "/" <> objectKey
+
+-- | @since 0.3.1.0
+_Object :: Prism' Text Object
+_Object = prism' renderObject parseObject
+
+-- | An AWS S3 bucket, without an object key.
+--
+-- >>> "bucket-name" ^? _Bucket
+-- Just (Bucket {bucketName = "bucket-name"})
+--
+-- >>> "bucket-name/my/object" ^? _Bucket
+-- Nothing
+--
+-- >>> let b = Bucket "my-bucket" in renderObject . Object b <$> ["obj1", "obj2"]
+-- ["my-bucket/obj1","my-bucket/obj2"]
+--
+-- @since 0.3.1.0
+newtype Bucket = Bucket {bucketName :: Text}
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (Hashable)
+
+-- | @since 0.3.1.0
+parseBucket :: Text -> Maybe Bucket
+parseBucket t = case T.breakOn "/" t of
+  (bucket, "") -> Just $ Bucket bucket
+  _ -> Nothing
+
+-- | @since 0.3.1.0
+renderBucket :: Bucket -> Text
+renderBucket Bucket {bucketName} = bucketName
+
+-- | @since 0.3.1.0
+_Bucket :: Prism' Text Bucket
+_Bucket = prism' renderBucket parseBucket
diff --git a/test/Network/AWS/ARN/S3/Test.hs b/test/Network/AWS/ARN/S3/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Network/AWS/ARN/S3/Test.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Network.AWS.ARN.S3.Test where
+
+import Data.Text (Text)
+import Network.AWS.ARN.Internal.Lens (Lens', set, (^?))
+import Network.AWS.ARN.S3
+import Test.Tasty
+import Test.Tasty.HUnit
+
+test_all :: TestTree
+test_all =
+  testGroup
+    "Network.AWS.ARN.S3"
+    [ testGroup
+        "Object"
+        [ testGroup
+            "parsing"
+            [ testCase "s3 bucket" $
+                "bucket-name" ^? _Object @?= Nothing,
+              testCase "s3 bucket and object" $
+                "bucket-name/my/object" ^? _Object @?= Just (Object (Bucket "bucket-name") "my/object")
+            ],
+          testGroup
+            "updating"
+            [ testCase "setting object key" $
+                set (_Object . rObjectKey) "my/other/object" "bucket-name/my/object"
+                  @?= "bucket-name/my/other/object"
+            ]
+        ]
+    , testGroup
+        "S3 bucket"
+        [ testGroup
+            "parsing"
+            [ testCase "s3 bucket" $
+                "bucket-name" ^? _Bucket @?= Just (Bucket "bucket-name"),
+              testCase "s3 bucket and object" $
+                "bucket-name/my/object" ^? _Bucket @?= Nothing
+            ]
+        ]
+    ]
+
+rObjectKey :: Lens' Object Text
+rObjectKey l r@Object {objectKey} = (\o -> r {objectKey = o}) <$> l objectKey
