diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog for `google-cloud-common`
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to the
+[Haskell Package Versioning Policy](https://pvp.haskell.org/).
+
+## Unreleased
+
+## 0.1.0.0 - YYYY-MM-DD
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Tushar Adhatrao
+
+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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1 @@
+# google-cloud-common
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/google-cloud-common.cabal b/google-cloud-common.cabal
new file mode 100644
--- /dev/null
+++ b/google-cloud-common.cabal
@@ -0,0 +1,70 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.37.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:           google-cloud-common
+version:        0.1.0.0
+synopsis:       GCP Client for Haskell
+description:    A common module for all GCP Client packages.
+category:       Web
+homepage:       https://github.com/tusharad/google-cloud-haskell#readme
+bug-reports:    https://github.com/tusharad/google-cloud-haskell/issues
+author:         tushar
+maintainer:     tusharadhatrao@gmail.com
+copyright:      2025 tushar
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/tusharad/google-cloud-haskell
+
+library
+  exposed-modules:
+      Google.Cloud.Common.Core
+  other-modules:
+      Paths_google_cloud_common
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
+  build-depends:
+      aeson <3
+    , base >=4.7 && <5
+    , bytestring >=0.9.1.4
+    , containers >=0.6 && <1
+    , http-conduit >=2.2 && <2.4
+    , jwt
+    , mtl
+    , process
+    , text >=1.2 && <3
+    , time
+  default-language: Haskell2010
+
+test-suite google-cloud-common-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_google_cloud_common
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      aeson <3
+    , base >=4.7 && <5
+    , bytestring >=0.9.1.4
+    , containers >=0.6 && <1
+    , google-cloud-common
+    , hspec >=1.3
+    , http-conduit >=2.2 && <2.4
+    , jwt
+    , mtl
+    , process
+    , text >=1.2 && <3
+    , time
+  default-language: Haskell2010
diff --git a/src/Google/Cloud/Common/Core.hs b/src/Google/Cloud/Common/Core.hs
new file mode 100644
--- /dev/null
+++ b/src/Google/Cloud/Common/Core.hs
@@ -0,0 +1,249 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Google.Cloud.Common.Core
+  ( genAccessToken
+  , RequestOptions (..)
+  , RequestMethod (..)
+  , AccessTokenResp (..)
+  , GoogleApplicationCred (..)
+  , doRequest
+  , doRequestJSON
+  , toPath 
+  , googleLoggingUrl 
+  ) where
+
+import Control.Exception (try)
+import Control.Monad.Error.Class (MonadError (throwError))
+import Data.Aeson
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy.Char8 as BSL
+import Data.IORef
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import qualified Data.Text.Encoding as TE
+import qualified Data.Text.Lazy as T
+import Data.Text.Lazy.Encoding (decodeUtf8)
+import Data.Time.Clock.POSIX
+import GHC.IO (unsafePerformIO)
+import Network.HTTP.Simple
+import System.Environment (lookupEnv)
+import System.Process
+import qualified Web.JWT as JWT
+import qualified Data.Map.Strict as Map
+
+data GoogleApplicationCred = GooGleApplicationCred
+  { type_ :: Text
+  , projectId :: Text
+  , privateKeyId :: Text
+  , privateKey :: Text
+  , clientEmail :: Text
+  , clientId :: Text
+  , authUri :: Text
+  , tokenUri :: Text
+  , auth_provider_x509_cert_url :: Text
+  , client_x509_cert_url :: Text
+  , universe_domain :: Text
+  }
+  deriving (Eq, Show)
+
+data AccessTokenResp = AccessTokenResp
+  { accessToken :: String
+  , expiresIn :: Integer
+  , tokenType :: String
+  }
+  deriving (Eq, Show)
+
+instance FromJSON AccessTokenResp where
+  parseJSON (Object v) =
+    AccessTokenResp
+      <$> v .: "access_token"
+      <*> v .: "expires_in"
+      <*> v .: "token_type"
+  parseJSON _ = error "asd"
+
+instance FromJSON GoogleApplicationCred where
+  parseJSON (Object v) =
+    GooGleApplicationCred
+      <$> v .: "type"
+      <*> v .: "project_id"
+      <*> v .: "private_key_id"
+      <*> v .: "private_key"
+      <*> v .: "client_email"
+      <*> v .: "client_id"
+      <*> v .: "auth_uri"
+      <*> v .: "token_uri"
+      <*> v .: "auth_provider_x509_cert_url"
+      <*> v .: "client_x509_cert_url"
+      <*> v .: "universe_domain"
+  parseJSON _ = error "asd"
+
+{- | Authentication
+ - There are two ways of authentication,
+    1. Application will look for `GOOGLE_APPLICATION_CREDENTIALS` environment variable
+        and read the JSON file path.
+    2. If the env variable is not stored, it will use `print-access-token`.
+        Make sure gcloud is installed and logged in for this to work.
+-}
+genAccessToken :: IO (Either String BS.ByteString)
+genAccessToken = do
+  mbEnv <- lookupEnv "GOOGLE_APPLICATION_CREDENTIALS"
+  case mbEnv of
+    Just jsonFilePath -> do
+      eRes <- readJSONFile jsonFilePath
+      case eRes of
+        Left err -> pure $ Left err
+        Right jsonVal -> getValidToken jsonVal
+    Nothing -> genTokenViaPrintAccessToken
+
+-- Global IORef to store the token in memory
+{-# NOINLINE tokenCache #-}
+tokenCache :: IORef (Maybe (BS.ByteString, Integer))
+tokenCache = unsafePerformIO (newIORef Nothing)
+
+-- Create JWT Token
+createJWT :: GoogleApplicationCred -> IO (Maybe Text)
+createJWT GooGleApplicationCred {..} = do
+  now <- round <$> getPOSIXTime :: IO Integer
+  let expiration = now + 3600 -- 1-hour expiry
+  let claims =
+        mempty
+          { JWT.iss = JWT.stringOrURI clientEmail
+          , JWT.aud = Left <$> JWT.stringOrURI tokenUri
+          , JWT.exp = JWT.numericDate (fromIntegral expiration)
+          , JWT.iat = JWT.numericDate (fromIntegral now)
+          , JWT.unregisteredClaims = 
+                JWT.ClaimsMap (Map.singleton "scope" "https://www.googleapis.com/auth/cloud-platform")
+          }
+
+      mbPrivateKeyBS = JWT.readRsaSecret (TE.encodeUtf8 privateKey)
+      jwtHeader =
+        JWT.JOSEHeader
+          { JWT.alg = Just JWT.RS256
+          , JWT.typ = Just "jwt"
+          , JWT.kid = Just privateKeyId
+          , JWT.cty = Nothing
+          }
+  case mbPrivateKeyBS of
+    Nothing -> pure Nothing
+    Just pKey ->
+      return $
+        Just $
+          JWT.encodeSigned
+            (JWT.EncodeRSAPrivateKey pKey)
+            jwtHeader
+            claims
+
+-- Exchange JWT for OAuth2 Access Token
+fetchAccessToken :: Text -> IO (Maybe (BS.ByteString, Integer))
+fetchAccessToken jwt = do
+  now <- round <$> getPOSIXTime
+  let reqBody = 
+        "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" <> jwt
+  initialRequest <- parseRequest "https://oauth2.googleapis.com/token"
+  let request =
+        setRequestHeaders
+          [("Content-Type", "application/x-www-form-urlencoded")]
+          ( setRequestMethod
+              "POST"
+              (setRequestBodyLBS (BSL.fromStrict $ TE.encodeUtf8 reqBody) initialRequest)
+          )
+  response <- httpLbs request
+  print response
+  let rBody = eitherDecodeStrict (BS.toStrict $ getResponseBody response) 
+  case rBody of
+    Right AccessTokenResp {..} -> return (Just (BS.pack accessToken, now + 3600)) -- Expiry set to 1 hour
+    Left err -> do 
+        print err
+        return Nothing
+
+-- Get a valid token (reuses existing one if valid)
+getValidToken :: GoogleApplicationCred -> IO (Either String BS.ByteString)
+getValidToken json_ = do
+  now <- round <$> getPOSIXTime
+  cached <- readIORef tokenCache
+  case cached of
+    Just (token, expiry) | now < expiry -> return (Right token) -- Return cached token
+    _ -> do
+      maybeJwt <- createJWT json_
+      case maybeJwt of
+        Just jwt -> do
+          maybeToken <- fetchAccessToken jwt
+          case maybeToken of
+            Just (token, expiry) -> do
+              writeIORef tokenCache (Just (token, expiry)) -- Cache new token
+              return (Right token)
+            Nothing -> return $ Left "Something went wrong with token 1"
+        Nothing -> return $ Left "Something went wrong with token 2"
+
+readJSONFile :: FilePath -> IO (Either String GoogleApplicationCred)
+readJSONFile jsonFilePath = do
+  eRes <- try $ readFile jsonFilePath
+  case eRes of
+    Left err -> pure . Left $ show (err :: IOError)
+    Right content -> pure $ eitherDecodeStrict (BS.pack content)
+
+genTokenViaPrintAccessToken :: IO (Either String BS.ByteString)
+genTokenViaPrintAccessToken = do
+  eRes <- try $ readProcess "gcloud" (words "auth print-access-token") []
+  case eRes of
+    Right s -> pure . Right $ BS.pack (init s)
+    Left err -> pure . Left $ show (err :: IOError)
+
+data RequestMethod = GET | POST | PATCH | DELETE
+  deriving (Eq, Show)
+
+data RequestOptions = RequestOptions
+  { reqMethod :: RequestMethod
+  , reqUrl :: String
+  , mbReqHeaders :: Maybe RequestHeaders
+  , mbQueryParams :: Maybe Query
+  , mbReqBody :: Maybe BSL.ByteString
+  , mbReqPath :: Maybe String
+  }
+  deriving (Eq, Show)
+
+doRequest :: RequestOptions -> IO (Either String BSL.ByteString)
+doRequest RequestOptions {..} = do
+  token <- genAccessToken
+  case token of
+    Left err -> throwError (userError err)
+    Right tokenBS -> do
+      initialRequest <- parseRequest (reqUrl <> fromMaybe "" mbReqPath)
+      let request =
+            foldl
+              (flip ($))
+              initialRequest
+              [ 
+               maybe Prelude.id setRequestHeaders mbReqHeaders
+              , maybe Prelude.id setRequestQueryString mbQueryParams
+              , maybe Prelude.id setRequestBodyLBS mbReqBody
+              , addRequestHeader "Authorization" ("Bearer " <> tokenBS)
+              , setRequestMethod (BS.pack $ show reqMethod)
+              ]
+      eResp <- try $ httpLbs request
+      case eResp of
+        Left err -> pure $ Left (show (err :: HttpException))
+        Right resp -> do
+          let respStatus = getResponseStatusCode resp
+              respBody = getResponseBody resp
+          if respStatus <= 299 && respStatus >= 200
+            then return $ Right respBody
+            else return $ Left (T.unpack $ decodeUtf8 respBody)
+
+doRequestJSON :: (FromJSON b) => RequestOptions -> IO (Either String b)
+doRequestJSON reqOpts = do
+  eResp <- doRequest reqOpts
+  case eResp of
+    Left err -> return $ Left err
+    Right respBody -> do
+      let respBody' = if BSL.null respBody then "{}" else respBody
+      case eitherDecode respBody' of
+        Left err -> pure $ Left err
+        Right res -> pure $ Right res
+
+toPath :: [String] -> String
+toPath = foldl (\acc x -> acc <> "/" <> x) ""
+
+googleLoggingUrl :: String
+googleLoggingUrl = "https://logging.googleapis.com/v2"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Test.Hspec
+import Google.Cloud.Common.Core
+import Data.Aeson (decode)
+import qualified Data.ByteString.Lazy.Char8 as BSL
+import Data.Maybe (isJust)
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = do
+  describe "Google.Cloud.Common.Core" $ do
+    context "AccessTokenResp JSON parsing" $ do
+      it "successfully parses valid JSON" $ do
+        let json = "{\"access_token\":\"ya29.a0AfH6SM...\",\"expires_in\":3599,\"token_type\":\"Bearer\"}"
+        let parsed = decode (BSL.pack json) :: Maybe AccessTokenResp
+        parsed `shouldSatisfy` isJust
+
+      it "fails to parse invalid JSON" $ do
+        let json = "{\"invalid_json\":\"...\"}"
+        let parsed = decode (BSL.pack json) :: Maybe AccessTokenResp
+        parsed `shouldBe` Nothing
+
+    context "GoogleApplicationCred JSON parsing" $ do
+      it "successfully parses valid JSON" $ do
+        let json = "{\"type\":\"service_account\",\"project_id\":\"my-project\",\"private_key_id\":\"1234\",\"private_key\":\"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\"client_email\":\"my-email@my-project.iam.gserviceaccount.com\",\"client_id\":\"123456789\",\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\"token_uri\":\"https://oauth2.googleapis.com/token\",\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\",\"client_x509_cert_url\":\"https://www.googleapis.com/robot/v1/metadata/x509/my-email%40my-project.iam.gserviceaccount.com\",\"universe_domain\":\"googleapis.com\"}"
+        let parsed = decode (BSL.pack json) :: Maybe GoogleApplicationCred
+        parsed `shouldSatisfy` isJust
+
+      it "fails to parse invalid JSON" $ do
+        let json = "{\"invalid_json\":\"...\"}"
+        let parsed = decode (BSL.pack json) :: Maybe GoogleApplicationCred
+        parsed `shouldBe` Nothing
+
+    context "toPath" $ do
+      it "concatenates parts with slashes" $ do
+        toPath ["part1", "part2", "part3"] `shouldBe` "/part1/part2/part3"
+
+      it "returns empty string for empty list" $ do
+        toPath [] `shouldBe` ""
