diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Revision history for pgmq-core
+
+## 0.1.0.0 -- 2026-02-21
+
+* Initial release
+* Core types: `Message`, `MessageBody`, `MessageHeaders`, `MessageId`, `Queue`, `QueueName`, `PgmqError`
+* Queue name validation following pgmq-rs conventions
+* Template Haskell `Lift` instance for `QueueName`
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2025 Nadeem Bitar
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/pgmq-core.cabal b/pgmq-core.cabal
new file mode 100644
--- /dev/null
+++ b/pgmq-core.cabal
@@ -0,0 +1,43 @@
+cabal-version:   3.4
+name:            pgmq-core
+version:         0.1.0.0
+synopsis:        Core types for pgmq-hs, a Haskell client for PGMQ
+description:
+  Core types and type classes for pgmq-hs, a Haskell client library
+  for PGMQ (PostgreSQL Message Queue). Provides Message, Queue,
+  QueueName, and related types used across pgmq-hs packages.
+
+homepage:        https://github.com/topagentnetwork/pgmq-hs
+license:         MIT
+license-file:    LICENSE
+author:          Nadeem Bitar
+maintainer:      nadeem@topagentnetwork.com
+category:        Database
+build-type:      Simple
+extra-doc-files: CHANGELOG.md
+
+common warnings
+  ghc-options:
+    -Wall -Wcompat -Widentities -Wincomplete-uni-patterns
+    -Wincomplete-record-updates -Wredundant-constraints
+    -fhide-source-paths -Wmissing-export-lists -Wpartial-fields
+    -Wmissing-deriving-strategies
+
+library
+  import:             warnings
+  exposed-modules:    Pgmq.Types
+  default-extensions:
+    DeriveGeneric
+    GeneralisedNewtypeDeriving
+    ImportQualifiedPost
+    OverloadedStrings
+
+  build-depends:
+    , aeson             ^>=2.2
+    , base              >=4.18  && <5
+    , template-haskell  >=2.20  && <3
+    , text              ^>=2.1
+    , time              ^>=1.14
+
+  hs-source-dirs:     src
+  default-language:   GHC2024
diff --git a/src/Pgmq/Types.hs b/src/Pgmq/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Pgmq/Types.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE TemplateHaskellQuotes #-}
+
+module Pgmq.Types
+  ( MessageBody (..),
+    MessageHeaders (..),
+    MessageId (..),
+    Message (..),
+    Queue (..),
+    QueueName,
+    parseQueueName,
+    queueNameToText,
+    PgmqError (..),
+  )
+where
+
+import Data.Aeson (FromJSON, ToJSON, Value)
+import Data.Char (isAlphaNum, isAscii)
+import Data.Int (Int64)
+import Data.Text (Text)
+import Data.Text qualified as T
+import Data.Time (UTCTime)
+import GHC.Generics (Generic)
+import Language.Haskell.TH.Syntax (Lift (..))
+
+newtype MessageBody = MessageBody {unMessageBody :: Value}
+  deriving newtype (Eq, Ord, FromJSON, ToJSON)
+  deriving stock (Show, Generic)
+
+-- | Message headers for metadata (routing, tracing, etc.)
+-- Added in pgmq 1.5.0
+newtype MessageHeaders = MessageHeaders {unMessageHeaders :: Value}
+  deriving newtype (Eq, Ord, FromJSON, ToJSON)
+  deriving stock (Show, Generic)
+
+newtype MessageId = MessageId {unMessageId :: Int64}
+  deriving newtype (Eq, Ord, FromJSON, ToJSON)
+  deriving stock (Show, Generic)
+
+data Queue = Queue
+  { name :: !QueueName,
+    createdAt :: !UTCTime,
+    isPartitioned :: !Bool,
+    isUnlogged :: !Bool
+  }
+  deriving stock (Eq, Generic, Show)
+
+-- | https://tembo.io/pgmq/api/sql/types/
+-- Note: headers field added in pgmq 1.5.0
+-- Note: lastReadAt field added in pgmq 1.10.0
+data Message = Message
+  { messageId :: !MessageId,
+    visibilityTime :: !UTCTime,
+    enqueuedAt :: !UTCTime,
+    lastReadAt :: !(Maybe UTCTime),
+    readCount :: !Int64,
+    body :: !MessageBody,
+    headers :: !(Maybe Value)
+  }
+  deriving stock (Eq, Generic, Show)
+
+newtype QueueName = QueueName Text
+  deriving newtype (Eq, Ord, FromJSON, ToJSON)
+  deriving stock (Show, Generic)
+
+instance Lift QueueName where
+  lift (QueueName t) = [|QueueName t|]
+  liftTyped (QueueName t) = [||QueueName t||]
+
+queueNameToText :: QueueName -> Text
+queueNameToText (QueueName t) = t
+
+newtype PgmqError = InvalidQueueName Text
+  deriving stock (Show, Generic)
+
+-- Adopted from https://github.com/tembo-io/pgmq/blob/e4d4b84bf302df77be2d1f877c5cf8ef8861bfc7/pgmq-rs/src/util.rs#L94
+parseQueueName :: Text -> Either PgmqError QueueName
+parseQueueName t
+  | not isShortEnough = Left $ InvalidQueueName "The queue name is too long."
+  | not hasValidCharacters = Left $ InvalidQueueName "The queue name contains invalid characters."
+  | otherwise = Right $ QueueName t
+  where
+    isShortEnough = T.length t <= maxQueueNameLength
+    hasValidCharacters = T.all isValidChar t
+    isValidChar c = (isAscii c && isAlphaNum c) || c == '_'
+
+    -- PostgreSQL identifier length information
+    -- https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
+    maxIdentifierLength = 63 -- PostgreSQL truncates beyond this length
+    longestPrefix :: Text = "archived_at_idx_"
+    maxQueueNameLength = maxIdentifierLength - T.length longestPrefix
