diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,13 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.1.0.0] - 2018-07-01
+
+### Added
+- Initial release @domenkozar
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2018 Domen Kožar
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1 @@
+https://github.com/cachix/cachix/blob/master/README.md
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/cachix.cabal b/cachix.cabal
new file mode 100644
--- /dev/null
+++ b/cachix.cabal
@@ -0,0 +1,196 @@
+-- This file has been generated from package.yaml by hpack version 0.21.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 4ecd761e07cb906fa23f7c69e728be19cfd4d7d067f5860ff640827b44f5dfaf
+
+name:           cachix
+version:        0.1.0.0
+description:    Please see the README on Github at <https://github.com/cachix/cachix#readme>
+homepage:       https://github.com/cachix/cachix#readme
+bug-reports:    https://github.com/cachix/cachix/issues
+author:         Domen Kožar
+maintainer:     domen@enlambda.com
+copyright:      2018 Domen Kožar
+license:        Apache-2.0
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    ChangeLog.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/cachix/cachix
+
+library
+  exposed-modules:
+      Cachix.Client
+      Cachix.Client.Commands
+      Cachix.Client.Config
+      Cachix.Client.InstallationMode
+      Cachix.Client.NixConf
+      Cachix.Client.NixVersion
+      Cachix.Client.OptionsParser
+      Cachix.Client.Servant
+      Cachix.Client.URI
+  other-modules:
+      Paths_cachix
+  hs-source-dirs:
+      src
+  default-extensions: OverloadedStrings NoImplicitPrelude RecordWildCards
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns
+  build-depends:
+      async
+    , base >=4.7 && <5
+    , base16-bytestring
+    , base64-bytestring
+    , bifunctors
+    , bytestring
+    , cachix-api
+    , conduit
+    , conduit-combinators
+    , conduit-extra
+    , cookie
+    , cryptonite
+    , dhall
+    , directory
+    , ed25519
+    , fsnotify
+    , here
+    , http-client
+    , http-client-tls
+    , http-conduit
+    , http-types
+    , lzma-conduit
+    , megaparsec
+    , memory
+    , mmorph
+    , optparse-applicative
+    , process
+    , protolude
+    , resourcet
+    , servant >=0.14
+    , servant-auth
+    , servant-auth-client >=0.3.3.0
+    , servant-client >=0.14
+    , servant-generic
+    , servant-streaming-client >=0.3.0.0
+    , streaming
+    , text
+    , unix
+    , uri-bytestring
+    , versions
+  default-language: Haskell2010
+
+executable cachix
+  main-is: Main.hs
+  other-modules:
+      Paths_cachix
+  hs-source-dirs:
+      cachix
+  default-extensions: OverloadedStrings NoImplicitPrelude RecordWildCards
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      async
+    , base >=4.7 && <5
+    , base16-bytestring
+    , base64-bytestring
+    , bifunctors
+    , bytestring
+    , cachix
+    , cachix-api
+    , conduit
+    , conduit-combinators
+    , conduit-extra
+    , cookie
+    , cryptonite
+    , dhall
+    , directory
+    , ed25519
+    , fsnotify
+    , here
+    , http-client
+    , http-client-tls
+    , http-conduit
+    , http-types
+    , lzma-conduit
+    , megaparsec
+    , memory
+    , mmorph
+    , optparse-applicative
+    , process
+    , protolude
+    , resourcet
+    , servant >=0.14
+    , servant-auth
+    , servant-auth-client >=0.3.3.0
+    , servant-client >=0.14
+    , servant-generic
+    , servant-streaming-client >=0.3.0.0
+    , streaming
+    , text
+    , unix
+    , uri-bytestring
+    , versions
+  default-language: Haskell2010
+
+test-suite cachix-test
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules:
+      InstallationModeSpec
+      NixConfSpec
+      NixVersionSpec
+      Spec
+      Paths_cachix
+  hs-source-dirs:
+      test
+  default-extensions: OverloadedStrings NoImplicitPrelude RecordWildCards
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      async
+    , base >=4.7 && <5
+    , base16-bytestring
+    , base64-bytestring
+    , bifunctors
+    , bytestring
+    , cachix
+    , cachix-api
+    , conduit
+    , conduit-combinators
+    , conduit-extra
+    , cookie
+    , cryptonite
+    , dhall
+    , directory
+    , ed25519
+    , fsnotify
+    , here
+    , hspec
+    , http-client
+    , http-client-tls
+    , http-conduit
+    , http-types
+    , lzma-conduit
+    , megaparsec
+    , memory
+    , mmorph
+    , optparse-applicative
+    , process
+    , protolude
+    , resourcet
+    , servant >=0.14
+    , servant-auth
+    , servant-auth-client >=0.3.3.0
+    , servant-client >=0.14
+    , servant-generic
+    , servant-streaming-client >=0.3.0.0
+    , streaming
+    , text
+    , unix
+    , uri-bytestring
+    , versions
+  default-language: Haskell2010
diff --git a/cachix/Main.hs b/cachix/Main.hs
new file mode 100644
--- /dev/null
+++ b/cachix/Main.hs
@@ -0,0 +1,25 @@
+module Main (main) where
+
+import Control.Exception (handle, SomeException)
+import System.IO
+import System.Exit (exitFailure)
+import GHC.IO.Encoding
+
+import qualified Cachix.Client as CC
+
+
+main :: IO ()
+main = do
+  setLocaleEncoding utf8
+  hSetBuffering stdout LineBuffering
+  hSetBuffering stderr LineBuffering
+  handleExceptions CC.main
+
+handleExceptions :: IO a -> IO a
+handleExceptions = handle handler
+  where
+    handler :: SomeException -> IO a
+    handler e = do
+      hPutStrLn stderr ""
+      hPrint stderr e
+      exitFailure
diff --git a/src/Cachix/Client.hs b/src/Cachix/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client.hs
@@ -0,0 +1,41 @@
+module Cachix.Client
+  ( main
+  ) where
+
+import Data.Version                (showVersion)
+import Paths_cachix                (version)
+import Protolude
+import Network.HTTP.Simple         ( setRequestHeader )
+import Network.HTTP.Client.TLS     ( newTlsManagerWith, tlsManagerSettings )
+import Network.HTTP.Client         ( managerResponseTimeout, managerModifyRequest
+                                   , responseTimeoutNone, ManagerSettings)
+import Servant.Client              ( mkClientEnv)
+
+import Cachix.Client.OptionsParser ( CachixCommand(..), CachixOptions(..), getOpts )
+import Cachix.Client.Config        ( readConfig )
+import Cachix.Client.Commands      as Commands
+import Cachix.Client.URI           ( getBaseUrl)
+
+
+main :: IO ()
+main = do
+  (CachixOptions{..}, command) <- getOpts
+  config <- readConfig
+  manager <- newTlsManagerWith customManagerSettings
+  let env = mkClientEnv manager $ getBaseUrl host
+  case command of -- TODO: might want readerT here with client, config and env and opts
+    AuthToken token -> Commands.authtoken env config token
+    Create name -> Commands.create env config name
+    Push name paths watchStore -> Commands.push env config name paths watchStore
+    Use name shouldEchoNixOS -> Commands.use env config name shouldEchoNixOS
+    Version -> putText cachixVersion
+
+customManagerSettings :: ManagerSettings
+customManagerSettings = tlsManagerSettings
+  { managerResponseTimeout = responseTimeoutNone
+  -- managerModifyRequest :: Request -> IO Request
+  , managerModifyRequest = \request -> return $ setRequestHeader "User-Agent" [toS cachixVersion] request
+  }
+
+cachixVersion :: Text
+cachixVersion = "cachix " <> toS (showVersion version)
diff --git a/src/Cachix/Client/Commands.hs b/src/Cachix/Client/Commands.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/Commands.hs
@@ -0,0 +1,282 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+module Cachix.Client.Commands
+  ( authtoken
+  , create
+  , push
+  , use
+  ) where
+
+import           Crypto.Sign.Ed25519
+import           Control.Concurrent           (threadDelay)
+import qualified Control.Concurrent.QSem       as QSem
+import           Control.Concurrent.Async     (mapConcurrently)
+import           Control.Monad                (forever)
+import           Control.Monad.Trans.Resource (ResourceT)
+import qualified Data.ByteString.Base64        as B64
+import           Data.Conduit
+import           Data.Conduit.Process
+import qualified Data.Conduit.List             as CL
+import           Data.Conduit.Lzma              ( compress )
+import           Data.IORef
+import           Data.List                      ( isSuffixOf )
+import           Data.Maybe                     ( fromJust )
+import           Data.String.Here
+import qualified Data.Text                      as T
+import           Network.HTTP.Types (status404)
+import           Protolude
+import           Servant.API
+import           Servant.Client
+import           Servant.Auth                   ()
+import           Servant.Auth.Client
+import           Servant.Streaming.Client       ()
+import           Servant.Generic
+import           System.Directory               ( doesFileExist )
+import           System.FSNotify
+import           System.IO                      ( stdin, hIsTerminalDevice )
+import           System.Process                 ( readProcessWithExitCode, readProcess )
+import           System.Environment             ( lookupEnv )
+import qualified Streaming.Prelude             as S
+
+import qualified Cachix.Api                    as Api
+import           Cachix.Api.Signing             (fingerprint, passthroughSizeSink, passthroughHashSink)
+import           Cachix.Client.Config           ( Config(..)
+                                                , BinaryCacheConfig(..)
+                                                , writeConfig
+                                                , mkConfig
+                                                )
+import qualified Cachix.Client.Config          as Config
+import           Cachix.Client.InstallationMode
+import           Cachix.Client.NixVersion       ( getNixMode
+                                                , NixMode
+                                                )
+import qualified Cachix.Client.NixConf         as NixConf
+import           Cachix.Client.Servant
+
+
+cachixClient :: Api.CachixAPI AsClient
+cachixClient = fromServant $ client Api.servantApi
+
+cachixBCClient :: Text -> Api.BinaryCacheAPI AsClient
+cachixBCClient name = fromServant $ Api.cache cachixClient name
+
+authtoken :: ClientEnv -> Maybe Config -> Text -> IO ()
+authtoken _ maybeConfig token = do
+  -- TODO: check that token actually authenticates!
+  writeConfig $ case maybeConfig of
+    Just config -> config { authToken = token }
+    Nothing -> mkConfig token
+  putStrLn ([hereLit|
+Continue by creating a binary cache with:
+
+    $ cachix create <name>
+
+and share it with others over https://<name>.cachix.org
+  |] :: Text)
+
+create :: ClientEnv -> Maybe Config -> Text -> IO ()
+create _ Nothing _ = throwIO $ NoConfig "start with: $ cachix authtoken <token>"
+create env (Just config@Config{..}) name = do
+  (PublicKey pk, SecretKey sk) <- createKeypair
+
+  let bc = Api.BinaryCacheCreate $ toS $ B64.encode pk
+  res <- (`runClientM` env) $ Api.create (cachixBCClient name) (Token (toS authToken)) bc
+  case res of
+    -- TODO: handle all kinds of errors
+    Left err -> panic $ show err
+    Right _ -> do
+      -- write signing key to config
+      let signingKey = toS $ B64.encode sk
+          bcc = BinaryCacheConfig name signingKey
+      writeConfig $ config { binaryCaches = binaryCaches <> [bcc] }
+
+      putStrLn ([iTrim|
+Signing key has been saved on your local machine. To populate
+your binary cache:
+
+    $ nix-build | cachix push ${name}
+
+Or if you'd like to use the signing key on another machine or CI:
+
+    $ export CACHIX_SIGNING_KEY=${signingKey}
+    $ nix-build | cachix push ${name}
+
+To instruct Nix to use the binary cache:
+
+    $ cachix use ${name}
+
+      |] :: Text)
+
+use :: ClientEnv -> Maybe Config -> Text -> Bool -> IO ()
+use env _ name shouldEchoNixOS = do
+  -- 1. get cache public key
+  res <- (`runClientM` env) $ Api.get (cachixBCClient name)
+  case res of
+    -- TODO: handle 404
+    Left err -> panic $ show err
+    Right binaryCache -> do
+      eitherNixMode <- getNixMode
+      case eitherNixMode of
+        Left err -> panic err
+        Right nixMode -> do
+          user <- getUser
+          nc <- NixConf.read NixConf.Global
+          isTrusted <- isTrustedUser $ NixConf.readLines (catMaybes [nc]) NixConf.isTrustedUsers
+          isNixOS <- doesFileExist "/etc/NIXOS"
+          let nixEnv = NixEnv
+                { nixMode = nixMode
+                , isRoot = user == "root"
+                , isTrusted = isTrusted
+                , isNixOS = isNixOS
+                }
+          addBinaryCache binaryCache $
+            if shouldEchoNixOS
+            then EchoNixOS
+            else getInstallationMode nixEnv
+
+
+-- TODO: lots of room for perfomance improvements
+push :: ClientEnv -> Maybe Config -> Text -> [Text] -> Bool -> IO ()
+push env config name rawPaths False = do
+  hasNoStdin <- hIsTerminalDevice stdin
+  when (not hasNoStdin && not (null rawPaths)) $ throwIO $ AmbiguousInput "You provided both stdin and store path arguments, pick only one to proceed."
+  inputStorePaths <-
+    if hasNoStdin
+    then return rawPaths
+    else T.words <$> getContents
+
+  -- TODO: if empty, take whole nix store and warn: nix store-path --all
+  when (null inputStorePaths) $ throwIO $ NoInput "You need to specify store paths either as stdin or as a cli argument"
+
+  -- Query list of paths
+  -- TODO: split args if too big
+  (exitcode, out, err) <- readProcessWithExitCode "nix-store" (fmap toS (["-qR"] <> inputStorePaths)) mempty
+
+  -- TODO: make pool size configurable, on beefier machines this could be doubled
+  _ <- mapConcurrentlyBounded 4 (pushStorePath env config name) (T.lines (toS out))
+  putText "All done."
+push env config name _ True = withManager $ \mgr -> do
+  _ <- watchDir mgr "/nix/store" filterF action
+  putText "Watching /nix/store for new builds ..."
+  forever $ threadDelay 1000000
+  where
+    action :: Action
+    action (Removed fp _) = pushStorePath env config name $ toS $ dropEnd 5 fp
+    action _ = return ()
+
+    filterF :: ActionPredicate
+    filterF (Removed fp _) | ".lock" `isSuffixOf` fp = True
+    filterF _ = False
+
+    dropEnd :: Int -> [a] -> [a]
+    dropEnd index xs = take (length xs - index) xs
+
+pushStorePath :: ClientEnv -> Maybe Config -> Text -> Text -> IO ()
+pushStorePath env config name storePath = do
+  -- use secret key from config or env
+  maybeEnvSK <- lookupEnv "CACHIX_SIGNING_KEY"
+  let matches Config{..} = filter (\bc -> Config.name bc == name) binaryCaches
+      maybeBCSK = case config of
+        Nothing -> Nothing
+        Just c -> Config.secretKey <$> head (matches c)
+      -- TODO: better error msg
+      sk = SecretKey $ toS $ B64.decodeLenient $ toS $ fromJust $ maybeBCSK <|> toS <$> maybeEnvSK <|> panic "You need to: export CACHIX_SIGNING_KEY=XXX"
+      (storeHash, _) = splitStorePath $ toS storePath
+  -- Check if narinfo already exists
+  -- TODO: query also cache.nixos.org? server-side?
+  res <- (`runClientM` env) $ Api.narinfoHead
+    (cachixBCClient name)
+    (Api.NarInfoC storeHash)
+  case res of
+    Right NoContent -> return ()
+    Left err | isErr err status404 -> go sk
+             | otherwise -> panic $ show err -- TODO: retry
+    where
+      go sk = do
+        let (storeHash, storeSuffix) = splitStorePath $ toS storePath
+        putStrLn $ "pushing " <> storePath
+
+        narSizeRef <- liftIO $ newIORef 0
+        fileSizeRef <- liftIO $ newIORef 0
+        narHashRef <- liftIO $ newIORef ("" :: Text)
+        fileHashRef <- liftIO $ newIORef ("" :: Text)
+
+        -- stream store path as xz compressed nar file
+        (ClosedStream, (source, close), ClosedStream, cph) <- streamingProcess $ shell ("nix-store --dump " <> toS storePath)
+        let stream'
+              = source
+             .| passthroughSizeSink narSizeRef
+             .| passthroughHashSink narHashRef
+             .| compress Nothing
+             .| passthroughSizeSink fileSizeRef
+             .| passthroughHashSink fileHashRef
+
+            conduitToStreaming :: S.Stream (S.Of ByteString) (ResourceT IO) ()
+            conduitToStreaming = runConduit (transPipe lift stream' .| CL.mapM_ S.yield)
+        -- for now we need to use letsencrypt domain instead of cloudflare due to its upload limits
+        let newEnv = env {
+              baseUrl = (baseUrl env) { baseUrlHost = toS name <> "." <> baseUrlHost (baseUrl env)}
+            }
+        -- TODO: http retry: retry package?
+        res <- (`runClientM` newEnv) $ Api.createNar
+          (cachixBCClient name)
+          (contentType (Proxy :: Proxy Api.XNixNar), conduitToStreaming)
+        close
+        exitcode <- waitForStreamingProcess cph
+        case res of
+          Left err -> panic $ show err
+          Right NoContent -> do
+            narSize <- readIORef narSizeRef
+            narHashB16 <- readIORef narHashRef
+            fileHash <- readIORef fileHashRef
+            fileSize <- readIORef fileSizeRef
+
+            -- TODO: #3: implement using pure haskell
+            narHash <- ("sha256:" <>) . T.strip . toS <$> readProcess "nix-hash" ["--type", "sha256", "--to-base32", toS narHashB16] mempty
+
+            (exitcode, out, err) <- readProcessWithExitCode "nix-store" ["-q", "--deriver", toS storePath] mempty
+            let deriverRaw = T.strip $ toS out
+                deriver = if deriverRaw == "unknown-deriver"
+                          then deriverRaw
+                          else T.drop 11 deriverRaw
+
+            (exitcode, out, err) <- readProcessWithExitCode "nix-store" ["-q", "--references", toS storePath] mempty
+
+            let
+                references = sort $ T.lines $ T.strip $ toS out
+                fp = fingerprint storePath narHash narSize references
+                sig = dsign sk fp
+                nic = Api.NarInfoCreate
+                  { cStoreHash = storeHash
+                  , cStoreSuffix = storeSuffix
+                  , cNarHash = narHash
+                  , cNarSize = narSize
+                  , cFileSize = fileSize
+                  , cFileHash = fileHash
+                  , cReferences = fmap (T.drop 11) references
+                  , cDeriver = deriver
+                  , cSig = toS $ B64.encode $ unSignature sig
+                  }
+            -- Upload narinfo with signature
+            res <- (`runClientM` env) $ Api.createNarinfo
+              (cachixBCClient name)
+              (Api.NarInfoC storeHash)
+              nic
+            case res of
+              Left err -> panic $ show err -- TODO: handle json errors
+              Right NoContent -> return ()
+
+
+-- Utils
+
+
+mapConcurrentlyBounded :: Traversable t => Int -> (a -> IO b) -> t a -> IO (t b)
+mapConcurrentlyBounded bound action items = do
+  qs <- QSem.newQSem bound
+  let wrapped x = bracket_ (QSem.waitQSem qs) (QSem.signalQSem qs) (action x)
+  mapConcurrently wrapped items
+
+splitStorePath :: Text -> (Text, Text)
+splitStorePath storePath =
+  (T.take 32 (T.drop 11 storePath), T.drop 44 storePath)
diff --git a/src/Cachix/Client/Config.hs b/src/Cachix/Client/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/Config.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveAnyClass #-}
+module Cachix.Client.Config
+  ( Config(..)
+  , BinaryCacheConfig(..)
+  , readConfig
+  , writeConfig
+  , mkConfig
+  ) where
+
+import Dhall     hiding ( Text )
+import Dhall.Pretty     ( prettyExpr )
+import GHC.Generics     ( Generic )
+import System.Directory ( doesFileExist, createDirectoryIfMissing
+                        , getXdgDirectory, XdgDirectory(..)
+                        )
+import System.Posix.Files ( setFileMode, unionFileModes, ownerReadMode
+                          , ownerWriteMode )
+import Protolude
+
+
+data BinaryCacheConfig = BinaryCacheConfig
+ { name :: Text
+ , secretKey :: Text
+ } deriving (Show, Generic, Interpret, Inject)
+
+data Config = Config
+ { authToken :: Text
+ , binaryCaches :: [BinaryCacheConfig]
+ } deriving (Show, Generic, Interpret, Inject)
+
+mkConfig :: Text -> Config
+mkConfig authtoken = Config
+  { authToken = authtoken
+  , binaryCaches = []
+  }
+
+readConfig :: IO (Maybe Config)
+readConfig = do
+  filename <- getFilename
+  doesExist <- doesFileExist filename
+  if doesExist
+  then Just <$> input auto (toS filename)
+  else return Nothing
+
+
+getFilename :: IO FilePath
+getFilename = do
+  dir <- getXdgDirectory XdgConfig "cachix"
+  createDirectoryIfMissing True dir
+  return $ dir <> "/cachix.dhall"
+
+writeConfig :: Config -> IO ()
+writeConfig config = do
+  filename <- getFilename
+  let doc = prettyExpr $ embed inject config
+  writeFile filename $ show doc
+  assureFilePermissions filename
+  putStrLn $ "Written to " <> filename
+
+-- Does: chmod rw file
+assureFilePermissions :: FilePath -> IO ()
+assureFilePermissions fp =
+  setFileMode fp $ unionFileModes ownerReadMode ownerWriteMode
diff --git a/src/Cachix/Client/InstallationMode.hs b/src/Cachix/Client/InstallationMode.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/InstallationMode.hs
@@ -0,0 +1,123 @@
+{-# LANGUAGE QuasiQuotes #-}
+module Cachix.Client.InstallationMode
+  ( InstallationMode(..)
+  , NixEnv(..)
+  , CachixException(..)
+  , getInstallationMode
+  , addBinaryCache
+  , isTrustedUser
+  , getUser
+  ) where
+
+import           Protolude
+import           Data.String.Here
+import qualified Data.Text as T
+import qualified Cachix.Client.NixConf as NixConf
+import           Cachix.Client.NixVersion ( NixMode(..) )
+import           Cachix.Api as Api
+import           System.Directory               ( getPermissions, writable )
+import           System.Environment             ( lookupEnv )
+
+data CachixException
+  = UnsupportedNixVersion Text
+  | UserEnvNotSet Text
+  | MustBeRoot Text
+  | NixOSInstructions Text
+  | AmbiguousInput Text
+  | NoInput Text
+  | NoConfig Text
+  deriving (Show, Typeable)
+
+instance Exception CachixException
+
+data NixEnv = NixEnv
+  { nixMode :: NixMode
+  , isTrusted :: Bool
+  , isRoot :: Bool
+  , isNixOS :: Bool
+  }
+
+data InstallationMode
+  = Install NixConf.NixConfLoc
+  | UnsupportedNix1X
+  | EchoNixOS
+  | EchoNixOSWithTrustedUser
+  | UntrustedRequiresSudo
+  | Nix20RequiresSudo
+  deriving (Show, Eq)
+
+getInstallationMode :: NixEnv -> InstallationMode
+getInstallationMode NixEnv{..}
+  | nixMode == Nix1XX = UnsupportedNix1X
+  | isNixOS && isRoot = EchoNixOS
+  | isNixOS && (not isTrusted) = EchoNixOSWithTrustedUser
+  | (not isNixOS) && isRoot = Install NixConf.Global
+  | nixMode == Nix20 = Nix20RequiresSudo
+  | isTrusted = Install NixConf.Local
+  | not isTrusted = UntrustedRequiresSudo
+
+
+-- | Add a Binary cache to nix.conf, print nixos config or fail
+addBinaryCache :: Api.BinaryCache -> InstallationMode -> IO ()
+addBinaryCache _ UnsupportedNix1X = throwIO $
+  UnsupportedNixVersion "Nix 1.x is not supported, please upgrade to Nix 2.0.1 or greater"
+addBinaryCache Api.BinaryCache{..} EchoNixOS = do
+  putText [iTrim|
+nix = {
+  binaryCaches = [
+    "${uri}"
+  ];
+  binaryCachePublicKeys = [
+    ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)}
+  ];
+};
+  |]
+  throwIO $ NixOSInstructions "Add above lines to your NixOS configuration file"
+addBinaryCache Api.BinaryCache{..} EchoNixOSWithTrustedUser = do
+  -- TODO: DRY
+  user <- getUser
+  putText [iTrim|
+nix = {
+  binaryCaches = [
+    "${uri}"
+  ];
+  binaryCachePublicKeys = [
+    ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)}
+  ];
+  trustedUsers = [ "root" "${user}" ];
+};
+  |]
+  throwIO $ NixOSInstructions "Add above lines to your NixOS configuration file"
+addBinaryCache _ UntrustedRequiresSudo = throwIO $
+  MustBeRoot "Run command as root OR execute: $ echo \"trusted-users = root $USER\" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon"
+addBinaryCache _ Nix20RequiresSudo = throwIO $
+  MustBeRoot "Run command as root OR upgrade to latest Nix - to be able to use it without root (recommended)"
+addBinaryCache bc@Api.BinaryCache{..} (Install ncl) = do
+  -- TODO: might need locking one day
+  gnc <- NixConf.read NixConf.Global
+  lnc <- NixConf.read NixConf.Local
+  let final = if ncl == NixConf.Global then gnc else lnc
+      input = if ncl == NixConf.Global then [gnc] else [gnc, lnc]
+  NixConf.write ncl $ NixConf.add bc (catMaybes input) (fromMaybe (NixConf.NixConf []) final)
+  filename <- NixConf.getFilename ncl
+  putStrLn $ "Configured " <> uri <> " binary cache in " <> toS filename
+
+isTrustedUser :: [Text] -> IO Bool
+isTrustedUser users = do
+  user <- getUser
+  -- to detect single user installations
+  permissions <- getPermissions "/nix/store"
+  unless (groups == []) $ do
+    -- TODO: support Nix group syntax
+    putText "Warn: cachix doesn't yet support checking if user is trusted via groups, so it will recommend sudo"
+    putStrLn $ "Warn: groups found " <> T.intercalate "," groups
+  return $ (writable permissions) || (user `elem` users)
+  where
+    groups = filter (\u -> T.head u == '@') users
+
+getUser :: IO Text
+getUser = do
+  maybeUser <- lookupEnv "USER"
+  case maybeUser of
+    Nothing -> throwIO $ UserEnvNotSet "$USER must be set"
+    Just user -> return $ toS user
diff --git a/src/Cachix/Client/NixConf.hs b/src/Cachix/Client/NixConf.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/NixConf.hs
@@ -0,0 +1,173 @@
+{- (Very limited) parser, rendered and modifier of nix.conf
+-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveFunctor #-}
+module Cachix.Client.NixConf
+  ( NixConf
+  , NixConfG(..)
+  , NixConfLine(..)
+  , NixConfLoc(..)
+  , render
+  , add
+  , read
+  , update
+  , write
+  , getFilename
+  , parser
+  , parse
+  , readLines
+  , writeLines
+  , isTrustedUsers
+  , defaultPublicURI
+  , defaultSigningKey
+  ) where
+
+import Control.Exception (catch)
+import Data.Char (isSpace)
+import Data.Text (unwords, unlines)
+import Data.List (nub)
+import Protolude
+import qualified Text.Megaparsec as Mega
+import Text.Megaparsec.Char
+import System.Directory ( doesFileExist, createDirectoryIfMissing
+                        , getXdgDirectory, XdgDirectory(..)
+                        )
+import Cachix.Api (BinaryCache(..))
+
+
+data NixConfLine
+  = Substituters [Text]
+  | TrustedUsers [Text]
+  | TrustedPublicKeys [Text]
+  | BinaryCachePublicKeys [Text]
+  | BinaryCaches [Text]
+  | Other Text
+  deriving (Show, Eq)
+
+newtype NixConfG a = NixConf a
+  deriving (Show, Eq, Functor)
+
+type NixConf = NixConfG [NixConfLine]
+
+readLines :: [NixConf] -> (NixConfLine -> Maybe [Text]) -> [Text]
+readLines nixconfs predicate = concat $ fmap f nixconfs
+  where
+    f (NixConf xs) = foldl foldIt [] xs
+
+    foldIt :: [Text] -> NixConfLine -> [Text]
+    foldIt prev new = prev <> (fromMaybe [] $ predicate new)
+
+writeLines :: (NixConfLine -> Maybe [Text]) -> NixConfLine -> NixConf -> NixConf
+writeLines predicate addition nixconf = fmap f nixconf
+  where
+    f x = filter (isNothing . predicate) x <> [addition]
+
+isSubstituter :: NixConfLine -> Maybe [Text]
+isSubstituter (Substituters xs) = Just xs
+isSubstituter (BinaryCaches xs) = Just xs
+isSubstituter _ = Nothing
+
+isPublicKey :: NixConfLine -> Maybe [Text]
+isPublicKey (TrustedPublicKeys xs) = Just xs
+isPublicKey (BinaryCachePublicKeys xs) = Just xs
+isPublicKey _ = Nothing
+
+isTrustedUsers :: NixConfLine -> Maybe [Text]
+isTrustedUsers (TrustedUsers xs) = Just xs
+isTrustedUsers _ = Nothing
+
+-- | Pure version of addIO
+add :: BinaryCache -> [NixConf] -> NixConf -> NixConf
+add BinaryCache{..} toRead toWrite =
+  writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys) $
+    writeLines isSubstituter (Substituters $ nub substituters) toWrite
+  where
+    -- Note: some defaults are always appended since overriding some setttings in nix.conf overrides defaults otherwise
+    substituters = (defaultPublicURI : readLines toRead isSubstituter) <> [uri]
+    publicKeys = (defaultSigningKey : readLines toRead isPublicKey) <> publicSigningKeys
+
+defaultPublicURI :: Text
+defaultPublicURI = "https://cache.nixos.org"
+defaultSigningKey :: Text
+defaultSigningKey = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
+
+render :: NixConf -> Text
+render (NixConf nixconflines) = unlines $ fmap go nixconflines
+  where
+    go :: NixConfLine -> Text
+    go (Substituters xs) = "substituters = " <> unwords xs
+    go (BinaryCaches xs) = "substituters = " <> unwords xs
+    go (TrustedUsers xs) = "trusted-users = " <> unwords xs
+    go (TrustedPublicKeys xs) = "trusted-public-keys = " <> unwords xs
+    go (BinaryCachePublicKeys xs) = "trusted-public-keys = " <> unwords xs
+    go (Other line) = line
+
+write :: NixConfLoc -> NixConf -> IO ()
+write ncl nc = do
+  filename <- getFilename ncl
+  writeFile filename $ render nc
+
+read :: NixConfLoc -> IO (Maybe NixConf)
+read ncl = do
+  filename <- getFilename ncl
+  doesExist <- doesFileExist filename
+  if not doesExist
+  then return Nothing
+  else do
+    result <- parse <$> readFile filename
+    case result of
+      Left err -> do
+        putStrLn (Mega.parseErrorPretty err)
+        panic $ toS filename <> " failed to parse, please copy the above error and contents of nix.conf and open an issue at https://github.com/cachix/cachix"
+      Right conf -> return $ Just conf
+
+update :: NixConfLoc -> (Maybe NixConf -> NixConf) -> IO ()
+update ncl f = do
+  nc <- f <$> read ncl
+  write ncl nc
+
+data NixConfLoc = Global | Local
+  deriving (Show, Eq)
+
+getFilename :: NixConfLoc -> IO FilePath
+getFilename ncl = do
+  dir <- case ncl of
+    Global -> return "/etc/nix"
+    Local -> getXdgDirectory XdgConfig "nix"
+  _ <- catch (createDirectoryIfMissing True dir) $ \e ->
+    hPutStr stderr ("Warning: Couldn't create " <> dir <> " :" <> show (e :: IOException))
+  return $ dir <> "/nix.conf"
+
+-- nix.conf Parser
+
+type Parser = Mega.Parsec Void Text
+
+-- TODO: handle comments
+parseLine :: ([Text] -> NixConfLine) -> Text -> Parser NixConfLine
+parseLine constr name = Mega.try $ do
+    _ <- optional (some (char ' '))
+    _ <- string name
+    _ <- many (char ' ')
+    _ <- char '='
+    _ <- many (char ' ')
+    values <- Mega.sepBy1 (many (satisfy (not . isSpace))) (some (char ' '))
+    _ <- many spaceChar
+    return $ constr (fmap toS values)
+
+parseOther :: Parser NixConfLine
+parseOther = Other . toS <$> Mega.manyTill anyChar eol
+
+parseAltLine :: Parser NixConfLine
+parseAltLine =
+      parseLine Substituters "substituters"
+  <|> parseLine TrustedPublicKeys "trusted-public-keys"
+  <|> parseLine TrustedUsers "trusted-users"
+  <|> parseLine BinaryCachePublicKeys "binary-cache-public-keys"
+  <|> parseLine BinaryCaches "binary-caches"
+  <|> parseOther
+
+parser :: Parser NixConf
+parser = NixConf <$> many parseAltLine
+
+parse :: Text -> Either (Mega.ParseError (Mega.Token Text) Void) NixConf
+parse = Mega.parse parser "nix.conf"
diff --git a/src/Cachix/Client/NixVersion.hs b/src/Cachix/Client/NixVersion.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/NixVersion.hs
@@ -0,0 +1,37 @@
+module Cachix.Client.NixVersion
+  ( getNixMode
+  , parseNixMode
+  , NixMode(..)
+  ) where
+
+import Protolude
+import System.Process (readProcessWithExitCode)
+import Data.Versions
+import Data.Text as T
+
+
+data NixMode
+  = Nix20
+  | Nix201
+  | Nix1XX
+  deriving (Show, Eq)
+
+getNixMode :: IO (Either Text NixMode)
+getNixMode = do
+  (exitcode, out, err) <- readProcessWithExitCode "nix-env" ["--version"] mempty
+  unless (err == "") $ putStrLn $ "nix-env stderr: " <> err
+  return $ case exitcode of
+    ExitFailure i -> Left $ "'nix-env --version' exited with " <> show i
+    ExitSuccess -> parseNixMode $ toS out
+
+parseNixMode :: Text -> Either Text NixMode
+parseNixMode output =
+  let
+    verStr = T.drop 14 $ T.strip output
+    err = "Couldn't parse 'nix-env --version' output: " <> output
+  in case versioning verStr of
+    Left _ -> Left err
+    Right ver | verStr == "" -> Left err
+              | ver < Ideal (SemVer 1 99 0 [] []) -> Right Nix1XX
+              | ver < Ideal (SemVer 2 0 1 [] []) -> Right Nix20
+              | otherwise -> Right Nix201
diff --git a/src/Cachix/Client/OptionsParser.hs b/src/Cachix/Client/OptionsParser.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/OptionsParser.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE QuasiQuotes #-}
+module Cachix.Client.OptionsParser
+  ( CachixCommand(..)
+  , CachixOptions(..)
+  , BinaryCacheName
+  , getOpts
+  ) where
+
+import Data.Bifunctor            (first)
+import Protolude hiding          (option)
+import URI.ByteString            (URIRef, Absolute, parseURI, strictURIParserOptions
+                                 , serializeURIRef')
+import URI.ByteString.QQ
+import Options.Applicative
+
+
+data CachixOptions = CachixOptions
+  { host :: URIRef Absolute
+  , verbose :: Bool
+  } deriving Show
+
+parserCachixOptions :: Parser CachixOptions
+parserCachixOptions = CachixOptions
+  <$> option uriOption ( long "host"
+                       <> short 'h'
+                       <> value [uri|https://cachix.org|]
+                       <> metavar "URI"
+                       <> showDefaultWith (toS . serializeURIRef')
+                       <> help "Host to connect to"
+                       )
+  <*> switch ( long "verbose"
+             <> short 'v'
+             <> help "Verbose mode"
+             )
+
+uriOption :: ReadM (URIRef Absolute)
+uriOption = eitherReader $ \s ->
+  first show $ parseURI strictURIParserOptions $ toS s
+
+type BinaryCacheName = Text
+
+data CachixCommand
+  = AuthToken Text
+  | Create BinaryCacheName
+  | Push BinaryCacheName [Text] Bool -- TODO: refactor to a record
+  | Use BinaryCacheName Bool --- TODO: refactor to a record
+  | Version
+  deriving Show
+
+
+parserCachixCommand :: Parser CachixCommand
+parserCachixCommand = subparser $
+  command "authtoken" (infoH authtoken (progDesc "Configure token for authentication to cachix.org")) <>
+  command "create" (infoH create (progDesc "Create a new binary cache")) <>
+  command "push" (infoH push (progDesc "Upload Nix store paths to the binary cache")) <>
+  command "use" (infoH use (progDesc "Configure nix.conf to enable binary cache during builds"))
+  where
+    authtoken = AuthToken <$> strArgument (metavar "TOKEN")
+    create = Create <$> strArgument (metavar "NAME")
+    push = Push <$> strArgument (metavar "NAME")
+                <*> many (strArgument (metavar "PATHS..."))
+                <*> switch (long "watch-store" <> short 'w' <> help "Run in daemon mode and push store paths as they are added to /nix/store")
+    use = Use <$> strArgument (metavar "NAME")
+              <*> switch (long "nixos" <> short 'n' <> help "Output NixOS configuration lines")
+
+getOpts :: IO (CachixOptions, CachixCommand)
+getOpts = customExecParser (prefs showHelpOnEmpty) opts
+
+opts :: ParserInfo (CachixOptions, CachixCommand)
+opts = infoH parser desc
+  where parser = (,) <$> parserCachixOptions <*> (parserCachixCommand <|> versionParser)
+        versionParser :: Parser CachixCommand
+        versionParser = flag' Version ( long "version"
+                     <> short 'V'
+                     <> help "Show cachix version"
+                     )
+
+desc :: InfoMod a
+desc = fullDesc
+    <> progDesc "Sign into https://cachix.org to get started."
+    <> header "cachix.org command interface"
+    -- TODO: usage footer
+
+infoH :: Parser a -> InfoMod a -> ParserInfo a
+infoH a = info (helper <*> a)
diff --git a/src/Cachix/Client/Servant.hs b/src/Cachix/Client/Servant.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/Servant.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Cachix.Client.Servant
+  ( AsClient
+  , isErr
+  ) where
+
+import Protolude
+import Network.HTTP.Types (Status)
+import Servant.Client
+import Servant.Generic
+
+-- TODO: servant-generic-client https://github.com/chpatrick/servant-generic/issues/10
+data AsClient
+type instance AsClient :- api = Client ClientM api
+
+isErr :: ServantError -> Status -> Bool
+isErr (FailureResponse resp) status | responseStatusCode resp == status = True
+isErr _ _ = False
diff --git a/src/Cachix/Client/URI.hs b/src/Cachix/Client/URI.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/URI.hs
@@ -0,0 +1,37 @@
+-- | Ugly glue between URI and BaseUrl
+-- | TODO: mark as Internal module
+{-# LANGUAGE GADTs #-}
+module Cachix.Client.URI
+  ( getBaseUrl
+  ) where
+
+import Protolude
+import qualified URI.ByteString as UBS
+import URI.ByteString hiding (Scheme)
+import Servant.Client
+
+
+getBaseUrl :: URIRef Absolute -> BaseUrl
+getBaseUrl URI{..} =
+  case uriAuthority of
+    Nothing -> panic "missing host in url"
+    Just authority ->
+      BaseUrl
+        getScheme
+        (toS (hostBS (authorityHost authority)))
+        getPort
+        (toS uriPath)
+      where
+        getScheme :: Scheme
+        getScheme = case uriScheme of
+          UBS.Scheme "http" -> Http
+          UBS.Scheme "https" -> Https
+          _ -> panic "uri can only be http/https"
+
+        getPort :: Int
+        getPort = maybe defaultPort portNumber $ authorityPort authority
+
+        defaultPort :: Int
+        defaultPort = case getScheme of
+          Http -> 80
+          Https -> 443
diff --git a/test/InstallationModeSpec.hs b/test/InstallationModeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/InstallationModeSpec.hs
@@ -0,0 +1,94 @@
+module InstallationModeSpec where
+
+import Protolude
+import Test.Hspec
+
+import qualified Cachix.Client.NixConf as NixConf
+import           Cachix.Client.NixVersion ( NixMode(..) )
+import           Cachix.Client.InstallationMode
+
+
+spec :: Spec
+spec = do
+  describe "getInstallationMode" $ do
+    it "Nix 1.X is unsupported" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix1XX
+          , isTrusted = False -- any
+          , isRoot = False -- any
+          , isNixOS = False -- any
+          }
+      in getInstallationMode nixenv `shouldBe` UnsupportedNix1X
+    it "NixOS with root prints configuration" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix20 -- any except 1.0
+          , isTrusted = True -- any
+          , isRoot = True
+          , isNixOS = True
+          }
+      in getInstallationMode nixenv `shouldBe` EchoNixOS
+    it "NixOS without trust prints configuration plus trust" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix20
+          , isTrusted = False
+          , isRoot = False
+          , isNixOS = True
+          }
+      in getInstallationMode nixenv `shouldBe` EchoNixOSWithTrustedUser
+    it "NixOS non-root trusted results into local install" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix201
+          , isTrusted = True
+          , isRoot = False
+          , isNixOS = True
+          }
+      in getInstallationMode nixenv `shouldBe` Install NixConf.Local
+    it "non-NixOS root results into global install" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix201
+          , isTrusted = True
+          , isRoot = True
+          , isNixOS = False
+          }
+      in getInstallationMode nixenv `shouldBe` Install NixConf.Global
+    it "non-NixOS non-root trusted results into local install" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix201
+          , isTrusted = True
+          , isRoot = False
+          , isNixOS = False
+          }
+      in getInstallationMode nixenv `shouldBe` Install NixConf.Local
+    it "non-NixOS non-root non-trusted results into required sudo" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix201
+          , isTrusted = False
+          , isRoot = False
+          , isNixOS = False
+          }
+      in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo
+    it "non-NixOS non-root non-trusted on Nix 2.0.0 results into required sudo with upgrade warning" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix20
+          , isTrusted = False
+          , isRoot = False
+          , isNixOS = False
+          }
+      in getInstallationMode nixenv `shouldBe` Nix20RequiresSudo
+    it "non-NixOS non-root trusted on Nix 2.0.0 results into required sudo with upgrade warning" $
+      let
+        nixenv = NixEnv
+          { nixMode = Nix20
+          , isTrusted = True
+          , isRoot = False
+          , isNixOS = False
+          }
+      in getInstallationMode nixenv `shouldBe` Nix20RequiresSudo
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,13 @@
+module Main where
+
+import Protolude
+import Test.Hspec.Runner
+import Test.Hspec.Formatters
+import qualified Spec
+
+main :: IO ()
+main = hspecWith config Spec.spec
+  where
+    config = defaultConfig
+      { configColorMode = ColorAlways
+      }
diff --git a/test/NixConfSpec.hs b/test/NixConfSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/NixConfSpec.hs
@@ -0,0 +1,147 @@
+{-# LANGUAGE QuasiQuotes #-}
+module NixConfSpec where
+
+import Protolude
+import Data.String.Here
+import Test.Hspec
+
+import Cachix.Client.NixConf as NixConf
+import Cachix.Api (BinaryCache(..))
+
+
+property :: Text -> Expectation
+property x = NixConf.render <$> parse x `shouldBe` Right x
+
+bc = BinaryCache
+ { name = "name"
+ , uri = "https://name.cachix.org"
+ , publicSigningKeys = ["pub"]
+ }
+
+spec :: Spec
+spec = do
+  describe "render . parse" $ do
+    it "handles single value substituters" $
+      property "substituters = a\n"
+    it "handles multi value substituters" $
+      property "substituters = a b c\n"
+    it "handles all known keys" $
+      property "substituters = a b c\ntrusted-users = him me\ntrusted-public-keys = a\n"
+    it "random content" $
+      property "blabla = foobar\nfoo = bar\n"
+  describe "add" $ do
+    it "merges binary caches from both files" $
+      let
+        globalConf = NixConf
+          [ Substituters ["bc1"]
+          , TrustedPublicKeys ["pub1"]
+          ]
+        localConf = NixConf
+          [ Substituters ["bc2"]
+          , TrustedPublicKeys ["pub2"]
+          ]
+        result = NixConf
+          [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"]
+          , TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]
+          ]
+      in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "is noop if binary cache exists in one file" $
+      let
+        globalConf = NixConf
+          [ Substituters [defaultPublicURI, "https://name.cachix.org"]
+          , TrustedPublicKeys [defaultSigningKey, "pub"]
+          ]
+        localConf = NixConf [ ]
+        result = NixConf
+          [ Substituters [defaultPublicURI, "https://name.cachix.org"]
+          , TrustedPublicKeys [defaultSigningKey, "pub"]
+          ]
+      in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "preserves other nixconf entries" $
+      let
+        globalConf = NixConf
+          [ Substituters ["http"]
+          , TrustedPublicKeys ["pub1"]
+          , TrustedUsers ["user"]
+          , Other "foo"
+          ]
+        localConf = NixConf
+          [ TrustedUsers ["user2"]
+          , Other "bar"
+          ]
+        result = NixConf
+          [ TrustedUsers ["user2"]
+          , Other "bar"
+          , Substituters [defaultPublicURI, "http", "https://name.cachix.org"]
+          , TrustedPublicKeys [defaultSigningKey, "pub1", "pub"]
+          ]
+      in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "removed duplicates" $
+      let
+        globalConf = NixConf
+          [ Substituters ["bc1"]
+          , BinaryCaches ["bc1"]
+          , TrustedPublicKeys ["pub1"]
+          , BinaryCachePublicKeys ["pub1"]
+          ]
+        localConf = NixConf
+          [ Substituters ["bc2", "bc2"]
+          , TrustedPublicKeys ["pub2", "pub2"]
+          ]
+        result = NixConf
+          [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"]
+          , TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]
+          ]
+      in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "can mix and match old and new names" $
+      let
+        globalConf = NixConf
+          [ BinaryCaches ["bc1"]
+          , BinaryCachePublicKeys ["pub1"]
+          ]
+        localConf = NixConf
+          [ Substituters ["bc2"]
+          , TrustedPublicKeys ["pub2"]
+          ]
+        result = NixConf
+          [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"]
+          , TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]
+          ]
+      in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "adds binary cache and defaults if no existing entries exist" $
+      let
+        globalConf = NixConf []
+        localConf = NixConf []
+        result = NixConf
+          [ Substituters [defaultPublicURI, "https://name.cachix.org"]
+          , TrustedPublicKeys [defaultSigningKey, "pub"]
+          ]
+      in add bc [globalConf, localConf] localConf `shouldBe` result
+  describe "parse" $ do
+    it "parses substituters" $
+      parse "substituters = a\n" `shouldBe` (Right $ NixConf [Substituters ["a"]])
+    it "parses long key" $
+      parse "binary-caches-parallel-connections = 40\n" `shouldBe` (Right $ NixConf [Other "binary-caches-parallel-connections = 40"])
+    it "parses substituters with multiple values" $
+      parse "substituters = a b c\n" `shouldBe` (Right $ NixConf [Substituters ["a", "b", "c"]])
+    it "parses equal sign after the first key as literal" $
+      parse "substituters = a b c= d\n" `shouldBe` (Right $ NixConf [Substituters ["a", "b", "c=", "d"]])
+    it "parses a complex example" $
+      parse realExample `shouldBe` (Right $ NixConf [ Other ""
+                                                    , Substituters ["a","b","c"]
+                                                    , TrustedUsers ["him","me"]
+                                                    , TrustedPublicKeys ["a"]
+                                                    , Other "blabla =  asd"
+                                                    , Other "# comment"
+                                                    , Other ""
+                                                    , Other ""])
+
+realExample = [hereLit|
+substituters = a  b c
+trusted-users = him me
+trusted-public-keys  =  a
+blabla =  asd
+# comment
+
+
+|]
diff --git a/test/NixVersionSpec.hs b/test/NixVersionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/NixVersionSpec.hs
@@ -0,0 +1,22 @@
+module NixVersionSpec where
+
+import Protolude
+import Test.Hspec
+
+import Cachix.Client.NixVersion
+
+spec :: Spec
+spec = do
+  describe "parseNixMode" $ do
+    it "parses 'nix-env (Nix) 2.0' as Nix20" $
+      parseNixMode "nix-env (Nix) 2.0" `shouldBe` Right Nix20
+    it "parses 'nix-env (Nix) 2.0.1' as Nix201" $
+      parseNixMode "nix-env (Nix) 2.0.1" `shouldBe` Right Nix201
+    it "parses 'nix-env (Nix) 1.11.13' as Nix1XX" $
+      parseNixMode "nix-env (Nix) 1.11.13" `shouldBe` Right Nix1XX
+    it "parses 'nix-env (Nix) 2.0.5' as Nix201" $
+      parseNixMode "nix-env (Nix) 2.0.5" `shouldBe` Right Nix201
+    it "parses 'nix-env (Nix) 2.0.1pre6053_444b921' as Nix201" $
+      parseNixMode "nix-env (Nix) 2.0.1pre6053_444b921" `shouldBe` Right Nix201
+    it "fails with unknown string 'foobar'" $
+      parseNixMode "foobar" `shouldBe` Left "Couldn't parse 'nix-env --version' output: foobar"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
