diff --git a/README.md b/README.md
index cafb0b975fba09278df6a13eb315fe162847c2cf..a019fbd5f5fc4cb6d269ba202167c37a61d38740 100644
--- a/README.md
+++ b/README.md
@@ -4,41 +4,139 @@
 [![Codecov](https://img.shields.io/codecov/c/github/nhooyr/ws.svg)](https://codecov.io/gh/nhooyr/ws)
 [![GitHub release](https://img.shields.io/github/release/nhooyr/ws.svg)](https://github.com/nhooyr/ws/releases)
 
-ws is a clean and idiomatic WebSocket library for Go.
+ws is a minimal and idiomatic WebSocket library for Go.
 
 This library is in heavy development.
 
 ## Install
 
 ```bash
-go get nhooyr.io/ws
+go get nhooyr.io/ws@master
 ```
 
-## Why
+## Example
+
+### Server
+
+```go
+func main() {
+	fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		c, err := ws.Accept(w, r,
+			ws.AcceptSubprotocols("echo"),
+		)
+		if err != nil {
+			log.Printf("server handshake failed: %v", err)
+			return
+		}
+		defer c.Close(ws.StatusInternalError, "")
+
+		ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
+		defer cancel()
+
+		type myJsonStruct struct {
+			MyField string `json:"my_field"`
+		}
+		err = wsjson.Write(ctx, c, myJsonStruct{
+			MyField: "foo",
+		})
+		if err != nil {
+			log.Printf("failed to write json struct: %v", err)
+			return
+		}
+
+		c.Close(ws.StatusNormalClosure, "")
+	})
+	// For production deployments, use a net/http.Server configured
+	// with the appropriate timeouts.
+	err := http.ListenAndServe("localhost:8080", fn)
+	if err != nil {
+		log.Fatalf("failed to listen and serve: %v", err)
+	}
+}
+```
 
-There is no other Go WebSocket library with a clean API.
+### Client
+
+```go
+func main() {
+	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+
+	c, _, err := ws.Dial(ctx, "ws://localhost:8080")
+	if err != nil {
+		log.Fatalf("failed to ws dial: %v", err)
+	}
+	defer c.Close(ws.StatusInternalError, "")
+
+	type myJsonStruct struct {
+		MyField string `json:"my_field"`
+	}
+	err = wsjson.Write(ctx, c, myJsonStruct{
+		MyField: "foo",
+	})
+	if err != nil {
+		log.Fatalf("failed to write json struct: %v", err)
+	}
+
+	c.Close(ws.StatusNormalClosure, "")
+}
+```
 
-Comparisons with existing WebSocket libraries below.
+See [example_test.go](example_test.go) for more examples.
 
-### [x/net/websocket](https://godoc.org/golang.org/x/net/websocket)
+## Features
 
+- Full support of the WebSocket protocol
+- Simple to use because of the minimal API
+- Uses the context package for cancellation
+- Uses net/http's Client to do WebSocket dials
+- JSON and Protobuf helpers in wsjson and wspb subpackages
+- Compression extension is supported
+- Highly optimized
+- API will be ready for WebSockets over HTTP/2
 
-Unmaintained and the API does not reflect WebSocket semantics.
+## Design considerations
 
-See https://github.com/golang/go/issues/18152
+- Minimal API is easier to maintain and for others to learn
+- Context based cancellation is more ergonomic and robust than setting deadlines
+- No pings or pongs because TCP keep alives work fine for HTTP/1.1 and they do not make
+  sense with HTTP/2
+- net.Conn is never exposed as WebSocket's over HTTP/2 will not have a net.Conn.
+- Functional options make the API very clean and easy to extend
+- Compression is very useful for JSON payloads
+- Protobuf and JSON helpers make code terse
+- Using net/http's Client for dialing means we do not have to reinvent dialing hooks
+  and configurations. Just pass in a custom net/http client if you want custom dialing.
+
+## Comparison
 
 ### [gorilla/websocket](https://github.com/gorilla/websocket)
 
-This package is the community standard but it is very old and over time
+This package is the community standard but it is very old and over timennn
 has accumulated cruft. There are many ways to do the same thing and the API
-overall is just not very clear. 
+overall is just not very clear. Just compare the godoc of
+[nhooyr/ws](godoc.org/github.com/nhooyr/ws) side by side with
+[gorilla/websocket](godoc.org/github.com/gorilla/websocket).
 
-The callback hooks are also confusing. The API for this library has been designed
-such that there is only one way to do things and callbacks have been avoided.
+The API for nhooyr/ws has been designed such that there is only one way to do things
+and with HTTP/2 in mind which makes using it correctly and safely much easier.
 
-Performance sensitive applications should use ws/wscore directly.
+### [x/net/websocket](https://godoc.org/golang.org/x/net/websocket)
+
+Unmaintained and the API does not reflect WebSocket semantics. Should never be used.
+
+See https://github.com/golang/go/issues/18152
 
 ### [gobwas/ws](https://github.com/gobwas/ws)
 
-This library has an extremely flexible API but that comes at a cost of usability
-and clarity. Its just not clear and simple how to do things in a safe manner. 
+This library has an extremely flexible API but that comes at the cost of usability
+and clarity. Its just not clear how to do things in a safe manner. 
+
+This library is fantastic in terms of performance though. The author put in significant
+effort to ensure its speed and I have tried to apply as many of its teachings as
+I could into nhooyr/ws.
+
+If you want a library that gives you absolute control over everything, this is the library,
+but for most users, the API provided by nhooyr/ws will definitely fit better as it will
+be just as performant but much easier to use.
diff --git a/accept.go b/accept.go
index fb227eb3ddf2541e02024289fc4dbe068937c574..6ab95ee7528162b098c93dc57ad95100f217ca47 100644
--- a/accept.go
+++ b/accept.go
@@ -24,6 +24,7 @@ func AcceptSubprotocols(subprotocols ...string) AcceptOption {
 // Use this option with caution to avoid exposing your WebSocket
 // server to a CSRF attack.
 // See https://stackoverflow.com/a/37837709/4283659
+// You can use a * to specify wildcards in domain names.
 func AcceptOrigins(origins ...string) AcceptOption {
 	panic("TODO")
 }
diff --git a/datatype.go b/datatype.go
index ae62f0eabd57d94ea0877eee6660e0bd1ff8c25b..98e1a42ee695ba4d3c7ce9c42915860e9d0b213c 100644
--- a/datatype.go
+++ b/datatype.go
@@ -1,15 +1,11 @@
 package ws
 
-import (
-	"nhooyr.io/ws/wscore"
-)
-
 // DataType represents the Opcode of a WebSocket data frame.
 //go:generate stringer -type=DataType
 type DataType int
 
 // DataType constants.
 const (
-	Text   DataType = DataType(wscore.OpText)
-	Binary DataType = DataType(wscore.OpBinary)
+	Text   DataType = DataType(opText)
+	Binary DataType = DataType(opBinary)
 )
diff --git a/dial.go b/dial.go
index 119dbff92b1ef04856ed33f1dfa32bbb5c84815e..36cb964b01ad489d98d2d619868875edde1f0081 100644
--- a/dial.go
+++ b/dial.go
@@ -33,9 +33,10 @@ func DialSubprotocols(subprotocols ...string) DialOption {
 
 // We use this key for all client requests as the Sec-WebSocket-Key header is useless.
 // See https://stackoverflow.com/a/37074398/4283659.
+// We also use the same mask key for every message as it too does not make a difference.
 var secWebSocketKey = base64.StdEncoding.EncodeToString(make([]byte, 16))
 
-// Dial performs a websocket handshake on the given url with the given options.
+// Dial performs a WebSocket handshake on the given url with the given options.
 func Dial(ctx context.Context, u string, opts ...DialOption) (*Conn, *http.Response, error) {
 	panic("TODO")
 }
diff --git a/doc.go b/doc.go
index d746de69f4e5a50ae89268b35a73be1d1d9c1ab6..f54a122f77898780443e23a89a9a52375db0f4d1 100644
--- a/doc.go
+++ b/doc.go
@@ -1,2 +1,4 @@
-// Package ws implements the WebSocket protocol defined in RFC 6455.
+// Package ws is a minimal and idiomatic implementation of the WebSocket protocol.
+//
+// For now the docs are at https://github.com/nhooyr/ws#ws. I will move them here later.
 package ws
diff --git a/example_test.go b/example_test.go
index f0c2303a60113c4f62dde2a122b83338bf2ba5ad..882302a3050a4bbb7bacd793e8964d828329c6c9 100644
--- a/example_test.go
+++ b/example_test.go
@@ -41,7 +41,8 @@ func ExampleAccept_echo() {
 			r.SetContext(ctx)
 			r.Limit(32768)
 
-			w := c.MessageWriter(ctx, typ)
+			w := c.MessageWriter(typ)
+			w.SetContext(ctx)
 			_, err = io.Copy(w, r)
 			if err != nil {
 				return err
@@ -83,10 +84,13 @@ func ExampleAccept() {
 		}
 		defer c.Close(ws.StatusInternalError, "")
 
+		ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
+		defer cancel()
+
 		type myJsonStruct struct {
 			MyField string `json:"my_field"`
 		}
-		err = wsjson.Write(r.Context(), c, myJsonStruct{
+		err = wsjson.Write(ctx, c, myJsonStruct{
 			MyField: "foo",
 		})
 		if err != nil {
@@ -112,7 +116,6 @@ func ExampleDial() {
 	c, _, err := ws.Dial(ctx, "ws://localhost:8080")
 	if err != nil {
 		log.Fatalf("failed to ws dial: %v", err)
-		return
 	}
 	defer c.Close(ws.StatusInternalError, "")
 
@@ -124,7 +127,6 @@ func ExampleDial() {
 	})
 	if err != nil {
 		log.Fatalf("failed to write json struct: %v", err)
-		return
 	}
 
 	c.Close(ws.StatusNormalClosure, "")
diff --git a/wscore/header.go b/header.go
similarity index 64%
rename from wscore/header.go
rename to header.go
index bcbd41eb02b2db5d0564f7cf6e80e5e6559fed50..42eb6143deeca179b62648ff1c8e334405a6ea5a 100644
--- a/wscore/header.go
+++ b/header.go
@@ -1,17 +1,18 @@
-package wscore
+package ws
 
 import (
 	"io"
 )
 
-// Header represents a WebSocket frame header.
+// header represents a WebSocket frame header.
 // See https://tools.ietf.org/html/rfc6455#section-5.2
-type Header struct {
+// The fields are exported for easy printing for debugging.
+type header struct {
 	Fin    bool
 	Rsv1   bool
 	Rsv2   bool
 	Rsv3   bool
-	Opcode Opcode
+	Opcode opcode
 
 	PayloadLength int64
 
@@ -20,7 +21,7 @@ type Header struct {
 }
 
 // Bytes returns the bytes of the header.
-func (h Header) Bytes() []byte {
+func (h header) Bytes() []byte {
 	panic("TODO")
 }
 
diff --git a/wscore/mask.go b/mask.go
similarity index 77%
rename from wscore/mask.go
rename to mask.go
index c6c08ccd1e85ca8ff821ec957c64f2303fd20326..e3679cb860994d7d00eec4833eedc7404b7e6fa6 100644
--- a/wscore/mask.go
+++ b/mask.go
@@ -1,6 +1,6 @@
-package wscore
+package ws
 
-// Mask applies the websocket masking algorithm to p
+// Mask applies the WebSocket masking algorithm to p
 // with the given key where the first 3 bits of pos
 // are the starting position in the key.
 // See https://tools.ietf.org/html/rfc6455#section-5.3
@@ -10,6 +10,6 @@ package wscore
 //
 // For targets that do not support unsafe, please report an issue.
 // There is a mask by byte function below that will be used for such targets.
-func Mask(key [4]byte, pos int, p []byte) int {
+func mask(key [4]byte, pos int, p []byte) int {
 	panic("TODO")
 }
diff --git a/opcode.go b/opcode.go
new file mode 100644
index 0000000000000000000000000000000000000000..5616bb578b15bbbebcd6531aef73545ede875202
--- /dev/null
+++ b/opcode.go
@@ -0,0 +1,17 @@
+package ws
+
+// opcode represents a WebSocket Opcode.
+//go:generate stringer -type=opcode
+type opcode int
+
+// opcode constants.
+const (
+	opContinuation opcode = iota
+	opText
+	opBinary
+	// 3 - 7 are reserved for further non-control frames.
+	opClose opcode = 8 + iota
+	opPing
+	opPong
+	// 11-16 are reserved for further control frames.
+)
diff --git a/opcode_string.go b/opcode_string.go
new file mode 100644
index 0000000000000000000000000000000000000000..84afd17a09da3f4a5a33ee219a099a9dd83b5d1f
--- /dev/null
+++ b/opcode_string.go
@@ -0,0 +1,39 @@
+// Code generated by "stringer -type=opcode"; DO NOT EDIT.
+
+package ws
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[opContinuation-0]
+	_ = x[opText-1]
+	_ = x[opBinary-2]
+	_ = x[opClose-11]
+	_ = x[opPing-12]
+	_ = x[opPong-13]
+}
+
+const (
+	_opcode_name_0 = "opContinuationopTextopBinary"
+	_opcode_name_1 = "opCloseopPingopPong"
+)
+
+var (
+	_opcode_index_0 = [...]uint8{0, 14, 20, 28}
+	_opcode_index_1 = [...]uint8{0, 7, 13, 19}
+)
+
+func (i opcode) String() string {
+	switch {
+	case 0 <= i && i <= 2:
+		return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]]
+	case 11 <= i && i <= 13:
+		i -= 11
+		return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]]
+	default:
+		return "opcode(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+}
diff --git a/ws.go b/ws.go
index 7ec54ed7a82e48c934e37886a38b4cae3eab13c8..964a31843e332ba9d12f2f5adf02a5fe70071cd1 100644
--- a/ws.go
+++ b/ws.go
@@ -2,7 +2,6 @@ package ws
 
 import (
 	"context"
-	"net"
 )
 
 const (
@@ -10,8 +9,7 @@ const (
 )
 
 // Conn represents a WebSocket connection.
-type Conn struct {
-}
+type Conn struct{}
 
 // Subprotocol returns the negotiated subprotocol.
 // An empty string means the default protocol.
@@ -19,16 +17,11 @@ func (c *Conn) Subprotocol() string {
 	panic("TODO")
 }
 
-// NetConn returns the net.Conn underlying the Conn.
-func (c *Conn) NetConn() net.Conn {
-	panic("TODO")
-}
-
 // MessageWriter returns a writer bounded by the context that will write
 // a WebSocket data frame of type dataType to the connection.
 // Ensure you close the MessageWriter once you have written to entire message.
 // Concurrent calls to MessageWriter are ok.
-func (c *Conn) MessageWriter(ctx context.Context, dataType DataType) *MessageWriter {
+func (c *Conn) MessageWriter(dataType DataType) *MessageWriter {
 	panic("TODO")
 }
 
@@ -42,13 +35,15 @@ func (c *Conn) ReadMessage(ctx context.Context) (DataType, *MessageReader, error
 // Close closes the WebSocket connection with the given status code and reason.
 // It will write a WebSocket close frame with a timeout of 5 seconds.
 func (c *Conn) Close(code StatusCode, reason string) error {
+	// This function also will not wait for a close frame from the peer like the RFC
+	// wants because that makes no sense and I don't think anyone actually follows that.
+	// Definitely worth seeing what popular browsers do later.
 	panic("TODO")
 }
 
 // MessageWriter enables writing to a WebSocket connection.
 // Ensure you close the MessageWriter once you have written to entire message.
-type MessageWriter struct {
-}
+type MessageWriter struct{}
 
 // Write writes the given bytes to the WebSocket connection.
 // The frame will automatically be fragmented as appropriate
@@ -58,6 +53,18 @@ func (w *MessageWriter) Write(p []byte) (n int, err error) {
 	panic("TODO")
 }
 
+// SetContext bounds the writer to the context.
+// This must be called before any write.
+func (w *MessageWriter) SetContext(ctx context.Context) {
+	panic("TODO")
+}
+
+// Compress marks the message to be compressed.
+// This must be called before any write.
+func (w *MessageWriter) Compress() {
+	panic("TODO")
+}
+
 // Close flushes the frame to the connection.
 // This must be called for every MessageWriter.
 func (w *MessageWriter) Close() error {
@@ -65,17 +72,18 @@ func (w *MessageWriter) Close() error {
 }
 
 // MessageReader enables reading a data frame from the WebSocket connection.
-type MessageReader struct {
-}
+type MessageReader struct{}
 
 // SetContext bounds the read operation to the ctx.
 // By default, the context is the one passed to conn.ReadMessage.
 // You still almost always want a separate context for reading the message though.
+// Must be called before any read.
 func (r *MessageReader) SetContext(ctx context.Context) {
 	panic("TODO")
 }
 
 // Limit limits the number of bytes read by the reader.
+// Must be called before any read.
 func (r *MessageReader) Limit(bytes int) {
 	panic("TODO")
 }
diff --git a/wscore/opcode.go b/wscore/opcode.go
deleted file mode 100644
index 878eb80ec93a7150b34989792e3e52a7a5c9f7b2..0000000000000000000000000000000000000000
--- a/wscore/opcode.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package wscore
-
-// Opcode represents a WebSocket Opcode.
-//go:generate stringer -type=Opcode
-type Opcode int
-
-// Opcode constants.
-const (
-	OpContinuation Opcode = iota
-	OpText
-	OpBinary
-	// 3 - 7 are reserved for further non-control frames.
-	OpClose Opcode = 8 + iota
-	OpPing
-	OpPong
-	// 11-16 are reserved for further control frames.
-)
diff --git a/wscore/opcode_string.go b/wscore/opcode_string.go
deleted file mode 100644
index 8ed5c1c700689582e53dee8fb6963726ad686fea..0000000000000000000000000000000000000000
--- a/wscore/opcode_string.go
+++ /dev/null
@@ -1,39 +0,0 @@
-// Code generated by "stringer -type=Opcode"; DO NOT EDIT.
-
-package wscore
-
-import "strconv"
-
-func _() {
-	// An "invalid array index" compiler error signifies that the constant values have changed.
-	// Re-run the stringer command to generate them again.
-	var x [1]struct{}
-	_ = x[OpContinuation-0]
-	_ = x[OpText-1]
-	_ = x[OpBinary-2]
-	_ = x[OpClose-11]
-	_ = x[OpPing-12]
-	_ = x[OpPong-13]
-}
-
-const (
-	_Opcode_name_0 = "OpContinuationOpTextOpBinary"
-	_Opcode_name_1 = "OpCloseOpPingOpPong"
-)
-
-var (
-	_Opcode_index_0 = [...]uint8{0, 14, 20, 28}
-	_Opcode_index_1 = [...]uint8{0, 7, 13, 19}
-)
-
-func (i Opcode) String() string {
-	switch {
-	case 0 <= i && i <= 2:
-		return _Opcode_name_0[_Opcode_index_0[i]:_Opcode_index_0[i+1]]
-	case 11 <= i && i <= 13:
-		i -= 11
-		return _Opcode_name_1[_Opcode_index_1[i]:_Opcode_index_1[i+1]]
-	default:
-		return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")"
-	}
-}