diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for lager
+
+## 0.1.0.0 -- 3-26-2026
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2026 dopamane
+
+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,16 @@
+# Concurrent Lager :beer:
+
+```hs
+import Lager
+
+main =
+  withLager "APP" [defConsole] $ \l -> do
+    logDebug   l "Cheers! 🍻"
+    logWarning l "Warning!"
+```
+
+Build & Docs
+```
+cabal build
+cabal haddock
+```
diff --git a/lager.cabal b/lager.cabal
new file mode 100644
--- /dev/null
+++ b/lager.cabal
@@ -0,0 +1,39 @@
+cabal-version:      3.0
+name:               lager
+version:            0.1.0.0
+synopsis:           Concurrent logging
+license:            MIT
+license-file:       LICENSE
+author:             dopamane
+maintainer:         dwc1295@gmail.com
+copyright:          David Cox
+category:           Logger
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+                    README.md
+
+source-repository head
+  type:     git
+  location: https://github.com/dopamane/lager
+
+library
+  hs-source-dirs:   lib
+  default-language: Haskell2010
+  ghc-options:      -Wall -O2
+  exposed-modules:  Lager
+  build-depends:    async
+                  , base < 5.0
+                  , prettyprinter
+                  , prettyprinter-ansi-terminal
+                  , stm
+                  , text
+
+test-suite test
+  hs-source-dirs:   test
+  default-language: Haskell2010
+  ghc-options:      -Wall -O2 -threaded
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  build-depends:    async
+                  , base
+                  , lager
diff --git a/lib/Lager.hs b/lib/Lager.hs
new file mode 100644
--- /dev/null
+++ b/lib/Lager.hs
@@ -0,0 +1,276 @@
+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Concurrent Lager 🍺
+--
+-- @
+-- import Lager
+--
+-- main =
+--   'withLager' \"APP\" ['File' 'Info' \"log.txt\"] $ \\l -> do
+--     'logDebug'   l "Cheers! 🍻"
+--     'logWarning' l "Warning!"
+-- @
+module Lager
+  ( -- * Logging
+    Lager
+  , withLager
+  , newLager
+  , newLagerSTM
+  , runLager
+  , drinkLager
+  , logDebug
+  , logInfo
+  , logNotice
+  , logWarning
+  , logErr
+  , logCrit
+  , logAlert
+  , logEmerg
+  , logSTM
+  , logStream
+  , logSub
+  , Msg(..)
+  , -- * Target
+    Target(..)
+  , defConsole
+  , Level(..)
+  , defLevelRGB
+  , -- * Exception
+    LagerException(..)
+  ) where
+
+import Control.Applicative
+import Control.Concurrent
+import Control.Concurrent.Async
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad
+import Data.Functor
+import Data.Ord
+import Data.Text.Lazy (Text)
+import qualified Data.Text.Lazy    as T
+import qualified Data.Text.Lazy.IO as T
+import GHC.Generics
+import Prettyprinter
+import Prettyprinter.Render.Terminal
+import System.IO
+
+-- | Logging handle
+data Lager = Lager
+  { nm :: Text        -- source name
+  , tg :: [Target]
+  , wc :: TChan Msg   -- write chan
+  , rc :: [TChan Msg] -- read chans
+  , drink :: TVar Bool
+  , drunk :: TVar Bool
+  }
+
+-- | Log message
+data Msg = Msg
+  { lvl :: Level
+  , txt :: Text -- ^ message
+  , src :: Text -- ^ logger source
+  }
+
+-- | Acquire a new handle
+newLager :: Text -> [Target] -> IO Lager
+newLager nm' = atomically . newLagerSTM nm'
+
+-- | Acquire a new handle in STM
+newLagerSTM :: Text -> [Target] -> STM Lager
+newLagerSTM nm' tgt' = do
+  wc' <- newBroadcastTChan
+  Lager nm' tgt' wc'
+    <$> replicateM (length tgt') (dupTChan wc')
+    <*> newTVar False
+    <*> newTVar False
+
+-- | Acquire a 'newLager', concurrently 'runLager',
+-- then finally 'drinkLager' if the body terminates
+-- or throws an exception.
+withLager :: Text -> [Target] -> (Lager -> IO a) -> IO a
+withLager nm' tgt' k = do
+  l <- newLager nm' tgt'
+  either id id
+    <$> race (k l `finally` drinkLager l)
+             (runLager l >> forever (threadDelay maxBound))
+
+-- | Finish logging. Blocks until all outputs are written by 'runLager'.
+drinkLager :: Lager -> IO ()
+drinkLager l = do
+  atomically $ writeTVar (drink l) True
+  atomically $ checkDrunk l
+
+checkDrink :: Lager -> STM ()
+checkDrink = check <=< readTVar . drink
+
+checkDrunk :: Lager -> STM ()
+checkDrunk = check <=< readTVar . drunk
+
+logDebug :: Lager -> Text -> IO ()
+logDebug l = lager l Debug
+
+logInfo :: Lager -> Text -> IO ()
+logInfo l = lager l Info
+
+logNotice :: Lager -> Text -> IO ()
+logNotice l = lager l Notice
+
+logWarning :: Lager -> Text -> IO ()
+logWarning l = lager l Warning
+
+logErr :: Lager -> Text -> IO ()
+logErr l = lager l Err
+
+logCrit :: Lager -> Text -> IO ()
+logCrit l = lager l Crit
+
+logAlert :: Lager -> Text -> IO ()
+logAlert l = lager l Alert
+
+logEmerg :: Lager -> Text -> IO ()
+logEmerg l = lager l Emerg
+
+-- | Log text in IO
+lager :: Lager -> Level -> Text -> IO ()
+lager l lvl' = atomically . logSTM l lvl'
+
+-- | Log text in STM
+logSTM :: Lager -> Level -> Text -> STM ()
+logSTM l lvl' msg = do
+  throwIfDrunk l
+  writeTChan (wc l) $ Msg lvl' msg (nm l)
+
+-- | Extend the logger name
+logSub :: Text -> Lager -> Lager
+logSub nm' l = Lager nm'' [] (wc l) [] (drink l) (drunk l)
+  where
+    nm'' | T.null nm'    = nm l
+         | T.null (nm l) = nm'
+         | otherwise     = nm l <> "|" <> nm'
+
+-- | Stream log messages
+logStream :: Lager -> (IO Msg -> IO a) -> IO a
+logStream l k = do
+  r <- atomically $ throwIfDrunk l *> dupTChan (wc l)
+  k  $ atomically $ throwIfDrunk l *> readTChan  r
+
+throwIfDrunk :: Lager -> STM ()
+throwIfDrunk l = do
+  isDrunk <- readTVar $ drunk l
+  when isDrunk $ throwSTM LagerDaemonTerminated
+
+-- | Log level
+data Level
+  = Emerg -- ^ emergency
+  | Alert
+  | Crit  -- ^ critcal
+  | Err   -- ^ error
+  | Warning
+  | Notice
+  | Info
+  | Debug
+  deriving (Enum, Eq, Generic, Read, Show)
+
+-- | 'Debug' < 'Emerg'
+instance Ord Level where
+  compare = comparing $ negate . fromEnum
+
+annLevelColor :: Level -> [(Level, Color)] -> Doc AnsiStyle -> Doc AnsiStyle
+annLevelColor l m = case lookup l m of
+  Just c  -> annotate $ color c
+  Nothing -> id
+
+-- | Default 'Console' color schema
+defLevelRGB :: [(Level, Color)]
+defLevelRGB =
+  [ (Emerg, Red), (Alert, Red), (Crit, Red), (Err, Red)
+  , (Warning, Yellow), (Notice, Green), (Debug, Cyan)
+  ]
+
+-- | Log output
+data Target
+  = Console Level [(Level, Color)] -- ^ stdout, optional RGB
+  | Journal Level                  -- ^ journald stdout
+  | File Level FilePath
+  deriving (Eq, Generic, Show)
+
+-- | Default 'Console' target with 'Info' log level
+-- and 'defLevelRGB' color schema.
+defConsole :: Target
+defConsole = Console Info defLevelRGB
+
+-- | Run logging daemon
+runLager :: Lager -> IO ()
+runLager l =
+  mapConcurrently_ (runTarget l) (zip (tg l) (rc l))
+    `finally` atomically (writeTVar (drunk l) True)
+
+runTarget :: Lager -> (Target, TChan Msg) -> IO ()
+runTarget lgr (t, c) = case t of
+  Console l m -> runHandle lgr stdout (renderConsoleRGB m) l c
+  Journal l -> runHandle lgr stdout renderJournal l c
+  File l path ->
+    withFile path WriteMode $ \hndl ->
+      runHandle lgr hndl renderConsole l c
+
+renderJournal :: Msg -> Text
+renderJournal msg =
+  "<" <> T.pack (show $ fromEnum $ lvl msg) <> "> " <> renderConsole msg
+
+renderConsole :: Msg -> Text
+renderConsole msg
+  | T.null (src msg) = txt msg
+  | otherwise        = "[" <> src msg <> "] " <> txt msg
+
+renderConsoleRGB :: [(Level, Color)] -> Msg -> Text
+renderConsoleRGB m msg =
+  renderLazy $
+  layoutPretty defaultLayoutOptions $
+  annLevelColor (lvl msg) m $
+  pretty $
+  renderConsole msg
+
+runHandle
+  :: Lager
+  -> Handle
+  -> (Msg -> Text)
+  -> Level
+  -> TChan Msg
+  -> IO ()
+runHandle lgr hndl render l c = loop
+  where
+    loop = join $ atomically $ do
+      m <- Just `fmap` readTChan c <|> (checkDrink lgr $> Nothing)
+      case m of
+        Just a  -> return $ logHandle hndl render l a >> loop
+        Nothing -> do
+          msgs <- listTChan c
+          return $ mapM_ (logHandle hndl render l) msgs
+
+logHandle :: Handle -> (Msg -> Text) -> Level -> Msg -> IO ()
+logHandle hndl render l msg
+  | not (visible l msg) = return ()
+  | otherwise           = T.hPutStrLn hndl $ render msg
+
+listTChan :: TChan a -> STM [a]
+listTChan t = loop
+  where
+    loop = do
+      isEmpty <- isEmptyTChan t
+      if isEmpty
+        then return []
+        else (:) <$> readTChan t <*> loop
+
+visible :: Level -> Msg -> Bool
+visible lvl' msg = lvl msg >= lvl'
+
+data LagerException
+  = LagerDaemonTerminated
+    -- ^ the daemon is already closed due to 'drinkLager'
+
+instance Show LagerException where
+  show LagerDaemonTerminated = "lager: daemon terminated"
+
+instance Exception LagerException
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import Lager
+
+main :: IO ()
+main = withLager "APP" [Console Debug defLevelRGB, File Info "log.txt"] $ \l -> do
+  logNotice l "Hello World!"
+  logDebug l "Invisible"
+  logErr l "NOO"
+  logInfo l "YES"
+  logWarning  l "HI"
+  let l' = logSub "SUB" l
+  logInfo l' "SUB!"
