diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Changelog for elm2nix
+
+## 0.1.0 (2018-12-28)
+
+- Initial release (@domenkozar)
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Domen Kožar (c) 2017-2019
+
+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 Domen Kožar 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,90 @@
+# elm2nix
+
+[![Build Status](https://travis-ci.org/domenkozar/elm2nix.svg?branch=master)](https://travis-ci.org/domenkozar/elm2nix)
+[![Hackage](https://img.shields.io/hackage/v/elm2nix.svg)](https://hackage.haskell.org/package/elm2nix)
+
+Convert an [Elm](http://elm-lang.org/) project into
+[Nix](https://nixos.org/nix/) expressions.
+
+It consists of multiple commands:
+- `elm2nix convert`: Given `elm.json` in current directory, all dependencies are
+  parsed and their sha256sum calculated
+- `elm2nix snapshot`: Downloads snapshot of http://package.elm-lang.org into `versions.dat`
+- `elm2nix init`: Generates `default.nix` that glues everything together
+
+## Assumptions
+
+Supports Elm 0.19.x
+
+## Installation
+
+    $ nix-shell -p stack --run "stack install --nix"
+
+## Usage
+
+    $ git clone https://github.com/evancz/elm-todomvc.git
+    $ cd elm-todomvc
+    $ ~/.local/bin/elm2nix init > default.nix
+    $ ~/.local/bin/elm2nix convert > elm-srcs.nix
+    $ ~/.local/bin/elm2nix snapshot > versions.dat
+    $ nix-build
+    $ chromium ./result/index.html
+
+## Running tests (as per CI)
+
+    $ ./scripts/tests.sh
+
+## FAQ
+
+### Why is mkDerivation inlined into `default.nix`?
+
+As it's considered experimental, it's generated for now. Might change in the future.
+
+### How to use with ParcelJS and Yarn?
+
+Instead of running `elm2nix init`, use something like:
+
+```nix
+{ pkgs ? import <nixpkgs> {}
+}:
+
+let
+  yarnPkg = pkgs.yarn2nix.mkYarnPackage {
+    name = "myproject-node-packages";
+    packageJSON = ./package.json;
+    unpackPhase = ":";
+    src = null;
+    yarnLock = ./yarn.lock;
+    publishBinsFor = ["parcel-bundler"];
+  };
+in pkgs.stdenv.mkDerivation {
+  name = "myproject-frontend";
+  src = pkgs.lib.cleanSource ./.;
+
+  buildInputs = with pkgs.elmPackages; [
+    elm
+    elm-format
+    yarnPkg
+    pkgs.yarn
+  ];
+
+  patchPhase = ''
+    rm -rf elm-stuff
+    ln -sf ${yarnPkg}/node_modules .
+  '';
+
+  shellHook = ''
+    ln -fs ${yarnPkg}/node_modules .
+  '';
+
+  configurePhase = pkgs.elmPackages.fetchElmDeps {
+    elmPackages = import ./elm-srcs.nix;
+    versionsDat = ./versions.dat;
+  };
+
+  installPhase = ''
+    mkdir -p $out
+    parcel build -d $out index.html
+  '';
+}
+```
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/data/default.nix b/data/default.nix
new file mode 100644
--- /dev/null
+++ b/data/default.nix
@@ -0,0 +1,42 @@
+{ nixpkgs ? <nixpkgs>
+, config ? {}
+}:
+
+with (import nixpkgs config);
+
+let
+  mkDerivation =
+    { srcs ? ./elm-srcs.nix
+    , src
+    , name
+    , srcdir ? "./src"
+    , targets ? []
+    , versionsDat ? ./versions.dat
+    }:
+    stdenv.mkDerivation {
+      inherit name src;
+
+      buildInputs = [ elmPackages.elm ];
+
+      buildPhase = pkgs.elmPackages.fetchElmDeps {
+        elmPackages = import srcs;
+        inherit versionsDat;
+      };
+
+      installPhase = let
+        elmfile = module: "\${srcdir}/\${builtins.replaceStrings ["."] ["/"] module}.elm";
+      in ''
+        mkdir -p \$out/share/doc
+        \${lib.concatStrings (map (module: ''
+          echo "compiling \${elmfile module}"
+          elm make \${elmfile module} --output \$out/\${module}.html --docs \$out/share/doc/\${module}.json
+        '') targets)}
+      '';
+    };
+in mkDerivation {
+  name = "${name}";
+  srcs = ./elm-srcs.nix;
+  src = ./.;
+  targets = ["Main"];
+  srcdir = "${srcdir}";
+}
diff --git a/elm2nix.cabal b/elm2nix.cabal
new file mode 100644
--- /dev/null
+++ b/elm2nix.cabal
@@ -0,0 +1,85 @@
+-- This file has been generated from package.yaml by hpack version 0.18.1.
+--
+-- see: https://github.com/sol/hpack
+
+name:           elm2nix
+version:        0.1.0
+synopsis:       Turn your Elm project into buildable Nix project
+description:    Please see the README on Github at <https://github.com/domenkozar/elm2nix#readme>
+homepage:       https://github.com/domenkozar/elm2nix#readme
+bug-reports:    https://github.com/domenkozar/elm2nix/issues
+author:         Domen Kožar
+maintainer:     domen@dev.si
+copyright:      2017-2019 Domen Kožar
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  2.0
+
+extra-source-files:
+    ChangeLog.md
+    README.md
+    data/default.nix
+
+source-repository head
+  type: git
+  location: https://github.com/domenkozar/elm2nix
+
+library
+  hs-source-dirs:
+      src
+  build-depends:
+      base >= 4.7 && < 5
+    , aeson
+    , async
+    , bytestring
+    , binary
+    , containers
+    , data-default
+    , directory
+    , filepath
+    , here
+    , mtl
+    , process
+    , req
+    , text
+    , transformers
+    , unordered-containers
+  exposed-modules:
+      Elm2Nix
+      Elm2Nix.FixedOutput
+      Elm2Nix.PackagesSnapshot
+  other-modules:
+      Paths_elm2nix
+  autogen-modules:
+      Paths_elm2nix
+  default-language: Haskell2010
+
+executable elm2nix
+  main-is: Main.hs
+  hs-source-dirs:
+      elm2nix
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  autogen-modules:
+      Paths_elm2nix
+  other-modules:
+      Paths_elm2nix
+  build-depends:
+      base >= 4.7 && < 5
+    , elm2nix
+    , directory
+    , optparse-applicative
+    , here
+    , ansi-wl-pprint
+  default-language: Haskell2010
+
+test-suite elm2nix-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >= 4.7 && < 5
+    , elm2nix
+  default-language: Haskell2010
diff --git a/elm2nix/Main.hs b/elm2nix/Main.hs
new file mode 100644
--- /dev/null
+++ b/elm2nix/Main.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE QuasiQuotes #-}
+module Main
+  ( main
+  ) where
+
+import Data.Semigroup ((<>))
+import Data.Version (showVersion)
+import Data.String.Here (hereLit)
+import Options.Applicative
+import System.IO
+import System.Directory (getCurrentDirectory)
+import qualified Elm2Nix
+import qualified Paths_elm2nix as This
+import qualified Text.PrettyPrint.ANSI.Leijen as PP
+
+
+data Command
+  = Init
+  | Convert
+  | Snapshot
+
+main :: IO ()
+main = do
+  hSetBuffering stdout LineBuffering
+  hSetBuffering stderr LineBuffering
+  cmd <- getOpts
+  cwd <- getCurrentDirectory
+  case cmd of
+    Convert -> Elm2Nix.convert
+    Init -> Elm2Nix.initialize
+    Snapshot -> Elm2Nix.snapshot cwd
+
+getOpts :: IO Command
+getOpts = customExecParser p (infoH opts rest)
+  where
+    infoH a = info (helper <*> a)
+    rest = fullDesc
+      <> progDesc "Convert Elm project to Nix expressions"
+      <> header ("elm2nix " ++ showVersion This.version)
+      <> footerDoc (Just $ PP.string exampleText)
+    p = prefs showHelpOnEmpty
+
+    exampleText :: String
+    exampleText = [hereLit|
+
+    Usage:
+
+      $ elm2nix init > default.nix
+      $ elm2nix convert > elm-srcs.nix
+      $ nix-build
+
+      Note: You have to run elm2nix from top-level directory of an Elm project.
+    |]
+
+    opts :: Parser Command
+    opts = subparser
+      ( command "init" (infoH (pure Init) (progDesc "Generate default.nix (printed to stdout)"))
+     <> command "convert" (infoH (pure Convert) (progDesc "Generate Nix expressions for elm.json using nix-prefetch-url"))
+     <> command "snapshot" (infoH (pure Snapshot) (progDesc "Generate versions.dat"))
+      )
diff --git a/src/Elm2Nix.hs b/src/Elm2Nix.hs
new file mode 100644
--- /dev/null
+++ b/src/Elm2Nix.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Elm2Nix
+    ( convert
+    , initialize
+    , snapshot
+    ) where
+
+import Control.Concurrent.Async (mapConcurrently)
+import Control.Monad (liftM2)
+import Control.Monad.Except (liftIO, MonadIO)
+import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
+import Data.Aeson (Value(..))
+import Data.List (intercalate)
+import Data.HashMap.Strict (HashMap)
+import Data.String.Here
+import Data.Text (Text)
+import System.Exit (exitFailure)
+import System.IO (hPutStrLn, stderr)
+
+import qualified Data.HashMap.Strict as HM
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.Aeson as Json
+import qualified Data.Text as Text
+
+import Elm2Nix.FixedOutput (FixedDerivation(..), prefetch)
+import Elm2Nix.PackagesSnapshot (snapshot)
+
+
+newtype Elm2Nix a = Elm2Nix { runElm2Nix_ :: ExceptT Elm2NixError IO a }
+  deriving (Functor, Applicative, Monad, MonadIO)
+
+type Dep = (String, String)
+
+data Elm2NixError =
+    ElmJsonReadError String
+  | UnexpectedValue Value
+  | KeyNotFound Text
+  deriving Show
+
+runElm2Nix :: Elm2Nix a -> IO (Either Elm2NixError a)
+runElm2Nix = runExceptT . runElm2Nix_
+
+throwErr :: Elm2NixError -> Elm2Nix a
+throwErr e = Elm2Nix (throwE e)
+
+parseElmJsonDeps :: Value -> Either Elm2NixError [Dep]
+parseElmJsonDeps obj =
+  case obj of
+    Object hm -> do
+      deps <- tryLookup hm "dependencies"
+      case deps of
+        Object dhm -> do
+          direct   <- tryLookup dhm "direct"
+          indirect <- tryLookup dhm "indirect"
+          liftM2 (++) (parseDeps direct) (parseDeps indirect)
+        v -> Left (UnexpectedValue v)
+    v -> Left (UnexpectedValue v)
+  where
+    parseDep :: Text -> Value -> Either Elm2NixError Dep
+    parseDep name (String ver) = Right (Text.unpack name, Text.unpack ver)
+    parseDep _ v               = Left (UnexpectedValue v)
+
+    parseDeps :: Value -> Either Elm2NixError [Dep]
+    parseDeps (Object hm) = mapM (uncurry parseDep) (HM.toList hm)
+    parseDeps v           = Left (UnexpectedValue v)
+
+    maybeToRight :: b -> Maybe a -> Either b a
+    maybeToRight _ (Just x) = Right x
+    maybeToRight y Nothing  = Left y
+
+    tryLookup :: HashMap Text Value -> Text -> Either Elm2NixError Value
+    tryLookup hm key =
+      maybeToRight (KeyNotFound key) (HM.lookup key hm)
+
+-- CMDs
+
+convert :: IO ()
+convert = runCLI $ do
+  liftIO (hPutStrLn stderr "Resolving elm.json dependencies into Nix ...")
+  res <- liftIO (fmap Json.eitherDecode (LBS.readFile "elm.json"))
+  elmJson <- either (throwErr . ElmJsonReadError) return res
+
+  deps <- either throwErr return (parseElmJsonDeps elmJson)
+  liftIO (hPutStrLn stderr "Prefetching tarballs and computing sha256 hashes ...")
+
+  sources <- liftIO (mapConcurrently (uncurry prefetch) deps)
+  liftIO (putStrLn (generateNixSources sources))
+
+initialize :: IO ()
+initialize = runCLI $
+  liftIO (putStrLn [template|data/default.nix|])
+  where
+    -- | Converts Package.Name to Nix friendly name
+    baseName :: Text
+    baseName = "elm-app"
+    version :: Text
+    version = "0.1.0"
+    toNixName :: Text -> Text
+    toNixName = Text.replace "/" "-"
+    name :: String
+    name = Text.unpack (toNixName baseName <> "-" <> version)
+    srcdir :: String
+    srcdir = "./src" -- TODO: get from elm.json
+
+-- Utils
+
+runCLI :: Elm2Nix a -> IO a
+runCLI m = do
+  result <- runElm2Nix m
+  case result of
+        Right a ->
+          return a
+        Left err -> do
+          depErrToStderr err
+          exitFailure
+
+depErrToStderr :: Elm2NixError -> IO ()
+depErrToStderr err =
+  hPutStrLn stderr $
+      case err of
+        UnexpectedValue v -> "Unexpected Value: \n" ++ show v
+        ElmJsonReadError s -> "Error reading json: " ++ s
+        KeyNotFound key -> "Key not found in json: " ++ Text.unpack key
+
+generateNixSources :: [FixedDerivation] -> String
+generateNixSources dss =
+  [iTrim|
+{
+${intercalate "\n" (map f dss)}
+}
+  |]
+  where
+    f :: FixedDerivation -> String
+    f ds =
+      [i|
+      "${drvName ds}" = {
+        sha256 = "${drvHash ds}";
+        version = "${drvVersion ds}";
+      };|]
diff --git a/src/Elm2Nix/FixedOutput.hs b/src/Elm2Nix/FixedOutput.hs
new file mode 100644
--- /dev/null
+++ b/src/Elm2Nix/FixedOutput.hs
@@ -0,0 +1,54 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Elm2Nix.FixedOutput
+  ( FixedDerivation(..)
+  , prefetch
+  ) where
+
+import System.Exit
+import System.Process
+import qualified Data.ByteString.Lazy.Char8 as BS
+
+
+-- fixed output derivation metadata
+data FixedDerivation = FixedDerivation
+  { drvHash :: String -- ^ Computed sha256 hash
+  , drvPath :: String -- ^ Nix store path of the derivation
+  , drvUrl :: String -- ^ URL to the tarball
+  , drvName :: String
+  , drvVersion :: String
+  } deriving (Show, Eq)
+
+-- | Use nix-prefetch-url to obtain resulting path and it's hash
+-- | Partially taken from cabal2nix/src/Distribution/Nixpkgs/Fetch.hs
+prefetch :: String -> String -> IO FixedDerivation
+prefetch name version = do
+  (Nothing, Just stdoutH, _, processH) <-
+    createProcess (proc "nix-prefetch-url" args) { env     = Nothing
+                                                 , std_in  = Inherit
+                                                 , std_err = Inherit
+                                                 , std_out = CreatePipe
+                                                 }
+  exitCode <- waitForProcess processH
+  case exitCode of
+    ExitFailure _ -> error "nix-prefetch-url exited with non-zero"
+    ExitSuccess   -> do
+      buf <- BS.hGetContents stdoutH
+      let ls = BS.lines buf
+      case length ls of
+        0 -> error "unknown nix-prefetch-url output"
+        2 -> return $ FixedDerivation (BS.unpack $ head ls)
+                                       (BS.unpack $ head $ tail ls)
+                                       url
+                                       name
+                                       version
+        _ -> error "unknown nix-prefetch-url output"
+  where
+  url :: String
+  url =
+    "https://github.com/"
+      ++ name
+      ++ "/archive/"
+      ++ version
+      ++ ".tar.gz"
+  args :: [String]
+  args = ["--print-path", url]
diff --git a/src/Elm2Nix/PackagesSnapshot.hs b/src/Elm2Nix/PackagesSnapshot.hs
new file mode 100644
--- /dev/null
+++ b/src/Elm2Nix/PackagesSnapshot.hs
@@ -0,0 +1,139 @@
+{- Downloads binary serialized https://package.elm-lang.org/all-packages
+   as Elm compiler expects it to parse.
+
+  Takes Elm upstream code from:
+  - https://github.com/elm/compiler/blob/master/builder/src/Deps/Cache.hs
+  - https://github.com/elm/compiler/blob/master/builder/src/Deps/Website.hs
+  - https://github.com/elm/compiler/blob/master/compiler/src/Elm/Package.hs
+
+-}
+{-# LANGUAGE OverloadedStrings #-}
+module Elm2Nix.PackagesSnapshot
+  ( snapshot
+  ) where
+
+import Control.Monad (liftM2, liftM3)
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import Data.Binary (Binary, put, get, putWord8, getWord8)
+import qualified Data.Map as Map
+import Data.Default (def)
+import Data.Map (Map)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Data.Word (Word16)
+import qualified Network.HTTP.Req as Req
+import System.FilePath ((</>))
+
+
+data Name =
+  Name
+    { _author :: !Text
+    , _project :: !Text
+    }
+    deriving (Eq, Ord)
+
+data Package =
+  Package
+    { _name :: !Name
+    , _version :: !Version
+    }
+    deriving (Eq, Ord)
+
+data Version =
+  Version
+    { _major :: {-# UNPACK #-} !Word16
+    , _minor :: {-# UNPACK #-} !Word16
+    , _patch :: {-# UNPACK #-} !Word16
+    }
+    deriving (Eq, Ord)
+
+data PackageRegistry =
+  PackageRegistry Int (Map Name [Version])
+
+instance Binary Name where
+  get =
+    liftM2 Name get get
+
+  put (Name author project) =
+    do  put author
+        put project
+
+instance Binary Package where
+  get =
+    liftM2 Package get get
+
+  put (Package name version) =
+    do  put name
+        put version
+
+instance Binary Version where
+  get =
+    do  word <- getWord8
+        if word == 0
+          then liftM3 Version get get get
+          else
+            do  minor <- fmap fromIntegral getWord8
+                patch <- fmap fromIntegral getWord8
+                return (Version (fromIntegral word) minor patch)
+
+  put (Version major minor patch) =
+    if major < 256 && minor < 256 && patch < 256 then
+      do  putWord8 (fromIntegral major)
+          putWord8 (fromIntegral minor)
+          putWord8 (fromIntegral patch)
+    else
+      do  putWord8 0
+          put major
+          put minor
+          put patch
+
+instance Binary PackageRegistry where
+  get = liftM2 PackageRegistry get get
+  put (PackageRegistry a b) = put a >> put b
+
+snapshot :: String -> IO ()
+snapshot dir = do
+  r <- Req.runReq def $
+    Req.req
+    Req.POST
+    (Req.https "package.elm-lang.org" Req./: "all-packages")
+    Req.NoReqBody
+    Req.jsonResponse
+    mempty
+  let packages = unwrap $ case Aeson.fromJSON (Req.responseBody r) of
+         Aeson.Error s -> error s
+         Aeson.Success val -> val
+      size = Map.foldr ((+) . length) 0 packages
+      registry = PackageRegistry size packages
+  Binary.encodeFile (dir </> "versions.dat") registry
+
+newtype Packages = Packages { unwrap :: Map.Map Name [Version] }
+
+instance Aeson.FromJSON Packages where
+  parseJSON v = Packages <$> Aeson.parseJSON v
+
+instance Aeson.FromJSON Version where
+  parseJSON = Aeson.withText "string" $ \x ->
+    case Text.splitOn "." x of
+      [major, minor, patch] ->
+        return $ Version
+                  (read (Text.unpack major))
+                  (read (Text.unpack minor))
+                  (read (Text.unpack patch))
+      _ ->
+        fail "failure parsing version"
+
+
+instance Aeson.FromJSON Name where
+  parseJSON = Aeson.withText "string" $ \x ->
+    case Text.splitOn "/" x of
+      [author, package] -> return $ Name author package
+      lst -> fail $ "wrong package name: " <> show lst
+
+
+instance Aeson.FromJSONKey Name where
+  fromJSONKey = Aeson.FromJSONKeyTextParser $ \x ->
+    case Text.splitOn "/" x of
+      [author, package] -> return $ Name author package
+      lst -> fail $ "wrong package name: " <> show lst
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,2 @@
+main :: IO ()
+main = putStrLn "Test suite not yet implemented"
