diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for pa-label
+
+## 0.1.0.0 -- 2023-05-19
+
+* First version; contains the `Label` type, `T2`/`T3`, `E2`/`E3`, and some helpers.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2023, Possehl Analytics GmbH
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of  nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,70 @@
+# `pa-label`
+
+One-module library to provide nice anonymous labelled tuples and enums.
+
+If you have a function:
+
+```
+doBlorb
+  :: Text
+  -> Text
+  -> Bool
+  -> Bool
+  -> Bool
+  -> IO Text
+doBlorb username password read write insert = do …
+```
+
+what do you do? At call-site you don’t see the names of any of the types.
+
+Before GHC 9.2, one strategy would have been to create a newtype for every single argument:
+
+```
+data Username = Username Text
+data Password = Password Text
+data Read = Read Bool
+data Write = Write Bool
+data Insert = Insert Bool
+```
+
+but that is very verbose and leaks into global scope!
+
+Instead, do this:
+
+```
+doBlorb
+  :: Label "username" Text
+  -> Label "password" Text
+  -> Label "read" Bool
+  -> Label "write" Bool
+  -> Label "insert" Bool
+  -> IO (Label "blorb" Text)
+doBlorb username password read write insert = do …
+```
+
+then it becomes clear to the call-site, what to do. In the function you can access the labels with record dots to get to the inner types, e.g. `username.username`, `read.read`.
+
+Even better, you can bundle things into anonymous tuples:
+
+```
+doBlorb
+  :: T2 "username" Text
+        "password" Text
+  -> T3 "read" Bool
+        "write" Bool
+        "insert" Bool
+  -> IO (Label "blorb" Text)
+doBlorb user permission = do …
+```
+
+and access inside the function like `user.username` or `permission.read`. So much nicer!
+
+We provide tuples up to size 3, for anything bigger the you should really just create a normal record with `data`. The great thing is that no use-site has to be adjusted as long as you name the record fields the same and use record-dot syntax everywhere.
+
+There’s some experimental support for anonymous enums (`E2/E3`), they are useful in some situations, but not as often as anonymous tuples in our experience.
+
+---
+
+Approaches like [vinyl](https://hackage.haskell.org/package/vinyl) or [superrecord](https://hackage.haskell.org/package/superrecord) are more general, but very hard to understand and lead to bad error messages.
+
+We have found that in practice it is nicer to use the a lot less magical and slightly more verbose `T2/T3` and switch to normal records once you reach 4 fields.
diff --git a/pa-label.cabal b/pa-label.cabal
new file mode 100644
--- /dev/null
+++ b/pa-label.cabal
@@ -0,0 +1,28 @@
+cabal-version:      3.0
+name:               pa-label
+version:            0.1.0.0
+synopsis:           Labels, and labelled tuples and enums (GHC >9.2)
+description:
+license:            BSD-3-Clause
+license-file:       LICENSE
+-- author:
+maintainer:         Philip Patsch <philip.patsch@possehl-analytics.com>
+copyright:          2023 Possehl Analytics GmbH
+homepage:           https://github.com/possehl-analytics/pa-hackage
+category:           Data
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+                    README.md
+-- extra-source-files:
+
+common warnings
+    ghc-options: -Wall
+
+library
+    import:           warnings
+    exposed-modules:  Label
+    -- other-modules:
+    -- other-extensions:
+    build-depends:    base ^>=4.16.4.0
+    hs-source-dirs:   src
+    default-language: GHC2021
diff --git a/src/Label.hs b/src/Label.hs
new file mode 100644
--- /dev/null
+++ b/src/Label.hs
@@ -0,0 +1,328 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+module Label
+  ( -- * Labels
+    Label,
+    label,
+    label',
+    getLabel,
+    mapLabel,
+    traverseLabel,
+
+    -- * Named Tuples
+    T2 (..),
+    focusOnField,
+    monoMapT2,
+    tupleToT2,
+    T3 (..),
+    monoMapT3,
+    tupleToT3,
+
+    -- * Named Sums/Enums
+    E2 (..),
+    mapE2,
+    monoMapE2,
+    monoFoldE2,
+    monoTraverseE2,
+    partitionE2,
+    isE21,
+    isE22,
+    getE21,
+    getE22,
+    E3 (..),
+    mapE3,
+  )
+where
+
+import Data.Data (Proxy (..))
+import Data.Either (partitionEithers)
+import Data.Function ((&))
+import Data.Functor ((<&>))
+import Data.Typeable (Typeable)
+import GHC.Records (HasField (..))
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+
+-- | A labelled value.
+--
+-- Use 'label'/'label'' to construct,
+-- then use dot-syntax to get the inner value.
+newtype Label (label :: Symbol) value = Label value
+  deriving stock (Eq, Ord)
+  deriving newtype (Typeable, Semigroup, Monoid)
+
+instance (KnownSymbol label, Show value) => Show (Label label value) where
+  showsPrec d (Label val) =
+    showParen (d > 10) $
+      showString "label @"
+        . showsPrec 11 (symbolVal (Proxy @label))
+        . showString " "
+        . showsPrec 11 val
+
+-- | Attach a label to a value; should be used with a type application to name the label.
+--
+-- @
+-- let f = label @"foo" 'f' :: Label "foo" Char
+-- in f.foo :: Char
+-- @
+--
+-- Use dot-syntax to get the labelled value.
+label :: forall label value. value -> Label label value
+label value = Label value
+
+-- | Attach a label to a value; Pass it a proxy with the label name in the argument type.
+-- This is intended for passing through the label value;
+-- you can also use 'label'.
+--
+--
+-- @
+-- let f = label' (Proxy @"foo") 'f' :: Label "foo" Char
+-- in f.foo :: Char
+-- @
+--
+-- Use dot-syntax to get the labelled value.
+label' :: forall label value. (Proxy label) -> value -> Label label value
+label' Proxy value = Label value
+
+-- | Fetches the labelled value.
+instance HasField label (Label label value) value where
+  getField :: (Label label value) -> value
+  getField (Label value) = value
+
+-- | Fetch a value from a record, like 'getField', but also keep it wrapped by its label.
+getLabel :: forall label record a. HasField label record a => record -> Label label a
+getLabel rec = rec & getField @label & label @label
+
+-- | 'fmap' over the contents of the labbelled value. Helper.
+mapLabel :: forall label a b. (a -> b) -> Label label a -> Label label b
+mapLabel f (Label a) = Label @label $ f a
+
+-- | 'traverse' over the contents of the labbelled value. Helper.
+traverseLabel :: forall label f a b. Functor f => (a -> f b) -> Label label a -> f (Label label b)
+traverseLabel fab (Label a) = Label @label <$> fab a
+
+-- | A named 2-element tuple. Since the elements are named, you can access them with `.`.
+--
+-- @
+-- let t2 = T2 (label @"myfield" 'c') (label @"otherfield" True) :: T2 "myfield" Char "otherfield" Bool
+-- in (
+--   t2.myfield :: Char,
+--   t2.otherfield :: Bool
+-- )
+-- @
+data T2 (l1 :: Symbol) t1 (l2 :: Symbol) t2 = T2 (Label l1 t1) (Label l2 t2)
+  deriving stock (Show, Eq, Ord)
+
+-- | Access the first field by label
+instance HasField l1 (T2 l1 t1 l2 t2) t1 where
+  getField (T2 t1 _) = getField @l1 t1
+
+-- | Access the second field by label
+instance HasField l2 (T2 l1 t1 l2 t2) t2 where
+  getField (T2 _ t2) = getField @l2 t2
+
+instance (Semigroup t1, Semigroup t2) => Semigroup (T2 l1 t1 l2 t2) where
+  T2 t1 t2 <> T2 t1' t2' = T2 (t1 <> t1') (t2 <> t2')
+
+instance (Monoid t1, Monoid t2) => Monoid (T2 l1 t1 l2 t2) where
+  mempty = T2 mempty mempty
+
+-- | Given a record with some field, “focus” on that field by pulling it into the first part of the T2,
+-- and put the original record into the second part of the T2.
+--
+-- This can be useful when you have a function that requires something with a field,
+-- but the field itself is nested somewhere in the record.
+--
+-- Example:
+--
+-- @
+-- data Foo = Foo
+--   { nested :: Label "myId" Text
+--   }
+--
+-- foo = Foo {nested = "hi"}
+--
+-- fn :: HasField "myId" rec Text => rec -> Text
+-- fn rec = rec.myId <> "!"
+--
+-- x = fn (focusOnField @"myId" (.nested) foo) == "hi!"
+-- @
+--
+-- Note that you will have to give `focusOnField` a type annotation of which label to use,
+-- otherwise it cannot infer it.
+focusOnField ::
+  forall field rec subrec t.
+  HasField field subrec t =>
+  (rec -> subrec) ->
+  rec ->
+  T2 field t "dat" rec
+focusOnField zoom rec = T2 (getLabel @field (rec & zoom)) (label @"dat" rec)
+
+-- | Map a function over all fields in the tuple. All fields have to have the same type.
+monoMapT2 :: (t -> t') -> T2 l1 t l2 t -> T2 l1 t' l2 t'
+monoMapT2 f (T2 a b) = T2 (mapLabel f a) (mapLabel f b)
+
+-- | Convert a tuple to a T2 by giving its elements names.
+--
+-- @tupleToT2 @"left" @"right" ('c', True) :: T2 "left" Char "right" Bool@
+tupleToT2 :: forall l1 l2 t1 t2. (t1, t2) -> T2 l1 t1 l2 t2
+tupleToT2 (t1, t2) = T2 (label @l1 t1) (label @l2 t2)
+
+-- | A named 3-element tuple. Since the elements are named, you can access them with `.`. See 'T2' for an example.
+data T3 (l1 :: Symbol) t1 (l2 :: Symbol) t2 (l3 :: Symbol) t3 = T3 (Label l1 t1) (Label l2 t2) (Label l3 t3)
+  deriving stock (Show, Eq, Ord)
+
+-- | Access the first field by label
+instance HasField l1 (T3 l1 t1 l2 t2 l3 t3) t1 where
+  getField (T3 t1 _ _) = getField @l1 t1
+
+-- | Access the second field by label
+instance HasField l2 (T3 l1 t1 l2 t2 l3 t3) t2 where
+  getField (T3 _ t2 _) = getField @l2 t2
+
+-- | Access the third field by label
+instance HasField l3 (T3 l1 t1 l2 t2 l3 t3) t3 where
+  getField (T3 _ _ t3) = getField @l3 t3
+
+instance (Semigroup t1, Semigroup t2, Semigroup t3) => Semigroup (T3 l1 t1 l2 t2 l3 t3) where
+  T3 t1 t2 t3 <> T3 t1' t2' t3' = T3 (t1 <> t1') (t2 <> t2') (t3 <> t3')
+
+instance (Monoid t1, Monoid t2, Monoid t3) => Monoid (T3 l1 t1 l2 t2 l3 t3) where
+  mempty = T3 mempty mempty mempty
+
+-- | Map a function over all fields in the tuple. All fields have to have the same type.
+monoMapT3 :: (t -> t') -> T3 l1 t l2 t l3 t -> T3 l1 t' l2 t' l3 t'
+monoMapT3 f (T3 a b c) = T3 (mapLabel f a) (mapLabel f b) (mapLabel f c)
+
+-- | Convert a tuple to a T3 by giving its elements names.
+--
+-- @tupleToT3 @"left" @"right" @"grip" ('c', True, Maybe 'x') :: T3 "left" Char "right" Bool "grip" (Maybe Char)@
+tupleToT3 :: forall l1 l2 l3 t1 t2 t3. (t1, t2, t3) -> T3 l1 t1 l2 t2 l3 t3
+tupleToT3 (t1, t2, t3) = T3 (label @l1 t1) (label @l2 t2) (label @l3 t3)
+
+-- | A named 2-alternative sum (“'Either' with labels”).
+data E2 (l1 :: Symbol) t1 (l2 :: Symbol) t2
+  = E21 (Label l1 t1)
+  | E22 (Label l2 t2)
+  deriving stock (Eq, Show)
+
+instance (Bounded t1, Bounded t2) => Bounded (E2 l1 t1 l2 t2) where
+  minBound = E21 (label @l1 minBound)
+  maxBound = E22 (label @l2 maxBound)
+
+-- TODO: instance for arbitrary Enum types?
+instance Enum (E2 l1 () l2 ()) where
+  toEnum 0 = E21 (label @l1 ())
+  toEnum 1 = E22 (label @l2 ())
+  toEnum _ = error "E2: toEnum"
+
+  fromEnum (E21 _) = 0
+  fromEnum (E22 _) = 1
+
+-- | Map a separate function over every possibility in this enum. The label names stay the same.
+--
+-- Each function has access to its label, this is intentional so that you have to mention the label once (e.g. by using dot-notation), to prevent confusing the cases.
+mapE2 ::
+  forall l1 t1 t1' l2 t2 t2'.
+  (Label l1 t1 -> t1') ->
+  (Label l2 t2 -> t2') ->
+  E2 l1 t1 l2 t2 ->
+  E2 l1 t1' l2 t2'
+mapE2 f1 f2 = \case
+  E21 lbl -> lbl & getLabel @l1 & f1 & label @l1 & E21
+  E22 lbl -> lbl & getLabel @l2 & f2 & label @l2 & E22
+
+-- | Map a single function over every possiblity in this enum. All fields have to have the same type.
+monoMapE2 :: (t -> t') -> E2 l1 t l2 t -> E2 l1 t' l2 t'
+monoMapE2 f = \case
+  E21 lbl -> lbl & mapLabel f & E21
+  E22 lbl -> lbl & mapLabel f & E22
+
+-- | If ever branch of this enum has the same type, fold the enum into its contents.
+-- This loses the distinction between cases.
+monoFoldE2 :: E2 l1 t l2 t -> t
+monoFoldE2 = \case
+  E21 (Label t) -> t
+  E22 (Label t) -> t
+
+-- | Partition a list of E2 into two lists that each keep their respective label.
+-- Like 'partitionEithers', but with labels.
+partitionE2 :: forall l1 t1 l2 t2. [E2 l1 t1 l2 t2] -> T2 l1 [t1] l2 [t2]
+partitionE2 es =
+  es
+    <&> ( \case
+            E21 (Label t1) -> Left t1
+            E22 (Label t2) -> Right t2
+        )
+    & partitionEithers
+    & (\(t1s, t2s) -> T2 (label @l1 t1s) (label @l2 t2s))
+
+-- | Map a monadic (actually just a functor-ic) function over each possibility in this enum. All fields have to have the same type.
+monoTraverseE2 :: Functor f => (t -> f t') -> E2 l1 t l2 t -> f (E2 l1 t' l2 t')
+monoTraverseE2 f = \case
+  E21 lbl -> lbl & traverseLabel f <&> E21
+  E22 lbl -> lbl & traverseLabel f <&> E22
+
+-- | Check the E21 case. Use TypeApplications to make sure you are checking the right case.
+--
+-- >>> isE21 @"foo" (E21 (label @"foo" 'c') :: E2 "foo" Char "bar" Int)
+-- True
+isE21 :: forall l1 t1 l2 t2. E2 l1 t1 l2 t2 -> Bool
+isE21 = \case
+  E21 _ -> True
+  E22 _ -> False
+
+-- | Check the E22 case. Use TypeApplications to make sure you are checking the right case.
+--
+-- >>> isE22 @"bar" (E21 (label @"foo" 'c') :: E2 "foo" Char "bar" Int)
+-- False
+isE22 :: forall l2 t2 l1 t1. E2 l1 t1 l2 t2 -> Bool
+isE22 = \case
+  E21 _ -> False
+  E22 _ -> True
+
+getE21 :: forall l1 t1 l2 t2. E2 l1 t1 l2 t2 -> Maybe t1
+getE21 = \case
+  E21 lbl -> Just $ getField @l1 lbl
+  E22 _ -> Nothing
+
+getE22 :: forall l2 t2 l1 t1. E2 l1 t1 l2 t2 -> Maybe t2
+getE22 = \case
+  E21 _ -> Nothing
+  E22 lbl -> Just $ getField @l2 lbl
+
+-- | A named 3-alternative sum (“'Either' with labels”).
+data E3 (l1 :: Symbol) t1 (l2 :: Symbol) t2 (l3 :: Symbol) t3
+  = E31 (Label l1 t1)
+  | E32 (Label l2 t2)
+  | E33 (Label l3 t3)
+  deriving stock (Eq, Show)
+
+instance (Bounded t1, Bounded t3) => Bounded (E3 l1 t1 l2 t2 l3 t3) where
+  minBound = E31 (label @l1 minBound)
+  maxBound = E33 (label @l3 maxBound)
+
+-- TODO: instance for arbitrary Enum types?
+instance Enum (E3 l1 () l2 () l3 ()) where
+  toEnum 0 = E31 (label @l1 ())
+  toEnum 1 = E32 (label @l2 ())
+  toEnum 2 = E33 (label @l3 ())
+  toEnum _ = error "E3: toEnum"
+
+  fromEnum (E31 _) = 0
+  fromEnum (E32 _) = 1
+  fromEnum (E33 _) = 2
+
+-- | Map a function over every element in this enum. The label names stay the same.
+mapE3 ::
+  forall l1 t1 t1' l2 t2 t2' l3 t3 t3'.
+  (Label l1 t1 -> t1') ->
+  (Label l2 t2 -> t2') ->
+  (Label l3 t3 -> t3') ->
+  E3 l1 t1 l2 t2 l3 t3 ->
+  E3 l1 t1' l2 t2' l3 t3'
+mapE3 f1 f2 f3 = \case
+  E31 lbl -> lbl & getLabel @l1 & f1 & label @l1 & E31
+  E32 lbl -> lbl & getLabel @l2 & f2 & label @l2 & E32
+  E33 lbl -> lbl & getLabel @l3 & f3 & label @l3 & E33
