diff --git a/api/endpoints.go b/api/endpoints.go
index abf6eb2338101b76970ca9c71c498630283b866a..f80b20c2610836526edd1e1a660b112134510c40 100644
--- a/api/endpoints.go
+++ b/api/endpoints.go
@@ -25,7 +25,7 @@ import (
 type ServerInterface interface {
 	// Returns main page
 	// (GET /)
-	Index(w http.ResponseWriter, r *http.Request)
+	GetIndex(w http.ResponseWriter, r *http.Request)
 }
 
 // ServerInterfaceWrapper converts contexts to parameters.
@@ -37,12 +37,12 @@ type ServerInterfaceWrapper struct {
 
 type MiddlewareFunc func(http.Handler) http.Handler
 
-// Index operation middleware
-func (siw *ServerInterfaceWrapper) Index(w http.ResponseWriter, r *http.Request) {
+// GetIndex operation middleware
+func (siw *ServerInterfaceWrapper) GetIndex(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 
 	handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		siw.Handler.Index(w, r)
+		siw.Handler.GetIndex(w, r)
 	}))
 
 	for _, middleware := range siw.HandlerMiddlewares {
@@ -166,24 +166,24 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
 		ErrorHandlerFunc:   options.ErrorHandlerFunc,
 	}
 
-	m.HandleFunc("GET "+options.BaseURL+"/", wrapper.Index)
+	m.HandleFunc("GET "+options.BaseURL+"/", wrapper.GetIndex)
 
 	return m
 }
 
