diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..9d7115cb4f09e145a773c4b9b2c7bd6c7cb81234
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+server
+server.*
+db/
+
+.idea/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..80e85be11942bc118a744ea9990829d53030cfbd
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "assets/public"]
+	path = assets/public
+	url = https://git.randomchars.net/leva/school-assistant/web.git
diff --git a/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000000000000000000000000000000000000..da55c1fc1701119b81138f33666693438cb857bc
--- /dev/null
+++ b/GNUmakefile
@@ -0,0 +1,15 @@
+.NOTPARALLEL: build start
+
+all: build
+run: build start
+
+LDFLAGS = -s -w -X 'git.randomchars.net/levatax/leva/school-assistant/server/store.version=$(shell echo -n `git describe --tags`; if ! [ "`git status -s`" = '' ]; then echo -n '-dirty'; fi)' -X 'git.randomchars.net/levatax/leva/school-assistant/server/store.revision=$(shell git rev-parse --short HEAD)'
+
+.PHONY: build
+build:
+	@echo "Building server..."
+	@go build -tags=jsoniter -ldflags="$(LDFLAGS)" -o build/server
+
+.PHONY: start
+start:
+	@./build/server
\ No newline at end of file
diff --git a/api.go b/api.go
new file mode 100644
index 0000000000000000000000000000000000000000..3987e2ac1ab42f56250c73c7859b2145d2a3c688
--- /dev/null
+++ b/api.go
@@ -0,0 +1,5 @@
+package main
+
+func registerAPI() {
+	// TODO: API routes
+}
diff --git a/assets/README b/assets/README
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/cleanup.go b/cleanup.go
new file mode 100644
index 0000000000000000000000000000000000000000..a8247010cd6679bf59b1b6ac8b036788f8013881
--- /dev/null
+++ b/cleanup.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+	"context"
+	"log"
+	"time"
+)
+
+var (
+	reExec bool
+)
+
+func cleanup(restart bool) {
+	var err error
+
+	// Set restart
+	reExec = restart
+
+	// Shutdown web server
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+	defer cancel()
+	err = server.Shutdown(ctx)
+	if err != nil {
+		log.Printf("error shutting down web server: %s", err)
+	}
+}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..10850e0496d8ed6dd1e3c9ef68e9964f4756d6d3
--- /dev/null
+++ b/config.go
@@ -0,0 +1,83 @@
+package main
+
+import (
+	"flag"
+	"github.com/BurntSushi/toml"
+	"log"
+	"os"
+)
+
+var (
+	configPath string
+	read       bool
+)
+
+var config conf
+
+type conf struct {
+	System systemConf
+	Web    webConf
+}
+
+type systemConf struct {
+	StorePath           string
+	FilePermission      int
+	DirectoryPermission int
+	BackupInterval      int
+}
+
+type webConf struct {
+	Host  string
+	Port  int
+	Unix  bool
+	Proxy bool
+	Log   bool
+}
+
+func init() {
+	flag.StringVar(&configPath, "c", "server.conf", "specify location of configuration file")
+}
+
+func configSetup() {
+	if read {
+		panic("config already read")
+	}
+	defer func() { read = true }()
+
+	if meta, err := toml.DecodeFile(configPath, &config); err != nil {
+		if !os.IsNotExist(err) {
+			log.Fatalf("error parsing config: %s", err)
+		}
+
+		// generate
+		var file *os.File
+		if file, err = os.Create(configPath); err != nil {
+			log.Fatalf("error creating config: %s", err)
+		}
+		if err = toml.NewEncoder(file).Encode(defConf); err != nil {
+			log.Fatalf("error encoding config: %s", err)
+		}
+		config = defConf
+		return
+	} else {
+		for _, key := range meta.Undecoded() {
+			log.Printf("unused key in config: %s", key.String())
+		}
+	}
+}
+
+var defConf = conf{
+	System: systemConf{
+		StorePath:           "db",
+		FilePermission:      0600,
+		DirectoryPermission: 0700,
+		BackupInterval:      0,
+	},
+	Web: webConf{
+		Host:  "127.0.0.1",
+		Port:  7777,
+		Unix:  false,
+		Proxy: true,
+		Log:   false,
+	},
+}
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..dabbe5a1ce6069c1685e10620e37ba3b39e0bb0b
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,9 @@
+
+[server]
+  host = "127.0.0.1"
+  port = 7777
+  proxy = true
+  unix = false
+
+[system]
+  loglevel = "info"
diff --git a/database/badger/backup.go b/database/badger/backup.go
new file mode 100644
index 0000000000000000000000000000000000000000..ab29eb09834532ffd9f0b6a14b635f27307718ea
--- /dev/null
+++ b/database/badger/backup.go
@@ -0,0 +1,70 @@
+package badger
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"time"
+)
+
+var backupTicker *time.Ticker
+
+func setupBackup(db *Badger, interval int) error {
+	if interval > 0 {
+		log.Printf("periodical database backup interval %v seconds", interval)
+		if _, err := os.Stat("backup"); os.IsNotExist(err) {
+			err = os.Mkdir("backup", 0700)
+			if err != nil {
+				return err
+			}
+		}
+		backupTicker = time.NewTicker(time.Duration(interval) * time.Second)
+		go func() {
+			for {
+				select {
+				case <-backupTicker.C:
+					var intermediate *os.File
+					c := func() {
+						if err := intermediate.Close(); err != nil {
+							log.Printf("error closing intermediate backup file: %s", err)
+						}
+					}
+
+					if i, err := os.Create("backup/.intermediate"); err != nil {
+						log.Printf("error creating intermediate backup file: %s", err)
+						continue
+					} else {
+						intermediate = i
+					}
+
+					var ver uint64
+					if v, err := db.DB.Backup(intermediate, 0); err != nil {
+						log.Printf("error doing backup: %s", err)
+						c()
+						continue
+					} else {
+						ver = v
+					}
+
+					if err := os.Rename("backup/.intermediate", fmt.Sprintf("backup/%v", ver)); err != nil {
+						log.Printf("error renaming intermediate backup file: %s", err)
+						if err = os.Remove("backup/.intermediate"); err != nil {
+							log.Printf("error removing intermediate file: %s", err)
+							log.Printf("backup has been disabled, see above two messages")
+							c()
+							break
+						}
+						c()
+						continue
+					}
+					c()
+					log.Printf("database backup successful, version %v", ver)
+				}
+			}
+		}()
+		return nil
+	} else {
+		log.Printf("periodical database backup disabled")
+		return nil
+	}
+}
diff --git a/database/badger/main.go b/database/badger/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..a67bb242a8164a4f388e11f010647f7f94e80fdd
--- /dev/null
+++ b/database/badger/main.go
@@ -0,0 +1,250 @@
+// Package badger implements database backend based on badger.
+package badger
+
+import (
+	"github.com/dgraph-io/badger/v3"
+	"log"
+	"strings"
+	"time"
+)
+
+// Database is an instance of the database backend.
+var Database Badger
+
+// Badger represents a badger database instance.
+type Badger struct {
+	DB *badger.DB
+}
+
+// DBType returns the name of the database as a string.
+func (db *Badger) DBType() string {
+	return "Badger"
+}
+
+// Open opens the database.
+func (db *Badger) Open(path string, backupInterval int) error {
+	opts := badger.DefaultOptions(path).
+		WithDir(path).
+		WithLogger(nil).
+		WithValueDir(path).
+		WithSyncWrites(false).
+		WithNumMemtables(2).
+		WithNumLevelZeroTables(2).
+		WithValueThreshold(1)
+
+	if instance, err := badger.Open(opts); err != nil {
+		return err
+	} else {
+		db.DB = instance
+	}
+
+	go (func() {
+		for {
+			if err := db.DB.RunValueLogGC(0.5); err == nil {
+				time.Sleep(100 * time.Millisecond)
+			} else {
+				switch err {
+				case badger.ErrRejected:
+					log.Print("value log garbage collection rejected")
+				case badger.ErrNoRewrite:
+				default:
+					log.Printf("error running value log garbage collection: %s", err)
+				}
+				time.Sleep(30 * time.Second)
+			}
+		}
+	})()
+
+	if err := setupBackup(db, backupInterval); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Close closes the database.
+func (db *Badger) Close() error {
+	if backupTicker != nil {
+		backupTicker.Stop()
+	}
+	return db.DB.Close()
+}
+
+// Size returns the size of the database.
+func (db *Badger) Size() int64 {
+	lsm, vlog := db.DB.Size()
+	return lsm + vlog
+}
+
+// Set adds a key-value pair to the database.
+func (db *Badger) Set(key, value string) error {
+	return db.DB.Update(func(txn *badger.Txn) (err error) {
+		return txn.Set([]byte(key), []byte(value))
+	})
+}
+
+// Get gets the value of a key from the database.
+func (db *Badger) Get(key string) (string, error) {
+	var data string
+
+	err := db.DB.View(func(txn *badger.Txn) error {
+		item, err := txn.Get([]byte(key))
+		if err != nil {
+			return err
+		}
+
+		val, err := item.ValueCopy(nil)
+		if err != nil {
+			return err
+		}
+
+		data = string(val)
+
+		return nil
+	})
+
+	return data, err
+}
+
+// Del deletes a key from the database.
+func (db *Badger) Del(keys []string) error {
+	return db.DB.Update(func(txn *badger.Txn) error {
+		for _, key := range keys {
+			err := txn.Delete([]byte(key))
+			if err != nil {
+				break
+			}
+		}
+
+		return nil
+	})
+}
+
+// HSet adds a key-value pair to a hashmap.
+func (db *Badger) HSet(hashmap, key, value string) error {
+	err := db.Set(hashmap+"/{HASH}/"+key, value)
+	return err
+}
+
+// HGet gets the value of a key from a hashmap.
+func (db *Badger) HGet(hashmap, key string) (string, error) {
+	data, err := db.Get(hashmap + "/{HASH}/" + key)
+	if err == badger.ErrKeyNotFound {
+		return "", nil
+	}
+	return data, err
+}
+
+// HDel deletes a key from a hashmap.
+func (db *Badger) HDel(hashmap string, keys []string) error {
+	if len(keys) > 0 {
+		for i, key := range keys {
+			keys[i] = hashmap + "/{HASH}/" + key
+		}
+	} else {
+		err := db.Iter(false, true, hashmap+"/{HASH}/", hashmap+"/{HASH}/", func(key, _ string) bool {
+			keys = append(keys, key)
+			return true
+		})
+		if err != nil {
+			if err == badger.ErrKeyNotFound {
+				return nil
+			}
+			return err
+		}
+	}
+	if err := db.Del(keys); err == badger.ErrKeyNotFound {
+		return nil
+	} else {
+		return err
+	}
+}
+
+// HGetAll gets all key-value pairs of a hashmap.
+func (db *Badger) HGetAll(hashmap string) (map[string]string, error) {
+	result := map[string]string{}
+	err := db.Iter(true, true, hashmap+"/{HASH}/", hashmap+"/{HASH}/",
+		func(key, value string) bool {
+			fields := strings.SplitN(key, "/{HASH}/", 2)
+			if len(fields) < 2 {
+				return true
+			}
+			result[fields[1]] = value
+			return true
+		})
+	if err == badger.ErrKeyNotFound {
+		return nil, nil
+	}
+	return result, err
+}
+
+// HKeys gets all keys of a hashmap.
+func (db *Badger) HKeys(hashmap string) ([]string, error) {
+	var result []string
+	err := db.Iter(false, true, hashmap+"/{HASH}/", hashmap+"/{HASH}/",
+		func(key, _ string) bool {
+			fields := strings.SplitN(key, "/{HASH}/", 2)
+			if len(fields) < 2 {
+				return true
+			}
+			result = append(result, fields[1])
+			return true
+		})
+	return result, err
+}
+
+// HLen gets the length of a hashmap.
+func (db *Badger) HLen(hashmap string) (int, error) {
+	length := 0
+	err := db.Iter(false, true, hashmap+"/{HASH}/", hashmap+"/{HASH}/",
+		func(_, _ string) bool {
+			length++
+			return true
+		})
+	return length, err
+}
+
+func seek(offset string, includeOffset bool, iterator *badger.Iterator) {
+	if offset == "" {
+		iterator.Rewind()
+	} else {
+		iterator.Seek([]byte(offset))
+		if !includeOffset && iterator.Valid() {
+			iterator.Next()
+		}
+	}
+}
+
+func validate(prefix string, iterator *badger.Iterator) bool {
+	if !iterator.Valid() {
+		return false
+	}
+	if prefix != "" && !iterator.ValidForPrefix([]byte(prefix)) {
+		return false
+	}
+	return true
+}
+
+// Iter iterates through stuff in the database.
+func (db *Badger) Iter(prefetch, includeOffset bool, offset, prefix string, handler func(key, value string) bool) error {
+	return db.DB.View(func(txn *badger.Txn) error {
+		opts := badger.DefaultIteratorOptions
+		opts.PrefetchValues = prefetch
+		iterator := txn.NewIterator(opts)
+		defer iterator.Close()
+		for seek(offset, includeOffset, iterator); validate(prefix, iterator); iterator.Next() {
+			var key, value []byte
+			item := iterator.Item()
+			key = item.KeyCopy(nil)
+
+			if prefetch {
+				value, _ = item.ValueCopy(nil)
+			}
+
+			if !handler(string(key), string(value)) {
+				break
+			}
+		}
+		return nil
+	})
+}
diff --git a/database/main.go b/database/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..8bc0925b73f7fd851d7b4da17c0e9d7cd81c1f95
--- /dev/null
+++ b/database/main.go
@@ -0,0 +1,36 @@
+package database
+
+import "git.randomchars.net/levatax/leva/school-assistant/server/database/badger"
+
+type Backend interface {
+	// DBType returns the name of the database as a string.
+	DBType() string
+	// Open opens the database.
+	Open(path string, backupInterval int) error
+	// Close closes the database.
+	Close() error
+	// Size returns the size of the database.
+	Size() int64
+	// Set adds a key-value pair to the database.
+	Set(key, value string) error
+	// Get gets the value of a key from the database.
+	Get(key string) (string, error)
+	// Del deletes a key from the database.
+	Del(keys []string) error
+	// HSet adds a key-value pair to a hashmap.
+	HSet(hashmap, key, value string) error
+	// HGet gets the value of a key from a hashmap.
+	HGet(hashmap, key string) (string, error)
+	// HDel deletes a key from a hashmap.
+	HDel(hashmap string, keys []string) error
+	// HGetAll gets all key-value pairs of a hashmap.
+	HGetAll(hashmap string) (map[string]string, error)
+	// HKeys gets all keys of a hashmap.
+	HKeys(hashmap string) ([]string, error)
+	// HLen gets the length of a hashmap.
+	HLen(hashmap string) (int, error)
+	// Iter iterates through stuff in the database.
+	Iter(prefetch, includeOffset bool, offset, prefix string, handler func(key, value string) bool) error
+}
+
+var Database = badger.Database
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..37424d4f076d0c4dbb07fd6904b2b9bdfc7ac97f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,23 @@
+module git.randomchars.net/levatax/leva/school-assistant/server
+
+go 1.17
+
+require github.com/dgraph-io/badger/v3 v3.2103.2
+
+require (
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/gin-gonic/gin v1.7.4 // indirect
+	github.com/go-playground/locales v0.13.0 // indirect
+	github.com/go-playground/universal-translator v0.17.0 // indirect
+	github.com/go-playground/validator/v10 v10.4.1 // indirect
+	github.com/golang/protobuf v1.3.3 // indirect
+	github.com/json-iterator/go v1.1.9 // indirect
+	github.com/leodido/go-urn v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.12 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
+	github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
+	github.com/ugorji/go/codec v1.1.7 // indirect
+	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
+	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
+	gopkg.in/yaml.v2 v2.2.8 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..6eccfaea7623d09b9ad4d2db758b994540648b7f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,164 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
+github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
+github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
+github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
+github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
+github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
+github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..0a74783f0a7c094c165e0a6ecc5d6f4ca34bed5a
--- /dev/null
+++ b/main.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
+)
+
+var executable string
+
+func init() {
+	var err error
+	executable, err = os.Executable()
+	if err != nil {
+		log.Printf("error getting executable path: %s\n restart is a no-op", err)
+	}
+}
+
+func main(){
+	// Parse flags
+	flag.Parse()
+
+	defer func() {
+		// Restart if needed
+		if reExec {
+			restart()
+		}
+	}()
+
+	// Setup configuration stuff
+	configSetup()
+	// TODO: store setup
+
+	// Web stuff
+	webSetup()
+	registerWebpage()
+	registerAPI()
+	runServer()
+
+	signalChannel := make(chan os.Signal, 1)
+	signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, os.Interrupt, os.Kill)
+	go func() {
+		// Cleanup on function return
+		defer func() { cleanup(false) }()
+		for {
+			currentSignal := <-signalChannel
+			switch currentSignal {
+			case os.Interrupt:
+				println()
+				return
+			default:
+				return
+			}
+		}
+	}()
+}
diff --git a/recover.go b/recover.go
new file mode 100644
index 0000000000000000000000000000000000000000..66d09569e5c9f16549e83a145e77b5bd6799b7a8
--- /dev/null
+++ b/recover.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"log"
+	"net/http"
+	"runtime/debug"
+)
+
+func recovery() gin.HandlerFunc {
+	return func(context *gin.Context) {
+		defer func() {
+			p := recover()
+			if p != nil {
+				log.Printf("panic in web server: %s", p)
+				context.JSON(http.StatusInternalServerError, gin.H{
+					"error": "panic",
+				})
+				fmt.Println(string(debug.Stack()))
+			}
+		}()
+		context.Next()
+	}
+}
diff --git a/restart.go b/restart.go
new file mode 100644
index 0000000000000000000000000000000000000000..930d536343a1d111b3f036daedf839c8b2edb594
--- /dev/null
+++ b/restart.go
@@ -0,0 +1,24 @@
+// +build !windows
+
+package main
+
+import (
+	"log"
+	"os"
+	"syscall"
+)
+
+func restart() {
+	if executable == "" {
+		return
+	}
+
+	if _, err := os.Stat(executable); err != nil {
+		log.Fatalf("stat: %s", err)
+	}
+
+	log.Printf("exec %s", executable)
+	if err := syscall.Exec(executable, os.Args, os.Environ()); err != nil {
+		log.Fatalf("error exec: %s", err)
+	}
+}
diff --git a/restart_windows.go b/restart_windows.go
new file mode 100644
index 0000000000000000000000000000000000000000..8464723e3cb459e831158679b251e18d58748027
--- /dev/null
+++ b/restart_windows.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"log"
+	"os"
+)
+
+func restart() {
+	if _, err := os.Stat(executable); err != nil {
+		log.Fatalf("Error getting executable path, %s", err)
+	}
+	log.Printf("Program found at %s.", executable)
+	wd, err := os.Getwd()
+	if err != nil {
+		log.Fatalf("Error getting working directory, %s", err)
+	}
+	log.Printf("Current working directory is %s.", wd)
+	_, err = os.StartProcess(executable, []string{}, &os.ProcAttr{
+		Dir:   wd,
+		Env:   nil,
+		Files: []*os.File{os.Stderr, os.Stdin, os.Stdout},
+		Sys:   nil,
+	})
+	if err != nil {
+		log.Fatalf("Error creating new process, %s", err)
+	}
+}
diff --git a/web.go b/web.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2eee2d057e11999a1a011ed5eac06837acf7b5c
--- /dev/null
+++ b/web.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+	"embed"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"io/fs"
+	"log"
+	"net"
+	"net/http"
+	"os"
+)
+
+//go:embed assets
+var assets embed.FS
+
+var (
+	server   = http.Server{}
+	listener net.Listener
+	router   *gin.Engine
+)
+
+func webSetup() {
+	router = gin.New()
+	router.Use(recovery())
+	router.ForwardedByClientIP = config.Web.Proxy
+
+	if config.Web.Log {
+		router.Use(gin.Logger())
+	}
+
+	router.NoRoute(func(context *gin.Context) {
+		context.Redirect(http.StatusTemporaryRedirect, "/web")
+	})
+
+	if config.Web.Unix {
+		if l, err := net.Listen("unix", config.Web.Host); err != nil {
+			log.Fatalf("error binding to %s: %s", config.Web.Host, err)
+		} else {
+			listener = l
+		}
+		log.Printf("listening on socket %s", config.Web.Host)
+
+		if err := os.Chmod(config.Web.Host, 0777); err != nil {
+			log.Printf("error chmod %s: %s", config.Web.Host, err)
+		}
+	} else {
+		address := fmt.Sprintf("%s:%v", config.Web.Host, config.Web.Port)
+		if l, err := net.Listen("tcp", address); err != nil {
+			log.Fatalf("error binding to %s: %s", address, err)
+		} else {
+			listener = l
+		}
+		log.Printf("listening on %s", address)
+	}
+
+	server.Handler = router
+}
+
+func registerWebpage() {
+	router.GET("/", func(context *gin.Context) {
+		context.Redirect(http.StatusTemporaryRedirect, "web")
+	})
+
+	if stat, err := os.Stat("assets/public"); err == nil && stat.IsDir() {
+		log.Println("serving assets from filesystem")
+		router.Static("/web", "assets/public")
+	} else {
+		log.Println("serving bundle assets")
+		var public fs.FS
+		if public, err = fs.Sub(assets, "assets/public"); err != nil {
+			log.Fatalf("error getting subtree: %s", err)
+		}
+		router.StaticFS("/web", http.FS(public))
+	}
+}
+
+func runServer() {
+	if err := server.Serve(listener); err != nil {
+		if err == http.ErrServerClosed {
+			log.Println("server closed")
+		} else {
+			log.Fatalf("error serve: %s", err)
+		}
+	}
+}