diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for typson-beam
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Aaron Allen (c) 2020
+
+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 Aaron Allen 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,1 @@
+# typson-beam
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Typson/Beam.hs b/src/Typson/Beam.hs
new file mode 100644
--- /dev/null
+++ b/src/Typson/Beam.hs
@@ -0,0 +1,103 @@
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE PolyKinds #-}
+--------------------------------------------------------------------------------
+-- |
+-- Module      : Typson.Beam
+-- Description : Provides the Beam integration
+-- Copyright   : (c) Aaron Allen, 2020
+-- Maintainer  : Aaron Allen <aaronallen8455@gmail.com>
+-- License     : BSD-style (see the file LICENSE)
+-- Stability   : experimental
+-- Portability : non-portable
+--
+--------------------------------------------------------------------------------
+module Typson.Beam
+  ( jsonPath
+  , JNullable(..)
+  , nullableJsonb
+  , nullableJson
+  ) where
+
+import qualified Data.Aeson as Aeson
+import           Data.Coerce (Coercible, coerce)
+import           Data.Kind (Type)
+import           Data.List (foldl')
+import qualified Data.List.NonEmpty as NE
+import           Data.Maybe (fromMaybe)
+import           Data.String (fromString)
+import qualified Database.Beam as B
+import qualified Database.Beam.Backend.SQL.SQL92 as B (HasSqlValueSyntax)
+import qualified Database.Beam.Postgres as B
+import qualified Database.PostgreSQL.Simple.FromField as Pg
+
+import           Typson
+
+-- | Use a type-safe JSON path as part of a query.
+--
+-- @
+-- select $ jsonPath (Proxy @("foo" :-> "bar")) fieldSchemaJ
+--        . fieldAccessor
+--      \<$> all_ someTable
+-- @
+jsonPath :: ( TypeAtPath o tree path ~ field
+            , ReflectPath path
+            , B.IsPgJSON json
+            , Coercible (json field) (JNullable json' field)
+            )
+         => proxy (path :: k) -- ^ A path proxy
+         -> ObjectTree tree o -- ^ Typson schema
+         -> B.QGenExpr ctxt B.Postgres s (json o) -- ^ Column selector
+         -> B.QGenExpr ctxt B.Postgres s (JNullable json' field)
+jsonPath path _ input = coerce $
+  case reflectPath path of
+    p NE.:| ps -> foldl' buildPath (buildPath input p) ps
+  where
+    buildPath p (Key k) = p B.->$ fromString k
+    buildPath p (Idx i) = p B.-># fromInteger i
+
+--------------------------------------------------------------------------------
+-- Selecting Optional JSON
+--------------------------------------------------------------------------------
+
+-- | Wraps a @PgJSON@ or @PgJSONB@, treating deserialization of SQL @NULL@ as
+-- json @null@. This is so that if you query for a path that might not exist,
+-- i.e. a path into an optional field, then an exception will not be raised
+-- when attempting to decode the result as JSON.
+newtype JNullable json a = JNullable (json a)
+  deriving (Ord, Eq, Show) via json a
+  deriving B.IsPgJSON via json
+
+deriving via (json a :: Type) instance (B.HasSqlValueSyntax syn (json a))
+  => B.HasSqlValueSyntax syn (JNullable json a)
+
+instance ( Pg.FromField (json a :: Type)
+         , B.Typeable (a :: Type)
+         , B.Typeable json
+         )
+  => B.FromBackendRow B.Postgres (JNullable json a)
+
+instance Pg.FromField (json a) => Pg.FromField (JNullable json a) where
+  fromField f mbBs = JNullable
+                 <$> Pg.fromField f (Just $ fromMaybe "null" mbBs)
+
+--------------------------------------------------------------------------------
+-- Schema DataTypes
+--------------------------------------------------------------------------------
+
+-- | Declares a nullable @PgJSONB@ field in a migration schema
+nullableJsonb :: forall a. (Aeson.ToJSON a, Aeson.FromJSON a)
+              => B.DataType B.Postgres (JNullable B.PgJSONB a)
+nullableJsonb = coerce (B.jsonb :: B.DataType B.Postgres (B.PgJSONB a))
+
+-- | Declares a nullable @PgJSON@ field in a migration schema
+nullableJson :: forall a. (Aeson.ToJSON a, Aeson.FromJSON a)
+             => B.DataType B.Postgres (JNullable B.PgJSON a)
+nullableJson = coerce (B.json :: B.DataType B.Postgres (B.PgJSON a))
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeFamilies #-}
+
+import           Control.Monad.Catch (handleAll)
+import qualified Data.ByteString.Char8 as BS
+import           Data.List (sort)
+import qualified Database.Beam as B
+import qualified Database.Beam.Migrate as B
+import qualified Database.Beam.Postgres as B
+import qualified Database.PostgreSQL.Simple as Pg
+import qualified Hedgehog.Gen as HH
+import qualified Hedgehog.Range as HH
+import           System.Environment (lookupEnv)
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+import           Typson.Beam
+import           Typson.Test.Beam.DbSchema (Db(..), EntityT(..), createTableMigration, db)
+import           Typson.Test.Generators
+import           Typson.Test.Types
+
+main :: IO ()
+main = defaultMain beamTestTree
+
+beamTestTree :: TestTree
+beamTestTree = withRunDb $ \runDb ->
+  testGroup "Beam Tests"
+  [ testCase "JSON Queries" $ do
+      graphs <- HH.sample (HH.list (HH.singleton 100) bazGen)
+      runDb (insertData graphs)
+
+      r1 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath basicPath1 bazJ <$> getAllGraphs
+      let a1 = JNullable . B.PgJSONB . basicPath1Getter <$> graphs
+      assertEqual "Basic Path 1" (sort r1) (sort a1)
+
+      r2 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath basicPath2 bazJ <$> getAllGraphs
+      let a2 = JNullable . B.PgJSONB . basicPath2Getter <$> graphs
+      assertEqual "Basic Path 2" (sort r2) (sort a2)
+
+      r3 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath basicPath3 bazJ <$> getAllGraphs
+      let a3 = JNullable . B.PgJSONB . basicPath3Getter <$> graphs
+      assertEqual "Basic Path 3" (sort r3) (sort a3)
+
+      r4 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath optionalPath1 bazJ <$> getAllGraphs
+      let a4 = JNullable . B.PgJSONB . optionalPath1Getter <$> graphs
+      assertEqual "Optional Path 1" (sort r4) (sort a4)
+
+      r5 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath optionalPath2 bazJ <$> getAllGraphs
+      let a5 = JNullable . B.PgJSONB . optionalPath2Getter <$> graphs
+      assertEqual "Optional Path 2" (sort r5) (sort a5)
+
+      r6 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath optionalPath3 bazJ <$> getAllGraphs
+      let a6 = JNullable . B.PgJSONB . optionalPath3Getter <$> graphs
+      assertEqual "Optional Path 3" (sort r6) (sort a6)
+
+      r7 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath listIdxPath1 bazJ <$> getAllGraphs
+      let a7 = JNullable . B.PgJSONB . listIdxPath1Getter <$> graphs
+      assertEqual "List Idx Path 1" (sort r7) (sort a7)
+
+      r8 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath listIdxPath2 bazJ <$> getAllGraphs
+      let a8 = JNullable . B.PgJSONB . listIdxPath2Getter <$> graphs
+      assertEqual "List Idx Path 2" (sort r8) (sort a8)
+
+      r9 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath listIdxPath3 bazJ <$> getAllGraphs
+      let a9 = JNullable . B.PgJSONB . listIdxPath3Getter <$> graphs
+      assertEqual "List Idx Path 3" (sort r9) (sort a9)
+
+      r10 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath unionPath1 bazJ <$> getAllGraphs
+      let a10 = JNullable . B.PgJSONB . unionPath1Getter <$> graphs
+      assertEqual "Union Query 1" (sort r10) (sort a10)
+
+      r11 <- runDb . B.runSelectReturningList . B.select $
+              jsonPath unionPath2 bazJ <$> getAllGraphs
+      let a11 = JNullable . B.PgJSONB . unionPath2Getter <$> graphs
+      assertEqual "Union Query 2" (sort r11) (sort a11)
+
+      r12 <- runDb . B.runSelectReturningList . B.select $
+               jsonPath textMapPath1 bazJ <$> getAllGraphs
+      let a12 = JNullable . B.PgJSONB . textMapPath1Getter <$> graphs
+      assertEqual "Text Map Query 1" (sort r12) (sort a12)
+
+      r13 <- runDb . B.runSelectReturningList . B.select $
+               jsonPath textMapPath2 bazJ <$> getAllGraphs
+      let a13 = JNullable . B.PgJSONB . textMapPath2Getter <$> graphs
+      assertEqual "Text Map Query 2" (sort r13) (sort a13)
+  ]
+
+getAllGraphs :: B.Q B.Postgres Db s (B.QGenExpr B.QValueContext B.Postgres s (JNullable B.PgJSONB Baz))
+getAllGraphs = _entityGraph <$> B.all_ (_dbEntity db)
+
+type DbRunner = forall b. B.Pg b -> IO b
+
+withRunDb :: (DbRunner -> TestTree) -> TestTree
+withRunDb mkTree = withDb $ \ioConn -> mkTree $ \action -> do
+  conn <- ioConn
+  B.runBeamPostgres conn action
+
+withDb :: (IO B.Connection -> TestTree) -> TestTree
+withDb = withResource connectToDb B.close
+
+connectToDb :: IO B.Connection
+connectToDb = do
+  Just connString <- lookupEnv "CONN_STRING"
+  conn <- B.connectPostgreSQL (BS.pack connString)
+
+  -- reset the table
+  _ <- handleAll (const $ pure 0) $ Pg.execute_ conn "DROP TABLE \"beam-entity\""
+
+  _ <- B.runBeamPostgres conn $
+    B.executeMigration B.runNoReturn createTableMigration
+
+  pure conn
+
+insertData :: [Baz] -> B.Pg ()
+insertData graphs =
+  let mkEntity g = EntityT { _entityId = B.default_
+                           , _entityGraph = B.val_ (JNullable $ B.PgJSONB g)
+                           }
+   in B.runInsert
+    . B.insert (_dbEntity db)
+    $ B.insertExpressions
+    $ map mkEntity graphs
diff --git a/test/Typson/Test/Beam/DbSchema.hs b/test/Typson/Test/Beam/DbSchema.hs
new file mode 100644
--- /dev/null
+++ b/test/Typson/Test/Beam/DbSchema.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE DeriveAnyClass #-}
+module Typson.Test.Beam.DbSchema
+  ( Db(..)
+  , db
+  , EntityT(..)
+  , createTableMigration
+  ) where
+
+import qualified Database.Beam as B
+import qualified Database.Beam.Postgres as B
+import qualified Database.Beam.Migrate as B
+import qualified Database.Beam.Backend.SQL.Types as B
+import           GHC.Generics (Generic)
+
+import           Typson.Beam (JNullable, nullableJsonb)
+import           Typson.Test.Types (Baz)
+
+newtype Db entity
+  = Db { _dbEntity :: entity (B.TableEntity EntityT) }
+  deriving (Generic, B.Database be)
+
+db :: B.DatabaseSettings be Db
+db = B.defaultDbSettings `B.withDbModification`
+       B.dbModification
+         { _dbEntity = B.setEntityName "beam-entity"
+         }
+
+data EntityT f
+  = EntityT
+    { _entityId :: B.C f (B.SqlSerial Int)
+    , _entityGraph :: B.C f (JNullable B.PgJSONB Baz)
+    } deriving (Generic, B.Beamable)
+
+instance B.Table EntityT where
+  newtype PrimaryKey EntityT f = EntityKey (B.C f (B.SqlSerial Int))
+    deriving (Generic, B.Beamable)
+
+  primaryKey = EntityKey . _entityId
+
+tableSchema :: B.Migration B.Postgres (B.CheckedDatabaseEntity B.Postgres db (B.TableEntity EntityT))
+tableSchema =
+  B.createTable "beam-entity"
+    ( EntityT (B.field "id" B.serial B.notNull B.unique)
+              (B.field "graph" nullableJsonb B.notNull)
+    )
+
+createTableMigration :: B.Migration B.Postgres (Db (B.CheckedDatabaseEntity B.Postgres db))
+createTableMigration = Db <$> tableSchema
diff --git a/typson-beam.cabal b/typson-beam.cabal
new file mode 100644
--- /dev/null
+++ b/typson-beam.cabal
@@ -0,0 +1,73 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: a8b6266051ddd3dedc036b170c169caf7d6474cdb0912c3abc4a6cd2d1be17f1
+
+name:           typson-beam
+version:        0.1.0.0
+synopsis:       Typson Beam Integration
+description:    Please see the README on GitHub at <https://github.com/aaronallen8455/typson#readme>
+category:       Database
+homepage:       https://github.com/aaronallen8455/typson#readme
+bug-reports:    https://github.com/aaronallen8455/typson/issues
+author:         Aaron Allen
+maintainer:     aaronallen8455@gmail.com
+copyright:      2020 Aaron Allen
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/aaronallen8455/typson
+
+library
+  exposed-modules:
+      Typson.Beam
+  other-modules:
+      Paths_typson_beam
+  hs-source-dirs:
+      src
+  build-depends:
+      aeson
+    , base >=4.7 && <5
+    , beam-core >=0.9.0.0 && <0.10.0.0
+    , beam-postgres >=0.5.0.0 && <0.6.0.0
+    , postgresql-simple
+    , typson-core
+  default-language: Haskell2010
+
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Typson.Test.Beam.DbSchema
+      Paths_typson_beam
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Werror=missing-fields -Werror=incomplete-patterns
+  build-depends:
+      HUnit
+    , aeson
+    , base >=4.7 && <5
+    , beam-core
+    , beam-migrate
+    , beam-postgres
+    , bytestring
+    , exceptions
+    , hedgehog
+    , microlens
+    , postgresql-simple
+    , tasty
+    , tasty-hedgehog
+    , tasty-hunit
+    , test-fixture
+    , typson-beam
+    , typson-core
+  default-language: Haskell2010
