diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for rt
+
+## 0.1.0.0 -- 2024-04-08
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2024, Jamie Willis
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the copyright holder nor the names of its
+   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 HOLDER 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/rt.cabal b/rt.cabal
new file mode 100644
--- /dev/null
+++ b/rt.cabal
@@ -0,0 +1,79 @@
+cabal-version:      3.0
+-- The cabal-version field refers to the version of the .cabal specification,
+-- and can be different from the cabal-install (the tool) version and the
+-- Cabal (the library) version you are using. As such, the Cabal (the library)
+-- version used must be equal or greater than the version stated in this field.
+-- Starting from the specification version 2.2, the cabal-version field must be
+-- the first thing in the cabal file.
+
+-- Initial package description 'rt' generated by
+-- 'cabal init'. For further documentation, see:
+--   http://haskell.org/cabal/users-guide/
+--
+-- The name of the package.
+name:               rt
+
+-- The package version.
+-- See the Haskell package versioning policy (PVP) for standards
+-- guiding when and how versions should be incremented.
+-- https://pvp.haskell.org
+-- PVP summary:     +-+------- breaking API changes
+--                  | | +----- non-breaking API additions
+--                  | | | +--- code changes with no API change
+version:            0.1.0.0
+
+-- A short (one-line) description of the package.
+synopsis:           A more fine-grained version of state threads (ST)
+
+-- A longer description of the package.
+description:        Introduces the RT monad, which can be used to have more fine-grained STRefs without an extra parameter
+                    in the monad.
+
+-- URL for the project homepage or repository.
+homepage:           https://github.com/j-mie6/reference-threads
+
+-- The license under which the package is released.
+license:            BSD-3-Clause
+
+-- The file containing the license text.
+license-file:       LICENSE
+
+-- The package author(s).
+author:             Jamie Willis
+
+-- An email address to which users can send suggestions, bug reports, and patches.
+maintainer:         j.willis19@imperial.ac.uk
+
+copyright: (c) 2024-present Jamie Willis
+category:           Control
+build-type:         Simple
+
+-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
+extra-doc-files:    CHANGELOG.md
+
+source-repository head
+    type:     git
+    location: git://github.com/j-mie6/reference-threads.git
+
+common warnings
+    ghc-options: -Wall
+
+library
+    -- Import common warning flags.
+    import:           warnings
+
+    -- Modules exported by the library.
+    exposed-modules:  Control.Monad.RT
+                      Control.Monad.RT.Unsafe
+                      Data.Ref
+                      Data.Ref.Impl
+                      --Data.Array.RT
+
+    -- Other library packages from which modules are imported.
+    build-depends:    base >=4.14 && < 5
+
+    -- Directories containing source files.
+    hs-source-dirs:   src
+
+    -- Base language which the package is written in.
+    default-language: Haskell2010
diff --git a/src/Control/Monad/RT.hs b/src/Control/Monad/RT.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Monad/RT.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE Trustworthy #-}
+{-# LANGUAGE MagicHash, UnboxedTuples #-}
+module Control.Monad.RT (RT, module Control.Monad.RT) where
+
+import Control.Monad.RT.Unsafe
+
+import GHC.Base (runRW#)
+import GHC.IO (IO(IO))
+import Data.Coerce (coerce)
+
+{-# INLINE runRT #-}
+runRT :: RT a -> a
+runRT (RT mx) = case runRW# mx of (# _, x #) -> x
+
+{-# INLINE rtToIO #-}
+rtToIO :: RT a -> IO a
+rtToIO = coerce
diff --git a/src/Control/Monad/RT/Unsafe.hs b/src/Control/Monad/RT/Unsafe.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Monad/RT/Unsafe.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE Unsafe #-}
+{-# LANGUAGE UnboxedTuples, MagicHash, DerivingVia, DataKinds #-}
+module Control.Monad.RT.Unsafe (module Control.Monad.RT.Unsafe) where
+
+import GHC.Base (State#, RealWorld)
+import GHC.IO (IO(IO))
+import Data.Coerce (coerce)
+
+-- do the instances by hand?
+newtype RT a = RT (State# RealWorld -> (# State# RealWorld, a #))
+  deriving (Functor, Applicative, Monad) via IO
+
+{-# INLINE unsafeIOToRT #-}
+unsafeIOToRT :: IO a -> RT a
+unsafeIOToRT = coerce
diff --git a/src/Data/Ref.hs b/src/Data/Ref.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Ref.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE Trustworthy #-}
+{-# LANGUAGE MagicHash, UnboxedTuples, RankNTypes, TypeOperators #-}
+module Data.Ref (Ref, module Data.Ref) where
+
+import Control.Monad.RT.Unsafe (RT(RT))
+import Data.Ref.Impl (Ref(Ref))
+
+import GHC.Base (newMutVar#, readMutVar#, writeMutVar#)
+
+import GHC.IORef (IORef(IORef))
+import GHC.STRef (STRef(STRef))
+
+import Data.Type.Equality ((:~:)(Refl))
+import Unsafe.Coerce (unsafeCoerce)
+
+
+{-# INLINABLE newRef #-}
+newRef :: a -> (forall r. Ref r a -> RT b) -> RT b
+newRef x k = RT $ \s# ->
+  case newMutVar# x s# of
+    (# s'#, ref# #) -> let RT k' = k (Ref ref#) in k' s'#
+
+{-# INLINE readRef #-}
+readRef :: Ref r a -> RT a
+readRef (Ref ref#) = RT $ \s# -> readMutVar# ref# s#
+
+{-# INLINABLE writeRef #-}
+writeRef :: Ref r a -> a -> RT ()
+writeRef (Ref ref#) x = RT $ \s# ->
+  case writeMutVar# ref# x s# of
+    s'# -> (# s'#, () #)
+
+-- unsafeFromIORef?
+{-# INLINE fromIORef #-}
+fromIORef :: IORef a -> Ref r a
+fromIORef (IORef (STRef ref#)) = Ref ref#
+
+-- returns a proof that if two references are equal, they must have the same lifetime
+lifetimeEqual :: Ref r1 a -> Ref r2 a -> Maybe (r1 :~: r2)
+lifetimeEqual (Ref ref1#) ref2
+  | Ref ref1# == ref2 = Just (unsafeCoerce Refl)
+  | otherwise         = Nothing
diff --git a/src/Data/Ref/Impl.hs b/src/Data/Ref/Impl.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Ref/Impl.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE DataKinds, MagicHash, RoleAnnotations #-}
+module Data.Ref.Impl (module Data.Ref.Impl) where
+
+import GHC.Base (MutVar#, RealWorld, sameMutVar#, isTrue#)
+
+{-
+The `r` parameter is deliberately nominal here, which prevents the use of
+coerce to "safely" escape the lifetime of a reference:
+
+> escape :: a -> RT (Ref r a)
+> escape x = newRef x (return . coerce)
+
+Would compile if the annotation was left as phantom (which is the inferred).
+This is clearly not something we want to allow, so nominal it is.
+-}
+type role Ref nominal representational
+-- Don't even expose the constructor, then it's pretty much safe?
+data Ref r a = Ref (MutVar# RealWorld a)
+
+-- It's nice that two references must clearly have the same lifetime to be equal
+instance Eq (Ref r a) where
+  Ref ref1# == Ref ref2# = isTrue# (sameMutVar# ref1# ref2#)