-type IndexRequestObject struct {
+type GetIndexRequestObject struct {
 }
 
-type IndexResponseObject interface {
-	VisitIndexResponse(w http.ResponseWriter) error
+type GetIndexResponseObject interface {
+	VisitGetIndexResponse(w http.ResponseWriter) error
 }
 
-type Index200TexthtmlResponse struct {
+type GetIndex200TexthtmlResponse struct {
 	Body          io.Reader
 	ContentLength int64
 }
 
-func (response Index200TexthtmlResponse) VisitIndexResponse(w http.ResponseWriter) error {
+func (response GetIndex200TexthtmlResponse) VisitGetIndexResponse(w http.ResponseWriter) error {
 	w.Header().Set("Content-Type", "text/html")
 	if response.ContentLength != 0 {
 		w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength))
@@ -201,7 +201,7 @@ func (response Index200TexthtmlResponse) VisitIndexResponse(w http.ResponseWrite
 type StrictServerInterface interface {
 	// Returns main page
 	// (GET /)
-	Index(ctx context.Context, request IndexRequestObject) (IndexResponseObject, error)
+	GetIndex(ctx context.Context, request GetIndexRequestObject) (GetIndexResponseObject, error)
 }
 
 type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc
@@ -233,23 +233,23 @@ type strictHandler struct {
 	options     StrictHTTPServerOptions
 }
 
-// Index operation middleware
-func (sh *strictHandler) Index(w http.ResponseWriter, r *http.Request) {
-	var request IndexRequestObject
+// GetIndex operation middleware
+func (sh *strictHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
+	var request GetIndexRequestObject
 
 	handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
-		return sh.ssi.Index(ctx, request.(IndexRequestObject))
+		return sh.ssi.GetIndex(ctx, request.(GetIndexRequestObject))
 	}
 	for _, middleware := range sh.middlewares {
-		handler = middleware(handler, "Index")
+		handler = middleware(handler, "GetIndex")
 	}
 
 	response, err := handler(r.Context(), w, r, request)
 
 	if err != nil {
 		sh.options.ResponseErrorHandlerFunc(w, r, err)
-	} else if validResponse, ok := response.(IndexResponseObject); ok {
-		if err := validResponse.VisitIndexResponse(w); err != nil {
+	} else if validResponse, ok := response.(GetIndexResponseObject); ok {
+		if err := validResponse.VisitGetIndexResponse(w); err != nil {
 			sh.options.ResponseErrorHandlerFunc(w, r, err)
 		}
 	} else if response != nil {
@@ -260,12 +260,12 @@ func (sh *strictHandler) Index(w http.ResponseWriter, r *http.Request) {
 // Base64 encoded, gzipped, json marshaled Swagger object
 var swaggerSpec = []string{
 
-	"H4sIAAAAAAAC/2RRwY7UMAz9leidq3aAW05wHAECsbsn4JBtPW1E41SOO2w16r8jh1k0Yi5VYz+/9/x8",
-	"QeRThr+gz6yhV/ulFOIMj190jvx+3obQRkIDDong8dHK7tM2BDRYxZCT6lJ8171i9wYDlV7iojEzPB4n",
-	"citHLe43PbunY4sGc+yJC5nilfnz8fGOMi/EJa/SU5tl7K5DXYpqKkqSypfTA8k59nQzNkZtr266ukdX",
-	"5dFAo86GfKp2+sxnEiVBgzNJ+Wv3TXtoD8Zv6mGJ8HhXSw2WoFMxz519RqqJ3S/7HAq5JYzkTlmcTuQe",
-	"vn5ofzAqpwSDHgd4HHmgFzQQKkvmQpX77eHwehPiqqD0ot2kabZH6SdKoZa3xXYpKpFH7Ptd8ClErj5g",
-	"vbKmFGSDxzfSVbi4274BSCwF+O+X/+5Q4/sXKfaf+58AAAD//yaFJNI8AgAA",
+	"H4sIAAAAAAAC/2RRTW/cIBD9K+idkdm2N07tqVq1VasmufVC8KyNagYEYzdW5P9eQTbSSntBMPPmfQyv",
+	"CHxJsK/wicV5aVeKLiyw+Etb4M/LProhEDTYRYLFt1ZW3/fRQWMtDTmL5GqNecceGiNVX0KWkBgWjzOp",
+	"lYNU9Y+e1dN5gMYSPHGlpnhl/nF+vKNMmbimtXgaUpnMdcjEIE1FqMT68/JAZQuebsamIMPVjek5TJeH",
+	"hgRZGvKp2/GJNypCBRoblfpm98NwGk6Nv6m7HGDxqZc0spO5Ns+mHRP1jd2HfXaVVHYTqUsqSmZSD7++",
+	"DH8YnbO4Bj2PsPhKcuaRXqBRqObElTr9x9Pp/VuIu4jQi5hZ4tIe1c8UXS/vucWpUgJPOI673UcXuFtB",
+	"69U1Rld2WPwmWQtXdds/jv8BAAD//xuhfPkSAgAA",
 }
 
 // GetSwagger returns the content of the embedded swagger specification file
diff --git a/api/implementation.go b/api/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..fa849e16eadc47135a55ac0748e189f95cefddfb
--- /dev/null
+++ b/api/implementation.go
@@ -0,0 +1,28 @@
+// Package api implements the units API.
+package api
+
+import (
+	"context"
+	"strings"
+)
+
+// UnitsAPI implements the units web ui.
+type UnitsAPI struct {
+}
+
+// GetIndex implements the main page.
+func (ua UnitsAPI) GetIndex(_ context.Context, _ GetIndexRequestObject) (GetIndexResponseObject, error) {
+	body := `<html>
+<head>
+    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
+</head>
+<body>
+    <h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1>
+</body>
+</html>`
+	response := GetIndex200TexthtmlResponse{
+		Body:          strings.NewReader(body),
+		ContentLength: int64(len(body)),
+	}
+	return response, nil
+}
diff --git a/api/server.gen.go b/api/server.gen.go
deleted file mode 100644
index e2bd3230a9c375942075cf06f79b880fb7b50f12..0000000000000000000000000000000000000000
--- a/api/server.gen.go
+++ /dev/null
@@ -1,135 +0,0 @@
-// Package api provides primitives to interact with the openapi HTTP API.
-//
-// Code generated by github.com/deepmap/oapi-codegen version v1.16.3 DO NOT EDIT.
-package api
-
-import (
-	"context"
-	"fmt"
-	"io"
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-	strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo"
-)
-
-// ServerInterface represents all server handlers.
-type ServerInterface interface {
-	// Returns main page
-	// (GET /)
-	Index(ctx echo.Context) error
-}
-
-// ServerInterfaceWrapper converts echo contexts to parameters.
-type ServerInterfaceWrapper struct {
-	Handler ServerInterface
-}
-
-// Index converts echo context to params.
-func (w *ServerInterfaceWrapper) Index(ctx echo.Context) error {
-	var err error
-
-	// Invoke the callback with all the unmarshaled arguments
-	err = w.Handler.Index(ctx)
-	return err
-}
-
-// This is a simple interface which specifies echo.Route addition functions which
-// are present on both echo.Echo and echo.Group, since we want to allow using
-// either of them for path registration
-type EchoRouter interface {
-	CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-	TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
-}
-
-// RegisterHandlers adds each server route to the EchoRouter.
-func RegisterHandlers(router EchoRouter, si ServerInterface) {
-	RegisterHandlersWithBaseURL(router, si, "")
-}
-
-// Registers handlers, and prepends BaseURL to the paths, so that the paths
-// can be served under a prefix.
-func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
-
-	wrapper := ServerInterfaceWrapper{
-		Handler: si,
-	}
-
-	router.GET(baseURL+"/", wrapper.Index)
-
-}
-
-type IndexRequestObject struct {
-}
-
-type IndexResponseObject interface {
-	VisitIndexResponse(w http.ResponseWriter) error
-}
-
-type Index200TexthtmlResponse struct {
-	Body          io.Reader
-	ContentLength int64
-}
-
-func (response Index200TexthtmlResponse) VisitIndexResponse(w http.ResponseWriter) error {
-	w.Header().Set("Content-Type", "text/html")
-	if response.ContentLength != 0 {
-		w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength))
-	}
-	w.WriteHeader(200)
-
-	if closer, ok := response.Body.(io.ReadCloser); ok {
-		defer closer.Close()
-	}
-	_, err := io.Copy(w, response.Body)
-	return err
-}
-
-// StrictServerInterface represents all server handlers.
-type StrictServerInterface interface {
-	// Returns main page
-	// (GET /)
-	Index(ctx context.Context, request IndexRequestObject) (IndexResponseObject, error)
-}
-
-type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc
-type StrictMiddlewareFunc = strictecho.StrictEchoMiddlewareFunc
-
-func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
-	return &strictHandler{ssi: ssi, middlewares: middlewares}
-}
-
-type strictHandler struct {
-	ssi         StrictServerInterface
-	middlewares []StrictMiddlewareFunc
-}
-
-// Index operation middleware
-func (sh *strictHandler) Index(ctx echo.Context) error {
-	var request IndexRequestObject
-
-	handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
-		return sh.ssi.Index(ctx.Request().Context(), request.(IndexRequestObject))
-	}
-	for _, middleware := range sh.middlewares {
-		handler = middleware(handler, "Index")
-	}
-
-	response, err := handler(ctx, request)
-
-	if err != nil {
-		return err
-	} else if validResponse, ok := response.(IndexResponseObject); ok {
-		return validResponse.VisitIndexResponse(ctx.Response())
-	} else if response != nil {
-		return fmt.Errorf("unexpected response type: %T", response)
-	}
-	return nil
-}
diff --git a/api/units.yaml b/api/units.yaml
index 39d98d401946922806acf9570e1b3e271ad9e490..1b0f6a99c3cbd22e07569297f0fa3df1126dcc1e 100644
--- a/api/units.yaml
+++ b/api/units.yaml
@@ -12,15 +12,17 @@ info:
   license:
     name: MIT
     url: https://opensource.org/license/mit
-servers:
-  - url: https://units.lyda.ie/
+
+# servers:
+#  - url: https://units.lyda.ie/
+
 paths:
   /:
     get:
       summary: Returns main page
       description: |
         The base page for the SPA.
-      operationId: index
+      operationId: GetIndex
       responses:
         '200':
           description: main page
diff --git a/cmd/root.go b/cmd/root.go
index f44849997588309f4f7ec2df3f4814160b2e688a..15628a8bd88b1647b61245a42bc741480560de23 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -29,7 +29,9 @@ TODO: example 3`,
 }
 
 func run(_ *cobra.Command, _ []string) {
-	server := &web.Config{}
+	fmt.Printf("Server listening on '%s'\n", viper.GetString("listen"))
+	server, err := web.New(viper.GetString("listen"))
+	cobra.CheckErr(err)
 
 	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 	defer stop()
@@ -48,16 +50,8 @@ func Execute() {
 
 func init() {
 	cobra.OnInitialize(initConfig)
-
-	// Here you will define your flags and configuration settings.
-	// Cobra supports persistent flags, which, if defined here,
-	// will be global for your application.
-
 	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.units.yaml)")
-
-	// Cobra also supports local flags, which will only run
-	// when this action is called directly.
-	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+	rootCmd.Flags().StringP("listen", "l", "0.0.0.0:8989", "host:port to listen on")
 }
 
 // initConfig reads in config file and ENV variables if set.
@@ -77,6 +71,7 @@ func initConfig() {
 	}
 
 	viper.AutomaticEnv() // read in environment variables that match
+	viper.BindPFlags(rootCmd.Flags())
 
 	// If a config file is found, read it in.
 	if err := viper.ReadInConfig(); err == nil {
diff --git a/go.mod b/go.mod
index 2d82a386cdbb2231ce0d0c100cf046bf440a7f19..7e8ed8568d035aa87c845e8a19697ecd53938101 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
 	github.com/getkin/kin-openapi v0.124.0 // indirect
 	github.com/go-openapi/jsonpointer v0.20.2 // indirect
 	github.com/go-openapi/swag v0.22.8 // indirect
+	github.com/gorilla/mux v1.8.1 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/invopop/yaml v0.2.0 // indirect
@@ -24,6 +25,7 @@ require (
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+	github.com/oapi-codegen/nethttp-middleware v1.0.2 // indirect
 	github.com/oapi-codegen/runtime v1.1.1 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
diff --git a/go.sum b/go.sum
index d79654a55679d7eaf00cb929e7a79fc4a899ccdc..a71f3670ff3ce7f3e06552b2eba82070cfdc50ce 100644
--- a/go.sum
+++ b/go.sum
@@ -15,6 +15,8 @@ github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicb
 github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -44,6 +46,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/oapi-codegen/nethttp-middleware v1.0.2 h1:A5tfAcKJhWIbIPnlQH+l/DtfVE1i5TFgPlQAiW+l1vQ=
+github.com/oapi-codegen/nethttp-middleware v1.0.2/go.mod h1:DfDalonSO+eRQ3RTb8kYoWZByCCPFRxm9WKq1UbY0E4=
 github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
 github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
 github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
diff --git a/web/web.go b/web/web.go
index 924dd6d73208e56ed25fe8a41014840db3d3032c..37e30ad13ee9fc44fd6f406ddc67566fcf372113 100644
--- a/web/web.go
+++ b/web/web.go
@@ -1,16 +1,69 @@
 // Package web implements the web interface.
 package web
 
-import "context"
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"time"
 
-// Config contains the web config.
-type Config struct {
+	"github.com/getkin/kin-openapi/openapi3"
+	om "github.com/oapi-codegen/nethttp-middleware"
+
+	"git.lyda.ie/kevin/units/api"
+)
+
+// Server contains the web config.
+type Server struct {
+	swagger *openapi3.T
+	listen  string
+}
+
+// New creates a new server.
+func New(listen string) (*Server, error) {
+	swagger, err := api.GetSwagger()
+	if err != nil {
+		return nil, err
+	}
+
+	svr := &Server{
+		swagger: swagger,
+		listen:  listen,
+	}
+
+	return svr, nil
 }
 
 // Serve serves the web interface
-func (c *Config) Serve(ctx context.Context) {
+func (svr *Server) Serve(ctx context.Context) {
+	oapiValidator := om.OapiRequestValidator(svr.swagger)
+	unitsAPI := &api.UnitsAPI{}
+	server := api.NewStrictHandler(unitsAPI, []api.StrictMiddlewareFunc{})
+	mux := http.NewServeMux()
+
+	// get an `http.Handler` that we can use
+	h := oapiValidator(api.HandlerFromMux(server, mux))
+
+	s := &http.Server{
+		Handler: h,
+		Addr:    svr.listen,
+	}
+
+	exit := make(chan struct{})
+	go func() {
+		err := s.ListenAndServe()
+		if err != nil {
+			fmt.Println(err)
+		}
+		close(exit)
+	}()
+
 	select {
+	case <-exit:
+		fmt.Println("Server exited unexpectedly")
 	case <-ctx.Done():
-		// exit now.
+		shutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*30))
+		defer cancel()
+		s.Shutdown(shutCtx)
 	}
 }