diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Timothy Clem (c) 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 Author name here 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,87 @@
+# twirp-haskell
+
+Very, very alpha implementation of Twirp service generation for Haskell.
+
+This project provides a number of things:
+
+1. A protoc plugin for generating a Haskell Twirp service (based on Servant) for Services defined in a proto file.
+2. A haskell library, `twirp` for quickly serving up the generated application.
+
+It requires the use of [proto-lens](https://github.com/google/proto-lens) to generate haskell datatypes from proto messages.
+
+An example, end-to-end application is included in `app`.
+
+## Requirements
+
+1. Install `protoc` (e.g., `brew install protoc`)
+2. Install the required protoc plugins:
+   - `cabal install proto-lens-protoc`
+   - `go get github.com/tclem/twirp-haskell/protoc-gen-twirp_haskell`
+   - `go get github.com/tclem/proto-lens-jsonpb/protoc-gen-jsonpb_haskell`
+
+## Usage
+
+Use the protoc plugin to generate a twirp service and associated protobuf types from a proto file.
+
+```
+protoc -I=. --proto_path=./proto \
+  --plugin=protoc-gen-haskell=`which proto-lens-protoc` --haskell_out=./app \
+  --jsonpb_haskell_out=./app \
+  --plugin=protoc-gen-twirp_haskell=./script/run-twirp_haskell --twirp_haskell_out=./app/Twirp/Example/Haberdasher \
+  haberdasher.proto
+```
+
+The result is a couple of files that describe your service. First, here are the types that define a Servant API:
+
+
+``` haskell
+-- Code generated by protoc-gen-twirp_haskell 0.1.0, DO NOT EDIT.
+{-# LANGUAGE TypeOperators #-}
+module Twirp.Example.Haberdasher.Haberdasher where
+
+import Servant
+import Twirp
+
+import Proto.Haberdasher
+
+--  This is an example set of twirp services.
+
+type HaberdasherAPI
+  =    "twirp" :> "twirp.example.haberdasher.Haberdasher" :> HaberdasherService
+  :<|> "twirp" :> "twirp.example.haberdasher.Health" :> HealthService
+
+--  Haberdasher service makes hats for clients.
+type HaberdasherService
+  --  MakeHat produces a hat of mysterious, randomly-selected color!
+  =    "MakeHat" :> ReqBody [Protobuf, JSON] Size :> Post '[Protobuf, JSON] Hat
+  --  Get paid
+  :<|> "GetBill" :> ReqBody [Protobuf, JSON] Hat :> Post '[Protobuf, JSON] Bill
+
+--  Health check service
+type HealthService
+  =    "Check" :> ReqBody [Protobuf, JSON] Ping :> Post '[Protobuf, JSON] Pong
+```
+
+The datatypes are defined in [`Proto.Haberdasher`](app/Proto/Haberdasher.hs).
+
+Plugging this into an existing warp/wai server is straightforward. See [`app/Main.hs`](app/Main.hs) for the full details:
+
+``` haskell
+type RequestID = String
+
+type API
+  = Header "X-Request-Id" RequestID
+  :> HaberdasherAPI
+
+main :: IO ()
+main = run 8003 app
+
+app :: Application
+app = twirpErrorResponses apiApp
+
+apiApp :: Application
+apiApp = serve (Proxy :: Proxy API) server
+
+server :: Server API
+server _requestID = (makeHat :<|> getBill) :<|> checkHealth
+```
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/src/Twirp.hs b/src/Twirp.hs
new file mode 100644
--- /dev/null
+++ b/src/Twirp.hs
@@ -0,0 +1,25 @@
+module Twirp
+  ( Protobuf
+  , module Middleware
+  ) where
+
+import Twirp.Middleware.Errors as Middleware
+
+import Data.Bifunctor (first)
+import Data.ByteString.Lazy.Char8 as BC
+import Data.ByteString.Builder as BB
+import Network.HTTP.Media ((//))
+import Data.ProtoLens.Encoding as Proto
+import Data.ProtoLens.Message (Message)
+import Servant.API
+
+data Protobuf
+
+instance Accept Protobuf where
+  contentType _ = "application" // "protobuf"
+
+instance Message a => MimeRender Protobuf a where
+  mimeRender _ = BB.toLazyByteString . Proto.buildMessage
+
+instance Message a => MimeUnrender Protobuf a where
+  mimeUnrender _ = first show . Proto.decodeMessage . BC.toStrict
diff --git a/src/Twirp/Middleware/Errors.hs b/src/Twirp/Middleware/Errors.hs
new file mode 100644
--- /dev/null
+++ b/src/Twirp/Middleware/Errors.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE DerivingVia, DeriveAnyClass, ScopedTypeVariables #-}
+module Twirp.Middleware.Errors where
+
+import Data.Aeson
+import GHC.Generics
+import Network.HTTP.Types
+import Network.Wai
+
+data TwirpError = TwirpError { code :: String, msg :: String }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (ToJSON)
+
+-- Rewrite error responses to use Twirp's error codes and JSON encoding.
+-- See: https://github.com/twitchtv/twirp/blob/master/docs/errors.md
+twirpErrorResponses :: Middleware
+twirpErrorResponses = modifyResponse go
+  where
+    go response = let status = responseStatus response in
+      case statusCode status of
+        400 -> responseLBS status headers (encode badRequest)
+        404 -> responseLBS status headers (encode notFound)
+        408 -> responseLBS status headers (encode canceled)
+        500 -> responseLBS status headers (encode serverError)
+        503 -> responseLBS status headers (encode unavailable)
+        _   -> response
+
+    headers = [(hContentType, "application/json; charset=utf-8")]
+
+    badRequest = TwirpError "invalid_argument" "Bad Request"
+    notFound = TwirpError "not_found" "Not found"
+    canceled = TwirpError "canceled" "Request Timeout"
+    unavailable = TwirpError "unavailable" "Service Unavailable"
+    serverError = TwirpError "internal" "Internal Server Error"
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"
diff --git a/twirp.cabal b/twirp.cabal
new file mode 100644
--- /dev/null
+++ b/twirp.cabal
@@ -0,0 +1,74 @@
+cabal-version:  2.4
+
+name:           twirp
+version:        0.2.0.0
+synopsis:       Haskell twirp foundations
+description:    Please see the README on GitHub at <https://github.com/tclem/twirp-haskell#readme>
+homepage:       https://github.com/tclem/twirp-haskell#readme
+bug-reports:    https://github.com/tclem/twirp-haskell/issues
+license:        BSD-3-Clause
+license-file:   LICENSE
+author:         Timothy Clem
+maintainer:     timothy.clem@gmail.com
+copyright:      2019 Timothy Clem
+category:       Web
+build-type:     Simple
+extra-source-files: README.md
+
+tested-with:    GHC == 8.6.5
+
+-- GHC extensions shared between targets
+common haskell
+  default-language:    Haskell2010
+  default-extensions:  DataKinds
+                     , DeriveFoldable
+                     , DeriveFunctor
+                     , DeriveGeneric
+                     , DeriveTraversable
+                     , FlexibleContexts
+                     , FlexibleInstances
+                     , MultiParamTypeClasses
+                     , OverloadedStrings
+                     , RecordWildCards
+                     , StandaloneDeriving
+                     , StrictData
+                     , TypeApplications
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+
+common dependencies
+  build-depends:
+      base >=4.7 && <5
+    , aeson ^>= 1.4.5.0
+    , bytestring >= 0.10.8
+    , http-media >= 0.8.0.0
+    , http-types >= 0.12.3
+    , proto-lens >= 0.5.0.0
+    , proto-lens-jsonpb >= 0.2.0.0
+    , proto-lens-runtime >= 0.5 && <0.7
+    , servant ^>= 0.16.2
+    , text ^>= 1.2.4.0
+    , wai ^>= 3.2.2.1
+
+library
+  import: haskell, dependencies
+  exposed-modules:
+      Twirp
+      Twirp.Middleware.Errors
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+  build-depends:
+
+test-suite twirp-test
+  import: haskell, dependencies
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      twirp
+
+source-repository head
+  type: git
+  location: https://github.com/tclem/twirp-haskell
