Repository: eduvpn/eduvpn-common (language: python)

Issue:
Client implementation accross files
As far as I can see, `Client` structure is implemented in https://github.com/eduvpn/eduvpn-common/blob/main/client/client.go . Many of its receiver functions (methods) are also implemented there. But for some reason there are few receiver functions implemented in https://github.com/eduvpn/eduvpn-common/blob/main/client/fsm.go . For example:

https://github.com/eduvpn/eduvpn-common/blob/6229767a7d5b0d3cf3125f2e9da0ca0e66ada814/client/fsm.go#L255

It is a bad practice to define receiver functions of the same type in multiple files. Is there any reason for such implementation?
Persist internal state regularly
Unregistering engine saves the internal state (user settings). Unregister is called when client is terminating. However, client might be terminated by force. We had this problem with Windows client: manually installing newer version of the client kills all running client processes and user changes are lost.

If eduvpn-common doesn't implement in-between saves, we will experience this issue again.

In .NET this was fixed by introducing a timer which is reset to 5 seconds on each settings change. When timer triggers, settings are saved.

See: https://github.com/Amebis/eduVPN/issues/222



Discussion / hints:
It's because of the fact that otherwise the file becomes a bit too big to deal with. I had it in one file before (e.g. https://github.com/eduvpn/eduvpn-common/blob/242903aa810797102b14e27dda988fff7ab833cc/client.go). Maybe right now it won't be so bad (~1k lines), but it becomes harder to also group the relevant functionality together.

Maybe the best way forward is that I try to split some of these functions into a new struct (so they don't take the client as receiver). Do you have any idea? I hate the fact that e.g. `SetSearchServer` even takes a client in the first place.


Fix patch:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index f7dc4a0f..ea3dc38c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -20,10 +20,18 @@ jobs:
         # Verify hash
         echo "d276b0e594d5980de6a7917ce74c348f28d3cb8b353ca4eaae344ae8a4c40bea  mdbook.tar.gz" | sha256sum --check
 
+        # Get mdbook-mermaid
+        curl -sSL https://github.com/badboy/mdbook-mermaid/releases/download/v0.12.6/mdbook-mermaid-v0.12.6-x86_64-unknown-linux-gnu.tar.gz -o mdbook-mermaid.tar.gz
+        # Verify hash
+        echo "04f9fa41f89b7a1809ef2525efc7ad2c112df4ae1241de25efe354940441ea70  mdbook-mermaid.tar.gz" | sha256sum --check
+
         # Extract
         mkdir mdbook
         tar -xzf mdbook.tar.gz --directory mdbook
+        mkdir mdbook-mermaid
+        tar -xzf mdbook-mermaid.tar.gz --directory mdbook-mermaid
         echo `pwd`/mdbook >> $GITHUB_PATH
+        echo `pwd`/mdbook-mermaid >> $GITHUB_PATH
     - name: Install sphinx
       run: |
         # Install python dependencies
@@ -53,8 +61,13 @@ jobs:
         cd wrappers/python/docs
         make html
 
-        # Go to docs directory & build mdbook
-        cd ../../../docs
+        # Go to root
+        cd ../../..
+
+        # Install mermaid preprocessor
+        mdbook-mermaid install docs
+
+        cd docs
         mdbook build
 
         # gh pages branch settings
@@ -71,7 +84,7 @@ jobs:
         mv ../book/* .
 
         # move sphinx files
-        mkdir api/python/rtd
+        mkdir -p api/python/rtd
         mv ../../wrappers/python/docs/build/html/* api/python/rtd
 
         # Disable jekyll as otherwise it won't find our sphinx paths
diff --git a/.gitignore b/.gitignore
index 2ce17499..6ce27166 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,9 @@
 *.iml
 .idea/
 .DS_Store
+
+# locales
+/client/locales/*/out.gotext.json
+/docs/book/
+/cmd/cli/configs/
+/ci/docker/selfsigned/
diff --git a/Makefile b/Makefile
index 402ce568..0891de2d 100644
--- a/Makefile
+++ b/Makefile
@@ -35,9 +35,9 @@ srpm:
 	rm -rf dist/*
 
 # Archive code with vendored dependencies
-	git clone . dist/libeduvpn-common-1.1.2
+	git clone . dist/libeduvpn-common-2.0.0
 	go mod vendor
-	cp -r vendor dist/libeduvpn-common-1.1.2/vendor
+	cp -r vendor dist/libeduvpn-common-2.0.0/vendor
 	tar -zcvf ~/rpmbuild/SOURCES/libeduvpn-common.tar.gz -C dist .
 
 # Cleanup
diff --git a/README.md b/README.md
index f0607d84..3106c7ad 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,10 @@ The only support language is Python at the moment. Other languages will come wit
 The documentation for this library can be found at [GitHub pages](https://eduvpn.github.io/eduvpn-common).
 
 ## Contributing
-Contributions are welcome.
+Contributions are welcome. Helping with translations can be done on weblate:
+<a href="https://hosted.weblate.org/engage/eduvpn-common/">
+<img src="https://hosted.weblate.org/widget/eduvpn-common/eduvpn-common/multi-auto.svg" alt="Translation status" />
+</a>
 
 ## License
 [MIT](./LICENSE)
diff --git a/ci/docker/eduvpnserver.docker b/ci/docker/eduvpnserver.docker
index fd9891da..d615918e 100644
--- a/ci/docker/eduvpnserver.docker
+++ b/ci/docker/eduvpnserver.docker
@@ -1,6 +1,6 @@
-FROM fedora:36
+FROM fedora:38
 
-# This dockerfile was adapted from https://github.com/eduvpn/documentation/blob/v3/deploy_fedora.sh
+# This dockerfile was adapted from https://codeberg.og/eduvpn/deploy/src/branch/v3/deploy_fedora.sh
 # DO NOT USE THIS IN PRODUCTION, ONLY FOR TESTING
 
 # Install git to clone the documentation repo
@@ -8,10 +8,10 @@ RUN dnf -y install git
 
 WORKDIR /eduvpn
 
-# Clone the documentation
-RUN git clone https://github.com/eduvpn/documentation
+# Clone the deploy
+RUN git clone https://codeberg.org/eduvpn/deploy
 
-WORKDIR /eduvpn/documentation
+WORKDIR /eduvpn/deploy
 
 # Checkout v3 branch
 RUN git checkout v3
diff --git a/client/client.go b/client/client.go
index edd476e0..c54dd0ed 100644
--- a/client/client.go
+++ b/client/client.go
@@ -1,10 +1,15 @@
+//go:generate go run golang.org/x/text/cmd/gotext -srclang=en update -out=zgotext.go -lang=da,de,en,es,fr,it,nl,sl,ukr
+
 // Package client implements the public interface for creating eduVPN/Let's Connect! clients
 package client
 
 import (
-	"fmt"
+	"context"
 	"strings"
+	"sync"
+	"time"
 
+	"github.com/eduvpn/eduvpn-common/i18nerr"
 	"github.com/eduvpn/eduvpn-common/internal/config"
 	"github.com/eduvpn/eduvpn-common/internal/discovery"
 	"github.com/eduvpn/eduvpn-common/internal/failover"
@@ -13,31 +18,12 @@ import (
 	"github.com/eduvpn/eduvpn-common/internal/log"
 	"github.com/eduvpn/eduvpn-common/internal/oauth"
 	"github.com/eduvpn/eduvpn-common/internal/server"
-	"github.com/eduvpn/eduvpn-common/internal/util"
-	"github.com/eduvpn/eduvpn-common/types"
+	"github.com/eduvpn/eduvpn-common/types/cookie"
+	discotypes "github.com/eduvpn/eduvpn-common/types/discovery"
+	srvtypes "github.com/eduvpn/eduvpn-common/types/server"
 	"github.com/go-errors/errors"
 )
 
-type (
-	// ServerBase is an alias to the internal ServerBase
-	// This contains the details for each server.
-	ServerBase = server.Base
-)
-
-func (c *Client) logError(err error) {
-	// Logs the error with the same level/verbosity as the error
-	if c.Debug {
-		log.Logger.Inherit(err, fmt.Sprintf("\nwith stacktrace: %s\n", err.(*errors.Error).ErrorStack()))
-	} else {
-		log.Logger.Inherit(err, "")
-	}
-}
-
-func (c *Client) isLetsConnect() bool {
-	// see https://git.sr.ht/~fkooman/vpn-user-portal/tree/v3/item/src/OAuth/VpnClientDb.php
-	return strings.HasPrefix(c.Name, "org.letsconnect-vpn.app")
-}
-
 // isAllowedClientID checks if the 'clientID' is in the list of allowed client IDs
 func isAllowedClientID(clientID string) bool {
 	allowList := []string{
@@ -89,16 +75,18 @@ func userAgentName(clientID string) string {
 	}
 }
 
+func (c *Client) isLetsConnect() bool {
+	// see https://git.sr.ht/~fkooman/vpn-user-portal/tree/v3/item/src/OAuth/VpnClientDb.php
+	return strings.HasPrefix(c.Name, "org.letsconnect-vpn.app")
+}
+
 // Client is the main struct for the VPN client.
 type Client struct {
 	// The name of the client
 	Name string `json:"-"`
 
-	// The language used for language matching
-	Language string `json:"-"` // language should not be saved
-
 	// The chosen server
-	Servers server.Servers `json:"servers"`
+	Servers server.List `json:"servers"`
 
 	// The list of servers and organizations from disco
 	Discovery discovery.Discovery `json:"discovery"`
@@ -115,74 +103,91 @@ type Client struct {
 	// Whether to enable debugging
 	Debug bool `json:"-"`
 
-	// The Failover monitor for the current VPN connection
-	Failover *failover.DroppedConMon `json:"-"`
+	// TokenSetter sets the tokens in the client
+	TokenSetter func(srv srvtypes.Current, tok srvtypes.Tokens) `json:"-"`
+
+	// TokenGetter gets the tokens from the client
+	TokenGetter func(srv srvtypes.Current) *srvtypes.Tokens `json:"-"`
 
-	// tokenCB is the token callback
-	tokenCB func(srv server.Server, tok oauth.Token)
+	mu sync.Mutex
 }
 
-func (c *Client) ForwardTokenUpdate(srv server.Server) {
-	if c.tokenCB == nil {
-		log.Logger.Debugf("No token update callback available")
-		return
+func (c *Client) updateTokens(srv server.Server) error {
+	if c.TokenGetter == nil {
+		return errors.New("no token getter defined")
+	}
+	pSrv, err := c.pubCurrentServer(srv)
+	if err != nil {
+		return err
+	}
+	// shouldn't happen
+	if pSrv == nil {
+		return errors.New("public server is nil when getting tokens")
+	}
+	tokens := c.TokenGetter(*pSrv)
+	if tokens == nil {
+		return errors.New("client returned nil for tokens")
+	}
+
+	server.UpdateTokens(srv, oauth.Token{
+		Access:           tokens.Access,
+		Refresh:          tokens.Refresh,
+		ExpiredTimestamp: time.Unix(tokens.Expires, 0),
+	})
+
+	return nil
+}
+
+func (c *Client) forwardTokens(srv server.Server) error {
+	if c.TokenSetter == nil {
+		return errors.New("no token setter defined")
+	}
+	pSrv, err := c.pubCurrentServer(srv)
+	if err != nil {
+		return err
+	}
+	if pSrv == nil {
+		return errors.New("public server is nil when updating tokens")
 	}
-	t := oauth.Token{}
 	o := srv.OAuth()
-	if o != nil {
-		t = o.Token()
-	} else {
-		log.Logger.Debugf("OAuth was nil when forwarding token callback")
+	if o == nil {
+		return errors.New("oauth was nil when forwarding tokens")
 	}
-	log.Logger.Debugf("Running token callback")
-	c.tokenCB(srv, t)
+	t := o.Token()
+	c.TokenSetter(*pSrv, t.Public())
+	return nil
 }
 
-// SetTokenUpdater sets the token updater callback
-func (c *Client) SetTokenUpdater(updater func(srv server.Server, tok oauth.Token)) {
-	c.tokenCB = updater
+func (c *Client) goTransition(id fsm.StateID) error {
+	handled, err := c.FSM.GoTransition(id)
+	if err != nil {
+		return i18nerr.WrapInternal(err, "state transition error")
+	}
+	if !handled {
+		log.Logger.Debugf("transition not handled by the client to internal state: '%s'", GetStateName(id))
+	}
+	return nil
 }
 
-// Register initializes the clientwith the following parameters:
+// New creates a new client with the following parameters:
 //   - name: the name of the client
 //   - directory: the directory where the config files are stored. Absolute or relative
 //   - stateCallback: the callback function for the FSM that takes two states (old and new) and the data as an interface
 //   - debug: whether or not we want to enable debugging
 //
 // It returns an error if initialization failed, for example when discovery cannot be obtained and when there are no servers.
-func (c *Client) Register(
-	name string,
-	version string,
-	directory string,
-	language string,
-	stateCallback func(FSMStateID, FSMStateID, interface{}) bool,
-	debug bool,
-) (err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	if !c.InFSMState(StateDeregistered) {
-		return errors.Errorf("fsm attempt to register while in '%v'", c.FSM.Current)
-	}
+func New(name string, version string, directory string, stateCallback func(FSMStateID, FSMStateID, interface{}) bool, debug bool) (c *Client, err error) {
+	// We create the client by filling fields one by one
+	c = &Client{}
 
 	if !isAllowedClientID(name) {
-		return errors.Errorf("client ID is not allowed: '%v', see https://git.sr.ht/~fkooman/vpn-user-portal/tree/v3/item/src/OAuth/VpnClientDb.php for a list of allowed IDs", name)
+		return nil, i18nerr.NewInternalf("The client registered with an invalid client ID: '%v'", name)
 	}
 
 	if len([]rune(version)) > 20 {
-		return errors.Errorf("version is not allowed: '%s', must be max 20 characters", version)
+		return nil, i18nerr.NewInternalf("The client registered with an invalid version: '%v'", version)
 	}
 
-	http.RegisterAgent(userAgentName(name), version)
-
-	c.Name = name
-
-	// TODO: Verify language setting?
-	c.Language = language
-
 	// Initialize the logger
 	lvl := log.LevelWarning
 	if debug {
@@ -190,9 +195,15 @@ func (c *Client) Register(
 	}
 
 	if err = log.Logger.Init(lvl, directory); err != nil {
-		return err
+		return nil, i18nerr.Wrapf(err, "The log file with directory: '%s' failed to initialize", directory)
 	}
 
+	// set client name
+	c.Name = name
+
+	// register HTTP agent
+	http.RegisterAgent(userAgentName(name), version)
+
 	// Initialize the FSM
 	c.FSM = newFSM(stateCallback, directory, debug)
 
@@ -211,63 +222,53 @@ func (c *Client) Register(
 		log.Logger.Infof("Previous configuration not found")
 	}
 
-	// Go to the No Server state with the saved servers after we're done
-	defer c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
+	return c, nil
+}
 
-	// Let's Connect! doesn't care about discovery
-	if c.isLetsConnect() {
-		return nil
+// Registering means updating the FSM to get to the initial state correctly
+func (c *Client) Register() error {
+	if !c.FSM.InState(StateDeregistered) {
+		return i18nerr.NewInternal("The client tried to re-initialize without deregistering first")
 	}
-
-	// Check if we are able to fetch discovery, and log if something went wrong
-	if _, err := c.DiscoServers(); err != nil {
-		log.Logger.Warningf("Failed to get discovery servers: %v", err)
+	err := c.goTransition(StateNoServer)
+	if err != nil {
+		return err
 	}
+	return nil
+}
 
-	if _, err := c.DiscoOrganizations(); err != nil {
-		log.Logger.Warningf("Failed to get discovery organizations: %v", err)
+// SaveState saves the internal state to the config
+func (c *Client) SaveState() {
+	log.Logger.Debugf("saving state configuration....")
+	// Save the config
+	if err := c.Config.Save(&c); err != nil {
+		log.Logger.Infof("failed saving state configuration: '%v'", err)
 	}
-
-	return nil
 }
 
 // Deregister 'deregisters' the client, meaning saving the log file and the config and emptying out the client struct.
 func (c *Client) Deregister() {
+	// First of all let's transition the state machine
+	_ = c.goTransition(StateDeregistered)
+
+	// SaveState saves the configuration
+	c.SaveState()
+
 	// Close the log file
 	_ = log.Logger.Close()
 
-	// Save the config
-	if err := c.Config.Save(&c); err != nil {
-		log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s", err.Error(), err.(*errors.Error).ErrorStack())
-	}
-
 	// Empty out the state
 	*c = Client{}
 }
 
-// askProfile asks the user for a profile by moving the FSM to the ASK_PROFILE state.
-func (c *Client) askProfile(srv server.Server) error {
-	ps, err := server.ValidProfiles(srv, c.SupportsWireguard)
-	if err != nil {
-		return err
-	}
-	return c.FSM.GoTransitionRequired(StateAskProfile, ps)
-}
-
 // DiscoOrganizations gets the organizations list from the discovery server
 // If the list cannot be retrieved an error is returned.
 // If this is the case then a previous version of the list is returned if there is any.
 // This takes into account the frequency of updates, see: https://github.com/eduvpn/documentation/blob/v3/SERVER_DISCOVERY.md#organization-list.
-func (c *Client) DiscoOrganizations() (orgs *types.DiscoveryOrganizations, err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
+func (c *Client) DiscoOrganizations(ck *cookie.Cookie) (orgs *discotypes.Organizations, err error) {
 	// Not supported with Let's Connect!
 	if c.isLetsConnect() {
-		return nil, errors.Errorf("discovery with Let's Connect is not supported")
+		return nil, i18nerr.NewInternal("Server/organization discovery with Let's Connect is not supported")
 	}
 
 	// Mark organizations as expired if we have not set an organization yet
@@ -275,29 +276,681 @@ func (c *Client) DiscoOrganizations() (orgs *types.DiscoveryOrganizations, err e
 		c.Discovery.MarkOrganizationsExpired()
 	}
 
-	return c.Discovery.Organizations()
+	orgs, err = c.Discovery.Organizations(ck.Context())
+	if err != nil {
+		err = i18nerr.Wrap(err, "An error occurred after getting the discovery files for the list of organizations")
+	}
+	return
 }
 
 // DiscoServers gets the servers list from the discovery server
 // If the list cannot be retrieved an error is returned.
 // If this is the case then a previous version of the list is returned if there is any.
 // This takes into account the frequency of updates, see: https://github.com/eduvpn/documentation/blob/v3/SERVER_DISCOVERY.md#server-list.
-func (c *Client) DiscoServers() (dss *types.DiscoveryServers, err error) {
+func (c *Client) DiscoServers(ck *cookie.Cookie) (dss *discotypes.Servers, err error) {
+	// Not supported with Let's Connect!
+	if c.isLetsConnect() {
+		return nil, i18nerr.NewInternal("Server/organization discovery with Let's Connect is not supported")
+	}
+
+	dss, err = c.Discovery.Servers(ck.Context())
+	if err != nil {
+		err = i18nerr.Wrap(err, "An error occurred after getting the discovery files for the list of servers")
+	}
+	return
+}
+
+// ExpiryTimes returns the different Unix timestamps regarding expiry
+// - The time starting at which the renew button should be shown, after 30 minutes and less than 24 hours
+// - The time starting at which the countdown button should be shown, less than 24 hours
+// - The list of times where notifications should be shown
+// These times are reset when the VPN gets disconnected
+func (c *Client) ExpiryTimes() (*srvtypes.Expiry, error) {
+	// Get current expiry time
+	srv, err := c.Servers.Current()
+	if err != nil {
+		return nil, i18nerr.Wrap(err, "The current server could not be found when getting it for expiry")
+	}
+	b, err := srv.Base()
+	if err != nil {
+		return nil, err
+	}
+
+	if b.StartTime.IsZero() {
+		return nil, i18nerr.New("No start time is defined for this server")
+	}
+
+	bT := b.RenewButtonTime()
+	cT := b.CountdownTime()
+	nT := b.NotificationTimes()
+	return &srvtypes.Expiry{
+		StartTime:         b.StartTime.Unix(),
+		EndTime:           b.EndTime.Unix(),
+		ButtonTime:        bT,
+		CountdownTime:     cT,
+		NotificationTimes: nT,
+	}, nil
+}
+
+func (c *Client) locationCallback(ck *cookie.Cookie) error {
+	locs := c.Discovery.SecureLocationList()
+	errChan := make(chan error)
+	go func() {
+		err := c.FSM.GoTransitionRequired(StateAskLocation, &srvtypes.RequiredAskTransition{
+			C:    ck,
+			Data: locs,
+		})
+		if err != nil {
+			errChan <- err
+		}
+	}()
+	loc, err := ck.Receive(errChan)
+	if err != nil {
+		return err
+	}
+	err = c.SetSecureLocation(ck, loc)
+	if err != nil {
+		return err
+	}
+	err = c.goTransition(StateChosenLocation)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Client) loginCallback(ck *cookie.Cookie, srv server.Server) error {
+	url, err := server.OAuthURL(srv, c.Name)
+	if err != nil {
+		return err
+	}
+	err = c.FSM.GoTransitionRequired(StateOAuthStarted, url)
+	if err != nil {
+		return err
+	}
+	err = server.OAuthExchange(ck.Context(), srv)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Client) callbacks(ck *cookie.Cookie, srv server.Server, forceauth bool, startup bool) error {
+	// location
+	if srv.NeedsLocation() {
+		if startup {
+			return i18nerr.Newf("The client tried to autoconnect to the VPN server: %s, but no secure internet location is found. Please manually connect again", server.Name(srv))
+		}
+		err := c.locationCallback(ck)
+		if err != nil {
+			return i18nerr.Wrap(err, "The secure internet location could not be set")
+		}
+	}
+
+	err := c.goTransition(StateChosenServer)
+	if err != nil {
+		log.Logger.Debugf("optional chosen server transition not possible: %v", err)
+	}
+	// oauth
+	// TODO: This should be ck.Context()
+	// But needsrelogin needs a rewrite to support this properly
+
+	// first make sure we get the most up to date tokens from the client
+	err = c.updateTokens(srv)
+	if err != nil {
+		log.Logger.Debugf("failed to get tokens from client: %v", err)
+	}
+	if server.NeedsRelogin(context.Background(), srv) || forceauth {
+		if startup {
+			return i18nerr.Newf("The client tried to autoconnect to the VPN server: %s, but you need to authorizate again. Please manually connect again", server.Name(srv))
+		}
+		// mark organizations as expired if the server is a secure internet server
+		b, berr := srv.Base()
+		if berr == nil && b.Type == srvtypes.TypeSecureInternet {
+			c.Discovery.MarkOrganizationsExpired()
+		}
+		err := c.loginCallback(ck, srv)
+		if err != nil {
+			return i18nerr.Wrap(err, "The authorization procedure failed to complete")
+		}
+	}
+	err = c.goTransition(StateAuthorized)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) profileCallback(ck *cookie.Cookie, srv server.Server, startup bool) error {
+	vp, err := server.HasValidProfile(ck.Context(), srv, c.SupportsWireguard)
+	if err != nil {
+		log.Logger.Warningf("failed to determine whether the current protocol is valid with error: %v", err)
+		return err
+	}
+	if !vp {
+		if startup {
+			return i18nerr.Newf("The client tried to autoconnect to the VPN server: %s, but no valid profiles were found. Please manually connect again", server.Name(srv))
+		}
+		vps, err := server.ValidProfiles(srv, c.SupportsWireguard)
+		if err != nil {
+			return i18nerr.Wrapf(err, "No suitable profiles could be found")
+		}
+		errChan := make(chan error)
+		go func() {
+			err := c.FSM.GoTransitionRequired(StateAskProfile, &srvtypes.RequiredAskTransition{
+				C:    ck,
+				Data: vps.Public(),
+			})
+			if err != nil {
+				errChan <- err
+			}
+		}()
+		pID, err := ck.Receive(errChan)
+		if err != nil {
+			return i18nerr.Wrapf(err, "Profile with ID: '%s' could not be set", pID)
+		}
+		err = server.Profile(srv, pID)
+		if err != nil {
+			return i18nerr.Wrapf(err, "Profile with ID: '%s' could not be obtained from the server", pID)
+		}
+	}
+	err = c.goTransition(StateChosenProfile)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// AddServer adds a server with identifier and type
+func (c *Client) AddServer(ck *cookie.Cookie, identifier string, _type srvtypes.Type, ni bool) (err error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	// If we have failed to add the server, we remove it again
+	// We add the server because we can then obtain it in other callback functions
+	previousState := c.FSM.Current
 	defer func() {
 		if err != nil {
-			c.logError(err)
+			_ = c.RemoveServer(identifier, _type) //nolint:errcheck
+		} else {
+			c.SaveState()
+		}
+		// If we must run callbacks, go to the previous state if we're not in it
+		if !ni && !c.FSM.InState(previousState) {
+			c.FSM.GoTransition(previousState) //nolint:errcheck
 		}
 	}()
 
-	// Not supported with Let's Connect!
+	if !ni {
+		err = c.goTransition(StateLoadingServer)
+		// this is already wrapped in an UI error
+		if err != nil {
+			return err
+		}
+	}
+
+	if _type != srvtypes.TypeSecureInternet {
+		identifier, err = http.EnsureValidURL(identifier, true)
+		if err != nil {
+			return i18nerr.Wrap(err, "The identifier that was passed to the library is incorrect")
+		}
+	}
+
+	var srv server.Server
+
+	switch _type {
+	case srvtypes.TypeInstituteAccess:
+		dSrv, err := c.Discovery.ServerByURL(identifier, "institute_access")
+		if err != nil {
+			return i18nerr.Wrapf(err, "Could not retrieve institute access server with URL: '%s' from discovery", identifier)
+		}
+		srv, err = c.Servers.AddInstituteAccess(ck.Context(), c.Name ,dSrv)
+		if err != nil {
+			return i18nerr.Wrapf(err, "The institute access server with URL: '%s' could not be added", identifier)
+		}
+	case srvtypes.TypeSecureInternet:
+		dOrg, dSrv, err := c.Discovery.SecureHomeArgs(identifier)
+		if err != nil {
+			// We mark the organizations as expired because we got an error
+			// Note that in the docs it states that it only should happen when the Org ID doesn't exist
+			// However, this is nice as well because it also catches the error where the SecureInternetHome server is not found
+			c.Discovery.MarkOrganizationsExpired()
+			return i18nerr.Wrapf(err, "The secure internet server with organisation ID: '%s' could not be retrieved from discovery", identifier)
+		}
+		srv, err = c.Servers.AddSecureInternet(ck.Context(), c.Name, dOrg, dSrv)
+		if err != nil {
+			return i18nerr.Wrapf(err, "The secure internet server with organisation ID: '%s' could not be added", identifier)
+		}
+	case srvtypes.TypeCustom:
+		srv, err = c.Servers.AddCustom(ck.Context(), c.Name, identifier)
+		if err != nil {
+			return i18nerr.Wrapf(err, "The custom server with URL: '%s' could not be added", identifier)
+		}
+	default:
+		return i18nerr.NewInternalf("Server type: '%v' is not valid to be added", _type)
+	}
+
+	// if we are non interactive, we run no callbacks
+	if ni {
+		return nil
+	}
+
+	// callbacks
+	err = c.callbacks(ck, srv, false, false)
+	// error is already UI wrapped
+	if err != nil {
+		return err
+	}
+	terr := c.forwardTokens(srv)
+	if terr != nil {
+		log.Logger.Debugf("failed to forward tokens after adding: %v", terr)
+	}
+	return nil
+}
+
+func (c *Client) config(ck *cookie.Cookie, srv server.Server, pTCP bool, forceAuth bool, startup bool) (cfg *srvtypes.Configuration, err error) {
+	// do the callbacks to ensure valid profile, location and authorization
+	err = c.callbacks(ck, srv, forceAuth, startup)
+	if err != nil {
+		return nil, err
+	}
+
+	err = c.goTransition(StateRequestConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	err = c.profileCallback(ck, srv, startup)
+	if err != nil {
+		return nil, err
+	}
+
+	cfgS, err := server.Config(ck.Context(), srv, c.SupportsWireguard, pTCP)
+	if err != nil {
+		return nil, i18nerr.Wrap(err, "The VPN configuration could not be obtained")
+	}
+	p, err := server.CurrentProfile(srv)
+	if err != nil {
+		return nil, i18nerr.Wrap(err, "The current profile could not be found")
+	}
+	pcfg := cfgS.Public(p.DefaultGateway)
+	return &pcfg, nil
+}
+
+func (c *Client) server(identifier string, _type srvtypes.Type) (srv server.Server, setter func(server.Server) error, err error) {
+	switch _type {
+	case srvtypes.TypeInstituteAccess:
+		srv, err = c.Servers.InstituteAccess(identifier)
+		setter = c.Servers.SetInstituteAccess
+	case srvtypes.TypeSecureInternet:
+		srv, err = c.Servers.SecureInternet(identifier)
+		setter = c.Servers.SetSecureInternet
+	case srvtypes.TypeCustom:
+		srv, err = c.Servers.CustomServer(identifier)
+		setter = c.Servers.SetCustom
+	default:
+		return nil, nil, i18nerr.NewInternalf("Not a valid server type: %v", _type)
+	}
+	return srv, setter, err
+}
+
+// GetConfig gets a VPN configuration
+func (c *Client) GetConfig(ck *cookie.Cookie, identifier string, _type srvtypes.Type, pTCP bool, startup bool) (cfg *srvtypes.Configuration, err error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	previousState := c.FSM.Current
+	defer func() {
+		if err == nil {
+			c.FSM.GoTransition(StateGotConfig) //nolint:errcheck
+			c.SaveState()
+		} else if !c.FSM.InState(previousState) {
+			// go back to the previous state if an error occurred
+			c.FSM.GoTransition(previousState) //nolint:errcheck
+		}
+	}()
+	if _type != srvtypes.TypeSecureInternet {
+		identifier, err = http.EnsureValidURL(identifier, true)
+		if err != nil {
+			return nil, i18nerr.Wrapf(err, "Identifier: '%s' for server with type: '%d' is not valid", identifier, _type)
+		}
+	}
+	err = c.goTransition(StateLoadingServer)
+	if err != nil {
+		return nil, err
+	}
+	srv, set, err := c.server(identifier, _type)
+	if err != nil {
+		return nil, err
+	}
+	// refresh the server endpoints
+	err = srv.RefreshEndpoints(ck.Context(), &c.Discovery)
+
+	// If we get a canceled error, return that, otherwise just log the error
+	if err != nil {
+		if errors.Is(err, context.Canceled) {
+			return nil, i18nerr.Wrap(err, "The operation for getting a VPN configuration was canceled")
+		}
+
+		log.Logger.Warningf("failed to refresh server endpoints: %v", err)
+	}
+
+	// get a config and retry with authorization if expired
+	cfg, err = c.config(ck, srv, pTCP, false, startup)
+	tErr := &oauth.TokensInvalidError{}
+	if err != nil && errors.As(err, &tErr) {
+		log.Logger.Debugf("the tokens were invalid, trying again...")
+		cfg, err = c.config(ck, srv, pTCP, true, startup)
+	}
+
+	// tokens might be updated, forward them
+	defer func() {
+		terr := c.forwardTokens(srv)
+		if terr != nil {
+			log.Logger.Debugf("failed to forward tokens after get config: %v", terr)
+		}
+	}()
+
+	// still an error, return nil with the error
+	if err != nil {
+		return nil, err
+	}
+
+	// set the current server
+	if err = set(srv); err != nil {
+		return nil, i18nerr.Wrapf(err, "Failed to set the server with identifier: '%s' as the current", identifier)
+	}
+
+	return cfg, nil
+}
+
+func (c *Client) RemoveServer(identifier string, _type srvtypes.Type) (err error) {
+	if _type != srvtypes.TypeSecureInternet {
+		identifier, err = http.EnsureValidURL(identifier, true)
+		if err != nil {
+			return i18nerr.Wrapf(err, "Identifier: '%s' for server with type: '%d' is not valid for removal", identifier, _type)
+		}
+	}
+	// miscellaneous error
+	var mErr error
+	switch _type {
+	case srvtypes.TypeInstituteAccess:
+		mErr = c.Servers.RemoveInstituteAccess(identifier)
+	case srvtypes.TypeSecureInternet:
+		mErr = c.Servers.RemoveSecureInternet(identifier)
+	case srvtypes.TypeCustom:
+		mErr = c.Servers.RemoveCustom(identifier)
+	default:
+		return i18nerr.NewInternalf("Not a valid server type: %v", _type)
+	}
+	if mErr != nil {
+		log.Logger.Debugf("failed to remove server with identifier: '%s' and type: '%d', error: %v", identifier, _type, mErr)
+	}
+	c.SaveState()
+	return nil
+}
+
+func (c *Client) CurrentServer() (*srvtypes.Current, error) {
+	srv, err := c.Servers.Current()
+	if err != nil {
+		return nil, err
+	}
+	return c.pubCurrentServer(srv)
+}
+
+func (c *Client) pubCurrentServer(srv server.Server) (*srvtypes.Current, error) {
+	b, err := srv.Base()
+	if err != nil {
+		return nil, err
+	}
+	pub, err := srv.Public()
+	if err != nil {
+		return nil, err
+	}
+	switch t := pub.(type) {
+	case *srvtypes.Server:
+		if b.Type == srvtypes.TypeInstituteAccess {
+			return &srvtypes.Current{
+				Institute: &srvtypes.Institute{
+					Server:          *t,
+					SupportContacts: b.SupportContact,
+					// TODO: delisted
+					Delisted: false,
+				},
+				Type: srvtypes.TypeInstituteAccess,
+			}, nil
+		}
+		return &srvtypes.Current{
+			Custom: t,
+			Type:   srvtypes.TypeCustom,
+		}, nil
+	case *srvtypes.SecureInternet:
+		t.SupportContacts = b.SupportContact
+		t.Locations = c.Discovery.SecureLocationList()
+		return &srvtypes.Current{
+			SecureInternet: t,
+			Type:           srvtypes.TypeSecureInternet,
+		}, nil
+	default:
+		panic("unknown type")
+	}
+}
+
+// TODO: This should not rely on interface{}
+func (c *Client) pubServer(srv server.Server) (interface{}, error) {
+	pub, err := srv.Public()
+	if err != nil {
+		return nil, err
+	}
+	b, err := srv.Base()
+	if err != nil {
+		return nil, err
+	}
+	switch t := pub.(type) {
+	case *srvtypes.Server:
+		if b.Type == srvtypes.TypeInstituteAccess {
+			return &srvtypes.Institute{
+				Server:          *t,
+				SupportContacts: b.SupportContact,
+				// TODO: delisted
+				Delisted: false,
+			}, nil
+		}
+		return t, nil
+	case *srvtypes.SecureInternet:
+		t.SupportContacts = b.SupportContact
+		t.Locations = c.Discovery.SecureLocationList()
+		return t, nil
+	default:
+		panic("unknown type")
+	}
+}
+
+func (c *Client) ServerList() (*srvtypes.List, error) {
+	if c.FSM.InState(StateDeregistered) {
+		return nil, i18nerr.NewInternal("Client is not registered")
+	}
+	var customServers []srvtypes.Server
+	for _, v := range c.Servers.CustomServers.Map {
+		if v == nil {
+			continue
+		}
+		p, err := c.pubServer(v)
+		if err != nil {
+			continue
+		}
+		c, ok := p.(*srvtypes.Server)
+		if !ok {
+			continue
+		}
+		customServers = append(customServers, *c)
+	}
+	var instituteServers []srvtypes.Institute
+	for _, v := range c.Servers.InstituteServers.Map {
+		if v == nil {
+			continue
+		}
+		p, err := c.pubServer(v)
+		if err != nil {
+			continue
+		}
+		i, ok := p.(*srvtypes.Institute)
+		if !ok {
+			continue
+		}
+		instituteServers = append(instituteServers, *i)
+	}
+	var secureInternet *srvtypes.SecureInternet
+	if c.Servers.HasSecureInternet() {
+		srv := &c.Servers.SecureInternetHomeServer
+		p, err := c.pubServer(srv)
+		if err == nil {
+			s, ok := p.(*srvtypes.SecureInternet)
+			if ok {
+				secureInternet = s
+			}
+		}
+	}
+	return &srvtypes.List{
+		Institutes:     instituteServers,
+		SecureInternet: secureInternet,
+		Custom:         customServers,
+	}, nil
+}
+
+func (c *Client) SetProfileID(pID string) (err error) {
+	srv, err := c.Servers.Current()
+	if err != nil {
+		return err
+	}
+	err = server.Profile(srv, pID)
+	if err == nil {
+		c.SaveState()
+	}
+	return err
+}
+
+func (c *Client) Cleanup(ck *cookie.Cookie) (err error) {
+	// get the current server
+	srv, err := c.Servers.Current()
+	if err != nil {
+		return i18nerr.Wrap(err, "Failed to get the current server to cleanup the connection")
+	}
+
+	err = srv.RefreshEndpoints(ck.Context(), &c.Discovery)
+
+	// If we get a canceled error, return that, otherwise just log the error
+	if err != nil {
+		if errors.Is(err, context.Canceled) {
+			return i18nerr.Wrap(err, "Cleanup was canceled")
+		}
+
+		log.Logger.Warningf("failed to refresh server endpoints: %v", err)
+	}
+
+
+	defer c.SaveState()
+	err = c.updateTokens(srv)
+	if err != nil {
+		log.Logger.Debugf("failed to update tokens for disconnect: %v", err)
+	}
+	err = server.Disconnect(ck.Context(), srv)
+	if err != nil {
+		return i18nerr.Wrap(err, "Failed to cleanup the VPN connection for the current server")
+	}
+	err = c.forwardTokens(srv)
+	if err != nil {
+		log.Logger.Debugf("failed to forward tokens after disconnect: %v", err)
+	}
+	return nil
+}
+
+func (c *Client) SetSecureLocation(ck *cookie.Cookie, countryCode string) (err error) {
 	if c.isLetsConnect() {
-		return nil, errors.Errorf("discovery with Let's Connect is not supported")
+		return i18nerr.NewInternal("Setting a secure internet location with Let's Connect! is not supported")
+	}
+
+	if !c.Servers.HasSecureInternet() {
+		return i18nerr.Newf("No secure internet server available to set a location for")
+	}
+
+	dSrv, err := c.Discovery.ServerByCountryCode(countryCode)
+	if err != nil {
+		return err
+	}
+
+	err = c.Servers.SecureInternetHomeServer.Location(ck.Context(), dSrv)
+	if err == nil {
+		c.SaveState()
+	}
+	return err
+}
+
+func (c *Client) RenewSession(ck *cookie.Cookie) (err error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	srv, err := c.Servers.Current()
+	if err != nil {
+		return i18nerr.Wrap(err, "Failed to get current server for renewing the session")
+	}
+	// The server has not been chosen yet, this means that we want to manually renew
+	// TODO: is this needed?
+	if !c.FSM.InState(StateLoadingServer) {
+		c.FSM.GoTransition(StateLoadingServer) //nolint:errcheck
+	}
+	err = srv.RefreshEndpoints(ck.Context(), &c.Discovery)
+
+	// If we get a canceled error, return that, otherwise just log the error
+	if err != nil {
+		if errors.Is(err, context.Canceled) {
+			return i18nerr.Wrap(err, "Renewing was canceled")
+		}
+
+		log.Logger.Warningf("failed to refresh server endpoints: %v", err)
+	}
+
+
+	// update tokens in the end
+	defer func() {
+		terr := c.forwardTokens(srv)
+		if terr != nil {
+			log.Logger.Debugf("failed to forward tokens after renew: %v", terr)
+		}
+	}()
+	defer c.SaveState()
+	// TODO: Maybe this can be deleted because we force auth now
+	server.MarkTokensForRenew(srv)
+	// run the callbacks by forcing auth
+	return c.callbacks(ck, srv, true, false)
+}
+
+func (c *Client) StartFailover(ck *cookie.Cookie, gateway string, mtu int, readRxBytes func() (int64, error)) (bool, error) {
+	f := failover.New(readRxBytes)
+
+	d, err := f.Start(ck.Context(), gateway, mtu)
+	if err != nil {
+		return d, i18nerr.Wrapf(err, "Failover failed to complete with gateway: '%s' and MTU: '%d'", gateway, mtu)
 	}
+	return d, nil
+}
 
-	return c.Discovery.Servers()
+func (c *Client) SetState(state FSMStateID) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	curr := c.FSM.Current
+	_, err := c.FSM.GoTransition(state)
+	if err != nil {
+		// self-transitions are only debug errors
+		if c.FSM.InState(state) {
+			log.Logger.Debugf("attempt an invalid self-transition: %s", c.FSM.GetStateName(state))
+			return nil
+		}
+		return i18nerr.WrapInternalf(err, "Failed internal state transition requested by the client from: '%s' to '%s'", GetStateName(curr), GetStateName(state))
+	}
+	return nil
 }
 
-// GetTranslated gets the translation for `languages` using the current state language.
-func (c *Client) GetTranslated(languages map[string]string) string {
-	return util.GetLanguageMatched(languages, c.Language)
+func (c *Client) InState(state FSMStateID) bool {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	return c.FSM.InState(state)
 }
diff --git a/client/fsm.go b/client/fsm.go
index b2e846a1..dbaa0f96 100644
--- a/client/fsm.go
+++ b/client/fsm.go
@@ -2,9 +2,6 @@ package client
 
 import (
 	"github.com/eduvpn/eduvpn-common/internal/fsm"
-	"github.com/eduvpn/eduvpn-common/internal/log"
-	"github.com/eduvpn/eduvpn-common/internal/server"
-	"github.com/go-errors/errors"
 )
 
 type (
@@ -24,13 +21,13 @@ const (
 	// StateAskLocation means the user selected a Secure Internet server but needs to choose a location.
 	StateAskLocation
 
-	// StateSearchServer means the user is currently selecting a server in the UI.
-	StateSearchServer
+	// StateChosenLocation means the user has selected a Secure Internet location
+	StateChosenLocation
 
 	// StateLoadingServer means we are loading the server details.
 	StateLoadingServer
 
-	// StateChosenServer means the user has chosen a server to connect to.
+	// StateChosenServer means the user has chosen a server to connect to and the server is initialized.
 	StateChosenServer
 
 	// StateOAuthStarted means the OAuth process has started.
@@ -45,16 +42,20 @@ const (
 	// StateAskProfile means the go code is asking for a profile selection from the UI.
 	StateAskProfile
 
-	// StateDisconnected means the user has gotten a config for a server but is not connected yet.
-	StateDisconnected
+	// StateChosenProfile means a profile has been chosen
+	StateChosenProfile
 
-	// StateDisconnecting means the OS is disconnecting and the Go code is doing the /disconnect.
-	StateDisconnecting
+	// StateGotConfig means a VPN configuration has been obtained
+	StateGotConfig
 
-	// StateConnecting means the OS is establishing a connection to the server.
+	// The following states are only here for clients that want to use them
+	// StateConnecting means the VPN is connecting
 	StateConnecting
 
-	// StateConnected means the user has been connected to the server.
+	// StateDisconnecting means the VPN is disconnecting
+	StateDisconnecting
+
+	// StateConnected means the VPN is connected
 	StateConnected
 )
 
@@ -66,26 +67,28 @@ func GetStateName(s FSMStateID) string {
 		return "No_Server"
 	case StateAskLocation:
 		return "Ask_Location"
-	case StateSearchServer:
-		return "Search_Server"
 	case StateLoadingServer:
 		return "Loading_Server"
 	case StateChosenServer:
 		return "Chosen_Server"
+	case StateChosenLocation:
+		return "Chosen_Location"
 	case StateOAuthStarted:
 		return "OAuth_Started"
-	case StateDisconnected:
-		return "Disconnected"
 	case StateRequestConfig:
 		return "Request_Config"
 	case StateAskProfile:
 		return "Ask_Profile"
+	case StateChosenProfile:
+		return "Chosen_Profile"
 	case StateAuthorized:
 		return "Authorized"
-	case StateDisconnecting:
-		return "Disconnecting"
+	case StateGotConfig:
+		return "Got_Config"
 	case StateConnecting:
 		return "Connecting"
+	case StateDisconnecting:
+		return "Disconnecting"
 	case StateConnected:
 		return "Connected"
 	default:
@@ -105,27 +108,23 @@ func newFSM(
 		StateNoServer: FSMState{
 			Transitions: []FSMTransition{
 				{To: StateNoServer, Description: "Reload list"},
+				{To: StateAskLocation, Description: "The client wants to ask for a location on the main screen"},
 				{To: StateLoadingServer, Description: "User clicks a server in the UI"},
-				{To: StateChosenServer, Description: "The server has been chosen"},
-				{
-					To:          StateSearchServer,
-					Description: "The user is trying to choose a new server in the UI",
-				},
-				{To: StateConnected, Description: "The user is already connected"},
-				{To: StateAskLocation, Description: "Change the location in the main screen"},
+				{To: StateConnected, Description: "The VPN is still active"},
 			},
 		},
-		StateSearchServer: FSMState{
+		StateAskLocation: FSMState{
 			Transitions: []FSMTransition{
-				{To: StateLoadingServer, Description: "User clicks a server in the UI"},
-				{To: StateNoServer, Description: "Cancel or Error"},
+				{To: StateChosenLocation, Description: "Location chosen"},
+				{To: StateNoServer, Description: "Go back or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
-		StateAskLocation: FSMState{
+		StateChosenLocation: FSMState{
 			Transitions: []FSMTransition{
-				{To: StateChosenServer, Description: "Location chosen"},
+				{To: StateChosenServer, Description: "Server has been chosen"},
 				{To: StateNoServer, Description: "Go back or Error"},
-				{To: StateSearchServer, Description: "Cancel or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
 		StateLoadingServer: FSMState{
@@ -136,66 +135,79 @@ func newFSM(
 					Description: "User chooses a Secure Internet server but no location is configured",
 				},
 				{To: StateNoServer, Description: "Go back or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
 		StateChosenServer: FSMState{
 			Transitions: []FSMTransition{
 				{To: StateAuthorized, Description: "Found tokens in config"},
 				{To: StateOAuthStarted, Description: "No tokens found in config"},
+				{To: StateNoServer, Description: "Go back or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
 		StateOAuthStarted: FSMState{
 			Transitions: []FSMTransition{
 				{To: StateAuthorized, Description: "User authorizes with browser"},
 				{To: StateNoServer, Description: "Go back or Error"},
-				{To: StateSearchServer, Description: "Cancel or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
 		StateAuthorized: FSMState{
 			Transitions: []FSMTransition{
 				{To: StateOAuthStarted, Description: "Re-authorize with OAuth"},
 				{To: StateRequestConfig, Description: "Client requests a config"},
-				{To: StateNoServer, Description: "Client wants to go back to the main screen"},
+				{To: StateLoadingServer, Description: "Load the server again"},
+				{To: StateNoServer, Description: "Go back or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
 		StateRequestConfig: FSMState{
 			Transitions: []FSMTransition{
 				{To: StateAskProfile, Description: "Multiple profiles found and no profile chosen"},
-				{To: StateDisconnected, Description: "Only one profile or profile already chosen"},
-				{To: StateNoServer, Description: "Cancel or Error"},
+				{To: StateChosenProfile, Description: "Only one profile or profile already chosen"},
 				{To: StateOAuthStarted, Description: "Re-authorize"},
+				{To: StateNoServer, Description: "Go back or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
 		StateAskProfile: FSMState{
 			Transitions: []FSMTransition{
-				{To: StateDisconnected, Description: "User chooses profile"},
 				{To: StateNoServer, Description: "Cancel or Error"},
-				{To: StateSearchServer, Description: "Cancel or Error"},
+				{To: StateChosenProfile, Description: "Profile has been chosen"},
+				{To: StateNoServer, Description: "Go back or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
-		StateDisconnected: FSMState{
+		StateChosenProfile: FSMState{
 			Transitions: []FSMTransition{
-				{To: StateConnecting, Description: "OS reports it is trying to connect"},
-				{To: StateRequestConfig, Description: "User reconnects"},
-				{To: StateNoServer, Description: "User wants to choose a new server"},
-				{To: StateOAuthStarted, Description: "Re-authorize with OAuth"},
+				{To: StateGotConfig, Description: "Config has been obtained"},
+				{To: StateNoServer, Description: "Go back or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 			},
 		},
-		StateDisconnecting: FSMState{
+		StateGotConfig: FSMState{
 			Transitions: []FSMTransition{
-				{To: StateDisconnected, Description: "Cancel or Error"},
-				{To: StateDisconnected, Description: "Done disconnecting"},
+				{To: StateNoServer, Description: "Choose a new server"},
+				{To: StateLoadingServer, Description: "Get a new configuration"},
+				{To: StateConnecting, Description: "VPN is connecting"},
 			},
 		},
 		StateConnecting: FSMState{
 			Transitions: []FSMTransition{
-				{To: StateDisconnected, Description: "Cancel or Error"},
+				{To: StateGotConfig, Description: "Go back or Error"},
 				{To: StateConnected, Description: "Done connecting"},
 			},
 		},
 		StateConnected: FSMState{
 			Transitions: []FSMTransition{
-				{To: StateDisconnecting, Description: "App wants to disconnect"},
+				{To: StateDisconnecting, Description: "The VPN is disconnecting"},
+			},
+		},
+		StateDisconnecting: FSMState{
+			Transitions: []FSMTransition{
+				{To: StateGotConfig, Description: "Done disconnecting"},
+				{To: StateConnected, Description: "Go back or Error"},
 			},
 		},
 	}
@@ -203,159 +215,3 @@ func newFSM(
 	returnedFSM.Init(StateDeregistered, states, callback, directory, GetStateName, debug)
 	return returnedFSM
 }
-
-// SetSearchServer sets the FSM to the SEARCH_SERVER state.
-// This indicates that the user wants to search for a new server.
-// Returns an error if this state transition is not possible.
-func (c *Client) SetSearchServer() error {
-	if err := c.FSM.CheckTransition(StateSearchServer); err != nil {
-		c.logError(err)
-		return err
-	}
-
-	// TODO(jwijenbergh): Should we handle `false` returned value here?
-	c.FSM.GoTransition(StateSearchServer)
-	return nil
-}
-
-// SetConnected sets the FSM to the CONNECTED state.
-// This indicates that the VPN is connected to the server.
-// Returns an error if this state transition is not possible.
-func (c *Client) SetConnected() error {
-	if c.InFSMState(StateConnected) {
-		// already connected, show no error
-		return nil
-	}
-
-	if err := c.FSM.CheckTransition(StateConnected); err != nil {
-		c.logError(err)
-		return err
-	}
-
-	srv, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		c.logError(err)
-		return err
-	}
-
-	c.FSM.GoTransitionWithData(StateConnected, srv)
-	return nil
-}
-
-// SetConnecting sets the FSM to the CONNECTING state.
-// This indicates that the VPN is currently connecting to the server.
-// Returns an error if this state transition is not possible.
-func (c *Client) SetConnecting() error {
-	if c.InFSMState(StateConnecting) {
-		// already loading connection, show no error
-		log.Logger.Warningf("Already connecting")
-		return nil
-	}
-	if err := c.FSM.CheckTransition(StateConnecting); err != nil {
-		c.logError(err)
-		return err
-	}
-
-	srv, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		c.logError(err)
-		return err
-	}
-
-	c.FSM.GoTransitionWithData(StateConnecting, srv)
-	return nil
-}
-
-// SetDisconnecting sets the FSM to the DISCONNECTING state.
-// This indicates that the VPN is currently disconnecting from the server.
-// Returns an error if this state transition is not possible.
-func (c *Client) SetDisconnecting() error {
-	if c.InFSMState(StateDisconnecting) {
-		// already disconnecting, show no error
-		log.Logger.Warningf("Already disconnecting")
-		return nil
-	}
-	if err := c.FSM.CheckTransition(StateDisconnecting); err != nil {
-		c.logError(err)
-		return err
-	}
-
-	srv, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		c.logError(err)
-		return err
-	}
-
-	c.FSM.GoTransitionWithData(StateDisconnecting, srv)
-	return nil
-}
-
-// SetDisconnected sets the FSM to the DISCONNECTED state.
-// This indicates that the VPN is currently disconnected from the server.
-// This also sends the /disconnect API call to the server.
-// Returns an error if this state transition is not possible.
-func (c *Client) SetDisconnected() error {
-	if c.InFSMState(StateDisconnected) {
-		// already disconnected, show no error
-		log.Logger.Warningf("Already disconnected")
-		return nil
-	}
-	if err := c.FSM.CheckTransition(StateDisconnected); err != nil {
-		c.logError(err)
-		return err
-	}
-
-	srv, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		c.logError(err)
-		return err
-	}
-
-	c.FSM.GoTransitionWithData(StateDisconnected, srv)
-
-	return nil
-}
-
-// goBackInternal uses the public go back but logs an error if it happened.
-func (c *Client) goBackInternal() {
-	err := c.GoBack()
-	if err != nil {
-		// TODO(jwijenbergh): Bit suspicious - logging level INFO, yet stacktrace logged.
-		log.Logger.Infof("failed going back: %s\nstacktrace:\n%s", err.Error(), err.(*errors.Error).ErrorStack())
-	}
-}
-
-// GoBack transitions the FSM back to the previous UI state, for now this is always the NO_SERVER state.
-func (c *Client) GoBack() error {
-	if c.InFSMState(StateDeregistered) {
-		err := errors.Errorf("fsm attempt going back from 'StateDeregistered'")
-		c.logError(err)
-		return err
-	}
-
-	// FIXME: Arbitrary back transitions don't work because we need the appropriate data
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-	return nil
-}
-
-// CancelOAuth cancels OAuth if one is in progress.
-// If OAuth is not in progress, it returns an error.
-// An error is also returned if OAuth is in progress, but it fails to cancel it.
-func (c *Client) CancelOAuth() error {
-	if !c.InFSMState(StateOAuthStarted) {
-		return errors.Errorf("fsm attempt cancelling OAuth while in '%v'", c.FSM.Current)
-	}
-
-	srv, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		c.logError(err)
-	} else {
-		server.CancelOAuth(srv)
-	}
-	return err
-}
-
-// InFSMState is a helper to check if the FSM is in state `checkState`.
-func (c *Client) InFSMState(checkState FSMStateID) bool {
-	return c.FSM.InState(checkState)
-}
diff --git a/client/locales/da/messages.gotext.json b/client/locales/da/messages.gotext.json
new file mode 100644
index 00000000..72f817a7
--- /dev/null
+++ b/client/locales/da/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "da",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": ""
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": ""
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": ""
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": ""
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": ""
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": ""
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": ""
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": ""
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": ""
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": ""
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": ""
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": ""
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": ""
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/de/messages.gotext.json b/client/locales/de/messages.gotext.json
new file mode 100644
index 00000000..a37e3a56
--- /dev/null
+++ b/client/locales/de/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "de",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": ""
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": ""
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": ""
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": ""
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": ""
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": ""
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": ""
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": ""
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": ""
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": ""
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": ""
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": ""
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": ""
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/en/messages.gotext.json b/client/locales/en/messages.gotext.json
new file mode 100644
index 00000000..03feb194
--- /dev/null
+++ b/client/locales/en/messages.gotext.json
@@ -0,0 +1,403 @@
+{
+    "language": "en",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "The log file with directory: '{Directory}' failed to initialize",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": "An error occurred after getting the discovery files for the list of organizations",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": "An error occurred after getting the discovery files for the list of servers",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": "The current server could not be found when getting it for expiry",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": "The secure internet location could not be set",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": "The authorization procedure failed to complete",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": "No suitable profiles could be found",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "Profile with ID: '{PID}' could not be set",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": "The identifier that was passed to the library is incorrect",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "The institute access server with URL: '{Identifier}' could not be added",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "The custom server with URL: '{Identifier}' could not be added",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": "The VPN configuration could not be obtained",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": "The current profile could not be found",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": "The operation for getting a VPN configuration was canceled",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": "Failed to get the current server to cleanup the connection",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": "Failed to cleanup the VPN connection for the current server",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": "No secure internet server available to set a location for",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": "Failed to get current server for renewing the session",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": "timeout reached",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": "with cause:",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": "An internal error occurred",
+            "translatorComment": "Copied from source.",
+            "fuzzy": true
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/es/messages.gotext.json b/client/locales/es/messages.gotext.json
new file mode 100644
index 00000000..e19ad9e0
--- /dev/null
+++ b/client/locales/es/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "es",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "El archivo de registro con el directorio: '{Directory}' no se puede inicializar",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": "Se ha producido un error al obtener los archivos de detección de la lista de las organizaciones"
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": "Se ha producido un error al obtener los archivos de detección de la lista de servidores"
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": ""
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": ""
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": ""
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": ""
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": ""
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": ""
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": ""
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": ""
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": ""
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": ""
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": ""
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/fr/messages.gotext.json b/client/locales/fr/messages.gotext.json
new file mode 100644
index 00000000..ba674002
--- /dev/null
+++ b/client/locales/fr/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "fr",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": ""
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": ""
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": ""
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": ""
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": ""
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": ""
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": ""
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": ""
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": ""
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": ""
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": ""
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": ""
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": ""
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/it/messages.gotext.json b/client/locales/it/messages.gotext.json
new file mode 100644
index 00000000..2691bffb
--- /dev/null
+++ b/client/locales/it/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "it",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": ""
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": ""
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": ""
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": ""
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": ""
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": ""
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": ""
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": ""
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": ""
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": ""
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": ""
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": ""
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": ""
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/nl/messages.gotext.json b/client/locales/nl/messages.gotext.json
new file mode 100644
index 00000000..0a05d777
--- /dev/null
+++ b/client/locales/nl/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "nl",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "Het log bestand met pad: '{Directory}' kon niet aangemaakt worden",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": "Er is een fout opgetreden met het ophalen van de lijst van organisaties"
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": "Er is een fout opgetreden met het ophalen van de lijst van servers"
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": "De huidige VPN server kon niet worden gevonden voor expiratie"
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": "De locatie voor de \"secure internet\" server kon niet worden gezet"
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": "Het authorizatie proces kon niet vervuld worden"
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": "Er zijn geen profielen gevonden om mee te verbinden"
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "Het profiel met identiteit: '{PID}' kon niet gezet worden",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "Het profiel met identiteit: '{PID}' kon niet opgehaald worden van de server",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": "De identieit die aan de library werd gegeven is niet correct"
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "De institute access server met URL: '{Identifier}' kon niet opgehaald worden van discovery",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "De institute access server met URL: '{Identifier}' kon niet toegevoegd worden",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "De secure internet server met identiteit: '{Identifier}' kon niet opgehaald worden van discovery",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "De secure internet server met identiteit: '{Identifier}' kon niet toegevoegd worden",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "De server met URL: '{Identifier}' kon niet toegevoegd worden",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": "De VPN configuratie kon niet opgehaald worden"
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": "Het huidig profiel kan niet gevonden worden"
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "De identiteit: '{Identifier}' voor server met type: '{Type}' is niet geldig",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": "De procedure om een VPN configuratie op te halen is geannuleerd"
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "De server met identiteit: '{Identifier}' kon niet als de hudige server gezet worden",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "Identiteit: '{Identifier}' voor server met type: '{Type}' is niet geldig om verwijderd te worden",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": "De huidige server kon niet opgehaald worden om de connectie te verwijderen"
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": "De VPN connectie is niet volledig opgeruimd"
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": "Er is geen \"secure internet\" server beschikbaar om de locatie voor in te stellen"
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": "De huidige server kon niet opgehaald worden om de sessie te hernieuwen"
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": "Er is een time-out opgetreden in de verbinding. Controleer uw internetverbinding"
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": "met oorzaak:"
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/sl/messages.gotext.json b/client/locales/sl/messages.gotext.json
new file mode 100644
index 00000000..d7ee311d
--- /dev/null
+++ b/client/locales/sl/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "sl",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "Napaka pri vzpostavitvi datoteke dnevnika v imeniku '{Directory}'",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": "Pri nalaganju datotek kataloga organizacij je prišlo do napake"
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": "Pri nalaganju datotek kataloga strežnikov je prišlo do napake"
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": "Ugotavljanje preteka ne more določiti izbranega strežnika"
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": "Napaka pri nastavljanju lokacije za varni splet"
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": "Napaka pri postopku avtorizacije"
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": "Ustreznih profilov ni bilo mogoče najti"
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "Profila z ID-jem '{PID}' ni bilo mogoče nastaviti",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "Profila z ID-jem '{PID}' ni bilo mogoče naložiti s strežnika",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": "ID poslan knjižnici je napačen"
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "Strežnika z naslovom URL '{Identifier}' za dostop do ustanove ni bilo možno najti v katalogu",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "Strežnika z naslovom '{Identifier}' za dostop do ustanove ni bilo možno dodati",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "Strežnika za varni splet organizacije z ID-jem '{Identifier}' ni bilo možno najti v katalogu",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "Strežnika za varni splet organizacije z ID-jem '{Identifier}' ni bilo možno dodati",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "Svojega strežnika z naslovom '{Identifier}' ni bilo možno dodati",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": "Napaka pri prenosu nastavitev VPN"
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": "Izbranega profila ni bilo mogoče najti"
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "ID '{Identifier}' za strežnik vrste '{Type}' ni veljaven",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": "Operacija za nalaganje nastavitev VPN je bila preklicana"
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "Napaka pri izbiri strežnika z ID-jem '{Identifier}'",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "ID '{Identifier}' strežnika vrste '{Type}' ni veljaven za odstranitev",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": "Napaka pri določanju izbranega strežnika za čiščenje povezave"
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": "Napaka pri čiščenju povezave VPN za izbrani strežnik"
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": "Za izbiro lokacije ni na voljo nobenega strežnika za varni splet"
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": "Napaka pri ugotavljanju izbranega strežnika za podaljšanje seje"
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": "čas je potekel"
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": "; razlog:"
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/locales/uk/messages.gotext.json b/client/locales/uk/messages.gotext.json
new file mode 100644
index 00000000..daa83e0b
--- /dev/null
+++ b/client/locales/uk/messages.gotext.json
@@ -0,0 +1,339 @@
+{
+    "language": "uk",
+    "messages": [
+        {
+            "id": "The log file with directory: '{Directory}' failed to initialize",
+            "message": "The log file with directory: '{Directory}' failed to initialize",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Directory",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "directory"
+                }
+            ]
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of organizations",
+            "message": "An error occurred after getting the discovery files for the list of organizations",
+            "translation": ""
+        },
+        {
+            "id": "An error occurred after getting the discovery files for the list of servers",
+            "message": "An error occurred after getting the discovery files for the list of servers",
+            "translation": ""
+        },
+        {
+            "id": "The current server could not be found when getting it for expiry",
+            "message": "The current server could not be found when getting it for expiry",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no secure internet location is found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet location could not be set",
+            "message": "The secure internet location could not be set",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but you need to authorizate again. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "The authorization procedure failed to complete",
+            "message": "The authorization procedure failed to complete",
+            "translation": ""
+        },
+        {
+            "id": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "message": "The client tried to autoconnect to the VPN server: {Namesrv}, but no valid profiles were found. Please manually connect again",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Namesrv",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "server.Name(srv)"
+                }
+            ]
+        },
+        {
+            "id": "No suitable profiles could be found",
+            "message": "No suitable profiles could be found",
+            "translation": ""
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be set",
+            "message": "Profile with ID: '{PID}' could not be set",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "Profile with ID: '{PID}' could not be obtained from the server",
+            "message": "Profile with ID: '{PID}' could not be obtained from the server",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "PID",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "pID"
+                }
+            ]
+        },
+        {
+            "id": "The identifier that was passed to the library is incorrect",
+            "message": "The identifier that was passed to the library is incorrect",
+            "translation": ""
+        },
+        {
+            "id": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "message": "Could not retrieve institute access server with URL: '{Identifier}' from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The institute access server with URL: '{Identifier}' could not be added",
+            "message": "The institute access server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be retrieved from discovery",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "message": "The secure internet server with organisation ID: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The custom server with URL: '{Identifier}' could not be added",
+            "message": "The custom server with URL: '{Identifier}' could not be added",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "The VPN configuration could not be obtained",
+            "message": "The VPN configuration could not be obtained",
+            "translation": ""
+        },
+        {
+            "id": "The current profile could not be found",
+            "message": "The current profile could not be found",
+            "translation": ""
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "The operation for getting a VPN configuration was canceled",
+            "message": "The operation for getting a VPN configuration was canceled",
+            "translation": ""
+        },
+        {
+            "id": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "message": "Failed to set the server with identifier: '{Identifier}' as the current",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                }
+            ]
+        },
+        {
+            "id": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "message": "Identifier: '{Identifier}' for server with type: '{Type}' is not valid for removal",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Identifier",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "identifier"
+                },
+                {
+                    "id": "Type",
+                    "string": "%[2]d",
+                    "type": "github.com/eduvpn/eduvpn-common/types/server.Type",
+                    "underlyingType": "int8",
+                    "argNum": 2,
+                    "expr": "_type"
+                }
+            ]
+        },
+        {
+            "id": "Failed to get the current server to cleanup the connection",
+            "message": "Failed to get the current server to cleanup the connection",
+            "translation": ""
+        },
+        {
+            "id": "Failed to cleanup the VPN connection for the current server",
+            "message": "Failed to cleanup the VPN connection for the current server",
+            "translation": ""
+        },
+        {
+            "id": "No secure internet server available to set a location for",
+            "message": "No secure internet server available to set a location for",
+            "translation": ""
+        },
+        {
+            "id": "Failed to get current server for renewing the session",
+            "message": "Failed to get current server for renewing the session",
+            "translation": ""
+        },
+        {
+            "id": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "message": "Failover failed to complete with gateway: '{Gateway}' and MTU: '{Mtu}'",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Gateway",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "gateway"
+                },
+                {
+                    "id": "Mtu",
+                    "string": "%[2]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 2,
+                    "expr": "mtu"
+                }
+            ]
+        },
+        {
+            "id": "timeout reached",
+            "message": "timeout reached",
+            "translation": ""
+        },
+        {
+            "id": "with cause:",
+            "message": "with cause:",
+            "translation": ""
+        },
+        {
+            "id": "An internal error occurred",
+            "message": "An internal error occurred",
+            "translation": ""
+        }
+    ]
+}
\ No newline at end of file
diff --git a/client/server.go b/client/server.go
deleted file mode 100644
index 283c531e..00000000
--- a/client/server.go
+++ /dev/null
@@ -1,690 +0,0 @@
-package client
-
-import (
-	"github.com/eduvpn/eduvpn-common/internal/failover"
-	"github.com/eduvpn/eduvpn-common/internal/http"
-	"github.com/eduvpn/eduvpn-common/internal/log"
-	"github.com/eduvpn/eduvpn-common/internal/oauth"
-	"github.com/eduvpn/eduvpn-common/internal/server"
-	"github.com/eduvpn/eduvpn-common/types"
-	"github.com/go-errors/errors"
-)
-
-type ConfigData = server.ConfigData
-
-// getConfigAuth gets a config with authorization and authentication.
-// It also asks for a profile if no valid profile is found.
-func (c *Client) getConfigAuth(srv server.Server, preferTCP bool, t oauth.Token) (*ConfigData, error) {
-	err := c.ensureLogin(srv, t)
-	if err != nil {
-		return nil, err
-	}
-
-	// TODO(jwijenbergh): Should we check if it returns false?
-	c.FSM.GoTransition(StateRequestConfig)
-
-	ok, err := server.HasValidProfile(srv, c.SupportsWireguard)
-	if err != nil {
-		return nil, err
-	}
-
-	// No valid profile, ask for one
-	if !ok {
-		if err = c.askProfile(srv); err != nil {
-			return nil, err
-		}
-	}
-
-	// We return the error otherwise we wrap it too much
-	return server.Config(srv, c.SupportsWireguard, preferTCP)
-}
-
-// retryConfigAuth retries the getConfigAuth function if the tokens are invalid.
-// If OAuth is cancelled, it makes sure that we only forward the error as additional info.
-func (c *Client) retryConfigAuth(srv server.Server, preferTCP bool, t oauth.Token) (*ConfigData, error) {
-	cfg, err := c.getConfigAuth(srv, preferTCP, t)
-	if err == nil {
-		return cfg, nil
-	}
-	// Only retry if the error is that the tokens are invalid
-	tErr := &oauth.TokensInvalidError{}
-	if errors.As(err, &tErr) {
-		// TODO: Is passing empty tokens correct here?
-		cfg, err = c.getConfigAuth(srv, preferTCP, oauth.Token{})
-		if err == nil {
-			return cfg, nil
-		}
-	}
-	c.goBackInternal()
-	return nil, err
-}
-
-// getConfig gets an OpenVPN/WireGuard configuration by contacting the server, moving the FSM towards the DISCONNECTED state and then saving the local configuration file.
-func (c *Client) getConfig(srv server.Server, preferTCP bool, t oauth.Token) (*ConfigData, error) {
-	if c.InFSMState(StateDeregistered) {
-		return nil, errors.Errorf("getConfig attempt in '%v'", StateDeregistered)
-	}
-
-	// Refresh the server endpoints
-	// This is the best effort
-	err := srv.RefreshEndpoints(&c.Discovery)
-	if err != nil {
-		log.Logger.Warningf("failed to refresh server endpoints: %v", err)
-	}
-
-	cfg, err := c.retryConfigAuth(srv, preferTCP, t)
-	if err != nil {
-		return nil, err
-	}
-
-	srv1, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		return nil, err
-	}
-
-	// Signal the server display info
-	c.FSM.GoTransitionWithData(StateDisconnected, srv1)
-
-	// Save the config
-	if err = c.Config.Save(&c); err != nil {
-		// TODO(jwijenbergh): Not sure why INFO level, yet stacktrace...
-		// TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well.
-		log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s",
-			err.Error(), err.(*errors.Error).ErrorStack())
-	}
-
-	return cfg, nil
-}
-
-// Cleanup cleans up the VPN connection by sending a /disconnect to the server
-func (c *Client) Cleanup(ct oauth.Token) error {
-	srv, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		c.logError(err)
-		return err
-	}
-	err = srv.RefreshEndpoints(&c.Discovery)
-	if err != nil {
-		log.Logger.Warningf("failed to refresh server endpoints: %v", err)
-	}
-
-	// If we need to relogin, update tokens
-	if server.NeedsRelogin(srv) {
-		server.UpdateTokens(srv, ct)
-	}
-	// update tokens to client
-	defer c.ForwardTokenUpdate(srv)
-	// Do the /disconnect API call
-	err = server.Disconnect(srv)
-	if err != nil {
-		// We log nothing here because this can happen regularly
-		// Maybe we should not log errors that we return directly anyways?
-		return err
-	}
-	// TODO: Tokens might be refreshed, return updated tokens
-	// Not implemented yet, because ideally we want this implemented with an interface
-	return nil
-}
-
-// SetSecureLocation sets the location for the current secure location server. countryCode is the secure location to be chosen.
-// This function returns an error e.g. if the server cannot be found or the location is wrong.
-func (c *Client) SetSecureLocation(countryCode string) error {
-	// Not supported with Let's Connect!
-	if c.isLetsConnect() {
-		err := errors.Errorf("discovery with Let's Connect is not supported")
-		c.logError(err)
-		return err
-	}
-
-	srv, err := c.Discovery.ServerByCountryCode(countryCode)
-	if err != nil {
-		c.goBackInternal()
-		c.logError(err)
-		return err
-	}
-
-	if err = c.Servers.SetSecureLocation(srv); err != nil {
-		c.goBackInternal()
-		c.logError(err)
-	}
-	return err
-}
-
-// RemoveSecureInternet removes the current secure internet server.
-// It returns an error if the server cannot be removed due to the state being DEREGISTERED.
-// Note that if the server does not exist, it returns nil as an error.
-func (c *Client) RemoveSecureInternet() error {
-	if c.InFSMState(StateDeregistered) {
-		err := errors.Errorf("RemoveSecureInternet attempt in '%v'", StateDeregistered)
-		c.logError(err)
-		return err
-	}
-	// No error because we can only have one secure internet server and if there are no secure internet servers, this is a NO-OP
-	c.Servers.RemoveSecureInternet()
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-	// Save the config
-	if err := c.Config.Save(&c); err != nil {
-		// TODO(jwijenbergh): Not sure why INFO level, yet stacktrace...
-		// TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well.
-		log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s",
-			err.Error(), err.(*errors.Error).ErrorStack())
-	}
-	return nil
-}
-
-// RemoveInstituteAccess removes the institute access server with `url`.
-// It returns an error if the server cannot be removed due to the state being DEREGISTERED.
-// Note that if the server does not exist, it returns nil as an error.
-func (c *Client) RemoveInstituteAccess(url string) error {
-	if c.InFSMState(StateDeregistered) {
-		err := errors.Errorf("RemoveInstituteAccess attempt in '%v'", StateDeregistered)
-		c.logError(err)
-		return err
-	}
-	// No error because this is a NO-OP if the server doesn't exist
-	c.Servers.RemoveInstituteAccess(url)
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-	// Save the config
-	if err := c.Config.Save(&c); err != nil {
-		// TODO(jwijenbergh): Not sure why INFO level, yet stacktrace...
-		// TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well.
-		log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s",
-			err.Error(), err.(*errors.Error).ErrorStack())
-	}
-	return nil
-}
-
-// RemoveCustomServer removes the custom server with `url`.
-// It returns an error if the server cannot be removed due to the state being DEREGISTERED.
-// Note that if the server does not exist, it returns nil as an error.
-func (c *Client) RemoveCustomServer(url string) error {
-	if c.InFSMState(StateDeregistered) {
-		err := errors.Errorf("RemoveCustomServer attempt in '%v'", StateDeregistered)
-		c.logError(err)
-		return err
-	}
-	// No error because this is a NO-OP if the server doesn't exist
-	c.Servers.RemoveCustomServer(url)
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-	// Save the config
-	if err := c.Config.Save(&c); err != nil {
-		// TODO(jwijenbergh): Not sure why INFO level, yet stacktrace...
-		// TODO(jwijenbergh): Even worse, why logging it but then return nil? The calling code will think that everything went well.
-		log.Logger.Infof("c.Config.Save failed: %s\nstacktrace:\n%s",
-			err.Error(), err.(*errors.Error).ErrorStack())
-	}
-	return nil
-}
-
-// AddInstituteServer adds an Institute Access server by `url`.
-func (c *Client) AddInstituteServer(url string) (srv server.Server, err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	// Not supported with Let's Connect!
-	if c.isLetsConnect() {
-		err = errors.Errorf("discovery with Let's Connect is not supported")
-		return nil, err
-	}
-
-	// Indicate that we're loading the server
-	c.FSM.GoTransition(StateLoadingServer)
-
-	// FIXME: Do nothing with discovery here as the client already has it
-	// So pass a server as the parameter
-	var dSrv *types.DiscoveryServer
-	dSrv, err = c.Discovery.ServerByURL(url, "institute_access")
-	if err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Add the secure internet server
-	srv, err = c.Servers.AddInstituteAccessServer(dSrv)
-	if err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Set the server as the current so OAuth can be cancelled
-	if err = c.Servers.SetInstituteAccess(srv); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Indicate that we want to authorize this server
-	c.FSM.GoTransition(StateChosenServer)
-
-	// Authorize it
-	if err = c.ensureLogin(srv, oauth.Token{}); err != nil {
-		// Removing is best effort
-		_ = c.RemoveInstituteAccess(url)
-		return nil, err
-	}
-
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-
-	// Also forward tokens using the callback
-	c.ForwardTokenUpdate(srv)
-	return srv, nil
-}
-
-// AddSecureInternetHomeServer adds a Secure Internet Home Server with `orgID` that was obtained from the Discovery file.
-// Because there is only one Secure Internet Home Server, it replaces the existing one.
-func (c *Client) AddSecureInternetHomeServer(orgID string) (srv server.Server, err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	// Not supported with Let's Connect!
-	if c.isLetsConnect() {
-		return nil, errors.Errorf("discovery with Let's Connect is not supported")
-	}
-
-	// Indicate that we're loading the server
-	c.FSM.GoTransition(StateLoadingServer)
-
-	// Get the secure internet URL from discovery
-	org, dSrv, err := c.Discovery.SecureHomeArgs(orgID)
-	if err != nil {
-		// We mark the organizations as expired because we got an error
-		// Note that in the docs it states that it only should happen when the Org ID doesn't exist
-		// However, this is nice as well because it also catches the error where the SecureInternetHome server is not found
-		c.Discovery.MarkOrganizationsExpired()
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Add the secure internet server
-	srv, err = c.Servers.AddSecureInternet(org, dSrv)
-	if err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Set the server as the current so OAuth can be cancelled
-	if err = c.Servers.SetSecureInternet(srv); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Server has been chosen for authentication
-	c.FSM.GoTransition(StateChosenServer)
-
-	// Authorize it
-	if err = c.ensureLogin(srv, oauth.Token{}); err != nil {
-		// Removing is best effort
-		_ = c.RemoveSecureInternet()
-		return nil, err
-	}
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-
-	// Also forward tokens using the callback
-	c.ForwardTokenUpdate(srv)
-	return srv, nil
-}
-
-// AddCustomServer adds a Custom Server by `url`.
-func (c *Client) AddCustomServer(url string) (srv server.Server, err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	if url, err = http.EnsureValidURL(url); err != nil {
-		return nil, err
-	}
-
-	// Indicate that we're loading the server
-	c.FSM.GoTransition(StateLoadingServer)
-
-	customServer := &types.DiscoveryServer{
-		BaseURL:     url,
-		DisplayName: map[string]string{"en": url},
-		Type:        "custom_server",
-	}
-
-	// A custom server is just an institute access server under the hood
-	if srv, err = c.Servers.AddCustomServer(customServer); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Set the server as the current so OAuth can be cancelled
-	if err = c.Servers.SetCustomServer(srv); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Server has been chosen for authentication
-	c.FSM.GoTransition(StateChosenServer)
-
-	// Authorize it
-	if err = c.ensureLogin(srv, oauth.Token{}); err != nil {
-		// removing is best effort
-		_ = c.RemoveCustomServer(url)
-		return nil, err
-	}
-
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-
-	// Also forward tokens using the callback
-	c.ForwardTokenUpdate(srv)
-	return srv, nil
-}
-
-// GetConfigInstituteAccess gets a configuration for an Institute Access Server.
-// It ensures that the Institute Access Server exists by creating or using an existing one with the url.
-// `preferTCP` indicates that the client wants to use TCP (through OpenVPN) to establish the VPN tunnel.
-func (c *Client) GetConfigInstituteAccess(url string, preferTCP bool, t oauth.Token) (cfg *ConfigData, err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	// Not supported with Let's Connect!
-	if c.isLetsConnect() {
-		return nil, errors.Errorf("discovery with Let's Connect is not supported")
-	}
-
-	c.FSM.GoTransition(StateLoadingServer)
-
-	// Get the server if it exists
-	var srv *server.InstituteAccessServer
-	if srv, err = c.Servers.GetInstituteAccess(url); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Set the server as the current
-	if err = c.Servers.SetInstituteAccess(srv); err != nil {
-		return nil, err
-	}
-
-	// The server has now been chosen
-	c.FSM.GoTransition(StateChosenServer)
-
-	if cfg, err = c.getConfig(srv, preferTCP, t); err != nil {
-		c.goBackInternal()
-	}
-
-	// Also forward tokens using the callback
-	c.ForwardTokenUpdate(srv)
-
-	return cfg, err
-}
-
-// GetConfigSecureInternet gets a configuration for a Secure Internet Server.
-// It ensures that the Secure Internet Server exists by creating or using an existing one with the orgID.
-// `preferTCP` indicates that the client wants to use TCP (through OpenVPN) to establish the VPN tunnel.
-func (c *Client) GetConfigSecureInternet(orgID string, preferTCP bool, t oauth.Token) (cfg *ConfigData, err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	log.Logger.Debugf("getting config for secure internet server with org ID: '%s", orgID)
-
-	// Not supported with Let's Connect!
-	if c.isLetsConnect() {
-		return nil, errors.Errorf("discovery with Let's Connect is not supported")
-	}
-
-	c.FSM.GoTransition(StateLoadingServer)
-
-	// Get the server if it exists
-	var srv *server.SecureInternetHomeServer
-	if srv, err = c.Servers.GetSecureInternetHomeServer(); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Set the server as the current
-	if err = c.Servers.SetSecureInternet(srv); err != nil {
-		return nil, err
-	}
-
-	c.FSM.GoTransition(StateChosenServer)
-
-	if cfg, err = c.getConfig(srv, preferTCP, t); err != nil {
-		c.goBackInternal()
-	}
-
-	// Also forward tokens using the callback
-	c.ForwardTokenUpdate(srv)
-
-	return cfg, err
-}
-
-// GetConfigCustomServer gets a configuration for a Custom Server.
-// It ensures that the Custom Server exists by creating or using an existing one with the url.
-// `preferTCP` indicates that the client wants to use TCP (through OpenVPN) to establish the VPN tunnel.
-func (c *Client) GetConfigCustomServer(url string, preferTCP bool, t oauth.Token) (cfg *ConfigData, err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	if url, err = http.EnsureValidURL(url); err != nil {
-		return nil, err
-	}
-
-	c.FSM.GoTransition(StateLoadingServer)
-
-	// Get the server if it exists
-	var srv *server.InstituteAccessServer
-	if srv, err = c.Servers.GetCustomServer(url); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	// Set the server as the current
-	if err = c.Servers.SetCustomServer(srv); err != nil {
-		c.goBackInternal()
-		return nil, err
-	}
-
-	c.FSM.GoTransition(StateChosenServer)
-
-	if cfg, err = c.getConfig(srv, preferTCP, t); err != nil {
-		c.goBackInternal()
-	}
-
-	// Also forward tokens using the callback
-	c.ForwardTokenUpdate(srv)
-
-	return cfg, err
-}
-
-// askSecureLocation asks the user to choose a Secure Internet location by moving the FSM to the STATE_ASK_LOCATION state.
-func (c *Client) askSecureLocation() error {
-	loc := c.Discovery.SecureLocationList()
-
-	// Ask for the location in the callback
-	if err := c.FSM.GoTransitionRequired(StateAskLocation, loc); err != nil {
-		return err
-	}
-
-	// The state has changed, meaning setting the secure location was not successful
-	if c.FSM.Current != StateAskLocation {
-		log.Logger.Debugf("fsm failed to transit; expected %v / actual %v", GetStateName(StateAskLocation), GetStateName(c.FSM.Current))
-		return errors.New("failed loading secure internet location")
-	}
-	return nil
-}
-
-// ChangeSecureLocation changes the location for an existing Secure Internet Server.
-// Changing a secure internet location is only possible when the user is in the main screen (STATE_NO_SERVER), otherwise it returns an error.
-// It also returns an error if something has gone wrong when selecting the new location.
-func (c *Client) ChangeSecureLocation() error {
-	if !c.InFSMState(StateNoServer) {
-		return errors.Errorf("ChangeSecureLocation attempt in %v (only %v allowed)",
-			c.FSM.Current, StateNoServer)
-	}
-
-	if err := c.askSecureLocation(); err != nil {
-		c.logError(err)
-		return err
-	}
-
-	// Go back to the main screen
-	c.FSM.GoTransitionWithData(StateNoServer, c.Servers)
-
-	return nil
-}
-
-// RenewSession renews the session for the current VPN server.
-// This logs the user back in.
-func (c *Client) RenewSession() (err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	var srv server.Server
-	if srv, err = c.Servers.GetCurrentServer(); err != nil {
-		return err
-	}
-
-	err = srv.RefreshEndpoints(&c.Discovery)
-	if err != nil {
-		log.Logger.Warningf("failed to refresh server endpoints: %v", err)
-	}
-
-	// The server has not been chosen yet, this means that we want to manually renew
-	if c.FSM.InState(StateNoServer) {
-		c.FSM.GoTransition(StateChosenServer)
-	}
-
-	server.MarkTokensForRenew(srv)
-	err = c.ensureLogin(srv, oauth.Token{})
-	c.ForwardTokenUpdate(srv)
-	return err
-}
-
-// ShouldRenewButton returns true if the renew button should be shown
-// If there is no server then this returns false and logs with INFO if so
-// In other cases it simply checks the expiry time and calculates according to: https://github.com/eduvpn/documentation/blob/b93854dcdd22050d5f23e401619e0165cb8bc591/API.md#session-expiry.
-func (c *Client) ShouldRenewButton() bool {
-	if !c.InFSMState(StateConnected) && !c.InFSMState(StateConnecting) &&
-		!c.InFSMState(StateDisconnected) &&
-		!c.InFSMState(StateDisconnecting) {
-		return false
-	}
-
-	srv, err := c.Servers.GetCurrentServer()
-	if err != nil {
-		log.Logger.Infof("no server to renew: %s\nstacktrace:\n%s", err.Error(), err.(*errors.Error).ErrorStack())
-		return false
-	}
-
-	return server.ShouldRenewButton(srv)
-}
-
-// ensureLogin logs the user back in if needed.
-// It runs the FSM transitions to ask for user input.
-func (c *Client) ensureLogin(srv server.Server, ct oauth.Token) (err error) {
-	// Relogin with oauth
-	// This moves the state to authorized
-	if !server.NeedsRelogin(srv) {
-		// OAuth was valid, ensure we are in the authorized state
-		c.FSM.GoTransition(StateAuthorized)
-		return nil
-	}
-
-	// Try again but update the tokens using the client provided tokens
-	server.UpdateTokens(srv, ct)
-	if !server.NeedsRelogin(srv) {
-		// OAuth was valid, ensure we are in the authorized state
-		c.FSM.GoTransition(StateAuthorized)
-		return nil
-	}
-
-	// Mark organizations as expired if the server is a secure internet server
-	b, err := srv.Base()
-	// We only try to update it when we found the server base
-	if err == nil && b.Type == "secure_internet" {
-		c.Discovery.MarkOrganizationsExpired()
-	}
-
-	// Tokens are not valid or the client gave an error when updating tokens
-	// Otherwise, do the OAuth exchange
-	var url string
-	if url, err = server.OAuthURL(srv, c.Name); err != nil {
-		return err
-	}
-
-	if err = c.FSM.GoTransitionRequired(StateOAuthStarted, url); err != nil {
-		return err
-	}
-
-	if err = server.OAuthExchange(srv); err != nil {
-		c.goBackInternal()
-	}
-	c.FSM.GoTransition(StateAuthorized)
-
-	return err
-}
-
-// SetProfileID sets a `profileID` for the current server.
-// An error is returned if this is not possible, for example when no server is configured.
-func (c *Client) SetProfileID(profileID string) (err error) {
-	defer func() {
-		if err != nil {
-			c.logError(err)
-		}
-	}()
-
-	var srv server.Server
-	if srv, err = c.Servers.GetCurrentServer(); err != nil {
-		c.goBackInternal()
-		return err
-	}
-
-	var b *server.Base
-	if b, err = srv.Base(); err != nil {
-		c.goBackInternal()
-		return err
-	}
-	b.Profiles.Current = profileID
-	return nil
-}
-
-func (c *Client) StartFailover(gateway string, wgMTU int, readRxBytes func() (int64, error)) (bool, error) {
-	currentServer, currentServerErr := c.Servers.GetCurrentServer()
-	if currentServerErr != nil {
-		return false, currentServerErr
-	}
-
-	// Check if the current profile supports OpenVPN
-	profile, profileErr := server.CurrentProfile(currentServer)
-	if profileErr != nil {
-		return false, profileErr
-	}
-
-	if !profile.SupportsOpenVPN() {
-		return false, errors.New("Profile does not support OpenVPN fallback")
-	}
-
-	c.Failover = failover.New(readRxBytes)
-
-	return c.Failover.Start(gateway, wgMTU)
-}
-
-func (c *Client) CancelFailover() error {
-	if c.Failover == nil {
-		return errors.New("No failover process")
-	}
-	c.Failover.Cancel()
-	return nil
-}
diff --git a/client/zgotext.go b/client/zgotext.go
new file mode 100644
index 00000000..0715ec19
--- /dev/null
+++ b/client/zgotext.go
@@ -0,0 +1,309 @@
+// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
+
+package client
+
+import (
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
+	"golang.org/x/text/message/catalog"
+)
+
+type dictionary struct {
+	index []uint32
+	data  string
+}
+
+func (d *dictionary) Lookup(key string) (data string, ok bool) {
+	p, ok := messageKeyToIndex[key]
+	if !ok {
+		return "", false
+	}
+	start, end := d.index[p], d.index[p+1]
+	if start == end {
+		return "", false
+	}
+	return d.data[start:end], true
+}
+
+func init() {
+	dict := map[string]catalog.Dictionary{
+		"da": &dictionary{index: daIndex, data: daData},
+		"de": &dictionary{index: deIndex, data: deData},
+		"en": &dictionary{index: enIndex, data: enData},
+		"es": &dictionary{index: esIndex, data: esData},
+		"fr": &dictionary{index: frIndex, data: frData},
+		"it": &dictionary{index: itIndex, data: itData},
+		"nl": &dictionary{index: nlIndex, data: nlData},
+		"sl": &dictionary{index: slIndex, data: slData},
+		"uk": &dictionary{index: ukIndex, data: ukData},
+	}
+	fallback := language.MustParse("en")
+	cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
+	if err != nil {
+		panic(err)
+	}
+	message.DefaultCatalog = cat
+}
+
+var messageKeyToIndex = map[string]int{
+	"An error occurred after getting the discovery files for the list of organizations": 1,
+	"An error occurred after getting the discovery files for the list of servers":       2,
+	"An internal error occurred": 31,
+	"Could not retrieve institute access server with URL: '%s' from discovery":                                                       13,
+	"Failed to cleanup the VPN connection for the current server":                                                                    25,
+	"Failed to get current server for renewing the session":                                                                          27,
+	"Failed to get the current server to cleanup the connection":                                                                     24,
+	"Failed to set the server with identifier: '%s' as the current":                                                                  22,
+	"Failover failed to complete with gateway: '%s' and MTU: '%d'":                                                                   28,
+	"Identifier: '%s' for server with type: '%d' is not valid":                                                                       20,
+	"Identifier: '%s' for server with type: '%d' is not valid for removal":                                                           23,
+	"No secure internet server available to set a location for":                                                                      26,
+	"No suitable profiles could be found":                                                                                            9,
+	"Profile with ID: '%s' could not be obtained from the server":                                                                    11,
+	"Profile with ID: '%s' could not be set":                                                                                         10,
+	"The VPN configuration could not be obtained":                                                                                    18,
+	"The authorization procedure failed to complete":                                                                                 7,
+	"The client tried to autoconnect to the VPN server: %s, but no secure internet location is found. Please manually connect again": 4,
+	"The client tried to autoconnect to the VPN server: %s, but no valid profiles were found. Please manually connect again":         8,
+	"The client tried to autoconnect to the VPN server: %s, but you need to authorizate again. Please manually connect again":        6,
+	"The current profile could not be found":                                                                                         19,
+	"The current server could not be found when getting it for expiry":                                                               3,
+	"The custom server with URL: '%s' could not be added":                                                                            17,
+	"The identifier that was passed to the library is incorrect":                                                                     12,
+	"The institute access server with URL: '%s' could not be added":                                                                  14,
+	"The log file with directory: '%s' failed to initialize":                                                                         0,
+	"The operation for getting a VPN configuration was canceled":                                                                     21,
+	"The secure internet location could not be set":                                                                                  5,
+	"The secure internet server with organisation ID: '%s' could not be added":                                                       16,
+	"The secure internet server with organisation ID: '%s' could not be retrieved from discovery":                                    15,
+	"timeout reached": 29,
+	"with cause:":     30,
+}
+
+var daIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	// Entry 20 - 3F
+	0x00000000,
+} // Size: 156 bytes
+
+const daData string = ""
+
+var deIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	// Entry 20 - 3F
+	0x00000000,
+} // Size: 156 bytes
+
+const deData string = ""
+
+var enIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x0000003a, 0x0000008c, 0x000000d8,
+	0x00000119, 0x0000019b, 0x000001c9, 0x00000244,
+	0x00000273, 0x000002ed, 0x00000311, 0x0000033b,
+	0x0000037a, 0x000003b5, 0x00000401, 0x00000442,
+	0x000004a1, 0x000004ed, 0x00000524, 0x00000550,
+	0x00000577, 0x000005b6, 0x000005f1, 0x00000632,
+	0x0000067d, 0x000006b8, 0x000006f4, 0x0000072e,
+	0x00000764, 0x000007a7, 0x000007b7, 0x000007c3,
+	// Entry 20 - 3F
+	0x000007de,
+} // Size: 156 bytes
+
+const enData string = "" + // Size: 2014 bytes
+	"\x02The log file with directory: '%[1]s' failed to initialize\x02An erro" +
+	"r occurred after getting the discovery files for the list of organizatio" +
+	"ns\x02An error occurred after getting the discovery files for the list o" +
+	"f servers\x02The current server could not be found when getting it for e" +
+	"xpiry\x02The client tried to autoconnect to the VPN server: %[1]s, but n" +
+	"o secure internet location is found. Please manually connect again\x02Th" +
+	"e secure internet location could not be set\x02The client tried to autoc" +
+	"onnect to the VPN server: %[1]s, but you need to authorizate again. Plea" +
+	"se manually connect again\x02The authorization procedure failed to compl" +
+	"ete\x02The client tried to autoconnect to the VPN server: %[1]s, but no " +
+	"valid profiles were found. Please manually connect again\x02No suitable " +
+	"profiles could be found\x02Profile with ID: '%[1]s' could not be set\x02" +
+	"Profile with ID: '%[1]s' could not be obtained from the server\x02The id" +
+	"entifier that was passed to the library is incorrect\x02Could not retrie" +
+	"ve institute access server with URL: '%[1]s' from discovery\x02The insti" +
+	"tute access server with URL: '%[1]s' could not be added\x02The secure in" +
+	"ternet server with organisation ID: '%[1]s' could not be retrieved from " +
+	"discovery\x02The secure internet server with organisation ID: '%[1]s' co" +
+	"uld not be added\x02The custom server with URL: '%[1]s' could not be add" +
+	"ed\x02The VPN configuration could not be obtained\x02The current profile" +
+	" could not be found\x02Identifier: '%[1]s' for server with type: '%[2]d'" +
+	" is not valid\x02The operation for getting a VPN configuration was cance" +
+	"led\x02Failed to set the server with identifier: '%[1]s' as the current" +
+	"\x02Identifier: '%[1]s' for server with type: '%[2]d' is not valid for r" +
+	"emoval\x02Failed to get the current server to cleanup the connection\x02" +
+	"Failed to cleanup the VPN connection for the current server\x02No secure" +
+	" internet server available to set a location for\x02Failed to get curren" +
+	"t server for renewing the session\x02Failover failed to complete with ga" +
+	"teway: '%[1]s' and MTU: '%[2]d'\x02timeout reached\x02with cause:\x02An " +
+	"internal error occurred"
+
+var esIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x0000004a, 0x000000ab, 0x00000104,
+	0x00000104, 0x00000104, 0x00000104, 0x00000104,
+	0x00000104, 0x00000104, 0x00000104, 0x00000104,
+	0x00000104, 0x00000104, 0x00000104, 0x00000104,
+	0x00000104, 0x00000104, 0x00000104, 0x00000104,
+	0x00000104, 0x00000104, 0x00000104, 0x00000104,
+	0x00000104, 0x00000104, 0x00000104, 0x00000104,
+	0x00000104, 0x00000104, 0x00000104, 0x00000104,
+	// Entry 20 - 3F
+	0x00000104,
+} // Size: 156 bytes
+
+const esData string = "" + // Size: 260 bytes
+	"\x02El archivo de registro con el directorio: '%[1]s' no se puede inicia" +
+	"lizar\x02Se ha producido un error al obtener los archivos de detección d" +
+	"e la lista de las organizaciones\x02Se ha producido un error al obtener " +
+	"los archivos de detección de la lista de servidores"
+
+var frIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	// Entry 20 - 3F
+	0x00000000,
+} // Size: 156 bytes
+
+const frData string = ""
+
+var itIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	// Entry 20 - 3F
+	0x00000000,
+} // Size: 156 bytes
+
+const itData string = ""
+
+var nlIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x0000003c, 0x00000084, 0x000000c7,
+	0x00000105, 0x00000105, 0x00000147, 0x00000147,
+	0x00000177, 0x00000177, 0x000001ab, 0x000001e5,
+	0x00000231, 0x0000026e, 0x000002c2, 0x00000309,
+	0x00000363, 0x000003b0, 0x000003e6, 0x00000414,
+	0x00000440, 0x00000484, 0x000004c4, 0x00000511,
+	0x0000056a, 0x000005b5, 0x000005e1, 0x00000632,
+	0x00000679, 0x00000679, 0x000006ca, 0x000006d7,
+	// Entry 20 - 3F
+	0x000006d7,
+} // Size: 156 bytes
+
+const nlData string = "" + // Size: 1751 bytes
+	"\x02Het log bestand met pad: '%[1]s' kon niet aangemaakt worden\x02Er is" +
+	" een fout opgetreden met het ophalen van de lijst van organisaties\x02Er" +
+	" is een fout opgetreden met het ophalen van de lijst van servers\x02De h" +
+	"uidige VPN server kon niet worden gevonden voor expiratie\x02De locatie " +
+	"voor de \x22secure internet\x22 server kon niet worden gezet\x02Het auth" +
+	"orizatie proces kon niet vervuld worden\x02Er zijn geen profielen gevond" +
+	"en om mee te verbinden\x02Het profiel met identiteit: '%[1]s' kon niet g" +
+	"ezet worden\x02Het profiel met identiteit: '%[1]s' kon niet opgehaald wo" +
+	"rden van de server\x02De identieit die aan de library werd gegeven is ni" +
+	"et correct\x02De institute access server met URL: '%[1]s' kon niet opgeh" +
+	"aald worden van discovery\x02De institute access server met URL: '%[1]s'" +
+	" kon niet toegevoegd worden\x02De secure internet server met identiteit:" +
+	" '%[1]s' kon niet opgehaald worden van discovery\x02De secure internet s" +
+	"erver met identiteit: '%[1]s' kon niet toegevoegd worden\x02De server me" +
+	"t URL: '%[1]s' kon niet toegevoegd worden\x02De VPN configuratie kon nie" +
+	"t opgehaald worden\x02Het huidig profiel kan niet gevonden worden\x02De " +
+	"identiteit: '%[1]s' voor server met type: '%[2]d' is niet geldig\x02De p" +
+	"rocedure om een VPN configuratie op te halen is geannuleerd\x02De server" +
+	" met identiteit: '%[1]s' kon niet als de hudige server gezet worden\x02I" +
+	"dentiteit: '%[1]s' voor server met type: '%[2]d' is niet geldig om verwi" +
+	"jderd te worden\x02De huidige server kon niet opgehaald worden om de con" +
+	"nectie te verwijderen\x02De VPN connectie is niet volledig opgeruimd\x02" +
+	"Er is geen \x22secure internet\x22 server beschikbaar om de locatie voor" +
+	" in te stellen\x02De huidige server kon niet opgehaald worden om de sess" +
+	"ie te hernieuwen\x02Er is een time-out opgetreden in de verbinding. Cont" +
+	"roleer uw internetverbinding\x02met oorzaak:"
+
+var slIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x0000003c, 0x0000007c, 0x000000bc,
+	0x000000f8, 0x000000f8, 0x00000128, 0x00000128,
+	0x00000149, 0x00000149, 0x00000172, 0x000001a5,
+	0x000001e5, 0x00000206, 0x0000025e, 0x000002a8,
+	0x00000300, 0x0000034e, 0x0000038a, 0x000003ac,
+	0x000003d4, 0x00000406, 0x0000043f, 0x0000046d,
+	0x000004ac, 0x000004ef, 0x00000528, 0x0000056a,
+	0x000005ac, 0x000005ac, 0x000005bc, 0x000005c6,
+	// Entry 20 - 3F
+	0x000005c6,
+} // Size: 156 bytes
+
+const slData string = "" + // Size: 1478 bytes
+	"\x02Napaka pri vzpostavitvi datoteke dnevnika v imeniku '%[1]s'\x02Pri n" +
+	"alaganju datotek kataloga organizacij je prišlo do napake\x02Pri nalagan" +
+	"ju datotek kataloga strežnikov je prišlo do napake\x02Ugotavljanje prete" +
+	"ka ne more določiti izbranega strežnika\x02Napaka pri nastavljanju lokac" +
+	"ije za varni splet\x02Napaka pri postopku avtorizacije\x02Ustreznih prof" +
+	"ilov ni bilo mogoče najti\x02Profila z ID-jem '%[1]s' ni bilo mogoče nas" +
+	"taviti\x02Profila z ID-jem '%[1]s' ni bilo mogoče naložiti s strežnika" +
+	"\x02ID poslan knjižnici je napačen\x02Strežnika z naslovom URL '%[1]s' z" +
+	"a dostop do ustanove ni bilo možno najti v katalogu\x02Strežnika z naslo" +
+	"vom '%[1]s' za dostop do ustanove ni bilo možno dodati\x02Strežnika za v" +
+	"arni splet organizacije z ID-jem '%[1]s' ni bilo možno najti v katalogu" +
+	"\x02Strežnika za varni splet organizacije z ID-jem '%[1]s' ni bilo možno" +
+	" dodati\x02Svojega strežnika z naslovom '%[1]s' ni bilo možno dodati\x02" +
+	"Napaka pri prenosu nastavitev VPN\x02Izbranega profila ni bilo mogoče na" +
+	"jti\x02ID '%[1]s' za strežnik vrste '%[2]d' ni veljaven\x02Operacija za " +
+	"nalaganje nastavitev VPN je bila preklicana\x02Napaka pri izbiri strežni" +
+	"ka z ID-jem '%[1]s'\x02ID '%[1]s' strežnika vrste '%[2]d' ni veljaven za" +
+	" odstranitev\x02Napaka pri določanju izbranega strežnika za čiščenje pov" +
+	"ezave\x02Napaka pri čiščenju povezave VPN za izbrani strežnik\x02Za izbi" +
+	"ro lokacije ni na voljo nobenega strežnika za varni splet\x02Napaka pri " +
+	"ugotavljanju izbranega strežnika za podaljšanje seje\x02čas je potekel" +
+	"\x02; razlog:"
+
+var ukIndex = []uint32{ // 33 elements
+	// Entry 0 - 1F
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	// Entry 20 - 3F
+	0x00000000,
+} // Size: 156 bytes
+
+const ukData string = ""
+
+// Total table size 6907 bytes (6KiB); checksum: EF5CC771
diff --git a/cmd/cli/main.go b/cmd/cli/main.go
index 02c8a7b5..aba5e83c 100644
--- a/cmd/cli/main.go
+++ b/cmd/cli/main.go
@@ -1,24 +1,18 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
-	"net/url"
 	"os"
+	"reflect"
 	"strings"
 
 	"github.com/eduvpn/eduvpn-common/client"
-	"github.com/eduvpn/eduvpn-common/internal/oauth"
-	"github.com/eduvpn/eduvpn-common/internal/server"
-	"github.com/go-errors/errors"
-)
-
-type ServerTypes int8
+	"github.com/eduvpn/eduvpn-common/types/cookie"
+	srvtypes "github.com/eduvpn/eduvpn-common/types/server"
 
-const (
-	ServerTypeInstituteAccess ServerTypes = iota
-	ServerTypeSecureInternet
-	ServerTypeCustom
+	"github.com/pkg/browser"
 )
 
 // Open a browser with xdg-open.
@@ -27,37 +21,86 @@ func openBrowser(data interface{}) {
 	if !ok {
 		return
 	}
-	// double check URL scheme
-	u, err := url.Parse(str)
+	fmt.Printf("OAuth: Authorization URL: %s\n", str)
+	fmt.Println("Opening browser...")
+	err := browser.OpenURL(str)
 	if err != nil {
-		fmt.Fprintln(os.Stderr, "failed parsing url", err)
-		return
+		fmt.Fprintln(os.Stderr, "failed to open browser with error:", err)
+		fmt.Println("Please open your browser manually")
 	}
-	// Double check the scheme
-	if u.Scheme != "https" {
-		fmt.Fprintln(os.Stderr, "got invalid scheme for URL:", u.String())
-		return
+}
+
+// GetLanguageMatched uses a map from language tags to strings to extract the right language given the tag
+// It implements it according to https://github.com/eduvpn/documentation/blob/dc4d53c47dd7a69e95d6650eec408e16eaa814a2/SERVER_DISCOVERY.md#language-matching
+func GetLanguageMatched(langMap map[string]string, langTag string) string {
+	// If no map is given, return the empty string
+	if len(langMap) == 0 {
+		return ""
+	}
+	// Try to find the exact match
+	if val, ok := langMap[langTag]; ok {
+		return val
+	}
+	// Try to find a key that starts with the OS language setting
+	for k := range langMap {
+		if strings.HasPrefix(k, langTag) {
+			return langMap[k]
+		}
+	}
+	// Try to find a key that starts with the first part of the OS language (e.g. de-)
+	pts := strings.Split(langTag, "-")
+	// We have a "-"
+	if len(pts) > 1 {
+		for k := range langMap {
+			if strings.HasPrefix(k, pts[0]+"-") {
+				return langMap[k]
+			}
+		}
+	}
+	// search for just the language (e.g. de)
+	for k := range langMap {
+		if k == pts[0] {
+			return langMap[k]
+		}
 	}
-	fmt.Println("Please open your browser with URL:", u.String())
-	// In practice, a client should open the browser here
-	// But be careful with which commands you execute with this input
-	// As a client you should do enough input validation such that opening the browser does not have unwanted side effects
-	// We do our best to validate the URL in this example by parsing if it's a URL and additionally failing if the scheme is not HTTPS
-	// Note that the library already tries it best to validate data from the server, but a client should always be careful which data it uses
+
+	// Pick one that is deemed best, e.g. en-US or en, but note that not all languages are always available!
+	// We force an entry that is english exactly or with an english prefix
+	for k := range langMap {
+		if k == "en" || strings.HasPrefix(k, "en-") {
+			return langMap[k]
+		}
+	}
+
+	// Otherwise just return one
+	for k := range langMap {
+		return langMap[k]
+	}
+
+	return ""
 }
 
 // Ask for a profile in the command line.
 func sendProfile(state *client.Client, data interface{}) {
 	fmt.Printf("Multiple VPN profiles found. Please select a profile by entering e.g. 1")
-	sps, ok := data.(*server.ProfileInfo)
+	d, ok := data.(*srvtypes.RequiredAskTransition)
 	if !ok {
-		fmt.Fprintln(os.Stderr, "invalid data type")
+		fmt.Fprintf(os.Stderr, "\ninvalid data type: %v\n", reflect.TypeOf(data))
+		return
+	}
+	sps, ok := d.Data.(srvtypes.Profiles)
+	if !ok {
+		fmt.Fprintf(os.Stderr, "\ninvalid data type for profiles: %v\n", reflect.TypeOf(d.Data))
 		return
 	}
 
 	ps := ""
-	for i, p := range sps.Info.ProfileList {
-		ps += fmt.Sprintf("\n%d - %s", i+1, p.DisplayName)
+	var options []string
+	i := 0
+	for k, v := range sps.Map {
+		ps += fmt.Sprintf("\n%d - %s", i+1, GetLanguageMatched(v.DisplayName, "en"))
+		options = append(options, k)
+		i++
 	}
 
 	// Show the profiles
@@ -65,15 +108,15 @@ func sendProfile(state *client.Client, data interface{}) {
 
 	var idx int
 	if _, err := fmt.Scanf("%d", &idx); err != nil || idx <= 0 ||
-		idx > len(sps.Info.ProfileList) {
+		idx > len(sps.Map) {
 		fmt.Fprintln(os.Stderr, "invalid profile chosen, please retry")
 		sendProfile(state, data)
 		return
 	}
 
-	p := sps.Info.ProfileList[idx-1]
-	fmt.Println("Sending profile ID", p.ID)
-	if err := state.SetProfileID(p.ID); err != nil {
+	p := options[idx-1]
+	fmt.Println("Sending profile ID", p)
+	if err := d.C.Send(p); err != nil {
 		fmt.Fprintln(os.Stderr, "failed setting profile with error", err)
 	}
 }
@@ -93,40 +136,26 @@ func stateCallback(state *client.Client, _ client.FSMStateID, newState client.FS
 }
 
 // Get a config for Institute Access or Secure Internet Server.
-func getConfig(state *client.Client, url string, srvType ServerTypes) (*client.ConfigData, error) {
+func getConfig(state *client.Client, url string, srvType srvtypes.Type) (*srvtypes.Configuration, error) {
 	if !strings.HasPrefix(url, "http") {
 		url = "https://" + url
 	}
-	// Prefer TCP is set to False
-	if srvType == ServerTypeInstituteAccess {
-		_, err := state.AddInstituteServer(url)
-		if err != nil {
-			return nil, err
-		}
-		return state.GetConfigInstituteAccess(url, false, oauth.Token{})
-	} else if srvType == ServerTypeCustom {
-		_, err := state.AddCustomServer(url)
-		if err != nil {
-			return nil, err
-		}
-		return state.GetConfigCustomServer(url, false, oauth.Token{})
-	}
-	_, err := state.AddSecureInternetHomeServer(url)
+	ck := cookie.NewWithContext(context.Background())
+	defer ck.Cancel() //nolint:errcheck
+	err := state.AddServer(&ck, url, srvType, false)
 	if err != nil {
 		return nil, err
 	}
-	return state.GetConfigSecureInternet(url, false, oauth.Token{})
+	return state.GetConfig(&ck, url, srvType, false, false)
 }
 
 // Get a config for a single server, Institute Access or Secure Internet.
-func printConfig(url string, srvType ServerTypes) {
-	c := &client.Client{}
-
-	err := c.Register(
-		"org.eduvpn.app.linux",
-		"1.1.2-cli",
+func printConfig(url string, srvType srvtypes.Type) {
+	var c *client.Client
+	c, err := client.New(
+		"org.letsconnect-vpn.app.linux",
+		"2.0.0-cli",
 		"configs",
-		"en",
 		func(old client.FSMStateID, new client.FSMStateID, data interface{}) bool {
 			stateCallback(c, old, new, data)
 			return true
@@ -137,19 +166,17 @@ func printConfig(url string, srvType ServerTypes) {
 		fmt.Printf("Register error: %v", err)
 		return
 	}
+	_ = c.Register()
 
 	defer c.Deregister()
 
 	cfg, err := getConfig(c, url, srvType)
 	if err != nil {
-		err1 := err.(*errors.Error)
-		// Show the usage of tracebacks and causes
-		fmt.Fprintf(os.Stderr, "Error getting config: %s\nCause:\n%s\nStack trace:\n%s\n\n'",
-			err1.Error(), err1.Err, err1.ErrorStack())
+		fmt.Fprintf(os.Stderr, "failed getting a config: %v\n", err)
 		return
 	}
 
-	fmt.Println("Obtained config:", cfg.Config)
+	fmt.Println("Obtained config:", cfg.VPNConfig)
 }
 
 // The main function
@@ -163,11 +190,11 @@ func main() {
 	// Connect to a VPN by getting an Institute Access config
 	switch {
 	case *cu != "":
-		printConfig(*cu, ServerTypeCustom)
+		printConfig(*cu, srvtypes.TypeCustom)
 	case *u != "":
-		printConfig(*u, ServerTypeInstituteAccess)
+		printConfig(*u, srvtypes.TypeInstituteAccess)
 	case *sec != "":
-		printConfig(*sec, ServerTypeSecureInternet)
+		printConfig(*sec, srvtypes.TypeSecureInternet)
 	default:
 		flag.PrintDefaults()
 	}
diff --git a/docs/book.toml b/docs/book.toml
index 68a1112a..b071f9bb 100644
--- a/docs/book.toml
+++ b/docs/book.toml
@@ -7,3 +7,4 @@ title = "eduVPN-common documentation"
 
 [output.html]
 site-url = "/eduvpn-common/"
+additional-css = ["custom.css"]
diff --git a/docs/custom.css b/docs/custom.css
new file mode 100644
index 00000000..fe5455ed
--- /dev/null
+++ b/docs/custom.css
@@ -0,0 +1,6 @@
+.statemachine {
+    position: relative;
+    width: 80vw;
+    left: 50%;
+    transform: translateX(-50%);
+}
diff --git a/docs/mermaid-init.js b/docs/mermaid-init.js
new file mode 100644
index 00000000..faadc62f
--- /dev/null
+++ b/docs/mermaid-init.js
@@ -0,0 +1,1 @@
+mermaid.initialize({startOnLoad:true, theme: 'neutral'});
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 09ba35d6..c2da7e1a 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -9,18 +9,12 @@
 	- [Package Formats](./gettingstarted/building/packageformats.md)
 	- [Building for release](./gettingstarted/building/release.md)
   - [Testing](./gettingstarted/testing.md)
-  - [Debugging](./gettingstarted/debugging/README.md)
-	- [Logging](./gettingstarted/debugging/logging.md)
-	- [Finite State Machine](./gettingstarted/debugging/fsm.md)
 - [API](./api/README.md)
-  - [Overview](./api/overview/README.md)
-    - [Registering](./api/overview/registering.md)
-	- [Discovery](./api/overview/discovery.md)
-	- [OpenVPN/Wireguard Config](./api/overview/getconfig.md)
-	- [Connecting/Disconnecting](./api/overview/connecting.md)
-	- [Deregistering](./api/overview/deregistering.md)
-	- [Typical flow](./api/overview/flow.md)
-  - [Go](./api/go/README.md)
-	- [Example](./api/go/example.md)
-  - [Python](./api/python/README.md)
-	- [Example](./api/python/example.md)
+  - [Language Interop](./api/languageinterop.md)
+  - [Architecture](./api/architecture.md)
+  - [Typical flow](./api/typicalflow.md)
+  - [Where to find docs](./api/wheretofinddocs.md)
+  - [State Machine](./api/statemachine.md)
+  - [Let's build a client](./api/letsbuildaclient.md)
+  - [Code examples](./api/codeexamples.md)
+  - [Function docs](./api/functiondocs.md)
diff --git a/docs/src/api/README.md b/docs/src/api/README.md
index 0b7ec558..64e7ee78 100644
--- a/docs/src/api/README.md
+++ b/docs/src/api/README.md
@@ -1,2 +1,2 @@
 # API
-This section gives the documentation for the API. We first give an overview of the API in a language-agnostic way. Afterwards, we continue with the language specific documentation. We recommend to read both to get a complete picture, regardless of the language you're using.
+This chapter is an introduction to the eduvpn-common API. It is meant as a high-level overview on how to use API and build your own eduVPN/Let's Connect! client. In this chapter, we *go* over the basics of how the interop between Go and language x works, say something about the architecture, explain where to find detailed API documentation, explain the state machine, give a typical flow for a client and give a follow along tutorial on building an eduVPN client using Python code. At last, we will also have a few code examples that can be used as a short reference.
diff --git a/docs/src/api/architecture.md b/docs/src/api/architecture.md
new file mode 100644
index 00000000..72f63efc
--- /dev/null
+++ b/docs/src/api/architecture.md
@@ -0,0 +1,11 @@
+# Architecture
+In the previous section, we have already hinted a bit on the exact architecture. This section will expand upon it by giving a figure of the basic structure
+
+```mermaid
+graph TD;
+A[Go]-- Compiles to -->B[C shared library .so/.dll];
+C[Language wrapper]-- Loads -->B
+Client -- Uses --> C;
+```
+
+As can be seen by this architecture, there is an intermediate layer between the client and the *shared* library. This wrapper eases the way of loading this library and then defining a more language specific API for it. In the eduvpn-common repo, we currently only support a Python wrapper. Clients themselves can define their own wrapper
diff --git a/docs/src/api/codeexamples.md b/docs/src/api/codeexamples.md
new file mode 100644
index 00000000..b3bcdc32
--- /dev/null
+++ b/docs/src/api/codeexamples.md
@@ -0,0 +1,16 @@
+# Code examples
+This chapter contains code examples that use the API
+
+## Go command line client
+The following is an example [in the repository](https://github.com/eduvpn/eduvpn-common/blob/v2/cmd/cli/main.go). It is a command line client with the following flags
+```
+  -get-custom string
+        The url of a custom server to connect to
+  -get-institute string
+        The url of an institute to connect to
+  -get-secure string
+        Gets secure internet servers
+```
+```go
+{{#include ../../../cmd/cli/main.go}}
+```
diff --git a/docs/src/api/functiondocs.md b/docs/src/api/functiondocs.md
new file mode 100644
index 00000000..854dd019
--- /dev/null
+++ b/docs/src/api/functiondocs.md
@@ -0,0 +1,558 @@
+This document was automatically generated from the exports/exports.go file
+
+# Table of contents
+- [About the API](#about-the-api)
+- [Functions](#functions)
+    * [AddServer](#addserver)
+    * [Cleanup](#cleanup)
+    * [CookieCancel](#cookiecancel)
+    * [CookieDelete](#cookiedelete)
+    * [CookieNew](#cookienew)
+    * [CookieReply](#cookiereply)
+    * [CurrentServer](#currentserver)
+    * [Deregister](#deregister)
+    * [DiscoOrganizations](#discoorganizations)
+    * [DiscoServers](#discoservers)
+    * [ExpiryTimes](#expirytimes)
+    * [FreeString](#freestring)
+    * [GetConfig](#getconfig)
+    * [InState](#instate)
+    * [Register](#register)
+    * [RemoveServer](#removeserver)
+    * [RenewSession](#renewsession)
+    * [ServerList](#serverlist)
+    * [SetProfileID](#setprofileid)
+    * [SetSecureLocation](#setsecurelocation)
+    * [SetState](#setstate)
+    * [SetSupportWireguard](#setsupportwireguard)
+    * [SetTokenHandler](#settokenhandler)
+    * [StartFailover](#startfailover)
+
+# About the API
+package main implements the main exported API to be used by other languages
+
+Some notes:
+
+- Errors are returned as JSON c strings. The JSON type is defined in
+types/error/error.go Error. Free them using FreeString. Same is the case for
+other string types, you should also free them
+
+- Types are converted from the Go representation to C using JSON strings
+
+- Cookies are used for cancellation, just fancy contexts. Create a cookie using
+`CookieNew`, pass it to the function that needs one as the first argument. To
+cancel the function, call `CookieCancel`, passing in the same cookie as argument
+
+- Cookies must also be freed, by using the CookieDelete function if the cookie
+is no longer needed
+
+- The state machine is used to track the state of a client. It is mainly used
+for asking for certain data from the client, e.g. asking for profiles and
+locations. But a client may also wish to build upon this state machine to build
+the whole UI around it. The SetState and InState functions are useful for this
+
+
+# Functions
+## AddServer
+Signature:
+ ```go
+func AddServer(c C.uintptr_t, _type C.int, id *C.char, ni C.int) *C.char
+```
+AddServer adds a server to the eduvpn-common server list `c` is the cookie
+that is used for cancellation. Create a cookie first with CookieNew.
+This same cookie is also used for replying to state transitions
+
+`_type` is the type of server that needs to be added. This type is defined
+in types/server/server.go Type
+
+`id` is the identifier of the string:
+
+  - In case of secure internet: The organization ID
+
+  - In case of custom server: The base URL
+
+  - In case of institute access: The base URL
+
+`ni` stands for non-interactive. If non-zero, any state transitions will not
+be run.
+
+This `ni` flag is useful for preprovisioned servers. For normal usage,
+you want to set this to zero (meaning: False)
+
+If the server cannot be added it returns the error as types/error/error.go
+Error
+
+# Note that the server is removed when an error has occured
+
+The following state callbacks are mandatory to handle:
+
+  - OAUTH_STARTED: This indicates that the OAuth procedure has been started,
+    it returns the URL as the data. The client should open the webbrowser
+    with this URL and continue the authorization process.
+
+## Cleanup
+Signature:
+ ```go
+func Cleanup(c C.uintptr_t) *C.char
+```
+Cleanup sends a /disconnect to cleanup the connection
+
+This MUST be called when disconnecting, see
+https://docs.eduvpn.org/server/v3/api.html#application-flow `c` is the
+Cookie that needs to be passed. Create a new Cookie using `CookieNew`
+
+If it was unsuccessful, it returns an error
+
+## CookieCancel
+Signature:
+ ```go
+func CookieCancel(c C.uintptr_t) *C.char
+```
+CookieCancel cancels the cookie
+
+This means that functions which take this as first argument, return if
+they're still running The error cause is always context.Canceled for that
+cancelled function: https://pkg.go.dev/context#pkg-variables
+
+This CookieCancel function can also return an error if cancelling was
+unsuccessful
+
+## CookieDelete
+Signature:
+ ```go
+func CookieDelete(c C.uintptr_t) *C.char
+```
+CookieDelete deletes the cookie by cancelling it and deleting the underlying
+cgo handle
+
+This function MUST be called when the cookie that is created using
+`CookieNew` is no longer needed
+
+## CookieNew
+Signature:
+ ```go
+func CookieNew() C.uintptr_t
+```
+CookieNew creates a new cookie and returns it
+
+This value should not be parsed or converted somehow by the client This
+value is simply to pass back to the Go library This value has two purposes:
+
+  - Cancel a long running function
+
+  - Send a reply to a state transition (ASK_PROFILE and ASK_LOCATION)
+
+Functions that take a cookie have it as the first argument
+
+## CookieReply
+Signature:
+ ```go
+func CookieReply(c C.uintptr_t, data *C.char) *C.char
+```
+CookieReply replies to a state transition using the cookie
+
+The data that is sent to the Go library is the second argument of this
+function
+
+  - `c` is the Cookie
+
+  - `data` is the data to send, e.g. a profile ID
+
+## CurrentServer
+Signature:
+ ```go
+func CurrentServer() (*C.char, *C.char)
+```
+CurrentServer gets the current server from eduvpn-common
+
+In eduvpn-common, a server is marked as 'current' if you have gotten a VPN
+configuration for it
+
+It returns the server as JSON, defined in types/server/server.go Current
+
+If there is no current server or some other, e.g. there is no current state,
+an error is returned with a nil string
+
+## Deregister
+Signature:
+ ```go
+func Deregister() *C.char
+```
+Deregister cleans up the state for the client.
+
+# If no client is available or deregistering fails, it returns an error
+
+This function SHOULD be called when the application exits such that the
+configuration file is saved correctly. Note that saving of the configuration
+file also happens in other cases, such as after getting a VPN configuration.
+Thus it is often not problematic if this function cannot be called due to a
+client crash
+
+## DiscoOrganizations
+Signature:
+ ```go
+func DiscoOrganizations(c C.uintptr_t) (*C.char, *C.char)
+```
+DiscoOrganizations gets the organizations from discovery, returned as
+types/discovery/discovery.go Organizations marshalled as JSON
+
+`c` is the Cookie that needs to be passed. Create a new Cookie using
+`CookieNew`
+
+If it was unsuccessful, it returns an error. Note that when the lib was
+built in release mode the data is almost always non-nil, even when an error
+has occurred This means it has just returned the cached list
+
+## DiscoServers
+Signature:
+ ```go
+func DiscoServers(c C.uintptr_t) (*C.char, *C.char)
+```
+DiscoServers gets the servers from discovery, returned as
+types/discovery/discovery.go Servers marshalled as JSON
+
+`c` is the Cookie that needs to be passed. Create a new Cookie using
+`CookieNew`
+
+If it was unsuccessful, it returns an error. Note that when the lib was
+built in release mode the data is almost always non-nil, even when an error
+has occurred This means it has just returned the cached list
+
+## ExpiryTimes
+Signature:
+ ```go
+func ExpiryTimes() (*C.char, *C.char)
+```
+ExpiryTimes gets the expiry times for the current server
+
+Expiry times are just fields that represent unix timestamps at which to
+do certain events regarding expiry, e.g. when to show the renew button,
+when to show expiry notifications
+
+The expiry times structure is defined in types/server/server.go `Expiry` If
+some error occurs, it is returned as types/error/error.go Error
+
+## FreeString
+Signature:
+ ```go
+func FreeString(addr *C.char)
+```
+FreeString frees a string that was allocated by the eduvpn-common Go library
+
+This happens when we return strings, such as errors from the Go lib back to
+the client. The client MUST thus ensure that this memory is freed using this
+function. Simply pass the pointer to the string in here
+
+## GetConfig
+Signature:
+ ```go
+func GetConfig(c C.uintptr_t, _type C.int, id *C.char, pTCP C.int, startup C.int) (*C.char, *C.char)
+```
+GetConfig gets a configuration for the server
+
+`c` is the cookie that is used for cancellation. Create a cookie first with
+CookieNew, this same cookie is also used for replying to state transitions
+
+`_type` is the type of server that needs to be added. This type is defined
+in types/server/server.go Type
+
+`id` is the identifier of the string
+
+  - In case of secure internet: The organization ID
+  - In case of custom server: The base URL
+  - In case of institute access: The base URL
+
+`pTCP` is if we prefer TCP or not to get the configuration, non-zero means
+yes
+
+`startup` is if the client is just starting up, set this to true (non-zero)
+if you autoconnect to a server on startup. If this startup value is true
+(non-zero) then any authorization or other callacks (profile/location) are
+not triggered
+
+After getting a configuration, the FSM moves to the GOT_CONFIG state
+The return data is the configuration, marshalled as JSON and defined in
+types/server/server.go Configuration
+
+If the config cannot be retrieved it returns an error as
+types/error/error.go Error.
+
+The current state callbacks MUST be handled:
+
+### ASK_PROFILE
+
+This asks the client for profile.
+
+This is called when the user/client has not set a profile for this server
+before, or the current profile is invalid
+
+When the user has selected a profile, reply with the choice using the
+`CookieReply` function and the profile ID e.g. CookieReply(cookie,
+"wireguard"). CookieReply can be done in the background as the Go library
+waits for a reply
+
+The data for this transition is defined in types/server/server.go
+RequiredAskTransition with embedded data Profiles in types/server/server.go.
+Note that RequiredTransition contains the cookie to be used for the
+CookieReply
+
+So a client would:
+
+- Parse the data to get the cookie and data
+
+- get the cookie
+
+- get the profiles from the data
+
+- show it in the UI and then reply with CookieReply using the choice
+
+### ASK_LOCATION
+
+This asks the client for a location. Note that under normal circumstances,
+this callback is not actually called as the home organization for the
+secure internet server is set as the current if for some reason, an invalid
+location has been configured, the library will ask the client for a new one
+
+When the user has selected a location, reply with the choice using the
+`CookieReply` function and the location ID e.g. CookieReply(cookie, "nl")
+
+CookieReply can be done in the background as the Go library waits for a
+reply The data for this transition is defined in types/server/server.go
+RequiredAskTransition with embedded data a list of strings ([]string)
+
+Note that RequiredTransition contains the cookie to be used for the
+CookieReply,
+
+So a client would:
+
+  - Parse the data to get the cookie and data
+
+  - get the cookie
+
+  - get the list of locations from the data
+
+  - show it in the UI and then reply with CookieReply using the choice
+
+### OAUTH_STARTED
+
+This indicates that the OAuth procedure has been started, it returns the URL
+as the data.
+
+The client should open the webbrowser with this URL and continue the
+authorization process. This is only called if authorization needs to be
+retriggered
+
+## InState
+Signature:
+ ```go
+func InState(fsmState C.int) (C.int, *C.char)
+```
+InState checks if the FSM is in `fsmState`
+
+## Register
+Signature:
+ ```go
+func Register(
+```
+	name *C.char,
+	version *C.char,
+	configDirectory *C.char,
+	cb C.StateCB,
+	debug C.int,
+) *C.char
+Register creates a new client and also registers the FSM to go to the
+initial state
+
+`Name` is the name of the client, must be a valid client ID
+
+`Version` is the version of the client. This version field is used for the
+user agent in all HTTP requests
+
+`cb` is the state callback. It takes three arguments: The old state, the new
+state and the data for the state as JSON
+
+  - Note that the states are defined in client/fsm.go, e.g. NO_SERVER (in
+    Go: StateNoServer), ASK_PROFILE (in Go: StateAskProfile)
+
+  - This callback returns non-zero if the state transition is handled.
+    This is used to check if the client handles the needed transitions
+
+debug, if non-zero, enables debugging mode for the library, this means:
+
+  - Log everything in debug mode, so you can get more detail of what is
+    going on
+
+  - Write the state graph to a file in the configDirectory. This can be used
+    to create a FSM png file with mermaid https://mermaid.js.org/
+
+After registering, the FSM is initialized and the state transition NO_SERVER
+should have been completed If some error occurs during registering, it is
+returned as a types/error/error.go Error
+
+## RemoveServer
+Signature:
+ ```go
+func RemoveServer(_type C.int, id *C.char) *C.char
+```
+RemoveServer removes a server from the eduvpn-common server list
+
+`_type` is the type of server that needs to be added. This type is defined
+in types/server/server.go Type
+
+`id` is the identifier of the string:
+
+  - In case of secure internet: The organization ID
+
+  - In case of custom server: The base URL
+
+  - In case of institute access: The base URL
+
+If the server cannot be removed it returns the error types/error/error.go
+Error
+
+Note that the server is not removed when an error has occured
+
+## RenewSession
+Signature:
+ ```go
+func RenewSession(c C.uintptr_t) *C.char
+```
+RenewSession renews the session of the VPN
+
+This essentially means that the OAuth tokens are deleted. And it also
+possibly re-runs every state callback you need when getting a config.
+So least you MUST handle the OAuth started transition
+
+It returns an error if unsuccessful
+
+## ServerList
+Signature:
+ ```go
+func ServerList() (*C.char, *C.char)
+```
+ServerList gets the list of servers that are currently added
+
+This is NOT the discovery list, but the servers that have previously been
+added with `AddServer`
+
+It returns the server list as a JSON string defined in
+types/server/server.go List
+
+If the server list cannot be retrieved it returns a nil string and an error
+
+## SetProfileID
+Signature:
+ ```go
+func SetProfileID(data *C.char) *C.char
+```
+SetProfileID sets the profile ID of the current serrver
+
+This MUST only be called if the user/client wishes to manually set a profile
+instead of the common lib asking for one using a transition
+
+`Data` is the profile ID
+
+It returns an error if unsuccessful
+
+## SetSecureLocation
+Signature:
+ ```go
+func SetSecureLocation(c C.uintptr_t, data *C.char) *C.char
+```
+SetSecureLocation sets the location for the secure internet server if it
+exists
+
+This MUST only be called if the user/client wishes to manually set a
+location instead of the common lib asking for one using a transition
+
+Because this does network requests to initialize the location, there is a
+cookie again :)
+
+`c` is the Cookie that needs to be passed. To create a cookie, first call
+`CookieNew` `Data` is the location ID
+
+It returns an error if unsuccessful
+
+## SetState
+Signature:
+ ```go
+func SetState(fsmState C.int) *C.char
+```
+SetState sets the state of the statemachine
+
+Note: this transitions the FSM into the new state without passing any data
+to it
+
+## SetSupportWireguard
+Signature:
+ ```go
+func SetSupportWireguard(support C.int) *C.char
+```
+SetSupportWireguard enables or disables WireGuard for the client
+
+By default WireGuard support is enabled To disable it you can pass a 0 int
+to this
+
+`support` thus indicates whether or not to enable WireGuard An error is
+returned if this is not possible
+
+## SetTokenHandler
+Signature:
+ ```go
+func SetTokenHandler(getter C.TokenGetter, setter C.TokenSetter) *C.char
+```
+SetTokenHandler sets the token getters and token setters for OAuth
+
+Because the data that is saved does not contain OAuth tokens for server,
+the common lib asks and sets the tokens using these callback functions.
+The client can thus pass callbacks to this function so that the tokens can
+be securely stored in a keyring
+
+Client must pass two arguments to these functions
+
+  - getter is the void function that gets tokens from the client. It takes
+    three arguments:
+
+  - The `server` for which to get the tokens for, marshalled as JSON and
+    defined in types/server/server.go `Current`
+
+  - The `output` buffer
+
+  - The `length` of the output buffer. This 'output buffer' must contain the
+    tokens, marshalled as JSON that is defined in types/server/server.go
+    `Tokens`
+
+setter is the void function that sets tokens. It takes two arguments:
+
+  - The `server` for which to get the tokens for, marshalled as JSON and
+    defined in types/server/server.go `Current`
+
+  - The `tokens`, defined in types/server/server.go `Tokens` marshalled as
+    JSON
+
+It returns an error when the tokens cannot be set
+
+## StartFailover
+Signature:
+ ```go
+func StartFailover(c C.uintptr_t, gateway *C.char, mtu C.int, readRxBytes C.ReadRxBytes) (C.int, *C.char)
+```
+StartFailover starts the 'failover' procedure in eduvpn-common
+
+Failover has one primary goal: check if the VPN can reach the gateway.
+This can be used to check whether or not the client needs to 'failover' to
+prefer TCP (if currently using UDP). Which is useful to go from a broken
+WireGuard connection to OpenVPN over TCP
+
+  - `c` is the cookie that is passed for cancellation. To create a cookie,
+    use the `CookieNew` function
+  - `gateway` is the gateway IP of the VPN
+  - `readRxBytes` is a function that returns the current rx bytes of the VPN
+    interface, this should return a `long long int` in c
+
+It returns a boolean whether or not the common lib has determined that it
+cannot reach the gateway. Non-zero=dropped, zero=not dropped.
+
+It also returns an error, if it fails to indicate if it has dropped or not.
+In this case, dropped is also set to zero
+
diff --git a/docs/src/api/go/README.md b/docs/src/api/go/README.md
deleted file mode 100644
index 2b71f993..00000000
--- a/docs/src/api/go/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Go
-The API that has no additional wrapper code is the Go API. To begin to use the Go library in a Go client you first need to import it:
-
-```go
-import "github.com/eduvpn/eduvpn-common/client"
-```
-
-This brings the client library into scope using the client prefix.
-
-The documentation to then use this API and the rest is online at [pkg.go.dev](https://pkg.go.dev/github.com/eduvpn/eduvpn-common).
-
-[We also provide an example](./example.md).
diff --git a/docs/src/api/go/example.md b/docs/src/api/go/example.md
deleted file mode 100644
index a7f293bd..00000000
--- a/docs/src/api/go/example.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Example with comments
-The following is an example [in the repository](https://github.com/eduvpn/eduvpn-common/blob/main/cmd/cli/main.go). It is a command line client with the following flags
-```
-  -get-custom string
-        The url of a custom server to connect to
-  -get-institute string
-        The url of an institute to connect to
-  -get-secure string
-        Gets secure internet servers
-```
-```go
-{{#include ../../../../cmd/cli/main.go}}
-```
-
-The documentation to then use this API is online at [pkg.go.dev](https://pkg.go.dev/github.com/eduvpn/eduvpn-common).
-
-[We also provide an example](./example.md).
diff --git a/docs/src/api/go/functions.md b/docs/src/api/go/functions.md
deleted file mode 100644
index 2cbab786..00000000
--- a/docs/src/api/go/functions.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Functions
-## Registering
-See [Overview](../overview/registering.html)
-```go
-func Register(name string, directory string, stateCallback func, debug bool) error
-```
-- `name`: The name of the client
-- `directory`: The directory where the configs and logging should be stored
-- `stateCallback`: function with three arguments, full type:
-  ```go
-  func StateCallback(oldState string, newState string, data string)
-  ```
-- `debug`: Whether or not we want to enable debugging
-
-Returns an `error` type, nil if no error
-
-## Discovery
-See [Overview](../overview/discovery.html)
-```go
-func GetDiscoServers() (string, error)
-func GetDiscoOrganizations() (string, error)
-```
-
-Returns a string of JSON data with the servers/organizations and an `error`, nil if no error
-
-## OpenVPN/Wireguard config
-See [Overview](../overview/getconfig.html)
-```go
-func GetConfigInstituteAccess(url string, preferTCP bool) (string, string, error)
-func GetConfigSecureInternet(url string, preferTCP bool) (string, string, error)
-```
-- `url`: The URL of the Institute Access or Secure Internet server to get a connect config for
-- `preferTCP`: Whether or not we want to prefer TCP
-
-Returns:
-- A `string` of the OpenVPN/Wireguard config
-- A `string`, `openvpn` or `wireguard` indicating if it is an OpenVPN or Wireguard config
-- An `error` (can be nil)
-
-### Cancelling OAuth
-```go
-func CancelOAuth() error
-```
-Returns an `error`, can be nil indicating no error
-
-### Setting a profile ID
-```go
-func SetProfileID(profileID string) error
-```
-- `profileID`: The profile ID to connect to
-
-Returns an `error`, can be nil indicating no error
-
-## Connecting/Disconnecting
-See [Overview](../overview/connecting.html)
-```go
-func SetConnected() error
-func SetDisconnected() error
-```
-
-Returns an `error`, can be nil indicating no error
-
-## Deregister
-See [Overview](../overview/deregistering.html)
-```go
-func Deregister() error
-```
-
-Returns an `error`, can be nil indicating no error
-
-# Note on Callbacks
-Some functions (e.g. [the API for getting an OpenVPN/Wireguard config](http://localhost:3000/api/overview/getconfig.html)) need a (or multiple) callback(s) set. In Go, the callback function is given in the [Register function](#registering). The signature of this function is the following:
-```go
-func StateCallback(oldState string, newState string, data string)
-```
-Because certain callbacks need to be set, you can simply compare against `oldState` and `newState`. To show how this can be done in practice, we will give an example in the next section.
diff --git a/docs/src/api/languageinterop.md b/docs/src/api/languageinterop.md
new file mode 100644
index 00000000..73218744
--- /dev/null
+++ b/docs/src/api/languageinterop.md
@@ -0,0 +1,12 @@
+# Go <-> language X interop
+Because this library is meant to be a *general* library for other clients to use that are written in different programming languages, we need to find a way to make this Go library available on each platform and codebase. The approach that we take is to build a C library from the Go library using Cgo. Cgo can have its disadvantages with performance and the constant conversion between Go and C types. To overcome those barriers, this library has the following goals (with some others noted here):
+- **Be high-level**. Functions should do as much as possible in Go. The exported API should fit in one file. Lots of low-level functions would be a constant conversion between C and Go which adds overhead
+- **Move as much state to Go as possible**. For example, Go keeps track of the servers you have configured and discovery. This makes the arguments to functions simple, clients should pass simple identifiers that Go can look up in the state
+- **Easy type conversion**: to convert between C and Go types, JSON is used. Whereas Protobuf, Cap'n'proto or flatbuffers are more performant, they are harder to debug, add thousands of lines of autogenerated code and are not human friendly. Using JSON, the clients can approach it the same way they would use with a server using a REST API. Another approach is to just  convert from Go -> C types -> language types. This was tried in version 1 of the library, but this ended up being too much work and manual memory management
+- **Make it as easy as possible for clients to manage UI and internal state**: we use a state machine that gives the clients information in which state the Go library is in, e.g. we're selecting a server profile, we're loading the server endpoints. This library is not only a layer to talk to eduVPN servers, but the whole engine for a client
+- **Implement features currently not present in existing clients**: WireGuard to OpenVPN failover, WireGuard over TCP
+- **Follow the official eduVPN specification** and also contribute changes when needed
+- **Secure**: We aim to follow the latest OAuth recommendations, to not store secret data and e.g. disable OpenVPN scripts from being ran by default
+
+And finally the most important goal:
+- **The advantages that this library brings for clients should outweigh the cost of incorporating it into the codebase**. Initial versions would take more work than we get out of it. However, when each eduVPN/Let's Connect! client uses this library we should expect a net gain. New features should be easier to implement for clients by simply requiring a new eduvpn-common version and using the necessary functions
diff --git a/docs/src/api/letsbuildaclient.md b/docs/src/api/letsbuildaclient.md
new file mode 100644
index 00000000..b675ee0c
--- /dev/null
+++ b/docs/src/api/letsbuildaclient.md
@@ -0,0 +1,317 @@
+# Let's build a client using Python
+To begin, let's follow the flow and see if we can figure out how it works.
+
+## Registering
+
+> The client starts up. It calls the Register function that communicates with the library that it has initialized
+
+In Python, this works like the following:
+- First import the library
+```python
+import eduvpn_common.main as edu
+
+class Transitions:
+    def __init__(self, common):
+        self.common = common
+
+# These arguments can be found in the docstring
+# But also in the exports.go file
+# For Python it's a bit different, we have split the arguments into the constructor and register
+# Here we pass the client ID for OAuth, the version of the client and the directory where config files should be found
+common=edu.EduVPN("org.eduvpn.app.linux", "0.0.1", "/tmp/test")
+
+common.register(debug=True)
+
+# we will come back to this later
+transitions = Transitions(common)
+common.register_class_callbacks(transitions)
+```
+
+Now after registering, we know that we have no servers configured (unless you're following this tutorial again with an existing `/tmp/test`). So we continue with step 4
+
+## Discovery
+
+>  If the client has no servers, or it wants to add a new server, the client calls `DiscoOrganizations` and `DiscoServers` to get the discovery files from the library.
+
+```python
+# Let's get them and print them
+print(common.get_disco_organizations())
+print(common.get_disco_servers())
+```
+
+We get a big JSON blob, so which format is this? From the Go documentation:
+
+> DiscoOrganizations gets the organizations from discovery, returned as types/discovery/discovery.go Organizations marshalled as JSON
+
+> DiscoServers gets the servers from discovery, returned as types/discovery/discovery.go Servers marshalled as JSON
+
+If you follow these files, you see two structs, Servers and Organizations. These structs have json tags associated with them. You can use this structure to figure out how to parse the returned data. In case of discovery, it's very similar to the [JSON files from the discovery server](https://disco.eduvpn.org/v2).
+
+## Adding a server
+
+The next bullet point that we implement is the following:
+
+> From this discovery list, it calls AddServer to add the server to the internal server list of eduvpn-common. This also calls necessary state transitions, e.g. for authorizing the server. The next call to ServerList then has this server included
+
+The discovery servers contains a server called the demo server. Let's try to add it. To add it we need to pass the type of server we're adding. From discovery we can deduce that this is an institute access server as the JSON looks like the following:
+
+```json
+{
+    "authentication_url_template": "",
+    "base_url": "https://demo.eduvpn.nl/",
+    "display_name": {
+        "en": "Demo"
+    },
+    "server_type": "institute_access", # this is why we know it is Institute Access
+    "support_contact": [
+        "mailto:eduvpn@surf.nl"
+    ]
+},
+```
+
+From the Go documentation, we know that the identifier must be the Base URL:
+
+> id is the identifier of the string
+> - In case of secure internet: The organization ID
+> - In case of custom server: The base URL
+> - In case of institute access: The base URL
+
+
+```python
+# Compare this to the Go version, the non-interactive field is optional here as it is default False
+common.add_server(edu.ServerType.INSTITUTE_ACCESS, "https://demo.eduvpn.nl/")
+```
+
+But we get an error!
+```bash
+eduvpn_common.main.WrappedError: fsm failed transition from 'Chosen_Server' to 'OAuth_Started', is this required transition handled?
+```
+
+This is the state machine we briefly mentioned before. Some functions require that you handle certain transitions. From the Go documentation, we can find this in the documentation as well that you must handle this transition. Let's handle it in Python to open the webbrowser for the OAuth process.
+
+We do this with the python wrapper by defining a class of state transitions. This class was already added and registered with `register_class_callbacks`. However, there was no transition added. Let's add it
+```python
+import webbrowser
+from eduvpn_common.event import class_state_transition
+from eduvpn_common.state import State, StateType
+
+class Transitions:
+    def __init__(self, common):
+        self.common = common
+
+    @class_state_transition(State.OAUTH_STARTED, StateType.ENTER)
+    def enter_oauth(self, old_state: State, url: str):
+        webbrowser.open(url)
+```
+
+Now if you re-rerun the whole code with this transition added, your webbrowser should open.
+
+Note that this state transition is essentially the same as the following code:
+
+```python
+-def handler(old: int, new: int, data: str):
+-    # it's 6 because https://github.com/eduvpn/eduvpn-common/blob/b660911b5db000b43970f3754b5767bb50741360/client/fsm.go#L33
+-    if new == 6:
+-        webbrowser.open(data)
+-        return True
+-    return False
+```
+
+This is the code that is passed to the Go library. It handles certain states and returns `False` (zero) if a state is not handled, `True` (non-zero) if it is. If you define your own wrapper you should build an abstraction layer that resolves to a handler similar as above. This handler should be passed as a C function to the Go library when registering.
+
+After you have authorized the application through the portal using the webbrowser, the server should have been added:
+
+```python
+print(common.get_servers())
+```
+
+Returns:
+
+```json
+{
+  "institute_access_servers": [
+    {
+      "display_name": {
+        "en": "Demo"
+      },
+      "identifier": "https://demo.eduvpn.nl/",
+      "profiles": {
+        "current": ""
+      },
+      "delisted": false
+    }
+  ]
+}
+```
+
+The format of this JSON is specified in the Go documentation:
+
+`(in exports/exports.go)`
+> It returns the server list as a JSON string defined in types/server/server.go List
+
+## Obtaining a VPN configuration from the server
+
+The next part of the flow is:
+
+> When the user selects a server to connect to in the UI, it calls the GetConfig to get a VPN configuration for this server. This function transitions the state machine multiple times. The client uses these state transitions for logging or even updating the UI. The client then connects
+
+Let's try it, the required arguments are the same for adding a config in the Python wrapper:
+
+```python
+print(common.get_config(edu.ServerType.INSTITUTE_ACCESS, "https://demo.eduvpn.nl"))
+```
+
+However, this gives an exception:
+
+```bash
+eduvpn_common.main.WrappedError: fsm failed transition from 'Request_Config' to 'Ask_Profile', is this required transition handled?
+```
+
+A similar error to the OAuth error we had before. This `Ask_Profile` transition is there for the client/user to choose a profile as this server has multiple profiles defined.
+
+To handle this transition and thus choose a profile to continue, we must do multiple steps:
+- Add the condition to the transitions class
+- Parse the data that we get back
+- Reply with a choice for the profile 
+
+If we add the condition and print the data:
+
+```python
+@class_state_transition(State.ASK_PROFILE, StateType.ENTER)
+def enter_ask_profile(self, old_state: State, data: str):
+    print("profiles:", data)
+```
+
+we get back the following JSON (from the Go docs: `The data for this transition is defined in types/server/server.go RequiredAskTransition with embedded data Profiles in types/server/server.go`):
+
+```python
+{
+  "cookie": 4,
+  "data": {
+    "map": {
+      "internet": {
+        "display_name": {
+          "en": "Internet"
+        },
+        "supported_protocols": [
+          1,
+          2
+        ]
+      },
+      "internet-split": {
+        "display_name": {
+          "en": "No rfc1918 routes"
+        },
+        "supported_protocols": [
+          1,
+          2
+        ]
+      }
+    },
+    "current": ""
+  }
+}
+```
+
+This thus gives you the list of profiles with a so-called "cookie". This *cookie* is used to confirm the choice to the Go library. To do so we must do the following to handle this:
+
+```python
+import json
+
+# Do this inside the Transitions class
+@class_state_transition(State.ASK_PROFILE, StateType.ENTER)
+def enter_ask_profile(self, old_state: State, data: str):
+    # parse the json
+    json_dict = json.loads(data)
+    
+    self.common.cookie_reply(json_dict["cookie"], "internet")
+```
+
+If we then re-run the code, we get back the following JSON (from the Go docs: `The return data is the configuration, marshalled as JSON and defined in types/server/server.go Configuration`)
+
+```python
+{
+  "config": "the WireGuard config",
+  "protocol": 2, # 2 specifies WireGuard
+  "default_gateway": true
+}
+```
+
+## Cleanup
+The flow also mentioned:
+
+> When the client is done, it calls `Deregister` such that the most up to date internal state is saved to disk. Note that eduvpn-common also saves the internal state .e.g. after obtaining a VPN configuration
+
+Let's be a nice client and do this:
+
+```python
+common.deregister()
+```
+
+If we then call any function, we get an error, so it is important that you do this on exit:
+
+```python
+print(common.get_servers())
+>>> eduvpn_common.main.WrappedError: No state available, did you register the client?
+```
+
+But when we register again and then get the list of servers, the servers are retrieved from disk:
+
+```python
+common=edu.EduVPN("org.eduvpn.app.linux", "0.0.1", "/tmp/test")
+common.register(debug=True)
+print(common.get_servers())
+```
+
+gives
+
+```json
+{
+  "institute_access_servers": [
+    {
+      "display_name": {
+        "en": "Demo"
+      },
+      "identifier": "https://demo.eduvpn.nl/",
+      "profiles": {
+        "map": {
+          "internet": {
+            "display_name": {
+              "en": "Internet"
+            },
+            "supported_protocols": [
+              1,
+              2
+            ]
+          },
+          "internet-split": {
+            "display_name": {
+              "en": "No rfc1918 routes"
+            },
+            "supported_protocols": [
+              1,
+              2
+            ]
+          }
+        },
+        "current": "internet"
+      },
+      "delisted": false
+    }
+  ]
+}
+```
+
+Note the difference with the previous JSON, the profiles are now initialized because we have gotten a configuration before.
+
+If the `/tmp/test` directory is removed (the argument that was passed to register), we get no servers again:
+
+```python
+import shutil
+shutil.rmtree("/tmp/test")
+common=edu.EduVPN("org.eduvpn.app.linux", "0.0.1", "/tmp/test")
+common.register(debug=True)
+print(common.get_servers())
+```
+
+gives `"{}"`, an empty JSON object string
diff --git a/docs/src/api/overview/README.md b/docs/src/api/overview/README.md
deleted file mode 100644
index 8af056a6..00000000
--- a/docs/src/api/overview/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# API overview
-
-This section defines the API in high-level, we explain what functions there are, what their use is and what a typical flow is for creating an eduVPN client with this library. The language specific documentation will be given in separate sections.
-
-## Note on types and names
-This section acts as an introduction to the API, as such this section will e.g. only give general typing information for the arguments and return values. Please read the language specific API documentation as well. To give an example, we will often say that an `Error` is returned. For Go this is the `error` type, whereas for Python this is simply a string with the error message.
-
-Additionally, the name of the function described will not be stated exactly as this has language specific differences. For example in Go we use the camel case construct, whereas for python snake case is used. E.g. compare `GetConnectConfig` (Go) and `get_connect_config` (Python)
diff --git a/docs/src/api/overview/connecting.md b/docs/src/api/overview/connecting.md
deleted file mode 100644
index c2a82a08..00000000
--- a/docs/src/api/overview/connecting.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Connecting/Disconnecting
-## Summary
-Name: `Set Connected` and `Set Disconnected`
-
-Arguments: None
-
-Returns: `Error`
-
-Used to signal to the FSM that we're connected/disconnected to the VPN
-
-## Detailed information
-This function is used to set the internal FSM state to connected. As the library does not actually connect to a VPN server, as this is platform specific, this must be called by the client to signal to the library that the user is connected to the VPN. If the FSM does not have a transition to the Connected state it will signal this with a returned error.
-
-The same function is used to signal the FSM that the VPN is disconnected.
diff --git a/docs/src/api/overview/deregistering.md b/docs/src/api/overview/deregistering.md
deleted file mode 100644
index 0503dbe1..00000000
--- a/docs/src/api/overview/deregistering.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Deregistering
-## Summary
-name: `Deregister`
-
-Arguments: None
-
-Returns: Nothing
-
-Used to cleanup the library by deregistering the client and saving the config files
-## Detailed information
-The deregister method is used to cleanup the library. It should be called when the client closes. This also saves the state to the directory that was passed in the [Register](./registering.html) method.
diff --git a/docs/src/api/overview/discovery.md b/docs/src/api/overview/discovery.md
deleted file mode 100644
index 4004ec45..00000000
--- a/docs/src/api/overview/discovery.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# Discovery
-## Summary
-Name: `Get Disco Servers` and `Get Disco Organizations`
-
-Arguments: None
-
-Returns: `structure for servers/organizations` and `Error for servers/organizations`
-
-Note: Depending on the wrapper they may be combined into one function, the return value of this function is then the following:
-`organizations, error for organizations, servers, errors for servers`
-
-Used to obtain the servers and organizations list from the discovery server.
-## Detailed information
-Discovery is the aspect of eduVPN that allows a client to gather all the servers and organizations it can connect to. For this a discovery server is used, which is registered as `https://disco.eduvpn.org` in the library. We refer to the [official eduVPN documentation](https://github.com/eduvpn/documentation/blob/v3/SERVER_DISCOVERY.md) to learn more about the exact way that these organizations and servers are structured.
-
-The data that this returns must be used by the client to build an UI. It is common for clients that the discovery functions get called on startup of the client. Note that there can be an error in retrieving the newest version of the servers/organizations. However, this library's goal is to ensure that a version is always available. Thus, a local copy is distributed with this library in the future.
-
-This library also internally looks at the version of the servers and organizations such that rollbacks attacks are prevented. The client does not have to do any additional checks for this.
diff --git a/docs/src/api/overview/flow.md b/docs/src/api/overview/flow.md
deleted file mode 100644
index 14b5a72f..00000000
--- a/docs/src/api/overview/flow.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Typical flow
-A typical flow of creating a client is calling the methods that we talked about in order that we introduced them:
-
-- The client starts, it registers with the library
-- A list of discovery servers/organizations is obtained using the library
-- The client selects an URL to connect to and calls the function to get an OpenVPN/Wireguard config from the library
-- The client uses the OS specific libraries and programs to use the OpenVPN/Wireguard config to establish a tunnel and calls the function to connect or disconnect
-- When the client is done it calls the deregister method to save all configuration and clean up
diff --git a/docs/src/api/overview/getconfig.md b/docs/src/api/overview/getconfig.md
deleted file mode 100644
index 2a057844..00000000
--- a/docs/src/api/overview/getconfig.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Getting an OpenVPN/Wireguard config
-## Summary
-name: `Get Config Institute Access`, `Get Config Custom Server`  and `Get Config Secure Internet`
-
-| Arguments  | Description                                     | type     |
-| ---------  | ----------------------------------------------- | -------- |
-| Identifier | The url/org ID of the VPN server to connect to  | string   |
-| Prefer TCP | Whether or not to prefer the use of TCP         | string   |
-
-Returns: `OpenVPN/Wireguard config (string)` `wireguard/openvpn type (string)`, `Error`
-
-Used to obtain the OpenVPN/Wireguard config
-
-## Detailed information
-
-To get a configuration that is used to actually establish a tunnel with the VPN server, we have the Get Config function for Institute Access, Custom Servers and Secure Internet (the exact name depends on the language you're using) function in the library. This function has two parameters *URL* and *Prefer TCP*.
-
-*URL/OrgID* is the base URL of the server to connect to
-e.g. `nl.eduvpn.org` in case of Institute Access or a Custom Server. In case of Secure Internet, the identifier is the Organization ID. Use the correct function to indicate if it is an Institute Access server or a Secure Internet server. 
-
-The *Prefer TCP* flag is a boolean that indicates whether or not we want to prefer TCP to connect over the VPN. This flag is useful if the user has enabled e.g. a setting that prefers the use of TCP, which is only supported by OpenVPN. Note that this setting may be ignored by the server.
-
-This function takes care of OAuth which has certain callbacks with data. Additionally, there are also callbacks that need to be registered for selecting the right profile to connect to. These callbacks will be explained now.
-
-The data that this function returns is the OpenVPN/Wireguard config as a string, the type of config (a string: "wireguard" or "openvpn") and an error if present.
-
-### Callback: OAuth started (OAuth_Started)
-
-OAuth has an important callback which is used to obtain the authorization URL by the client. This client needs to open this authorization URL in a web browser such that the user can authenticate with the VPN portal and then authorize the client to obtain OpenVPN/Wireguard configs.
-
-The callback for this is triggered whenever the OAuth Started state is triggered. The data which this callback has is the authorization url that needs to be opened in the web browser.
-
-The format of the authorization URL is e.g. this:
-
-`https://eduvpn.example.com/vpn-user-portal/oauth/authorize?client_id=org.eduvpn.app.linux&code_challenge=DsmGyWFBkvDXiIO33Fs40Z0fn4pxtzDCW2jKvAMptBg&code_challenge_method=S256&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2Fcallback&response_type=code&scope=config&state=vha2Krx-HpOyvFkWsWYmey0jrHQ6bnb06PQ6zBXX_bg`
-
-This callback can be cancelled by using a `Cancel OAuth` function.
-
-### Callback: Selecting a profile (Ask_Profile)
-
-Another aspect that needs to be taken into account is the fact that there can be multiple profiles that a client can connect to. When the function gets called for obtaining an OpenVPN/Wireguard configuration, it asks the client which profile it wants to connect to using the callback that gets triggered on the Ask Profile state. The data is the list of profiles
-For actually selecting the profile, there is a separate function which takes care of this. This function takes as only argument the profile ID as a string.
diff --git a/docs/src/api/overview/registering.md b/docs/src/api/overview/registering.md
deleted file mode 100644
index 5c406149..00000000
--- a/docs/src/api/overview/registering.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Registering a client
-## Summary
-Name: `Register`
-
-| Arguments | Description                             | type     |
-| --------- | --------------------------------------  | -------- |
-| Name      | Name of the client                      | string   |
-| Directory | Path to save logging and state          | string   |
-| Callback  | Function to be used as the FSM callback | function |
-| Debug     | Indicates whether or not to configure debugging capabilities. See [this section](../../../gettingstarted/debugging/index.html) for more information on debugging. | boolean |
-
-Returns: `Error`
-
-Used as initialization function of the library
-## Detailed information
-This library is made to build eduVPN clients. To create such a client, the register method is used. This method takes a *name*, *directory* and *callback*. This method needs to be called whenever a client wants to use this library. If this method is not called then the remaining methods will not be available to use.
-
-The *name* is the name of the client, also used as a client ID for OAuth. In general the name is the following for each official eduVPN client (documented [here](https://git.sr.ht/~fkooman/vpn-user-portal/tree/v3/item/src/OAuth/ClientDb.php)):
-
-
-| Platform | Client ID                |
-| -------- | ------------------------ |
-| Linux    | `org.eduvpn.app.linux`   |
-| Windows  | `org.eduvpn.app.windows` |
-| MacOS    | `org.eduvpn.app.macos`   |
-| Android  | `org.eduvpn.app.android` |
-| iOS      | `org.eduvpn.app.ios`     |
-
-The *directory* is the file path where logging and config files are stored. The library creates this directory if it doesn't exist. This can be an absolute or relative path. We recommend to use an absolute path to ensure that the right directory is chosen.
-
-The *callback* is the function that gets called when the internal Finite State Machine switches state. This callback function must consist of three arguments
-
-- Old state: The old state as a string, which is the current FSM state before the transition. See [FSM states](../../gettingstarted/debugging/fsm.html#state-explanation) for a list of states.
-- New state: The current state for the FSM after the transition, also a string. See [FSM states](../../gettingstarted/debugging/fsm.html#state-explanation) for a list of states.
-- Data: The data that gets sent by the library as a string. Most common this is JSON data to build the UI or in case of OAuth it is the authorization URL that needs to be opened by the browser. When there is no data this is an empty string.
diff --git a/docs/src/api/python/README.md b/docs/src/api/python/README.md
deleted file mode 100644
index 6bc66b62..00000000
--- a/docs/src/api/python/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# Python
-As the Go library is build as a *shared* library, it can be loaded by other languages. We have created wrapper code for Python to use this library. The api documentation can be found [here](./rtd/index.html). In the next chapter we will give a similar example to the Go example.
diff --git a/docs/src/api/python/example.md b/docs/src/api/python/example.md
deleted file mode 100644
index d4f6c08e..00000000
--- a/docs/src/api/python/example.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Example with comments
-
-This is an example that can also be found [in the repository](https://github.com/eduvpn/eduvpn-common/blob/main/wrappers/python/main.py). It gets a config from an Institute Access server with support for multiple profiles.
-
-```python
-{{#include ../../../../wrappers/python/example/main.py}}
-```
diff --git a/docs/src/api/statemachine.md b/docs/src/api/statemachine.md
new file mode 100644
index 00000000..29c05492
--- /dev/null
+++ b/docs/src/api/statemachine.md
@@ -0,0 +1,178 @@
+# Finite state machine
+
+The eduvpn-common library uses a finite state machine internally to keep track of which state the client is in and to communicate data callbacks (e.g. to communicate the Authorization URL in the OAuth process to the client).
+
+## Viewing the FSM
+To view the FSM in an image, register to the library with in debug mode. This
+outputs the graph with a `.graph` extension in the client-specified
+config directory. The format of this
+graph is from [Mermaid](https://mermaid-js.github.io/mermaid/#/). You
+can convert this to an image using the [Mermaid command-line client](https://github.com/mermaid-js/mermaid-cli) installed or from the Mermaid web site, the [Mermaid Live Editor](https://mermaid.live)
+
+## FSM example
+The following is an example of the FSM when the client has obtained a Wireguard/OpenVPN configuration from an eduVPN server
+
+<div class="statemachine">
+
+```mermaid
+graph TD
+
+style Deregistered fill:cyan
+Deregistered(Deregistered) -->|Client registers| No_Server
+
+style No_Server fill:white
+No_Server(No_Server) -->|Reload list| No_Server
+
+style No_Server fill:white
+No_Server(No_Server) -->|The client wants to ask for a location on the main screen| Ask_Location
+
+style No_Server fill:white
+No_Server(No_Server) -->|User clicks a server in the UI| Loading_Server
+
+style No_Server fill:white
+No_Server(No_Server) -->|The VPN is still active| Connected
+
+style Ask_Location fill:white
+Ask_Location(Ask_Location) -->|Location chosen| Chosen_Location
+
+style Ask_Location fill:white
+Ask_Location(Ask_Location) -->|Go back or Error| No_Server
+
+style Ask_Location fill:white
+Ask_Location(Ask_Location) -->|Go back or Error| Got_Config
+
+style Chosen_Location fill:white
+Chosen_Location(Chosen_Location) -->|Server has been chosen| Chosen_Server
+
+style Chosen_Location fill:white
+Chosen_Location(Chosen_Location) -->|Go back or Error| No_Server
+
+style Chosen_Location fill:white
+Chosen_Location(Chosen_Location) -->|Go back or Error| Got_Config
+
+style Loading_Server fill:white
+Loading_Server(Loading_Server) -->|Server info loaded| Chosen_Server
+
+style Loading_Server fill:white
+Loading_Server(Loading_Server) -->|User chooses a Secure Internet server but no location is configured| Ask_Location
+
+style Loading_Server fill:white
+Loading_Server(Loading_Server) -->|Go back or Error| No_Server
+
+style Loading_Server fill:white
+Loading_Server(Loading_Server) -->|Go back or Error| Got_Config
+
+style Chosen_Server fill:white
+Chosen_Server(Chosen_Server) -->|Found tokens in config| Authorized
+
+style Chosen_Server fill:white
+Chosen_Server(Chosen_Server) -->|No tokens found in config| OAuth_Started
+
+style Chosen_Server fill:white
+Chosen_Server(Chosen_Server) -->|Go back or Error| No_Server
+
+style Chosen_Server fill:white
+Chosen_Server(Chosen_Server) -->|Go back or Error| Got_Config
+
+style OAuth_Started fill:white
+OAuth_Started(OAuth_Started) -->|User authorizes with browser| Authorized
+
+style OAuth_Started fill:white
+OAuth_Started(OAuth_Started) -->|Go back or Error| No_Server
+
+style OAuth_Started fill:white
+OAuth_Started(OAuth_Started) -->|Go back or Error| Got_Config
+
+style Authorized fill:white
+Authorized(Authorized) -->|Re-authorize with OAuth| OAuth_Started
+
+style Authorized fill:white
+Authorized(Authorized) -->|Client requests a config| Request_Config
+
+style Authorized fill:white
+Authorized(Authorized) -->|Load the server again| Loading_Server
+
+style Authorized fill:white
+Authorized(Authorized) -->|Go back or Error| No_Server
+
+style Authorized fill:white
+Authorized(Authorized) -->|Go back or Error| Got_Config
+
+style Request_Config fill:white
+Request_Config(Request_Config) -->|Multiple profiles found and no profile chosen| Ask_Profile
+
+style Request_Config fill:white
+Request_Config(Request_Config) -->|Only one profile or profile already chosen| Chosen_Profile
+
+style Request_Config fill:white
+Request_Config(Request_Config) -->|Re-authorize| OAuth_Started
+
+style Request_Config fill:white
+Request_Config(Request_Config) -->|Go back or Error| No_Server
+
+style Request_Config fill:white
+Request_Config(Request_Config) -->|Go back or Error| Got_Config
+
+style Ask_Profile fill:white
+Ask_Profile(Ask_Profile) -->|Cancel or Error| No_Server
+
+style Ask_Profile fill:white
+Ask_Profile(Ask_Profile) -->|Profile has been chosen| Chosen_Profile
+
+style Ask_Profile fill:white
+Ask_Profile(Ask_Profile) -->|Go back or Error| No_Server
+
+style Ask_Profile fill:white
+Ask_Profile(Ask_Profile) -->|Go back or Error| Got_Config
+
+style Chosen_Profile fill:white
+Chosen_Profile(Chosen_Profile) -->|Config has been obtained| Got_Config
+
+style Chosen_Profile fill:white
+Chosen_Profile(Chosen_Profile) -->|Go back or Error| No_Server
+
+style Chosen_Profile fill:white
+Chosen_Profile(Chosen_Profile) -->|Go back or Error| Got_Config
+
+style Got_Config fill:white
+Got_Config(Got_Config) -->|Choose a new server| No_Server
+
+style Got_Config fill:white
+Got_Config(Got_Config) -->|Get a new configuration| Loading_Server
+
+style Got_Config fill:white
+Got_Config(Got_Config) -->|VPN is connecting| Connecting
+
+style Connecting fill:white
+Connecting(Connecting) -->|Go back or Error| Got_Config
+
+style Connecting fill:white
+Connecting(Connecting) -->|Done connecting| Connected
+
+style Disconnecting fill:white
+Disconnecting(Disconnecting) -->|Done disconnecting| Got_Config
+
+style Disconnecting fill:white
+Disconnecting(Disconnecting) -->|Go back or Error| Connected
+
+style Connected fill:white
+Connected(Connected) -->|The VPN is disconnecting| Disconnecting
+```
+
+</div>
+
+The current state is highlighted in the <span style="color:cyan">cyan</span> color.
+
+## State explanation
+
+For the explanation of what all the different states mean, see the [client documentation](https://github.com/eduvpn/eduvpn-common/blob/v2/client/fsm.go#L14-L50)
+
+## States that ask data
+
+In eduvpn-common, there are certain states that require attention from the client.
+
+- OAuth Started: A state that must be handled by the client. How a client can 'handle' this state, we will see in the next section. In this state, the client must open the webbrowser with the authorization URL to complete to OAuth process
+- Ask Profile: The state that asks for a profile selection to the client. Reply to this state by using a "cookie" and the CookieReply function. What this means will be discussed in the Python client example too
+- Ask Location: Same for ask profile but for selecting a secure internet location. Only called if one must be chosen, e.g. due to a selection that is no longer valid
+
+The rest of the states are miscellaneous states, meaning that the client can handle them however it wants to. However, it can be useful to handle most state transitions to e.g. show loading screens or for logging and debugging purposes.
diff --git a/docs/src/api/typicalflow.md b/docs/src/api/typicalflow.md
new file mode 100644
index 00000000..22603525
--- /dev/null
+++ b/docs/src/api/typicalflow.md
@@ -0,0 +1,13 @@
+# Typical flow for a client
+> **_NOTE:_** This uses the function names that are defined in the exports file in Go. For your own wrapper/the Python wrapper they are different. But the general flow is the same
+1. The client starts up. It calls the `Register` function that communicates with the library that it has initialized
+2. It gets the list of servers using `ServerList`
+3. When the user selects a server to connect to in the UI, it calls the `GetConfig` to get a VPN configuration for this server. This function transitions the state machine multiple times. The client uses these state transitions for logging or even updating the UI. The client then connects
+	- New feature in eduvpn-common: Check if the VPN can reach the gateway after the client is connected by calling `StartFailover`
+4. If the client has no servers, or it wants to add a new server, the client calls `DiscoOrganizations` and `DiscoServers` to get the discovery files from the library. This even returns cached copies if the organizations or servers should not have been updated [according to the documentation](https://docs.eduvpn.org/server/v3/server-discovery.html)
+	- From this discovery list, it calls `AddServer` to add the server to the internal server list of eduvpn-common. This also calls necessary state transitions, e.g. for authorizing the server. The next call to `ServerList` then has this server included
+	- It can then get a configuration for this server like we have explained in *step 3*
+5. When a configuration has been obtained, the internal state has changed and the client can get the current server that was configured using `CurrentServer`. `CurrentServer` can also be called after startup if a server was previously set as the current server.
+6. When the VPN disconnects, the client calls `Cleanup` so that the server resources are cleaned up by calling the `/disconnect` endpoint
+7. A server can be removed with the `RemoveServer` function
+8. When the client is done, it calls `Deregister` such that the most up to date internal state is saved to disk. Note that eduvpn-common also saves the internal state .e.g. after obtaining a VPN configuration
diff --git a/docs/src/api/wheretofinddocs.md b/docs/src/api/wheretofinddocs.md
new file mode 100644
index 00000000..cbf4a67b
--- /dev/null
+++ b/docs/src/api/wheretofinddocs.md
@@ -0,0 +1,6 @@
+# Where to find API docs
+The API documentation, depends on which wrapper you are using. If you're writing a wrapper yourself, or want some background information on how it works internally, you can find function docs in the [exports/exports.go](https://github.com/eduvpn/eduvpn-common/blob/v2/exports/exports.go) file.
+
+This file is commented using Go comment style. It gives a basic of what the function does, what it returns and what type of arguments you should pass to it. The API documentation for the Python wrapper can be [found here](https://eduvpn.github.io/eduvpn-common/api/python/rtd/index.html).
+
+There is also a Go API that is defined in the [client package](https://github.com/eduvpn/eduvpn-common/tree/v2/client). However, this is not the primary use case for the library.
diff --git a/docs/src/gettingstarted/building.md b/docs/src/gettingstarted/building.md
deleted file mode 100644
index e69de29b..00000000
diff --git a/docs/src/gettingstarted/building/example.md b/docs/src/gettingstarted/building/example.md
index 1f541b2f..38242f7a 100644
--- a/docs/src/gettingstarted/building/example.md
+++ b/docs/src/gettingstarted/building/example.md
@@ -23,6 +23,6 @@ make -C wrappers/python
 
 5. Install the wheel using pip
 ```bash
-pip install wrappers/python/dist/eduvpncommon-1.1.2-py3-none-linux_x86_64.whl
+pip install wrappers/python/dist/eduvpncommon-2.0.0-py3-none-linux_x86_64.whl
 ```
 Note that the name of your wheel changes on the platform and version.
diff --git a/docs/src/gettingstarted/debugging/README.md b/docs/src/gettingstarted/debugging/README.md
deleted file mode 100644
index 0b402a95..00000000
--- a/docs/src/gettingstarted/debugging/README.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# Debugging
-
-To debug this library, e.g. to discover bugs or to see how it works internally, the library comes with a few nice additions.
-
-## The debug variable
-To enable debugging, set debugging to True in the method that registers the code with the library (see [API](../api/index.html)). This sets the logging level to `INFO` (meaning show all messages), and generates a Finite State Machine (FSM) `.graph` file. We explain in more detail what these two components (logging and FSM) exactly are and how they can be used.
diff --git a/docs/src/gettingstarted/debugging/fsm.md b/docs/src/gettingstarted/debugging/fsm.md
deleted file mode 100644
index 15b1a120..00000000
--- a/docs/src/gettingstarted/debugging/fsm.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Finite state machine
-
-The eduvpn-common library uses a finite state machine internally to keep track of which state the client is in and to communicate data callbacks (e.g. to communicate the Authorization URL in the OAuth process to the client).
-
-## Viewing the FSM
-To view the FSM in an image, set the debug variable to `True`. This
-outputs the graph with a `.graph` extension in the client-specified
-config directory (See [API](../../api/index.html)). The format of this
-graph is from [Mermaid](https://mermaid-js.github.io/mermaid/#/). You
-can convert this to an image using the [Mermaid command-line client](https://github.com/mermaid-js/mermaid-cli) installed or from the Mermaid web site, the [Mermaid Live Editor](https://mermaid.live)
-
-## FSM example
-The following is an example of the FSM when the client has obtained a Wireguard/OpenVPN configuration from an eduVPN server
-
-![](./fsm_example.svg)
-
-The current state is highlighted in the <span style="color:cyan">cyan</span> color.
-
-## State explanation
-The states mean the following:
-- `Deregistered`: the app is not registered with the wrapper
-- `No_Server`: means the user has not chosen a server yet
-- `Ask_Location`: the user selected a Secure Internet server but needs to choose a location
-- `Search_Server`: the user is currently selecting a server in the UI
-- `Loading_Server`: means we are loading the server details
-- `Chosen_Server`: means the user has chosen a server to connect to
-- `OAuth_Started`: means the OAuth process has started
-- `Authorized`: means the OAuth process has finished and the user is now authorized with the server
-- `Request_Config`: the user has requested a config for connecting
-- `Ask_Profile`: the go code is asking for a profile selection from the UI
-- `Disconnected`: the user has gotten a config for a server but is not connected yet
-- `Disconnecting`: the OS is disconnecting and the Go code is doing the /disconnect
-- `Connecting`: the OS is establishing a connection to the server
-- `Connected`: the user has been connected to the server.
diff --git a/docs/src/gettingstarted/debugging/fsm_example.svg b/docs/src/gettingstarted/debugging/fsm_example.svg
deleted file mode 100644
index 8e20808f..00000000
--- a/docs/src/gettingstarted/debugging/fsm_example.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg viewBox="0 0 3237.51953125 1220" style="max-width: 3237.52px; background-color: white;" aria-labelledby="chart-title-mermaid-1672864142462 chart-desc-mermaid-1672864142462" role="img" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="mermaid-1672864142462"><title id="chart-title-mermaid-1672864142462"/><desc id="chart-desc-mermaid-1672864142462"/><style>#mermaid-1672864142462 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-1672864142462 .error-icon{fill:#552222;}#mermaid-1672864142462 .error-text{fill:#552222;stroke:#552222;}#mermaid-1672864142462 .edge-thickness-normal{stroke-width:2px;}#mermaid-1672864142462 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-1672864142462 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-1672864142462 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-1672864142462 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-1672864142462 .marker{fill:#333333;stroke:#333333;}#mermaid-1672864142462 .marker.cross{stroke:#333333;}#mermaid-1672864142462 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-1672864142462 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-1672864142462 .cluster-label text{fill:#333;}#mermaid-1672864142462 .cluster-label span{color:#333;}#mermaid-1672864142462 .label text,#mermaid-1672864142462 span{fill:#333;color:#333;}#mermaid-1672864142462 .node rect,#mermaid-1672864142462 .node circle,#mermaid-1672864142462 .node ellipse,#mermaid-1672864142462 .node polygon,#mermaid-1672864142462 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-1672864142462 .node .label{text-align:center;}#mermaid-1672864142462 .node.clickable{cursor:pointer;}#mermaid-1672864142462 .arrowheadPath{fill:#333333;}#mermaid-1672864142462 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-1672864142462 .flowchart-link{stroke:#333333;fill:none;}#mermaid-1672864142462 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-1672864142462 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-1672864142462 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-1672864142462 .cluster text{fill:#333;}#mermaid-1672864142462 .cluster span{color:#333;}#mermaid-1672864142462 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-1672864142462 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"/><g class="edgePaths"><g style="opacity: 1;" id="L-Deregistered-No_Server" class="edgePath LS-Deregistered LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead311)" d="M1539.1640625,46L1539.1640625,51.666666666666664C1539.1640625,57.333333333333336,1539.1640625,68.66666666666667,1539.1640625,80C1539.1640625,91.33333333333333,1539.1640625,102.66666666666667,1539.1640625,108.33333333333333L1539.1640625,114" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead311"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-No_Server-No_Server" class="edgePath LS-No_Server LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead312)" d="M1587.40625,123.49923070693984L1595.4453125,121.91602558911654C1603.484375,120.33282047129323,1619.5625,117.16641023564661,1629.611328125,115.58320511782331C1639.66015625,114,1643.6796875,114,1647.69921875,117.16666666666667C1651.71875,120.33333333333333,1655.73828125,126.66666666666667,1655.73828125,133C1655.73828125,139.33333333333334,1651.71875,145.66666666666666,1647.69921875,148.83333333333334C1643.6796875,152,1639.66015625,152,1629.611328125,150.41679488217667C1619.5625,148.83358976435338,1603.484375,145.66717952870678,1595.4453125,144.08397441088346L1587.40625,142.50076929306016" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead312"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-No_Server-Loading_Server" class="edgePath LS-No_Server LE-Loading_Server"><path style="fill:none" marker-end="url(#arrowhead313)" d="M1490.921875,139.50536191697228L1433.4563802083333,147.25446826414358C1375.9908854166667,155.00357461131486,1261.0598958333333,170.50178730565742,1213.267160475629,183.91756031949538C1165.4744251179245,197.33333333333334,1184.819943985849,208.66666666666666,1194.4927034198113,214.33333333333334L1204.1654628537735,220" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead313"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-No_Server-Chosen_Server" class="edgePath LS-No_Server LE-Chosen_Server"><path style="fill:none" marker-end="url(#arrowhead314)" d="M1490.921875,137.60600388436964L1406.4440104166667,145.67166990364137C1321.9661458333333,153.7373359229131,1153.0104166666667,169.86866796145657,1068.5325520833333,186.76766731406164C984.0546875,203.66666666666666,984.0546875,221.33333333333334,984.0546875,239C984.0546875,256.6666666666667,984.0546875,274.3333333333333,984.0546875,292C984.0546875,309.6666666666667,984.0546875,327.3333333333333,984.0546875,345C984.0546875,362.6666666666667,984.0546875,380.3333333333333,1002.3531593946541,394.8333333333333C1020.6516312893082,409.3333333333333,1057.2485750786163,420.6666666666667,1075.5470469732704,426.3333333333333L1093.8455188679245,432" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead314"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-No_Server-Search_Server" class="edgePath LS-No_Server LE-Search_Server"><path style="fill:none" marker-end="url(#arrowhead315)" d="M1490.921875,134.88994952819837L1273.4856770833333,143.40829127349863C1056.0494791666667,151.92663301879892,621.1770833333334,168.96331650939945,403.7408854166667,186.31499158803308C186.3046875,203.66666666666666,186.3046875,221.33333333333334,186.3046875,239C186.3046875,256.6666666666667,186.3046875,274.3333333333333,186.3046875,292C186.3046875,309.6666666666667,186.3046875,327.3333333333333,186.3046875,345C186.3046875,362.6666666666667,186.3046875,380.3333333333333,186.3046875,398C186.3046875,415.6666666666667,186.3046875,433.3333333333333,186.3046875,451C186.3046875,468.6666666666667,186.3046875,486.3333333333333,186.3046875,504C186.3046875,521.6666666666666,186.3046875,539.3333333333334,186.3046875,557C186.3046875,574.6666666666666,186.3046875,592.3333333333334,186.3046875,610C186.3046875,627.6666666666666,186.3046875,645.3333333333334,186.3046875,663C186.3046875,680.6666666666666,186.3046875,698.3333333333334,186.3046875,716C186.3046875,733.6666666666666,186.3046875,751.3333333333334,186.3046875,769C186.3046875,786.6666666666666,186.3046875,804.3333333333334,186.3046875,822C186.3046875,839.6666666666666,186.3046875,857.3333333333334,186.3046875,875C186.3046875,892.6666666666666,186.3046875,910.3333333333334,186.3046875,928C186.3046875,945.6666666666666,186.3046875,963.3333333333334,186.3046875,981C186.3046875,998.6666666666666,186.3046875,1016.3333333333334,396.2259114583333,1033.5769293989922C606.1471354166666,1050.8205254646512,1025.9895833333333,1067.6410509293025,1235.9108072916667,1076.051313661628L1445.83203125,1084.4615763939537" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead315"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-No_Server-Connected" class="edgePath LS-No_Server LE-Connected"><path style="fill:none" marker-end="url(#arrowhead316)" d="M1587.40625,135.77449272413602L1732.9576822916667,144.1454106034467C1878.5091145833333,152.51632848275736,2169.6119791666665,169.2581642413787,2315.1634114583335,186.46241545402268C2460.71484375,203.66666666666666,2460.71484375,221.33333333333334,2460.71484375,239C2460.71484375,256.6666666666667,2460.71484375,274.3333333333333,2460.71484375,292C2460.71484375,309.6666666666667,2460.71484375,327.3333333333333,2460.71484375,345C2460.71484375,362.6666666666667,2460.71484375,380.3333333333333,2460.71484375,398C2460.71484375,415.6666666666667,2460.71484375,433.3333333333333,2460.71484375,451C2460.71484375,468.6666666666667,2460.71484375,486.3333333333333,2460.71484375,504C2460.71484375,521.6666666666666,2460.71484375,539.3333333333334,2460.71484375,557C2460.71484375,574.6666666666666,2460.71484375,592.3333333333334,2460.71484375,610C2460.71484375,627.6666666666666,2460.71484375,645.3333333333334,2460.71484375,663C2460.71484375,680.6666666666666,2460.71484375,698.3333333333334,2460.71484375,716C2460.71484375,733.6666666666666,2460.71484375,751.3333333333334,2460.71484375,769C2460.71484375,786.6666666666666,2460.71484375,804.3333333333334,2460.71484375,822C2460.71484375,839.6666666666666,2460.71484375,857.3333333333334,2460.71484375,875C2460.71484375,892.6666666666666,2460.71484375,910.3333333333334,2460.71484375,928C2460.71484375,945.6666666666666,2460.71484375,963.3333333333334,2460.71484375,981C2460.71484375,998.6666666666666,2460.71484375,1016.3333333333334,2484.162109375,1031.728670338208C2507.609375,1047.124007343083,2554.50390625,1060.2480146861658,2577.951171875,1066.8100183577073L2601.3984375,1073.3720220292487" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead316"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-No_Server-Ask_Location" class="edgePath LS-No_Server LE-Ask_Location"><path style="fill:none" marker-end="url(#arrowhead317)" d="M1490.921875,135.14122149890412L1299.9453125,143.61768458242008C1108.96875,152.09414766593608,727.015625,169.04707383296804,536.0390625,186.35687024981735C345.0625,203.66666666666666,345.0625,221.33333333333334,345.0625,239C345.0625,256.6666666666667,345.0625,274.3333333333333,381.8190104166667,290.15985640968114C418.5755208333333,305.9863794860289,492.0885416666667,319.9727589720579,528.8450520833334,326.96594871507233L565.6015625,333.9591384580868" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead317"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Ask_Location-Chosen_Server" class="edgePath LS-Ask_Location LE-Chosen_Server"><path style="fill:none" marker-end="url(#arrowhead318)" d="M681.6640625,352.0110414588973L745.1067708333334,359.6758678824144C808.5494791666666,367.34069430593155,935.4348958333334,382.6703471529658,1008.8080532625786,396.0018402431495C1082.181210691824,409.3333333333333,1102.0421088836479,420.6666666666667,1111.9725579795597,426.3333333333333L1121.9030070754718,432" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead318"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Ask_Location-No_Server" class="edgePath LS-Ask_Location LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead319)" d="M681.6640625,332.1324072560876L711.8294270833334,325.44367271340633C741.9947916666666,318.7549381707251,802.3255208333334,305.3774690853625,832.4908854166666,289.8554012093479C862.65625,274.3333333333333,862.65625,256.6666666666667,862.65625,239C862.65625,221.33333333333334,862.65625,203.66666666666666,967.3671875,186.62991042386025C1072.078125,169.5931541810539,1281.5,153.18630836210778,1386.2109375,144.98288545263475L1490.921875,136.77946254316169" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead319"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Ask_Location-Search_Server" class="edgePath LS-Ask_Location LE-Search_Server"><path style="fill:none" marker-end="url(#arrowhead320)" d="M623.6328125,364L623.6328125,369.6666666666667C623.6328125,375.3333333333333,623.6328125,386.6666666666667,623.6328125,401.1666666666667C623.6328125,415.6666666666667,623.6328125,433.3333333333333,623.6328125,451C623.6328125,468.6666666666667,623.6328125,486.3333333333333,623.6328125,504C623.6328125,521.6666666666666,623.6328125,539.3333333333334,623.6328125,557C623.6328125,574.6666666666666,623.6328125,592.3333333333334,623.6328125,610C623.6328125,627.6666666666666,623.6328125,645.3333333333334,623.6328125,663C623.6328125,680.6666666666666,623.6328125,698.3333333333334,623.6328125,716C623.6328125,733.6666666666666,623.6328125,751.3333333333334,623.6328125,769C623.6328125,786.6666666666666,623.6328125,804.3333333333334,623.6328125,822C623.6328125,839.6666666666666,623.6328125,857.3333333333334,623.6328125,875C623.6328125,892.6666666666666,623.6328125,910.3333333333334,623.6328125,928C623.6328125,945.6666666666666,623.6328125,963.3333333333334,623.6328125,981C623.6328125,998.6666666666666,623.6328125,1016.3333333333334,760.666015625,1033.367998364968C897.69921875,1050.4026633966025,1171.765625,1066.8053267932053,1308.798828125,1075.0066584915064L1445.83203125,1083.2079901898078" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead320"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Search_Server-Loading_Server" class="edgePath LS-Search_Server LE-Loading_Server"><path style="fill:none" marker-end="url(#arrowhead321)" d="M1572.55078125,1076.038723908858L1613.0501302083333,1069.0322699240483C1653.5494791666667,1062.0258159392386,1734.5481770833333,1048.0129079696192,1775.0475260416667,1032.1731206514762C1815.546875,1016.3333333333334,1815.546875,998.6666666666666,1815.546875,981C1815.546875,963.3333333333334,1815.546875,945.6666666666666,1815.546875,928C1815.546875,910.3333333333334,1815.546875,892.6666666666666,1815.546875,875C1815.546875,857.3333333333334,1815.546875,839.6666666666666,1815.546875,822C1815.546875,804.3333333333334,1815.546875,786.6666666666666,1815.546875,769C1815.546875,751.3333333333334,1815.546875,733.6666666666666,1815.546875,716C1815.546875,698.3333333333334,1815.546875,680.6666666666666,1815.546875,663C1815.546875,645.3333333333334,1815.546875,627.6666666666666,1815.546875,610C1815.546875,592.3333333333334,1815.546875,574.6666666666666,1815.546875,557C1815.546875,539.3333333333334,1815.546875,521.6666666666666,1815.546875,504C1815.546875,486.3333333333333,1815.546875,468.6666666666667,1815.546875,451C1815.546875,433.3333333333333,1815.546875,415.6666666666667,1815.546875,398C1815.546875,380.3333333333333,1815.546875,362.6666666666667,1815.546875,345C1815.546875,327.3333333333333,1815.546875,309.6666666666667,1730.1360677083333,293.0143871462532C1644.7252604166667,276.36210762583977,1473.9036458333333,260.7242152516795,1388.4928385416667,252.90526906459937L1303.08203125,245.08632287751922" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead321"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Search_Server-No_Server" class="edgePath LS-Search_Server LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead322)" d="M1572.55078125,1079.1570309007473L1633.3509114583333,1071.630859083956C1694.1510416666667,1064.1046872671648,1815.7513020833333,1049.0523436335825,1876.5514322916667,1032.692838483458C1937.3515625,1016.3333333333334,1937.3515625,998.6666666666666,1937.3515625,981C1937.3515625,963.3333333333334,1937.3515625,945.6666666666666,1937.3515625,928C1937.3515625,910.3333333333334,1937.3515625,892.6666666666666,1937.3515625,875C1937.3515625,857.3333333333334,1937.3515625,839.6666666666666,1937.3515625,822C1937.3515625,804.3333333333334,1937.3515625,786.6666666666666,1937.3515625,769C1937.3515625,751.3333333333334,1937.3515625,733.6666666666666,1937.3515625,716C1937.3515625,698.3333333333334,1937.3515625,680.6666666666666,1937.3515625,663C1937.3515625,645.3333333333334,1937.3515625,627.6666666666666,1937.3515625,610C1937.3515625,592.3333333333334,1937.3515625,574.6666666666666,1937.3515625,557C1937.3515625,539.3333333333334,1937.3515625,521.6666666666666,1937.3515625,504C1937.3515625,486.3333333333333,1937.3515625,468.6666666666667,1937.3515625,451C1937.3515625,433.3333333333333,1937.3515625,415.6666666666667,1937.3515625,398C1937.3515625,380.3333333333333,1937.3515625,362.6666666666667,1937.3515625,345C1937.3515625,327.3333333333333,1937.3515625,309.6666666666667,1937.3515625,292C1937.3515625,274.3333333333333,1937.3515625,256.6666666666667,1937.3515625,239C1937.3515625,221.33333333333334,1937.3515625,203.66666666666666,1879.02734375,187.0701976403495C1820.703125,170.47372861403232,1704.0546875,154.94745722806468,1645.73046875,147.18432153508084L1587.40625,139.421185842097" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead322"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Loading_Server-Chosen_Server" class="edgePath LS-Loading_Server LE-Chosen_Server"><path style="fill:none" marker-end="url(#arrowhead323)" d="M1236.59765625,258L1236.59765625,263.6666666666667C1236.59765625,269.3333333333333,1236.59765625,280.6666666666667,1236.59765625,295.1666666666667C1236.59765625,309.6666666666667,1236.59765625,327.3333333333333,1236.59765625,345C1236.59765625,362.6666666666667,1236.59765625,380.3333333333333,1227.8946786556605,394.8333333333333C1219.1917010613208,409.3333333333333,1201.7857458726414,420.6666666666667,1193.082768278302,426.3333333333333L1184.3797906839623,432" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead323"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Loading_Server-Ask_Location" class="edgePath LS-Loading_Server LE-Ask_Location"><path style="fill:none" marker-end="url(#arrowhead324)" d="M1170.11328125,244.5689247504337L1075.7376302083333,252.47410395869474C981.3619791666666,260.37928316695576,792.6106770833334,276.1896415834779,700.349167158019,289.7614874584056C608.0876572327044,303.3333333333333,612.3159394654089,314.6666666666667,614.430080581761,320.3333333333333L616.5442216981132,326" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead324"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Loading_Server-No_Server" class="edgePath LS-Loading_Server LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead325)" d="M1269.0298496462265,220L1278.7026090801887,214.33333333333334C1288.375368514151,208.66666666666666,1307.7208873820755,197.33333333333334,1344.7028916077045,184.84249897170503C1381.6848958333333,172.35166461007668,1436.3033854166667,158.70332922015336,1463.6126302083333,151.8791615251917L1490.921875,145.05499383023005" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead325"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Chosen_Server-Authorized" class="edgePath LS-Chosen_Server LE-Authorized"><path style="fill:none" marker-end="url(#arrowhead326)" d="M1181.9067659198113,470L1189.8721747248428,475.6666666666667C1197.837583529874,481.3333333333333,1213.7684011399372,492.6666666666667,1238.026127653302,504.24750960929623C1262.2838541666667,515.8283525519258,1294.8684895833333,527.6567051038518,1311.1608072916667,533.5708813798146L1327.453125,539.4850576557776" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead326"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Chosen_Server-OAuth_Started" class="edgePath LS-Chosen_Server LE-OAuth_Started"><path style="fill:none" marker-end="url(#arrowhead327)" d="M1108.6640625,470L1094.78515625,475.6666666666667C1080.90625,481.3333333333333,1053.1484375,492.6666666666667,1039.26953125,507.1666666666667C1025.390625,521.6666666666666,1025.390625,539.3333333333334,1025.390625,557C1025.390625,574.6666666666666,1025.390625,592.3333333333334,1025.390625,610C1025.390625,627.6666666666666,1025.390625,645.3333333333334,1025.390625,663C1025.390625,680.6666666666666,1025.390625,698.3333333333334,1025.390625,716C1025.390625,733.6666666666666,1025.390625,751.3333333333334,1025.390625,769C1025.390625,786.6666666666666,1025.390625,804.3333333333334,1025.390625,822C1025.390625,839.6666666666666,1025.390625,857.3333333333334,1025.390625,875C1025.390625,892.6666666666666,1025.390625,910.3333333333334,1058.0104166666667,925.8510340522362C1090.6302083333333,941.3687347711392,1155.8697916666667,954.7374695422783,1188.4895833333333,961.4218369278478L1221.109375,968.1062043134175" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead327"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-OAuth_Started-Authorized" class="edgePath LS-OAuth_Started LE-Authorized"><path style="fill:none" marker-end="url(#arrowhead328)" d="M1346.953125,964.7230886558627L1370.61328125,958.6025738798857C1394.2734375,952.4820591039085,1441.59375,940.2410295519543,1465.25390625,925.2871814426438C1488.9140625,910.3333333333334,1488.9140625,892.6666666666666,1488.9140625,875C1488.9140625,857.3333333333334,1488.9140625,839.6666666666666,1488.9140625,822C1488.9140625,804.3333333333334,1488.9140625,786.6666666666666,1488.9140625,769C1488.9140625,751.3333333333334,1488.9140625,733.6666666666666,1488.9140625,716C1488.9140625,698.3333333333334,1488.9140625,680.6666666666666,1488.9140625,663C1488.9140625,645.3333333333334,1488.9140625,627.6666666666666,1476.809748427673,613.1666666666666C1464.7054343553457,598.6666666666666,1440.4968062106918,587.3333333333334,1428.3924921383648,581.6666666666666L1416.2881780660377,576" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead328"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-OAuth_Started-No_Server" class="edgePath LS-OAuth_Started LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead329)" d="M1346.953125,972.0157958431992L1398.3313802083333,964.6798298693326C1449.7096354166667,957.3438638954661,1552.4661458333333,942.671931947733,1603.8444010416667,926.5026326405332C1655.22265625,910.3333333333334,1655.22265625,892.6666666666666,1655.22265625,875C1655.22265625,857.3333333333334,1655.22265625,839.6666666666666,1655.22265625,822C1655.22265625,804.3333333333334,1655.22265625,786.6666666666666,1655.22265625,769C1655.22265625,751.3333333333334,1655.22265625,733.6666666666666,1655.22265625,716C1655.22265625,698.3333333333334,1655.22265625,680.6666666666666,1655.22265625,663C1655.22265625,645.3333333333334,1655.22265625,627.6666666666666,1655.22265625,610C1655.22265625,592.3333333333334,1655.22265625,574.6666666666666,1655.22265625,557C1655.22265625,539.3333333333334,1655.22265625,521.6666666666666,1655.22265625,504C1655.22265625,486.3333333333333,1655.22265625,468.6666666666667,1655.22265625,451C1655.22265625,433.3333333333333,1655.22265625,415.6666666666667,1655.22265625,398C1655.22265625,380.3333333333333,1655.22265625,362.6666666666667,1655.22265625,345C1655.22265625,327.3333333333333,1655.22265625,309.6666666666667,1655.22265625,292C1655.22265625,274.3333333333333,1655.22265625,256.6666666666667,1655.22265625,239C1655.22265625,221.33333333333334,1655.22265625,203.66666666666666,1642.8138757861634,189.16666666666666C1630.405095322327,174.66666666666666,1605.5875343946543,163.33333333333334,1593.1787539308177,157.66666666666666L1580.7699734669811,152" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead329"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-OAuth_Started-Search_Server" class="edgePath LS-OAuth_Started LE-Search_Server"><path style="fill:none" marker-end="url(#arrowhead330)" d="M1284.03125,1000L1284.03125,1005.6666666666666C1284.03125,1011.3333333333334,1284.03125,1022.6666666666666,1310.998046875,1034.6809938527551C1337.96484375,1046.695321038844,1391.8984375,1059.3906420776877,1418.865234375,1065.7383025971096L1445.83203125,1072.0859631165317" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead330"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Authorized-OAuth_Started" class="edgePath LS-Authorized LE-OAuth_Started"><path style="fill:none" marker-end="url(#arrowhead331)" d="M1327.453125,567.6538211170421L1295.4895833333333,574.711517597535C1263.5260416666667,581.7692140780281,1199.5989583333333,595.884607039014,1167.6354166666667,611.7756368528403C1135.671875,627.6666666666666,1135.671875,645.3333333333334,1135.671875,663C1135.671875,680.6666666666666,1135.671875,698.3333333333334,1135.671875,716C1135.671875,733.6666666666666,1135.671875,751.3333333333334,1135.671875,769C1135.671875,786.6666666666666,1135.671875,804.3333333333334,1135.671875,822C1135.671875,839.6666666666666,1135.671875,857.3333333333334,1135.671875,875C1135.671875,892.6666666666666,1135.671875,910.3333333333334,1151.5341981132076,924.8333333333334C1167.396521226415,939.3333333333334,1199.1211674528302,950.6666666666666,1214.9834905660377,956.3333333333334L1230.8458136792453,962" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead331"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Authorized-Request_Config" class="edgePath LS-Authorized LE-Request_Config"><path style="fill:none" marker-end="url(#arrowhead332)" d="M1423.953125,560.5086395403679L1537.3854166666667,568.7571996169733C1650.8177083333333,577.0057596935786,1877.6822916666667,593.5028798467893,1996.6530316430817,607.4181065900613C2115.623771619497,621.3333333333334,2126.700668238994,632.6666666666666,2132.239116548742,638.3333333333334L2137.7775648584907,644" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead332"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Authorized-No_Server" class="edgePath LS-Authorized LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead333)" d="M1414.6959758254718,538L1426.3254225628932,532.3333333333334C1437.9548693003144,526.6666666666666,1461.2137627751572,515.3333333333334,1472.8432095125784,500.8333333333333C1484.47265625,486.3333333333333,1484.47265625,468.6666666666667,1484.47265625,451C1484.47265625,433.3333333333333,1484.47265625,415.6666666666667,1484.47265625,398C1484.47265625,380.3333333333333,1484.47265625,362.6666666666667,1484.47265625,345C1484.47265625,327.3333333333333,1484.47265625,309.6666666666667,1484.47265625,292C1484.47265625,274.3333333333333,1484.47265625,256.6666666666667,1484.47265625,239C1484.47265625,221.33333333333334,1484.47265625,203.66666666666666,1490.3201650943395,189.16666666666666C1496.1676739386792,174.66666666666666,1507.8626916273586,163.33333333333334,1513.710200471698,157.66666666666666L1519.5577093160377,152" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead333"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Request_Config-Ask_Profile" class="edgePath LS-Request_Config LE-Ask_Profile"><path style="fill:none" marker-end="url(#arrowhead334)" d="M2156.34765625,682L2156.34765625,687.6666666666666C2156.34765625,693.3333333333334,2156.34765625,704.6666666666666,2168.13288620283,716C2179.9181161556603,727.3333333333334,2203.488576061321,738.6666666666666,2215.273806014151,744.3333333333334L2227.0590359669814,750" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead334"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Request_Config-Disconnected" class="edgePath LS-Request_Config LE-Disconnected"><path style="fill:none" marker-end="url(#arrowhead335)" d="M2223.72265625,670.5939457375932L2290.8645833333335,678.1616214479944C2358.0065104166665,685.7292971583955,2492.2903645833335,700.8646485791978,2559.4322916666665,717.2656576229323C2626.57421875,733.6666666666666,2626.57421875,751.3333333333334,2626.57421875,769C2626.57421875,786.6666666666666,2626.57421875,804.3333333333334,2664.1432291666665,820.1804859926248C2701.7122395833335,836.0276386519163,2776.8502604166665,850.0552773038326,2814.4192708333335,857.0690966297907L2851.98828125,864.0829159557488" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead335"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Request_Config-No_Server" class="edgePath LS-Request_Config LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead336)" d="M2174.9177476415093,644L2180.456195951258,638.3333333333334C2185.994644261006,632.6666666666666,2197.071540880503,621.3333333333334,2202.609989190252,606.8333333333334C2208.1484375,592.3333333333334,2208.1484375,574.6666666666666,2208.1484375,557C2208.1484375,539.3333333333334,2208.1484375,521.6666666666666,2208.1484375,504C2208.1484375,486.3333333333333,2208.1484375,468.6666666666667,2208.1484375,451C2208.1484375,433.3333333333333,2208.1484375,415.6666666666667,2208.1484375,398C2208.1484375,380.3333333333333,2208.1484375,362.6666666666667,2208.1484375,345C2208.1484375,327.3333333333333,2208.1484375,309.6666666666667,2208.1484375,292C2208.1484375,274.3333333333333,2208.1484375,256.6666666666667,2208.1484375,239C2208.1484375,221.33333333333334,2208.1484375,203.66666666666666,2104.69140625,186.63699443341508C2001.234375,169.60732220016348,1794.3203125,153.214644400327,1690.86328125,145.01830550040873L1587.40625,136.82196660049047" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead336"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Request_Config-OAuth_Started" class="edgePath LS-Request_Config LE-OAuth_Started"><path style="fill:none" marker-end="url(#arrowhead337)" d="M2088.97265625,666.9089202560496L1947.9485677083333,675.0907668800413C1806.9244791666667,683.272613504033,1524.8763020833333,699.6363067520165,1383.8522135416667,716.6514867093416C1242.828125,733.6666666666666,1242.828125,751.3333333333334,1242.828125,769C1242.828125,786.6666666666666,1242.828125,804.3333333333334,1242.828125,822C1242.828125,839.6666666666666,1242.828125,857.3333333333334,1242.828125,875C1242.828125,892.6666666666666,1242.828125,910.3333333333334,1247.2334905660377,924.8333333333334C1251.6388561320755,939.3333333333334,1260.449587264151,950.6666666666666,1264.8549528301887,956.3333333333334L1269.2603183962265,962" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead337"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Ask_Profile-Disconnected" class="edgePath LS-Ask_Profile LE-Disconnected"><path style="fill:none" marker-end="url(#arrowhead338)" d="M2317.03515625,774.9048366509124L2384.1119791666665,782.7540305424269C2451.1888020833335,790.6032244339416,2585.3424479166665,806.3016122169707,2674.5013020833335,820.2792834444368C2763.66015625,834.2569546719031,2807.82421875,846.5139093438062,2829.90625,852.6423866797577L2851.98828125,858.7708640157093" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead338"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Ask_Profile-No_Server" class="edgePath LS-Ask_Profile LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead339)" d="M2290.5818838443397,750L2297.74206466195,744.3333333333334C2304.90224547956,738.6666666666666,2319.2226071147797,727.3333333333334,2326.38278793239,712.8333333333334C2333.54296875,698.3333333333334,2333.54296875,680.6666666666666,2333.54296875,663C2333.54296875,645.3333333333334,2333.54296875,627.6666666666666,2333.54296875,610C2333.54296875,592.3333333333334,2333.54296875,574.6666666666666,2333.54296875,557C2333.54296875,539.3333333333334,2333.54296875,521.6666666666666,2333.54296875,504C2333.54296875,486.3333333333333,2333.54296875,468.6666666666667,2333.54296875,451C2333.54296875,433.3333333333333,2333.54296875,415.6666666666667,2333.54296875,398C2333.54296875,380.3333333333333,2333.54296875,362.6666666666667,2333.54296875,345C2333.54296875,327.3333333333333,2333.54296875,309.6666666666667,2333.54296875,292C2333.54296875,274.3333333333333,2333.54296875,256.6666666666667,2333.54296875,239C2333.54296875,221.33333333333334,2333.54296875,203.66666666666666,2209.1868489583335,186.53644340196334C2084.8307291666665,169.40622013726002,1836.1184895833333,152.81244027452001,1711.7623697916667,144.51555034315004L1587.40625,136.21866041178004" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead339"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Ask_Profile-Search_Server" class="edgePath LS-Ask_Profile LE-Search_Server"><path style="fill:none" marker-end="url(#arrowhead340)" d="M2216.11328125,774.0915378265622L2136.978515625,782.0762815221351C2057.84375,790.0610252177081,1899.57421875,806.0305126088541,1820.439453125,822.8485896377603C1741.3046875,839.6666666666666,1741.3046875,857.3333333333334,1741.3046875,875C1741.3046875,892.6666666666666,1741.3046875,910.3333333333334,1741.3046875,928C1741.3046875,945.6666666666666,1741.3046875,963.3333333333334,1741.3046875,981C1741.3046875,998.6666666666666,1741.3046875,1016.3333333333334,1713.1790364583333,1031.5887873535169C1685.0533854166667,1046.8442413737005,1628.8020833333333,1059.6884827474012,1600.6764322916667,1066.1106034342515L1572.55078125,1072.5327241211019" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead340"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Disconnected-Connecting" class="edgePath LS-Disconnected LE-Connecting"><path style="fill:none" marker-end="url(#arrowhead341)" d="M2851.98828125,886.8640747663551L2818.1959635416665,893.7200623052959C2784.4036458333335,900.5760498442368,2716.8190104166665,914.2880249221183,2694.0589008451257,926.8106791277258C2671.298791273585,939.3333333333334,2693.36320754717,950.6666666666666,2704.3954156839623,956.3333333333334L2715.427623820755,962" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead341"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Disconnected-Request_Config" class="edgePath LS-Disconnected LE-Request_Config"><path style="fill:none" marker-end="url(#arrowhead342)" d="M2917.3923938679245,856L2919.458505306604,850.3333333333334C2921.524616745283,844.6666666666666,2925.6568396226417,833.3333333333334,2927.722951061321,818.8333333333334C2929.7890625,804.3333333333334,2929.7890625,786.6666666666666,2929.7890625,769C2929.7890625,751.3333333333334,2929.7890625,733.6666666666666,2812.111328125,716.769477595231C2694.43359375,699.8722885237953,2459.078125,683.7445770475906,2341.400390625,675.6807213094884L2223.72265625,667.616865571386" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead342"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Disconnected-No_Server" class="edgePath LS-Disconnected LE-No_Server"><path style="fill:none" marker-end="url(#arrowhead343)" d="M2945.4442806603774,856L2955.8767443003144,850.3333333333334C2966.309207940252,844.6666666666666,2987.1741352201257,833.3333333333334,2997.6065988600626,818.8333333333334C3008.0390625,804.3333333333334,3008.0390625,786.6666666666666,3008.0390625,769C3008.0390625,751.3333333333334,3008.0390625,733.6666666666666,3008.0390625,716C3008.0390625,698.3333333333334,3008.0390625,680.6666666666666,3008.0390625,663C3008.0390625,645.3333333333334,3008.0390625,627.6666666666666,3008.0390625,610C3008.0390625,592.3333333333334,3008.0390625,574.6666666666666,3008.0390625,557C3008.0390625,539.3333333333334,3008.0390625,521.6666666666666,3008.0390625,504C3008.0390625,486.3333333333333,3008.0390625,468.6666666666667,3008.0390625,451C3008.0390625,433.3333333333333,3008.0390625,415.6666666666667,3008.0390625,398C3008.0390625,380.3333333333333,3008.0390625,362.6666666666667,3008.0390625,345C3008.0390625,327.3333333333333,3008.0390625,309.6666666666667,3008.0390625,292C3008.0390625,274.3333333333333,3008.0390625,256.6666666666667,3008.0390625,239C3008.0390625,221.33333333333334,3008.0390625,203.66666666666666,2771.2669270833335,186.29011272090318C2534.4947916666665,168.9135587751397,2060.9505208333335,151.8271175502794,1824.1783854166667,143.28389693784928L1587.40625,134.74067632541912" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead343"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Disconnected-OAuth_Started" class="edgePath LS-Disconnected LE-OAuth_Started"><path style="fill:none" marker-end="url(#arrowhead344)" d="M2851.98828125,876.986092023941L2601.654296875,885.4884100199507C2351.3203125,893.9907280159606,1850.65234375,910.9953640079802,1593.266767393868,925.1643486706568C1335.881191037736,939.3333333333334,1321.7780070754718,950.6666666666666,1314.7264150943397,956.3333333333334L1307.6748231132076,962" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead344"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Disconnecting-Disconnected" class="edgePath LS-Disconnecting LE-Disconnected"><path style="fill:none" marker-end="url(#arrowhead345)" d="M2955.9446491745284,1174L2965.7422857704405,1168.3333333333333C2975.539922366352,1162.6666666666667,2995.1351955581763,1151.3333333333333,3004.9328321540884,1136.8333333333333C3014.73046875,1122.3333333333333,3014.73046875,1104.6666666666667,3014.73046875,1087C3014.73046875,1069.3333333333333,3014.73046875,1051.6666666666667,3014.73046875,1034C3014.73046875,1016.3333333333334,3014.73046875,998.6666666666666,3014.73046875,981C3014.73046875,963.3333333333334,3014.73046875,945.6666666666666,3003.5825717374214,931.1666666666666C2992.4346747248433,916.6666666666666,2970.1388806996856,905.3333333333334,2958.990983687107,899.6666666666666L2947.8430866745284,894" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead345"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Disconnecting-Disconnected" class="edgePath LS-Disconnecting LE-Disconnected"><path style="fill:none" marker-end="url(#arrowhead346)" d="M2983.34375,1179.4780084360268L3012.6608072916665,1172.8983403633556C3041.9778645833335,1166.3186722906846,3100.6119791666665,1153.1593361453422,3129.9290364583335,1137.7463347393377C3159.24609375,1122.3333333333333,3159.24609375,1104.6666666666667,3159.24609375,1087C3159.24609375,1069.3333333333333,3159.24609375,1051.6666666666667,3159.24609375,1034C3159.24609375,1016.3333333333334,3159.24609375,998.6666666666666,3159.24609375,981C3159.24609375,963.3333333333334,3159.24609375,945.6666666666666,3127.5286458333335,930.0762938073107C3095.8111979166665,914.4859209479546,3032.3763020833335,900.9718418959092,3000.6588541666665,894.2148023698865L2968.94140625,887.4577628438639" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead346"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Connecting-Disconnected" class="edgePath LS-Connecting LE-Disconnected"><path style="fill:none" marker-end="url(#arrowhead347)" d="M2802.89453125,964.0730103806228L2820.8229166666665,958.0608419838524C2838.7513020833335,952.0486735870818,2874.6080729166665,940.0243367935409,2892.5364583333335,928.3455017301038C2910.46484375,916.6666666666666,2910.46484375,905.3333333333334,2910.46484375,899.6666666666666L2910.46484375,894" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead347"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Connecting-Connected" class="edgePath LS-Connecting LE-Connected"><path style="fill:none" marker-end="url(#arrowhead348)" d="M2752.41796875,1000L2752.41796875,1005.6666666666666C2752.41796875,1011.3333333333334,2752.41796875,1022.6666666666666,2741.4776434748424,1034C2730.5373181996856,1045.3333333333333,2708.656667649371,1056.6666666666667,2697.7163423742136,1062.3333333333333L2686.7760170990564,1068" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead348"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g><g style="opacity: 1;" id="L-Connected-Disconnecting" class="edgePath LS-Connected LE-Disconnecting"><path style="fill:none" marker-end="url(#arrowhead349)" d="M2650.09375,1106L2650.09375,1111.6666666666667C2650.09375,1117.3333333333333,2650.09375,1128.6666666666667,2685.5520833333335,1141.2171855921856C2721.0104166666665,1153.7677045177045,2791.9270833333335,1167.535409035409,2827.3854166666665,1174.4192612942613L2862.84375,1181.3031135531135" class="path"/><defs><marker orient="auto" markerHeight="6" markerWidth="8" markerUnits="strokeWidth" refY="5" refX="9" viewBox="0 0 10 10" id="arrowhead349"><path style="stroke-width: 1; stroke-dasharray: 1, 0;" class="arrowheadPath" d="M 0 0 L 10 5 L 0 10 z"/></marker></defs></g></g><g class="edgeLabels"><g style="opacity: 1;" transform="translate(1539.1640625,80)" class="edgeLabel"><g class="label" transform="translate(-53.3515625,-9)"><rect height="18" width="106.703125" ry="0" rx="0"/><foreignObject height="18" width="106.703125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Deregistered' L-LE-No_Server" id="L-L-Deregistered-No_Server">Client registers</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1659.7578125,133)" class="edgeLabel"><g class="label" transform="translate(-37.3515625,-9)"><rect height="18" width="74.703125" ry="0" rx="0"/><foreignObject height="18" width="74.703125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-No_Server' L-LE-No_Server" id="L-L-No_Server-No_Server">Reload list</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1146.12890625,186)" class="edgeLabel"><g class="label" transform="translate(-101.8046875,-9)"><rect height="18" width="203.609375" ry="0" rx="0"/><foreignObject height="18" width="203.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-No_Server' L-LE-Loading_Server" id="L-L-No_Server-Loading_Server">User clicks a server in the UI</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(984.0546875,292)" class="edgeLabel"><g class="label" transform="translate(-101.3984375,-9)"><rect height="18" width="202.796875" ry="0" rx="0"/><foreignObject height="18" width="202.796875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-No_Server' L-LE-Chosen_Server" id="L-L-No_Server-Chosen_Server">The server has been chosen</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(186.3046875,610)" class="edgeLabel"><g class="label" transform="translate(-178.3046875,-9)"><rect height="18" width="356.609375" ry="0" rx="0"/><foreignObject height="18" width="356.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-No_Server' L-LE-Search_Server" id="L-L-No_Server-Search_Server">The user is trying to choose a new server in the UI</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2460.71484375,610)" class="edgeLabel"><g class="label" transform="translate(-107.171875,-9)"><rect height="18" width="214.34375" ry="0" rx="0"/><foreignObject height="18" width="214.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-No_Server' L-LE-Connected" id="L-L-No_Server-Connected">The user is already connected</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(345.0625,239)" class="edgeLabel"><g class="label" transform="translate(-138.7578125,-9)"><rect height="18" width="277.515625" ry="0" rx="0"/><foreignObject height="18" width="277.515625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-No_Server' L-LE-Ask_Location" id="L-L-No_Server-Ask_Location">Change the location in the main screen</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1062.3203125,398)" class="edgeLabel"><g class="label" transform="translate(-58.265625,-9)"><rect height="18" width="116.53125" ry="0" rx="0"/><foreignObject height="18" width="116.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Ask_Location' L-LE-Chosen_Server" id="L-L-Ask_Location-Chosen_Server">Location chosen</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(862.65625,239)" class="edgeLabel"><g class="label" transform="translate(-59.1328125,-9)"><rect height="18" width="118.265625" ry="0" rx="0"/><foreignObject height="18" width="118.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Ask_Location' L-LE-No_Server" id="L-L-Ask_Location-No_Server">Go back or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(623.6328125,716)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Ask_Location' L-LE-Search_Server" id="L-L-Ask_Location-Search_Server">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1815.546875,663)" class="edgeLabel"><g class="label" transform="translate(-101.8046875,-9)"><rect height="18" width="203.609375" ry="0" rx="0"/><foreignObject height="18" width="203.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Search_Server' L-LE-Loading_Server" id="L-L-Search_Server-Loading_Server">User clicks a server in the UI</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1937.3515625,610)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Search_Server' L-LE-No_Server" id="L-L-Search_Server-No_Server">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1236.59765625,345)" class="edgeLabel"><g class="label" transform="translate(-64.9296875,-9)"><rect height="18" width="129.859375" ry="0" rx="0"/><foreignObject height="18" width="129.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Loading_Server' L-LE-Chosen_Server" id="L-L-Loading_Server-Chosen_Server">Server info loaded</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(603.859375,292)" class="edgeLabel"><g class="label" transform="translate(-238.796875,-9)"><rect height="18" width="477.59375" ry="0" rx="0"/><foreignObject height="18" width="477.59375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Loading_Server' L-LE-Ask_Location" id="L-L-Loading_Server-Ask_Location">User chooses a Secure Internet server but no location is configured</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1327.06640625,186)" class="edgeLabel"><g class="label" transform="translate(-59.1328125,-9)"><rect height="18" width="118.265625" ry="0" rx="0"/><foreignObject height="18" width="118.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Loading_Server' L-LE-No_Server" id="L-L-Loading_Server-No_Server">Go back or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1229.69921875,504)" class="edgeLabel"><g class="label" transform="translate(-80.5,-9)"><rect height="18" width="161" ry="0" rx="0"/><foreignObject height="18" width="161"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Chosen_Server' L-LE-Authorized" id="L-L-Chosen_Server-Authorized">Found tokens in config</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1025.390625,716)" class="edgeLabel"><g class="label" transform="translate(-90.28125,-9)"><rect height="18" width="180.5625" ry="0" rx="0"/><foreignObject height="18" width="180.5625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Chosen_Server' L-LE-OAuth_Started" id="L-L-Chosen_Server-OAuth_Started">No tokens found in config</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1488.9140625,769)" class="edgeLabel"><g class="label" transform="translate(-103.1484375,-9)"><rect height="18" width="206.296875" ry="0" rx="0"/><foreignObject height="18" width="206.296875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-OAuth_Started' L-LE-Authorized" id="L-L-OAuth_Started-Authorized">User authorizes with browser</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1655.22265625,557)" class="edgeLabel"><g class="label" transform="translate(-59.1328125,-9)"><rect height="18" width="118.265625" ry="0" rx="0"/><foreignObject height="18" width="118.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-OAuth_Started' L-LE-No_Server" id="L-L-OAuth_Started-No_Server">Go back or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1284.03125,1034)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-OAuth_Started' L-LE-Search_Server" id="L-L-OAuth_Started-Search_Server">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1135.671875,769)" class="edgeLabel"><g class="label" transform="translate(-87.15625,-9)"><rect height="18" width="174.3125" ry="0" rx="0"/><foreignObject height="18" width="174.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Authorized' L-LE-OAuth_Started" id="L-L-Authorized-OAuth_Started">Re-authorize with OAuth</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2104.546875,610)" class="edgeLabel"><g class="label" transform="translate(-83.6015625,-9)"><rect height="18" width="167.203125" ry="0" rx="0"/><foreignObject height="18" width="167.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Authorized' L-LE-Request_Config" id="L-L-Authorized-Request_Config">Client requests a config</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1484.47265625,345)" class="edgeLabel"><g class="label" transform="translate(-150.75,-9)"><rect height="18" width="301.5" ry="0" rx="0"/><foreignObject height="18" width="301.5"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Authorized' L-LE-No_Server" id="L-L-Authorized-No_Server">Client wants to go back to the main screen</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2156.34765625,716)" class="edgeLabel"><g class="label" transform="translate(-156.546875,-9)"><rect height="18" width="313.09375" ry="0" rx="0"/><foreignObject height="18" width="313.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Request_Config' L-LE-Ask_Profile" id="L-L-Request_Config-Ask_Profile">Multiple profiles found and no profile chosen</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2626.57421875,769)" class="edgeLabel"><g class="label" transform="translate(-145.859375,-9)"><rect height="18" width="291.71875" ry="0" rx="0"/><foreignObject height="18" width="291.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Request_Config' L-LE-Disconnected" id="L-L-Request_Config-Disconnected">Only one profile or profile already chosen</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2208.1484375,398)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Request_Config' L-LE-No_Server" id="L-L-Request_Config-No_Server">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1242.828125,822)" class="edgeLabel"><g class="label" transform="translate(-45.8046875,-9)"><rect height="18" width="91.609375" ry="0" rx="0"/><foreignObject height="18" width="91.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Request_Config' L-LE-OAuth_Started" id="L-L-Request_Config-OAuth_Started">Re-authorize</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2719.49609375,822)" class="edgeLabel"><g class="label" transform="translate(-72.921875,-9)"><rect height="18" width="145.84375" ry="0" rx="0"/><foreignObject height="18" width="145.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Ask_Profile' L-LE-Disconnected" id="L-L-Ask_Profile-Disconnected">User chooses profile</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2333.54296875,451)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Ask_Profile' L-LE-No_Server" id="L-L-Ask_Profile-No_Server">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1741.3046875,928)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Ask_Profile' L-LE-Search_Server" id="L-L-Ask_Profile-Search_Server">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2649.234375,928)" class="edgeLabel"><g class="label" transform="translate(-113.828125,-9)"><rect height="18" width="227.65625" ry="0" rx="0"/><foreignObject height="18" width="227.65625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Disconnected' L-LE-Connecting" id="L-L-Disconnected-Connecting">OS reports it is trying to connect</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2929.7890625,769)" class="edgeLabel"><g class="label" transform="translate(-58.25,-9)"><rect height="18" width="116.5" ry="0" rx="0"/><foreignObject height="18" width="116.5"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Disconnected' L-LE-Request_Config" id="L-L-Disconnected-Request_Config">User reconnects</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(3008.0390625,504)" class="edgeLabel"><g class="label" transform="translate(-124.9453125,-9)"><rect height="18" width="249.890625" ry="0" rx="0"/><foreignObject height="18" width="249.890625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Disconnected' L-LE-No_Server" id="L-L-Disconnected-No_Server">User wants to choose a new server</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(1349.984375,928)" class="edgeLabel"><g class="label" transform="translate(-87.15625,-9)"><rect height="18" width="174.3125" ry="0" rx="0"/><foreignObject height="18" width="174.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Disconnected' L-LE-OAuth_Started" id="L-L-Disconnected-OAuth_Started">Re-authorize with OAuth</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(3014.73046875,1034)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Disconnecting' L-LE-Disconnected" id="L-L-Disconnecting-Disconnected">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(3159.24609375,1034)" class="edgeLabel"><g class="label" transform="translate(-70.2734375,-9)"><rect height="18" width="140.546875" ry="0" rx="0"/><foreignObject height="18" width="140.546875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Disconnecting' L-LE-Disconnected" id="L-L-Disconnecting-Disconnected">Done disconnecting</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2910.46484375,928)" class="edgeLabel"><g class="label" transform="translate(-54.2421875,-9)"><rect height="18" width="108.484375" ry="0" rx="0"/><foreignObject height="18" width="108.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Connecting' L-LE-Disconnected" id="L-L-Connecting-Disconnected">Cancel or Error</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2752.41796875,1034)" class="edgeLabel"><g class="label" transform="translate(-60.046875,-9)"><rect height="18" width="120.09375" ry="0" rx="0"/><foreignObject height="18" width="120.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Connecting' L-LE-Connected" id="L-L-Connecting-Connected">Done connecting</span></div></foreignObject></g></g><g style="opacity: 1;" transform="translate(2650.09375,1140)" class="edgeLabel"><g class="label" transform="translate(-86.71875,-9)"><rect height="18" width="173.4375" ry="0" rx="0"/><foreignObject height="18" width="173.4375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span style="" class="edgeLabel L-LS-Connected' L-LE-Disconnecting" id="L-L-Connected-Disconnecting">App wants to disconnect</span></div></foreignObject></g></g></g><g class="nodes"><g style="opacity: 1;" transform="translate(1539.1640625,27)" id="flowchart-Deregistered-234" class="node default"><rect style="fill:white;" class="label-container" height="38" width="111.609375" y="-19" x="-55.8046875" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-45.8046875,-9)"><foreignObject height="18" width="91.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Deregistered</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(1539.1640625,133)" id="flowchart-No_Server-236" class="node default"><rect style="fill:white;fill:white;fill:white;fill:white;fill:white;fill:white;" class="label-container" height="38" width="96.484375" y="-19" x="-48.2421875" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-38.2421875,-9)"><foreignObject height="18" width="76.484375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">No_Server</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(1236.59765625,239)" id="flowchart-Loading_Server-242" class="node default"><rect style="fill:white;fill:white;fill:white;" class="label-container" height="38" width="132.96875" y="-19" x="-66.484375" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-56.484375,-9)"><foreignObject height="18" width="112.96875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Loading_Server</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(1155.19921875,451)" id="flowchart-Chosen_Server-245" class="node default"><rect style="fill:white;fill:white;" class="label-container" height="38" width="131.171875" y="-19" x="-65.5859375" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-55.5859375,-9)"><foreignObject height="18" width="111.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Chosen_Server</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(1509.19140625,1087)" id="flowchart-Search_Server-248" class="node default"><rect style="fill:white;fill:white;" class="label-container" height="38" width="126.71875" y="-19" x="-63.359375" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-53.359375,-9)"><foreignObject height="18" width="106.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Search_Server</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(2650.09375,1087)" id="flowchart-Connected-251" class="node default"><rect style="fill:white;" class="label-container" height="38" width="97.390625" y="-19" x="-48.6953125" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-38.6953125,-9)"><foreignObject height="18" width="77.390625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Connected</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(623.6328125,345)" id="flowchart-Ask_Location-254" class="node default"><rect style="fill:white;fill:white;fill:white;" class="label-container" height="38" width="116.0625" y="-19" x="-58.03125" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-48.03125,-9)"><foreignObject height="18" width="96.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Ask_Location</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(1375.703125,557)" id="flowchart-Authorized-281" class="node default"><rect style="fill:white;fill:white;fill:white;" class="label-container" height="38" width="96.5" y="-19" x="-48.25" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-38.25,-9)"><foreignObject height="18" width="76.5"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Authorized</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(1284.03125,981)" id="flowchart-OAuth_Started-284" class="node default"><rect style="fill:white;fill:white;fill:white;" class="label-container" height="38" width="125.84375" y="-19" x="-62.921875" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-52.921875,-9)"><foreignObject height="18" width="105.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">OAuth_Started</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(2156.34765625,663)" id="flowchart-Request_Config-299" class="node default"><rect style="fill:white;fill:white;fill:white;fill:white;" class="label-container" height="38" width="134.75" y="-19" x="-67.375" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-57.375,-9)"><foreignObject height="18" width="114.75"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Request_Config</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(2266.57421875,769)" id="flowchart-Ask_Profile-305" class="node default"><rect style="fill:white;fill:white;fill:white;" class="label-container" height="38" width="100.921875" y="-19" x="-50.4609375" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-40.4609375,-9)"><foreignObject height="18" width="80.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Ask_Profile</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(2910.46484375,875)" id="flowchart-Disconnected-308" class="node default"><rect style="fill:cyan;fill:cyan;fill:cyan;fill:cyan;" class="label-container" height="38" width="116.953125" y="-19" x="-58.4765625" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-48.4765625,-9)"><foreignObject height="18" width="96.953125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Disconnected</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(2752.41796875,981)" id="flowchart-Connecting-326" class="node default"><rect style="fill:white;fill:white;" class="label-container" height="38" width="100.953125" y="-19" x="-50.4765625" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-40.4765625,-9)"><foreignObject height="18" width="80.953125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Connecting</div></foreignObject></g></g></g><g style="opacity: 1;" transform="translate(2923.09375,1193)" id="flowchart-Disconnecting-336" class="node default"><rect style="fill:white;fill:white;" class="label-container" height="38" width="120.5" y="-19" x="-60.25" ry="5" rx="5"/><g transform="translate(0,0)" class="label"><g transform="translate(-50.25,-9)"><foreignObject height="18" width="100.5"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Disconnecting</div></foreignObject></g></g></g></g></g></g></svg>
\ No newline at end of file
diff --git a/docs/src/gettingstarted/debugging/logging.md b/docs/src/gettingstarted/debugging/logging.md
deleted file mode 100644
index 9a7eae28..00000000
--- a/docs/src/gettingstarted/debugging/logging.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Logging
-As said, logging is used. The logging gets saved in a client-specified directory (see [API](../../api/index.html)). Logging has the following levels:
-
-- `INFO`: Messages purely for info, these do not indicate any errors. They are merely there for debugging purposes
-- `WARNING`: These messages indicate a warning, e.g. non-fatal errors
-- `ERROR`: Fatal errors which refuses the app from working correctly
-
-By default only messages below or equal to `WARNING` are logged (`WARNING`, `ERROR`). However, if the debug variable is set to `True`, all messages will be logged into the log file.
diff --git a/exports/common.mk b/exports/common.mk
index 940b3d42..cb649ad7 100644
--- a/exports/common.mk
+++ b/exports/common.mk
@@ -19,7 +19,7 @@ LIB_SUFFIX ?= .so
 endif
 
 # Current version
-VERSION = 1.1.2
+VERSION = 2.0.0
 
 # Library name without prefixes/suffixes
 LIB_NAME ?= eduvpn_common
diff --git a/exports/disco.go b/exports/disco.go
deleted file mode 100644
index 08c921f0..00000000
--- a/exports/disco.go
+++ /dev/null
@@ -1,217 +0,0 @@
-package main
-
-/*
-// for free and size_t
-#include <stdlib.h>
-#include "error.h"
-
-typedef struct discoveryServer {
-  const char* authentication_url_template;
-  const char* base_url;
-  const char* country_code;
-  const char* display_name;
-  const char* keyword_list;
-  const char** public_key_list;
-  size_t total_public_keys;
-  const char* server_type;
-  const char** support_contact;
-  size_t total_support_contact;
-} discoveryServer;
-
-typedef struct discoveryServers {
-  unsigned long long int version;
-  discoveryServer** servers;
-  size_t total_servers;
-} discoveryServers;
-
-typedef struct discoveryOrganization {
-  const char* display_name;
-  const char* org_id;
-  const char* secure_internet_home;
-  const char* keyword_list;
-} discoveryOrganization;
-
-typedef struct discoveryOrganizations {
-  unsigned long long int version;
-  discoveryOrganization** organizations;
-  size_t total_organizations;
-} discoveryOrganizations;
-*/
-import "C"
-
-import (
-	"unsafe"
-
-	"github.com/eduvpn/eduvpn-common/client"
-	"github.com/eduvpn/eduvpn-common/types"
-)
-
-func getCPtrDiscoOrganization(
-	state *client.Client,
-	organization *types.DiscoveryOrganization,
-) *C.discoveryOrganization {
-	returnedStruct := (*C.discoveryOrganization)(
-		C.malloc(C.size_t(unsafe.Sizeof(C.discoveryOrganization{}))),
-	)
-	returnedStruct.display_name = C.CString(state.GetTranslated(organization.DisplayName))
-	returnedStruct.org_id = C.CString(organization.OrgID)
-	returnedStruct.secure_internet_home = C.CString(organization.SecureInternetHome)
-	returnedStruct.keyword_list = C.CString(state.GetTranslated(organization.KeywordList))
-	return returnedStruct
-}
-
-func getCPtrDiscoOrganizations(
-	state *client.Client,
-	organizations *types.DiscoveryOrganizations,
-) (C.size_t, **C.discoveryOrganization) {
-	totalOrganizations := C.size_t(len(organizations.List))
-	if totalOrganizations > 0 {
-		organizationsPtr := (**C.discoveryOrganization)(
-			C.malloc(totalOrganizations * C.size_t(unsafe.Sizeof(uintptr(0)))),
-		)
-		cOrganizations := unsafe.Slice(organizationsPtr, totalOrganizations)
-		index := 0
-		for _, organization := range organizations.List {
-			cOrganization := getCPtrDiscoOrganization(state, &organization)
-			cOrganizations[index] = cOrganization
-			index++
-		}
-		return totalOrganizations, organizationsPtr
-	}
-	return 0, nil
-}
-
-func getCPtrDiscoServer(
-	state *client.Client,
-	server *types.DiscoveryServer,
-) *C.discoveryServer {
-	returnedStruct := (*C.discoveryServer)(
-		C.malloc(C.size_t(unsafe.Sizeof(C.discoveryServer{}))),
-	)
-	returnedStruct.authentication_url_template = C.CString(server.AuthenticationURLTemplate)
-	returnedStruct.base_url = C.CString(server.BaseURL)
-	returnedStruct.country_code = C.CString(server.CountryCode)
-	returnedStruct.display_name = C.CString(state.GetTranslated(server.DisplayName))
-	returnedStruct.keyword_list = C.CString(state.GetTranslated(server.KeywordList))
-	returnedStruct.total_public_keys, returnedStruct.public_key_list = getCPtrListStrings(
-		server.PublicKeyList,
-	)
-	returnedStruct.server_type = C.CString(server.Type)
-	returnedStruct.total_support_contact, returnedStruct.support_contact = getCPtrListStrings(
-		server.SupportContact,
-	)
-	return returnedStruct
-}
-
-func getCPtrDiscoServers(
-	state *client.Client,
-	servers *types.DiscoveryServers,
-) (C.size_t, **C.discoveryServer) {
-	totalServers := C.size_t(len(servers.List))
-	if totalServers > 0 {
-		serversPtr := (**C.discoveryServer)(
-			C.malloc(totalServers * C.size_t(unsafe.Sizeof(uintptr(0)))),
-		)
-		cServers := unsafe.Slice(serversPtr, totalServers)
-		index := 0
-		for _, server := range servers.List {
-			cServer := getCPtrDiscoServer(state, &server)
-			cServers[index] = cServer
-			index++
-		}
-		return totalServers, serversPtr
-	}
-	return 0, nil
-}
-
-func freeDiscoOrganization(cOrganization *C.discoveryOrganization) {
-	C.free(unsafe.Pointer(cOrganization.display_name))
-	C.free(unsafe.Pointer(cOrganization.org_id))
-	C.free(unsafe.Pointer(cOrganization.secure_internet_home))
-	C.free(unsafe.Pointer(cOrganization.keyword_list))
-	C.free(unsafe.Pointer(cOrganization))
-}
-
-func freeDiscoServer(cServer *C.discoveryServer) {
-	C.free(unsafe.Pointer(cServer.authentication_url_template))
-	C.free(unsafe.Pointer(cServer.base_url))
-	C.free(unsafe.Pointer(cServer.country_code))
-	C.free(unsafe.Pointer(cServer.display_name))
-	C.free(unsafe.Pointer(cServer.keyword_list))
-	freeCListStrings(cServer.public_key_list, cServer.total_public_keys)
-	C.free(unsafe.Pointer(cServer.server_type))
-	freeCListStrings(cServer.support_contact, cServer.total_support_contact)
-	C.free(unsafe.Pointer(cServer))
-}
-
-//export FreeDiscoServers
-func FreeDiscoServers(cServers *C.discoveryServers) {
-	if cServers.total_servers > 0 {
-		servers := unsafe.Slice(cServers.servers, cServers.total_servers)
-		for i := C.size_t(0); i < cServers.total_servers; i++ {
-			freeDiscoServer(servers[i])
-		}
-		C.free(unsafe.Pointer(cServers.servers))
-	}
-	C.free(unsafe.Pointer(cServers))
-}
-
-//export FreeDiscoOrganizations
-func FreeDiscoOrganizations(cOrganizations *C.discoveryOrganizations) {
-	if cOrganizations.total_organizations > 0 {
-		organizations := unsafe.Slice(cOrganizations.organizations, cOrganizations.total_organizations)
-		for i := C.size_t(0); i < cOrganizations.total_organizations; i++ {
-			freeDiscoOrganization(organizations[i])
-		}
-		C.free(unsafe.Pointer(cOrganizations.organizations))
-	}
-	C.free(unsafe.Pointer(cOrganizations))
-}
-
-//export GetDiscoServers
-func GetDiscoServers(name *C.char) (*C.discoveryServers, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return nil, getError(stateErr)
-	}
-	servers, serversErr := state.DiscoServers()
-	// if we get no servers then we return immediately
-	if servers == nil {
-		return nil, getError(serversErr)
-	}
-	returnedStruct := (*C.discoveryServers)(
-		C.malloc(C.size_t(unsafe.Sizeof(C.discoveryServers{}))),
-	)
-	returnedStruct.version = C.ulonglong(servers.Version)
-	returnedStruct.total_servers, returnedStruct.servers = getCPtrDiscoServers(
-		state,
-		servers,
-	)
-	return returnedStruct, getError(serversErr)
-}
-
-//export GetDiscoOrganizations
-func GetDiscoOrganizations(name *C.char) (*C.discoveryOrganizations, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return nil, getError(stateErr)
-	}
-	organizations, organizationsErr := state.DiscoOrganizations()
-	// if we get no organizations then we return immediately
-	if organizations == nil {
-		return nil, getError(organizationsErr)
-	}
-	returnedStruct := (*C.discoveryOrganizations)(
-		C.malloc(C.size_t(unsafe.Sizeof(C.discoveryOrganizations{}))),
-	)
-
-	returnedStruct.version = C.ulonglong(organizations.Version)
-	returnedStruct.total_organizations, returnedStruct.organizations = getCPtrDiscoOrganizations(
-		state,
-		organizations,
-	)
-
-	return returnedStruct, getError(organizationsErr)
-}
diff --git a/exports/error.h b/exports/error.h
deleted file mode 100644
index 2f94bbc0..00000000
--- a/exports/error.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#ifndef ERROR_H
-#define ERROR_H
-
-typedef struct error {
-   const char* traceback;
-   const char* cause;
-} error;
-
-#endif /* ERROR_H */
diff --git a/exports/exports.go b/exports/exports.go
index c09e980c..a68dea9b 100644
--- a/exports/exports.go
+++ b/exports/exports.go
@@ -1,591 +1,641 @@
+// package main implements the main exported API to be used by other languages
+//
+// Some notes:
+//
+// - Errors are returned as JSON c strings. The JSON type is defined in types/error/error.go Error. Free them using FreeString. Same is the case for other string types, you should also free them
+//
+// - Types are converted from the Go representation to C using JSON strings
+//
+// - Cookies are used for cancellation, just fancy contexts. Create a cookie using `CookieNew`, pass it to the function that needs one as the first argument. To cancel the function, call `CookieCancel`, passing in the same cookie as argument
+//
+// - Cookies must also be freed, by using the CookieDelete function if the cookie is no longer needed
+//
+// - The state machine is used to track the state of a client. It is mainly used for asking for certain data from the client, e.g. asking for profiles and locations. But a client may also wish to build upon this state machine to build the whole UI around it. The SetState and InState functions are useful for this
 package main
 
 /*
+#include <stdint.h>
 #include <stdlib.h>
-#include "error.h"
-#include "server.h"
 
 typedef long long int (*ReadRxBytes)();
-typedef struct token {
-    const char* access;
-    const char* refresh;
-    unsigned long long int expired;
-} token;
 
-typedef void (*UpdateToken)(const char* name, server* srv, token* tok);
+typedef int (*StateCB)(int oldstate, int newstate, void* data);
 
-typedef struct configData {
-    const char* config;
-    const char* config_type;
-    token* tokens;
-} configData;
-
-typedef int (*PythonCB)(const char* name, int oldstate, int newstate, void* data);
+typedef void (*TokenGetter)(const char* server, char* out, size_t len);
+typedef void (*TokenSetter)(const char* server, const char* tokens);
 
 static long long int get_read_rx_bytes(ReadRxBytes read)
 {
    return read();
 }
-
-static void update_token(UpdateToken func, const char* name, server* srv, token* tok)
+static int call_callback(StateCB callback, int oldstate, int newstate, void* data)
 {
-   func(name, srv, tok);
+    return callback(oldstate, newstate, data);
 }
-
-static int call_callback(PythonCB callback, const char *name, int oldstate, int newstate, void* data)
+static void call_token_getter(TokenGetter getter, const char* server, char* out, size_t len)
 {
-    return callback(name, oldstate, newstate, data);
+   getter(server, out, len);
+}
+static void call_token_setter(TokenSetter setter, const char* server, const char* tokens)
+{
+   setter(server, tokens);
 }
 */
 import "C"
 
 import (
-	"time"
+	"bytes"
+	"context"
+	"encoding/json"
+	"runtime/cgo"
 	"unsafe"
 
-	"github.com/eduvpn/eduvpn-common/internal/log"
-	"github.com/eduvpn/eduvpn-common/internal/oauth"
-	"github.com/eduvpn/eduvpn-common/internal/server"
 	"github.com/go-errors/errors"
 
 	"github.com/eduvpn/eduvpn-common/client"
+	"github.com/eduvpn/eduvpn-common/i18nerr"
+	"github.com/eduvpn/eduvpn-common/internal/log"
+	"github.com/eduvpn/eduvpn-common/types/cookie"
+	errtypes "github.com/eduvpn/eduvpn-common/types/error"
+	srvtypes "github.com/eduvpn/eduvpn-common/types/server"
 )
 
-var PStateCallbacks map[string]C.PythonCB
-
-var VPNStates map[string]*client.Client
+var VPNState *client.Client
 
-func GetStateData(
-	state *client.Client,
-	stateID client.FSMStateID,
-	data interface{},
-) unsafe.Pointer {
-	switch stateID {
-	case client.StateNoServer:
-		return (unsafe.Pointer)(getTransitionDataServers(state, data))
-	case client.StateOAuthStarted:
-		if converted, ok := data.(string); ok {
-			return (unsafe.Pointer)(C.CString(converted))
-		}
-	case client.StateAskLocation:
-		return (unsafe.Pointer)(getTransitionSecureLocations(data))
-	case client.StateAskProfile:
-		return (unsafe.Pointer)(getTransitionProfiles(data))
-	case client.StateDisconnected:
-		return (unsafe.Pointer)(getTransitionServer(state, data))
-	case client.StateDisconnecting:
-		return (unsafe.Pointer)(getTransitionServer(state, data))
-	case client.StateConnecting:
-		return (unsafe.Pointer)(getTransitionServer(state, data))
-	case client.StateConnected:
-		return (unsafe.Pointer)(getTransitionServer(state, data))
-	default:
+func getCError(err error) *C.char {
+	if err == nil {
 		return nil
 	}
-	return nil
+	retErr := errtypes.Error{
+		Message: errtypes.Translated{
+			"en": err.Error(),
+		},
+		Misc: false,
+	}
+	v, ok := err.(*i18nerr.Error)
+	if ok {
+		retErr.Message = v.Translations()
+		retErr.Misc = v.Misc
+	}
+	retData, err := getReturnData(retErr)
+	if err != nil {
+		return C.CString("failed to get error return")
+	}
+	return C.CString(retData)
+}
+
+func getReturnData(data interface{}) (string, error) {
+	// If it is already a string return directly
+	if x, ok := data.(string); ok {
+		return x, nil
+	}
+
+	// Otherwise use JSON
+	b, err := json.Marshal(data)
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
 }
 
-func StateCallback(
-	state *client.Client,
-	name string,
+func stateCallback(
+	cb C.StateCB,
 	oldState client.FSMStateID,
 	newState client.FSMStateID,
 	data interface{},
 ) bool {
-	PStateCallback, exists := PStateCallbacks[name]
-	if !exists || PStateCallback == nil {
-		return false
-	}
-	nameC := C.CString(name)
 	oldStateC := C.int(oldState)
 	newStateC := C.int(newState)
-	dataC := GetStateData(state, newState, data)
-	handled := C.call_callback(PStateCallback, nameC, oldStateC, newStateC, dataC)
-	C.free(unsafe.Pointer(nameC))
-	// data_c gets freed by the wrapper
-	return handled == C.int(1)
-}
-
-func GetVPNState(name string) (*client.Client, error) {
-	state, exists := VPNStates[name]
-
-	if !exists || state == nil {
-		return nil, errors.Errorf("state with name %s not found", name)
+	d, err := getReturnData(data)
+	if err != nil {
+		return false
 	}
-
-	return state, nil
-}
-
+	dataC := C.CString(d)
+	handled := C.call_callback(cb, oldStateC, newStateC, unsafe.Pointer(dataC))
+	FreeString(dataC)
+	return handled != C.int(0)
+}
+
+func getVPNState() (*client.Client, error) {
+	if VPNState == nil {
+		return nil, errors.New("No state available, did you register the client?")
+	}
+	return VPNState, nil
+}
+
+// Register creates a new client and also registers the FSM to go to the initial state
+//
+// `Name` is the name of the client, must be a valid client ID
+//
+// `Version` is the version of the client. This version field is used for the user agent in all HTTP requests
+//
+// `cb` is the state callback. It takes three arguments: The old state, the new state and the data for the state as JSON
+//
+//   - Note that the states are defined in client/fsm.go, e.g. NO_SERVER (in Go: StateNoServer), ASK_PROFILE (in Go: StateAskProfile)
+//
+//   - This callback returns non-zero if the state transition is handled. This is used to check if the client handles the needed transitions
+//
+// debug, if non-zero, enables debugging mode for the library, this means:
+//
+//   - Log everything in debug mode, so you can get more detail of what is going on
+//
+//   - Write the state graph to a file in the configDirectory. This can be used to create a FSM png file with mermaid https://mermaid.js.org/
+//
+// After registering, the FSM is initialized and the state transition NO_SERVER should have been completed
+// If some error occurs during registering, it is returned as a types/error/error.go Error
+//
 //export Register
 func Register(
 	name *C.char,
 	version *C.char,
 	configDirectory *C.char,
-	language *C.char,
-	stateCallback C.PythonCB,
+	cb C.StateCB,
 	debug C.int,
-) *C.error {
-	nameStr := C.GoString(name)
-	versionStr := C.GoString(version)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		state = &client.Client{}
-	}
-	if VPNStates == nil {
-		VPNStates = make(map[string]*client.Client)
-	}
-	if PStateCallbacks == nil {
-		PStateCallbacks = make(map[string]C.PythonCB)
-	}
-	VPNStates[nameStr] = state
-	PStateCallbacks[nameStr] = stateCallback
-	registerErr := state.Register(
-		nameStr,
-		versionStr,
+) *C.char {
+	_, stateErr := getVPNState()
+	if stateErr == nil {
+		return getCError(errors.New("failed to register, a VPN state is already present"))
+	}
+	c, err := client.New(
+		C.GoString(name),
+		C.GoString(version),
 		C.GoString(configDirectory),
-		C.GoString(language),
 		func(old client.FSMStateID, new client.FSMStateID, data interface{}) bool {
-			return StateCallback(state, nameStr, old, new, data)
+			return stateCallback(cb, old, new, data)
 		},
 		debug != 0,
 	)
-
-	if registerErr != nil {
-		delete(VPNStates, nameStr)
-	}
-	return getError(registerErr)
-}
-
-//export SetTokenUpdater
-func SetTokenUpdater(name *C.char, updater C.UpdateToken) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
-	}
-	state.SetTokenUpdater(func(srv server.Server, tok oauth.Token) {
-		b, err := srv.Base()
-		if err != nil {
-			log.Logger.Warningf("No server base found for token updating with error: %v", err)
-			return
-		}
-		cName := C.CString(nameStr)
-		cSrv := getCPtrServer(state, b)
-		cTok := cToken(tok)
-		C.update_token(updater, cName, cSrv, cTok)
-		FreeString(cName)
-	})
-	return nil
-}
-
-//export Deregister
-func Deregister(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
-	}
-	state.Deregister()
-	return nil
-}
-
-func getError(err error) *C.error {
+	// Only update the state if we get no error
 	if err == nil {
-		return nil
-	}
-	errorStruct := (*C.error)(
-		C.malloc(C.size_t(unsafe.Sizeof(C.error{}))),
-	)
-	if err1, ok := err.(*errors.Error); ok {
-		if err1 == nil {
-			errorStruct.traceback = C.CString("N/A")
-			errorStruct.cause = C.CString("unknown error")
-			return errorStruct
-		}
-		errorStruct.traceback = C.CString(err1.ErrorStack())
-		if err1.Err == nil {
-			errorStruct.cause = C.CString(err1.Error())
-		} else {
-			errorStruct.cause = C.CString(err1.Err.Error())
+		// Update the global client such that other functions can retrieve it
+		// TODO: Use a sync.Once or return a CGO handler instead of a global state?
+		VPNState = c
+		// finally register the newly created client
+		err = c.Register()
+		if err != nil {
+			// Note: Registering can only fail for non-newly created clients
+			// We have obtained a fresh copy here
+			// This error is only there for the Go API where you can call register multiple times on an already client
+			panic(err)
 		}
-	} else {
-		errorStruct.traceback = C.CString("N/A")
-		errorStruct.cause = C.CString(err.Error())
 	}
-	return errorStruct
-}
 
-//export FreeError
-func FreeError(err *C.error) {
-	C.free(unsafe.Pointer(err.traceback))
-	C.free(unsafe.Pointer(err.cause))
-	C.free(unsafe.Pointer(err))
+	return getCError(err)
 }
 
-//export CancelOAuth
-func CancelOAuth(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+// ExpiryTimes gets the expiry times for the current server
+//
+// Expiry times are just fields that represent unix timestamps at which to do certain events regarding expiry,
+// e.g. when to show the renew button, when to show expiry notifications
+//
+// The expiry times structure is defined in types/server/server.go `Expiry`
+// If some error occurs, it is returned as types/error/error.go Error
+//
+//export ExpiryTimes
+func ExpiryTimes() (*C.char, *C.char) {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
+		return nil, getCError(stateErr)
 	}
-	cancelErr := state.CancelOAuth()
-	return getError(cancelErr)
-}
-
-//export RemoveSecureInternet
-func RemoveSecureInternet(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
+	exp, err := state.ExpiryTimes()
+	if err != nil {
+		return nil, getCError(err)
 	}
-	removeErr := state.RemoveSecureInternet()
-	return getError(removeErr)
-}
-
-//export AddInstituteAccess
-func AddInstituteAccess(name *C.char, url *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
-	}
-	// FIXME: Return server result
-	_, addErr := state.AddInstituteServer(C.GoString(url))
-	return getError(addErr)
-}
-
-//export AddSecureInternetHomeServer
-func AddSecureInternetHomeServer(name *C.char, orgID *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
+	ret, err := getReturnData(exp)
+	if err != nil {
+		return nil, getCError(err)
 	}
-	// FIXME: Return server result
-	_, addErr := state.AddSecureInternetHomeServer(C.GoString(orgID))
-	return getError(addErr)
+	return C.CString(ret), nil
 }
 
-//export AddCustomServer
-func AddCustomServer(name *C.char, url *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+// Deregister cleans up the state for the client.
+//
+// If no client is available or deregistering fails, it returns an error
+//
+// This function SHOULD be called when the application exits such that the configuration file is saved correctly.
+// Note that saving of the configuration file also happens in other cases, such as after getting a VPN configuration.
+// Thus it is often not problematic if this function cannot be called due to a client crash
+//
+//export Deregister
+func Deregister() *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
+		return getCError(stateErr)
 	}
-	// FIXME: Return server result
-	_, addErr := state.AddCustomServer(C.GoString(url))
-	return getError(addErr)
+	state.Deregister()
+	VPNState = nil
+	return nil
 }
 
-//export RemoveInstituteAccess
-func RemoveInstituteAccess(name *C.char, url *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+// AddServer adds a server to the eduvpn-common server list
+// `c` is the cookie that is used for cancellation. Create a cookie first with CookieNew. This same cookie is also used for replying to state transitions
+//
+// `_type` is the type of server that needs to be added. This type is defined in types/server/server.go Type
+//
+// `id` is the identifier of the string:
+//
+//   - In case of secure internet: The organization ID
+//
+//   - In case of custom server: The base URL
+//
+//   - In case of institute access: The base URL
+//
+// `ni` stands for non-interactive. If non-zero, any state transitions will not be run.
+//
+// This `ni` flag is useful for preprovisioned servers. For normal usage, you want to set this to zero (meaning: False)
+//
+// If the server cannot be added it returns the error as types/error/error.go Error
+//
+// Note that the server is removed when an error has occured
+//
+// The following state callbacks are mandatory to handle:
+//
+//   - OAUTH_STARTED: This indicates that the OAuth procedure has been started, it returns the URL as the data.
+//     The client should open the webbrowser with this URL and continue the authorization process.
+//
+//export AddServer
+func AddServer(c C.uintptr_t, _type C.int, id *C.char, ni C.int) *C.char {
+	// TODO: type
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
-	}
-	removeErr := state.RemoveInstituteAccess(C.GoString(url))
-	return getError(removeErr)
-}
-
-//export RemoveCustomServer
-func RemoveCustomServer(name *C.char, url *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+		return getCError(stateErr)
+	}
+	v, err := getCookie(c)
+	if err != nil {
+		return getCError(err)
+	}
+	err = state.AddServer(v, C.GoString(id), srvtypes.Type(_type), ni != 0)
+	return getCError(err)
+}
+
+// RemoveServer removes a server from the eduvpn-common server list
+//
+// `_type` is the type of server that needs to be added. This type is defined in types/server/server.go Type
+//
+// `id` is the identifier of the string:
+//
+//   - In case of secure internet: The organization ID
+//
+//   - In case of custom server: The base URL
+//
+//   - In case of institute access: The base URL
+//
+// If the server cannot be removed it returns the error types/error/error.go Error
+//
+// Note that the server is not removed when an error has occured
+//
+//export RemoveServer
+func RemoveServer(_type C.int, id *C.char) *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
-	}
-	removeErr := state.RemoveCustomServer(C.GoString(url))
-	return getError(removeErr)
-}
-
-func cToken(t oauth.Token) *C.token {
-	cTok := (*C.token)(C.malloc(C.size_t(unsafe.Sizeof(C.token{}))))
-	cTok.access = C.CString(t.Access)
-	cTok.refresh = C.CString(t.Refresh)
-	cTok.expired = C.ulonglong(t.ExpiredTimestamp.Unix())
-	return cTok
-}
-
-func cConfig(config *client.ConfigData) *C.configData {
-	// No config so return nil pointer
-	if config == nil {
-		return nil
-	}
-	cConf := (*C.configData)(C.malloc(C.size_t(unsafe.Sizeof(C.configData{}))))
-	cConf.config = C.CString(config.Config)
-	cConf.config_type = C.CString(config.Type)
-	cConf.tokens = cToken(config.Tokens)
-	return cConf
-}
-
-//export FreeTokens
-func FreeTokens(tokens *C.token) {
-	C.free(unsafe.Pointer(tokens.access))
-	C.free(unsafe.Pointer(tokens.refresh))
-	C.free(unsafe.Pointer(tokens))
-}
-
-//export FreeConfig
-func FreeConfig(config *C.configData) {
-	C.free(unsafe.Pointer(config.config))
-	C.free(unsafe.Pointer(config.config_type))
-	C.free(unsafe.Pointer(config.tokens.access))
-	C.free(unsafe.Pointer(config.tokens.refresh))
-	C.free(unsafe.Pointer(config.tokens))
-	C.free(unsafe.Pointer(config))
-}
-
-//export GetConfigSecureInternet
-func GetConfigSecureInternet(
-	name *C.char,
-	orgID *C.char,
-	preferTCP C.int,
-	prevTokens C.token,
-) (*C.configData, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+		return getCError(stateErr)
+	}
+	err := state.RemoveServer(C.GoString(id), srvtypes.Type(_type))
+	return getCError(err)
+}
+
+// CurrentServer gets the current server from eduvpn-common
+//
+// In eduvpn-common, a server is marked as 'current' if you have gotten a VPN configuration for it
+//
+// It returns the server as JSON, defined in types/server/server.go Current
+//
+// If there is no current server or some other, e.g. there is no current state, an error is returned with a nil string
+//
+//export CurrentServer
+func CurrentServer() (*C.char, *C.char) {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return nil, getError(stateErr)
-	}
-	preferTCPBool := preferTCP == 1
-	t := oauth.Token{
-		Access:           C.GoString(prevTokens.access),
-		Refresh:          C.GoString(prevTokens.refresh),
-		ExpiredTimestamp: time.Unix(int64(prevTokens.expired), 0),
-	}
-	cfg, configErr := state.GetConfigSecureInternet(C.GoString(orgID), preferTCPBool, t)
-	return cConfig(cfg), getError(configErr)
-}
-
-//export GetConfigInstituteAccess
-func GetConfigInstituteAccess(
-	name *C.char,
-	url *C.char,
-	preferTCP C.int,
-	prevTokens C.token,
-) (*C.configData, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+		return nil, getCError(stateErr)
+	}
+	srv, err := state.CurrentServer()
+	if err != nil {
+		return nil, getCError(err)
+	}
+	ret, err := getReturnData(srv)
+	if err != nil {
+		return nil, getCError(err)
+	}
+	return C.CString(ret), nil
+}
+
+// ServerList gets the list of servers that are currently added
+//
+// This is NOT the discovery list, but the servers that have previously been added with `AddServer`
+//
+// It returns the server list as a JSON string defined in types/server/server.go List
+//
+// If the server list cannot be retrieved it returns a nil string and an error
+//
+//export ServerList
+func ServerList() (*C.char, *C.char) {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return nil, getError(stateErr)
-	}
-	preferTCPBool := preferTCP == 1
-	t := oauth.Token{
-		Access:           C.GoString(prevTokens.access),
-		Refresh:          C.GoString(prevTokens.refresh),
-		ExpiredTimestamp: time.Unix(int64(prevTokens.expired), 0),
-	}
-	cfg, configErr := state.GetConfigInstituteAccess(C.GoString(url), preferTCPBool, t)
-	return cConfig(cfg), getError(configErr)
-}
-
-//export GetConfigCustomServer
-func GetConfigCustomServer(
-	name *C.char,
-	url *C.char,
-	preferTCP C.int,
-	prevTokens C.token,
-) (*C.configData, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+		return nil, getCError(stateErr)
+	}
+	list, err := state.ServerList()
+	if err != nil {
+		return nil, getCError(err)
+	}
+	ret, err := getReturnData(list)
+	if err != nil {
+		return nil, getCError(err)
+	}
+	return C.CString(ret), nil
+}
+
+// GetConfig gets a configuration for the server
+//
+// `c` is the cookie that is used for cancellation. Create a cookie first with CookieNew, this same cookie is also used for replying to state transitions
+//
+// `_type` is the type of server that needs to be added. This type is defined in types/server/server.go Type
+//
+// `id` is the identifier of the string
+//
+//   - In case of secure internet: The organization ID
+//   - In case of custom server: The base URL
+//   - In case of institute access: The base URL
+//
+// `pTCP` is if we prefer TCP or not to get the configuration, non-zero means yes
+//
+// `startup` is if the client is just starting up, set this to true (non-zero) if you autoconnect to a server on startup.
+// If this startup value is true (non-zero) then any authorization or other callacks (profile/location) are not triggered
+//
+// After getting a configuration, the FSM moves to the GOT_CONFIG state
+// The return data is the configuration, marshalled as JSON and defined in types/server/server.go Configuration
+//
+// If the config cannot be retrieved it returns an error as types/error/error.go Error.
+//
+// The current state callbacks MUST be handled:
+//
+// ### ASK_PROFILE
+//
+// This asks the client for profile.
+//
+// This is called when the user/client has not set a profile for this server before, or the current profile is invalid
+//
+// When the user has selected a profile, reply with the choice using the `CookieReply` function and the profile ID
+// e.g. CookieReply(cookie, "wireguard"). CookieReply can be done in the background as the Go library waits for a reply
+//
+// The data for this transition is defined in types/server/server.go RequiredAskTransition with embedded data Profiles in types/server/server.go.
+// Note that RequiredTransition contains the cookie to be used for the CookieReply
+//
+// So a client would:
+//
+// - Parse the data to get the cookie and data
+//
+// - get the cookie
+//
+// - get the profiles from the data
+//
+// - show it in the UI and then reply with CookieReply using the choice
+//
+// ### ASK_LOCATION
+//
+// This asks the client for a location. Note that under normal circumstances,
+// this callback is not actually called as the home organization for the secure internet server is set as the current
+// if for some reason, an invalid location has been configured, the library will ask the client for a new one
+//
+// When the user has selected a location, reply with the choice using the `CookieReply` function and the location ID
+// e.g. CookieReply(cookie, "nl")
+//
+// CookieReply can be done in the background as the Go library waits for a reply
+// The data for this transition is defined in types/server/server.go RequiredAskTransition with embedded data a list of strings ([]string)
+//
+// Note that RequiredTransition contains the cookie to be used for the CookieReply,
+//
+// So a client would:
+//
+//   - Parse the data to get the cookie and data
+//
+//   - get the cookie
+//
+//   - get the list of locations from the data
+//
+//   - show it in the UI and then reply with CookieReply using the choice
+//
+// ### OAUTH_STARTED
+//
+// This indicates that the OAuth procedure has been started, it returns the URL as the data.
+//
+// The client should open the webbrowser with this URL and continue the authorization process.
+// This is only called if authorization needs to be retriggered
+//
+//export GetConfig
+func GetConfig(c C.uintptr_t, _type C.int, id *C.char, pTCP C.int, startup C.int) (*C.char, *C.char) {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return nil, getError(stateErr)
-	}
-	preferTCPBool := preferTCP == 1
-	t := oauth.Token{
-		Access:           C.GoString(prevTokens.access),
-		Refresh:          C.GoString(prevTokens.refresh),
-		ExpiredTimestamp: time.Unix(int64(prevTokens.expired), 0),
+		return nil, getCError(stateErr)
+	}
+	ck, err := getCookie(c)
+	if err != nil {
+		return nil, getCError(err)
+	}
+	preferTCPBool := pTCP != 0
+	startupBool := startup != 0
+	cfg, err := state.GetConfig(ck, C.GoString(id), srvtypes.Type(_type), preferTCPBool, startupBool)
+	if cfg != nil && err == nil {
+		d, err := getReturnData(cfg)
+		if err == nil {
+			return C.CString(d), nil
+		}
 	}
-	cfg, configErr := state.GetConfigCustomServer(C.GoString(url), preferTCPBool, t)
-	return cConfig(cfg), getError(configErr)
+	return nil, getCError(err)
 }
 
+// SetProfileID sets the profile ID of the current serrver
+//
+// This MUST only be called if the user/client wishes to manually set a profile instead of the common lib asking for one using a transition
+//
+// `Data` is the profile ID
+//
+// It returns an error if unsuccessful
+//
 //export SetProfileID
-func SetProfileID(name *C.char, data *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+func SetProfileID(data *C.char) *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
+		return getCError(stateErr)
 	}
 	profileErr := state.SetProfileID(C.GoString(data))
-	return getError(profileErr)
-}
-
-//export ChangeSecureLocation
-func ChangeSecureLocation(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
-	}
-	locationErr := state.ChangeSecureLocation()
-	return getError(locationErr)
-}
-
+	return getCError(profileErr)
+}
+
+// SetSecureLocation sets the location for the secure internet server if it exists
+//
+// This MUST only be called if the user/client wishes to manually set a location instead of the common lib asking for one using a transition
+//
+// Because this does network requests to initialize the location, there is a cookie again :)
+//
+// `c` is the Cookie that needs to be passed. To create a cookie, first call `CookieNew`
+// `Data` is the location ID
+//
+// It returns an error if unsuccessful
+//
 //export SetSecureLocation
-func SetSecureLocation(name *C.char, data *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+func SetSecureLocation(c C.uintptr_t, data *C.char) *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
-	}
-	locationErr := state.SetSecureLocation(C.GoString(data))
-	return getError(locationErr)
-}
-
-//export GoBack
-func GoBack(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+		return getCError(stateErr)
+	}
+	ck, err := getCookie(c)
+	if err != nil {
+		return getCError(err)
+	}
+	locationErr := state.SetSecureLocation(ck, C.GoString(data))
+	return getCError(locationErr)
+}
+
+// DiscoServers gets the servers from discovery, returned as types/discovery/discovery.go Servers marshalled as JSON
+//
+// `c` is the Cookie that needs to be passed. Create a new Cookie using `CookieNew`
+//
+// If it was unsuccessful, it returns an error. Note that when the lib was built in release mode the data is almost always non-nil, even when an error has occurred
+// This means it has just returned the cached list
+//
+//export DiscoServers
+func DiscoServers(c C.uintptr_t) (*C.char, *C.char) {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
+		return nil, getCError(stateErr)
 	}
-	goBackErr := state.GoBack()
-	return getError(goBackErr)
-}
-
-//export SetSearchServer
-func SetSearchServer(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
+	ck, err := getCookie(c)
+	if err != nil {
+		return nil, getCError(err)
 	}
-	setSearchErr := state.SetSearchServer()
-	return getError(setSearchErr)
-}
-
-//export Cleanup
-func Cleanup(name *C.char, prevTokens C.token) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
+	servers, err := state.DiscoServers(ck)
+	if servers == nil && err != nil {
+		return nil, getCError(err)
 	}
-	t := oauth.Token{
-		Access:           C.GoString(prevTokens.access),
-		Refresh:          C.GoString(prevTokens.refresh),
-		ExpiredTimestamp: time.Unix(int64(prevTokens.expired), 0),
+	s, reterr := getReturnData(servers)
+	if reterr != nil {
+		return nil, getCError(reterr)
 	}
-	err := state.Cleanup(t)
-	return getError(err)
+	return C.CString(s), getCError(err)
 }
 
-//export SetDisconnected
-func SetDisconnected(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+// DiscoOrganizations gets the organizations from discovery, returned as types/discovery/discovery.go Organizations marshalled as JSON
+//
+// `c` is the Cookie that needs to be passed. Create a new Cookie using `CookieNew`
+//
+// If it was unsuccessful, it returns an error. Note that when the lib was built in release mode the data is almost always non-nil, even when an error has occurred
+// This means it has just returned the cached list
+//
+//export DiscoOrganizations
+func DiscoOrganizations(c C.uintptr_t) (*C.char, *C.char) {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
+		return nil, getCError(stateErr)
 	}
-	setDisconnectedErr := state.SetDisconnected()
-	return getError(setDisconnectedErr)
-}
-
-//export SetDisconnecting
-func SetDisconnecting(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
+	ck, err := getCookie(c)
+	if err != nil {
+		return nil, getCError(err)
 	}
-	setDisconnectingErr := state.SetDisconnecting()
-	return getError(setDisconnectingErr)
-}
-
-//export SetConnecting
-func SetConnecting(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
+	orgs, err := state.DiscoOrganizations(ck)
+	if orgs == nil && err != nil {
+		return nil, getCError(err)
 	}
-	setConnectingErr := state.SetConnecting()
-	return getError(setConnectingErr)
-}
-
-//export SetConnected
-func SetConnected(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
+	s, reterr := getReturnData(orgs)
+	if reterr != nil {
+		return nil, getCError(reterr)
 	}
-	setConnectedErr := state.SetConnected()
-	return getError(setConnectedErr)
+	return C.CString(s), getCError(err)
 }
 
-//export RenewSession
-func RenewSession(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return getError(stateErr)
-	}
-	renewSessionErr := state.RenewSession()
-	return getError(renewSessionErr)
-}
-
-//export ShouldRenewButton
-func ShouldRenewButton(name *C.char) C.int {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+// Cleanup sends a /disconnect to cleanup the connection
+//
+// This MUST be called when disconnecting, see https://docs.eduvpn.org/server/v3/api.html#application-flow
+// `c` is the Cookie that needs to be passed. Create a new Cookie using `CookieNew`
+//
+// If it was unsuccessful, it returns an error
+//
+//export Cleanup
+func Cleanup(c C.uintptr_t) *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return C.int(0)
+		return getCError(stateErr)
 	}
-	shouldRenewBool := state.ShouldRenewButton()
-	if shouldRenewBool {
-		return C.int(1)
+	ck, err := getCookie(c)
+	if err != nil {
+		return getCError(err)
 	}
-	return C.int(0)
+	err = state.Cleanup(ck)
+	return getCError(err)
 }
 
-//export InFSMState
-func InFSMState(name *C.char, checkState C.int) C.int {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+// RenewSession renews the session of the VPN
+//
+// This essentially means that the OAuth tokens are deleted.
+// And it also possibly re-runs every state callback you need when getting a config.
+// So least you MUST handle the OAuth started transition
+//
+// It returns an error if unsuccessful
+//
+//export RenewSession
+func RenewSession(c C.uintptr_t) *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return C.int(0)
+		return getCError(stateErr)
 	}
-	inStateBool := state.InFSMState(client.FSMStateID(checkState))
-	if inStateBool {
-		return C.int(1)
+	ck, err := getCookie(c)
+	if err != nil {
+		return getCError(err)
 	}
-	return C.int(0)
+	renewSessionErr := state.RenewSession(ck)
+	return getCError(renewSessionErr)
 }
 
+// SetSupportWireguard enables or disables WireGuard for the client
+//
+// By default WireGuard support is enabled
+// To disable it you can pass a 0 int to this
+//
+// `support` thus indicates whether or not to enable WireGuard
+// An error is returned if this is not possible
+//
 //export SetSupportWireguard
-func SetSupportWireguard(name *C.char, support C.int) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+func SetSupportWireguard(support C.int) *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
+		return getCError(stateErr)
 	}
-	state.SupportsWireguard = support == 1
+	state.SupportsWireguard = support != 0
 	return nil
 }
 
+// StartFailover starts the 'failover' procedure in eduvpn-common
+//
+// Failover has one primary goal: check if the VPN can reach the gateway.
+// This can be used to check whether or not the client needs to 'failover' to prefer TCP (if currently using UDP).
+// Which is useful to go from a broken WireGuard connection to OpenVPN over TCP
+//
+//   - `c` is the cookie that is passed for cancellation. To create a cookie, use the `CookieNew` function
+//   - `gateway` is the gateway IP of the VPN
+//   - `readRxBytes` is a function that returns the current rx bytes of the VPN interface, this should return a `long long int` in c
+//
+// It returns a boolean whether or not the common lib has determined that it cannot reach the gateway. Non-zero=dropped, zero=not dropped.
+//
+// It also returns an error, if it fails to indicate if it has dropped or not. In this case, dropped is also set to zero
+//
 //export StartFailover
-func StartFailover(name *C.char, gateway *C.char, mtu C.int, readRxBytes C.ReadRxBytes) (C.int, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+func StartFailover(c C.uintptr_t, gateway *C.char, mtu C.int, readRxBytes C.ReadRxBytes) (C.int, *C.char) {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return C.int(0), getError(stateErr)
+		return C.int(0), getCError(stateErr)
 	}
-	dropped, droppedErr := state.StartFailover(C.GoString(gateway), int(mtu), func() (int64, error) {
+	ck, err := getCookie(c)
+	if err != nil {
+		return C.int(0), getCError(err)
+	}
+	dropped, droppedErr := state.StartFailover(ck, C.GoString(gateway), int(mtu), func() (int64, error) {
 		rxBytes := int64(C.get_read_rx_bytes(readRxBytes))
-		if rxBytes == -1 {
+		if rxBytes < 0 {
 			return 0, errors.New("client gave an invalid rx bytes value")
 		}
 		return rxBytes, nil
 	})
 	if droppedErr != nil {
-		return C.int(0), getError(droppedErr)
+		return C.int(0), getCError(droppedErr)
 	}
 	droppedC := C.int(0)
 	if dropped {
@@ -594,24 +644,212 @@ func StartFailover(name *C.char, gateway *C.char, mtu C.int, readRxBytes C.ReadR
 	return droppedC, nil
 }
 
-//export CancelFailover
-func CancelFailover(name *C.char) *C.error {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
+// SetState sets the state of the statemachine
+//
+// Note: this transitions the FSM into the new state without passing any data to it
+//
+//export SetState
+func SetState(fsmState C.int) *C.char {
+	state, stateErr := getVPNState()
 	if stateErr != nil {
-		return getError(stateErr)
+		return getCError(stateErr)
 	}
-	cancelErr := state.CancelFailover()
-	if cancelErr != nil {
-		return getError(cancelErr)
+	return getCError(state.SetState(client.FSMStateID(fsmState)))
+}
+
+// InState checks if the FSM is in `fsmState`
+//
+//export InState
+func InState(fsmState C.int) (C.int, *C.char) {
+	state, stateErr := getVPNState()
+	if stateErr != nil {
+		return 0, getCError(stateErr)
 	}
-	return nil
+
+	if yes := state.InState(client.FSMStateID(fsmState)); yes {
+		return 1, nil
+	}
+	return 0, nil
 }
 
+// FreeString frees a string that was allocated by the eduvpn-common Go library
+//
+// This happens when we return strings, such as errors from the Go lib back to the client.
+// The client MUST thus ensure that this memory is freed using this function.
+// Simply pass the pointer to the string in here
+//
 //export FreeString
 func FreeString(addr *C.char) {
 	C.free(unsafe.Pointer(addr))
 }
 
+func getCookie(c C.uintptr_t) (*cookie.Cookie, error) {
+	if c == 0 {
+		return nil, errors.New("cookie is nil")
+	}
+	h := cgo.Handle(c)
+	v, ok := h.Value().(*cookie.Cookie)
+	if !ok {
+		return nil, errors.New("value is not a cookie")
+	}
+	// the cookie itself has a reference to the handle
+	// such that we can return the same exact handle in callbacks
+	// TODO: On first glance this might not make any sense, find a better way
+	v.H = h
+	return v, nil
+}
+
+// SetTokenHandler sets the token getters and token setters for OAuth
+//
+// Because the data that is saved does not contain OAuth tokens for server, the common lib asks and sets the tokens using these callback functions.
+// The client can thus pass callbacks to this function so that the tokens can be securely stored in a keyring
+//
+// Client must pass two arguments to these functions
+//
+//   - getter is the void function that gets tokens from the client. It takes three arguments:
+//
+//   - The `server` for which to get the tokens for, marshalled as JSON and defined in types/server/server.go `Current`
+//
+//   - The `output` buffer
+//
+//   - The `length` of the output buffer. This 'output buffer' must contain the tokens, marshalled as JSON that is defined in types/server/server.go `Tokens`
+//
+// setter is the void function that sets tokens. It takes two arguments:
+//
+//   - The `server` for which to get the tokens for, marshalled as JSON and defined in types/server/server.go `Current`
+//
+//   - The `tokens`, defined in types/server/server.go `Tokens` marshalled as JSON
+//
+// It returns an error when the tokens cannot be set
+//
+//export SetTokenHandler
+func SetTokenHandler(getter C.TokenGetter, setter C.TokenSetter) *C.char {
+	state, stateErr := getVPNState()
+	if stateErr != nil {
+		return getCError(stateErr)
+	}
+	state.TokenSetter = func(c srvtypes.Current, t srvtypes.Tokens) {
+		cJSON, err := getReturnData(c)
+		if err != nil {
+			log.Logger.Warningf("failed to get current server for setting tokens in exports: %v", err)
+			return
+		}
+		tJSON, err := getReturnData(t)
+		if err != nil {
+			log.Logger.Warningf("failed to get tokens for setting tokens in exports: %v", err)
+			return
+		}
+		c1 := C.CString(cJSON)
+		c2 := C.CString(tJSON)
+		C.call_token_setter(setter, c1, c2)
+		FreeString(c1)
+		FreeString(c2)
+	}
+
+	state.TokenGetter = func(c srvtypes.Current) *srvtypes.Tokens {
+		cJSON, err := getReturnData(c)
+		if err != nil {
+			log.Logger.Warningf("failed to get current server for getting tokens in exports: %v", err)
+			return nil
+		}
+		c1 := C.CString(cJSON)
+		// create an output buffer with size 2048
+		// In my testing tokens seem to be ~1033 bytes marshalled as JSON
+		d := make([]byte, 2048)
+
+		C.call_token_getter(getter, c1, (*C.char)(unsafe.Pointer(&d[0])), C.size_t(len(d)))
+		FreeString(c1)
+
+		// get null pointer index as unmarshalling wants it without
+		null := bytes.IndexByte(d, 0)
+		if null < 0 {
+			log.Logger.Warningf("output buffer is not NULL terminated")
+			return nil
+		}
+
+		var gotT srvtypes.Tokens
+		err = json.Unmarshal(d[:null], &gotT)
+		if err != nil {
+			log.Logger.Warningf("failed to get json data for getting tokens in exports: %v", err)
+			return nil
+		}
+		return &gotT
+	}
+
+	return nil
+}
+
+// CookieNew creates a new cookie and returns it
+//
+// This value should not be parsed or converted somehow by the client
+// This value is simply to pass back to the Go library
+// This value has two purposes:
+//
+//   - Cancel a long running function
+//
+//   - Send a reply to a state transition (ASK_PROFILE and ASK_LOCATION)
+//
+// Functions that take a cookie have it as the first argument
+//
+//export CookieNew
+func CookieNew() C.uintptr_t {
+	c := cookie.NewWithContext(context.Background())
+	return C.uintptr_t(cgo.NewHandle(&c))
+}
+
+// CookieReply replies to a state transition using the cookie
+//
+// The data that is sent to the Go library is the second argument of this function
+//
+//   - `c` is the Cookie
+//
+//   - `data` is the data to send, e.g. a profile ID
+//
+//export CookieReply
+func CookieReply(c C.uintptr_t, data *C.char) *C.char {
+	v, err := getCookie(c)
+	if err != nil {
+		return getCError(err)
+	}
+	err = v.Send(C.GoString(data))
+	return getCError(err)
+}
+
+// CookieDelete deletes the cookie by cancelling it and deleting the underlying cgo handle
+//
+// This function MUST be called when the cookie that is created using `CookieNew` is no longer needed
+//
+//export CookieDelete
+func CookieDelete(c C.uintptr_t) *C.char {
+	v, err := getCookie(c)
+	if err != nil {
+		return getCError(err)
+	}
+	// cancel the cookie and then delete the handle
+	err = v.Cancel()
+	cgo.Handle(c).Delete()
+	return getCError(err)
+}
+
+// CookieCancel cancels the cookie
+//
+// This means that functions which take this as first argument, return if they're still running
+// The error cause is always context.Canceled for that cancelled function: https://pkg.go.dev/context#pkg-variables
+//
+// This CookieCancel function can also return an error if cancelling was unsuccessful
+//
+//export CookieCancel
+func CookieCancel(c C.uintptr_t) *C.char {
+	v, err := getCookie(c)
+	if err != nil {
+		return getCError(err)
+	}
+	err = v.Cancel()
+	if err != nil {
+		return getCError(err)
+	}
+	return nil
+}
+
 // Not used in library, but needed to compile.
 func main() { panic("compile with -buildmode=c-shared") }
diff --git a/exports/server.h b/exports/server.h
deleted file mode 100644
index 4bc8a165..00000000
--- a/exports/server.h
+++ /dev/null
@@ -1,47 +0,0 @@
-#ifndef SERVER_H
-#define SERVER_H
-
-// The struct for a single server profile
-typedef struct serverProfile {
-  const char* id;
-  const char* display_name;
-  //const char* proto_list;
-  int default_gateway;
-} serverProfile;
-
-// The struct for all server profiles
-typedef struct serverProfiles {
-  int current;
-  serverProfile** profiles;
-  size_t total_profiles;
-} serverProfiles;
-
-// The struct for server locations
-typedef struct serverLocations {
-  const char** locations;
-  size_t total_locations;
-} serverLocations;
-
-// The struct for a single server
-typedef struct server {
-  const char* identifier;
-  const char* display_name;
-  const char* server_type;
-  const char* country_code;
-  const char** support_contact;
-  size_t total_support_contact;
-  serverLocations* locations;
-  serverProfiles* profiles;
-  unsigned long long int expire_time;
-} server;
-
-// The struct for all servers
-typedef struct servers {
-  server** custom_servers;
-  size_t total_custom;
-  server** institute_servers;
-  size_t total_institute;
-  server* secure_internet_server;
-} servers;
-
-#endif /* GRANDPARENT_H */
diff --git a/exports/servers.go b/exports/servers.go
deleted file mode 100644
index 73b8b6c0..00000000
--- a/exports/servers.go
+++ /dev/null
@@ -1,342 +0,0 @@
-package main
-
-/*
-// for free and size_t
-#include <stdlib.h>
-#include "error.h"
-#include "server.h"
-*/
-import "C"
-
-import (
-	"unsafe"
-
-	"github.com/eduvpn/eduvpn-common/client"
-	"github.com/eduvpn/eduvpn-common/internal/server"
-)
-
-// Get the pointer to the C struct for the profile
-// We allocate the struct, the profile ID and the display name
-func getCPtrProfile(profile *server.Profile) *C.serverProfile {
-	// Allocate the struct using malloc and the size of the struct
-	cProfile := (*C.serverProfile)(C.malloc(C.size_t(unsafe.Sizeof(C.serverProfile{}))))
-	cProfile.id = C.CString(profile.ID)
-	cProfile.display_name = C.CString(profile.DisplayName)
-	if profile.DefaultGateway {
-		cProfile.default_gateway = C.int(1)
-	} else {
-		cProfile.default_gateway = C.int(0)
-	}
-
-	return cProfile
-}
-
-// Get the pointer to the C struct for the profiles
-// We allocate the struct and the struct inside it for the profiles
-func getCPtrProfiles(serverProfiles *server.ProfileInfo) *C.serverProfiles {
-	goProfiles := serverProfiles.Info.ProfileList
-	// Allocate the profles struct using malloc and the size of a pointer
-	cProfiles := (*C.serverProfiles)(C.malloc(C.size_t(uintptr(0))))
-	totalProfiles := C.size_t(len(goProfiles))
-	// Defaults if we have no profiles
-	cProfiles.current = C.int(0)
-	cProfiles.profiles = nil
-	cProfiles.total_profiles = totalProfiles
-	// If we have profiles (which we should), we allocate the struct with malloc and the size of a pointer
-	// We then fill the struct by converting it to a go slice and get a C pointer for each profile
-	if totalProfiles > 0 {
-		profilesPtr := (**C.serverProfile)(C.malloc(totalProfiles * C.size_t(unsafe.Sizeof(uintptr(0)))))
-		profiles := unsafe.Slice(profilesPtr, totalProfiles)
-		index := 0
-		for _, profile := range goProfiles {
-			profiles[index] = getCPtrProfile(&profile)
-			index++
-		}
-		cProfiles.current = C.int(serverProfiles.CurrentProfileIndex())
-		cProfiles.profiles = (**C.serverProfile)(profilesPtr)
-	}
-	return cProfiles
-}
-
-// Free the profiles by looping through them if there are any
-// Also free the pointer itself
-//
-//export FreeProfiles
-func FreeProfiles(profiles *C.serverProfiles) {
-	// We should only free the profiles if we have them (which we should)
-	if profiles.total_profiles > 0 {
-		// Convert it to a go slice
-		profilesSlice := unsafe.Slice(profiles.profiles, profiles.total_profiles)
-		// Loop through the pointers and free th  allocated strings and the struct itself
-		for i := C.size_t(0); i < profiles.total_profiles; i++ {
-			C.free(unsafe.Pointer(profilesSlice[i].id))
-			C.free(unsafe.Pointer(profilesSlice[i].display_name))
-			C.free(unsafe.Pointer(profilesSlice[i]))
-		}
-		// Free the inner profiles struct
-		C.free(unsafe.Pointer(profiles.profiles))
-	}
-	// Free the profiles struct itself
-	C.free(unsafe.Pointer(profiles))
-}
-
-// Get a list of strings with a size as a c structure
-// Returns the size in size_t and the list of strings as a double pointer char
-func getCPtrListStrings(allStrings []string) (C.size_t, **C.char) {
-	// Get the total strings in size_t
-	totalStrings := C.size_t(len(allStrings))
-
-	// If we have strings
-	// Allocate memory for the strings array
-	if totalStrings > 0 {
-		stringsPtr := (**C.char)(C.malloc(totalStrings * C.size_t(unsafe.Sizeof(uintptr(0)))))
-		// Go slice conversion
-		cStrings := unsafe.Slice(stringsPtr, totalStrings)
-
-		// Loop through and allocate the string for each contact
-		for index, string := range allStrings {
-			cStrings[index] = C.CString(string)
-		}
-		return totalStrings, (**C.char)(stringsPtr)
-	}
-
-	// No strings then the length is zero and the char array is nil
-	return C.size_t(0), nil
-}
-
-// Function for freeing an array/list of strings
-// It takes the strings as a pointer to a string and the total strings in size_t
-func freeCListStrings(allStrings **C.char, totalStrings C.size_t) {
-	// If we have strings we should free them
-	// By converting to a Go slice, and freeing them ony by one
-	// At last free the pointer itself
-	if totalStrings > 0 {
-		stringsSlice := unsafe.Slice(allStrings, totalStrings)
-		for i := C.size_t(0); i < totalStrings; i++ {
-			C.free(unsafe.Pointer(stringsSlice[i]))
-		}
-		C.free(unsafe.Pointer(allStrings))
-	}
-}
-
-// Function for getting the server,
-// It gets the main state as a pointer as we need to convert some string maps to localized strings
-// It gets the base information for a server as well
-func getCPtrServer(state *client.Client, base *client.ServerBase) *C.server {
-	// Allocation using malloc and the size of the struct
-	cServer := (*C.server)(C.malloc(C.size_t(unsafe.Sizeof(C.server{}))))
-	// String allocation and translate the display name
-	identifier := base.URL
-	countryCode := ""
-	// A secure internet server has multiple locations
-	locations := []string{}
-	if base.Type == "secure_internet" {
-		identifier = state.Servers.SecureInternetHomeServer.HomeOrganizationID
-		countryCode = state.Servers.SecureInternetHomeServer.CurrentLocation
-		locations = state.Discovery.SecureLocationList()
-	}
-
-	cServer.identifier = C.CString(identifier)
-	cServer.display_name = C.CString(state.GetTranslated(base.DisplayName))
-	cServer.country_code = C.CString(countryCode)
-	cServer.server_type = C.CString(base.Type)
-	// Call the helper to get the list of support contacts
-	cServer.total_support_contact, cServer.support_contact = getCPtrListStrings(
-		base.SupportContact,
-	)
-	locationsStruct := (*C.serverLocations)(C.malloc(C.size_t(unsafe.Sizeof(C.servers{}))))
-	locationsStruct.total_locations, locationsStruct.locations = getCPtrListStrings(locations)
-	cServer.locations = locationsStruct
-
-	profiles := base.ValidProfiles(state.SupportsWireguard)
-	cServer.profiles = getCPtrProfiles(&profiles)
-	// No endtime is given if we get servers when it has been partially initialised
-	if base.EndTime.IsZero() {
-		cServer.expire_time = C.ulonglong(0)
-	} else {
-		// The expire time should be stored as an unsigned long long in unix time
-		cServer.expire_time = C.ulonglong(base.EndTime.Unix())
-	}
-	return cServer
-}
-
-// Function for freeing a single server
-// Gets the pointer to C struct
-//
-//export FreeServer
-func FreeServer(info *C.server) {
-	// Free strings
-	C.free(unsafe.Pointer(info.identifier))
-	C.free(unsafe.Pointer(info.display_name))
-	C.free(unsafe.Pointer(info.country_code))
-	C.free(unsafe.Pointer(info.server_type))
-
-	// Free arrays
-	freeCListStrings(info.support_contact, info.total_support_contact)
-	FreeSecureLocations(info.locations)
-	FreeProfiles(info.profiles)
-
-	// Free the struct itself
-	C.free(unsafe.Pointer(info))
-}
-
-// Get the C ptr to the servers, returns the length in size_t and the double pointer to the struct
-func getCPtrServers(
-	state *client.Client,
-	serverMap map[string]*server.InstituteAccessServer,
-) (C.size_t, **C.server) {
-	totalServers := C.size_t(len(serverMap))
-	// If we have servers, which is not always the case
-	if totalServers > 0 {
-		serversPtr := (**C.server)(C.malloc(totalServers * C.size_t(unsafe.Sizeof(uintptr(0)))))
-		servers := unsafe.Slice(serversPtr, totalServers)
-		index := 0
-		for _, currentServer := range serverMap {
-			cServer := getCPtrServer(state, &currentServer.Basic)
-			servers[index] = cServer
-			index++
-		}
-		return totalServers, serversPtr
-	}
-	return C.size_t(0), nil
-}
-
-// This function takes the servers as a C struct pointer as input
-// It frees all allocated memory for the server
-//
-//export FreeServers
-func FreeServers(cServers *C.servers) {
-	// Free the custom servers if there are any
-	if cServers.total_custom > 0 {
-		customServers := unsafe.Slice(cServers.custom_servers, cServers.total_custom)
-		for i := C.size_t(0); i < cServers.total_custom; i++ {
-			FreeServer(customServers[i])
-		}
-		C.free(unsafe.Pointer(cServers.custom_servers))
-	}
-	// Free the institute access servers if there are any
-	if cServers.total_institute > 0 {
-		instituteServers := unsafe.Slice(cServers.institute_servers, cServers.total_institute)
-
-		for i := C.size_t(0); i < cServers.total_institute; i++ {
-			FreeServer(instituteServers[i])
-		}
-		C.free(unsafe.Pointer(cServers.institute_servers))
-	}
-	// Free the secure internet server if there is one
-	if cServers.secure_internet_server != nil {
-		FreeServer(cServers.secure_internet_server)
-	}
-	// Free the structure itself
-	C.free(unsafe.Pointer(cServers))
-}
-
-// Return the servers as a C struct pointer
-// It takes the state as a pointer as we need to translate some strings
-// It also takes the servers as a pointer that belongs to the main state or gathered from the callback
-func getSavedServersWithOptions(state *client.Client, servers *server.Servers) *C.servers {
-	// Allocate the struct that we will return
-	// With the size of the c struct
-	returnedStruct := (*C.servers)(C.malloc(C.size_t(unsafe.Sizeof(C.servers{}))))
-
-	// Get the different categories of servers
-	totalCustom, customPtr := getCPtrServers(state, servers.CustomServers.Map)
-	totalInstitute, institutePtr := getCPtrServers(state, servers.InstituteServers.Map)
-	var secureServerPtr *C.server
-	secureInternetBase, secureInternetBaseErr := servers.SecureInternetHomeServer.Base()
-	if secureInternetBaseErr == nil && secureInternetBase != nil {
-		// FIXME: log error?
-		secureServerPtr = getCPtrServer(state, secureInternetBase)
-		// Give a new identifier
-		C.free(unsafe.Pointer(secureServerPtr.identifier))
-		secureServerPtr.identifier = C.CString(servers.SecureInternetHomeServer.HomeOrganizationID)
-		secureServerPtr.country_code = C.CString(servers.SecureInternetHomeServer.CurrentLocation)
-	}
-
-	// Fill the struct and return
-	returnedStruct.custom_servers = customPtr
-	returnedStruct.total_custom = totalCustom
-	returnedStruct.institute_servers = institutePtr
-	returnedStruct.total_institute = totalInstitute
-	returnedStruct.secure_internet_server = secureServerPtr
-	return returnedStruct
-}
-
-// This function takes the name as input which is the name of the client
-// It gets the state by name and then returns the saved servers as a c struct belonging to it
-//
-//export GetSavedServers
-func GetSavedServers(name *C.char) (*C.servers, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return nil, getError(stateErr)
-	}
-	servers := getSavedServersWithOptions(state, &state.Servers)
-	return servers, nil
-}
-
-// This function takes the name as input which is the name of the client
-// It gets the state by name and then returns the current server as a c struct belonging to it
-//
-//export GetCurrentServer
-func GetCurrentServer(name *C.char) (*C.server, *C.error) {
-	nameStr := C.GoString(name)
-	state, stateErr := GetVPNState(nameStr)
-	if stateErr != nil {
-		return nil, getError(stateErr)
-	}
-	server, serverErr := state.Servers.GetCurrentServer()
-	if serverErr != nil {
-		return nil, getError(serverErr)
-	}
-	base, baseErr := server.Base()
-	if baseErr != nil {
-		return nil, getError(baseErr)
-	}
-	cServer := getCPtrServer(state, base)
-	return cServer, nil
-}
-
-// This function takes the state as input which is the main state
-// It also takes the data as an interface and if it has the servers type gets the data as a c struct otherwise nil
-func getTransitionDataServers(state *client.Client, data interface{}) *C.servers {
-	if converted, ok := data.(server.Servers); ok {
-		return getSavedServersWithOptions(state, &converted)
-	}
-	return nil
-}
-
-//export FreeSecureLocations
-func FreeSecureLocations(locations *C.serverLocations) {
-	freeCListStrings(locations.locations, locations.total_locations)
-	C.free(unsafe.Pointer(locations))
-}
-
-func getTransitionSecureLocations(data interface{}) *C.serverLocations {
-	if locations, ok := data.([]string); ok {
-		returnedStruct := (*C.serverLocations)(C.malloc(C.size_t(unsafe.Sizeof(C.servers{}))))
-		returnedStruct.total_locations, returnedStruct.locations = getCPtrListStrings(locations)
-		return returnedStruct
-	}
-	return nil
-}
-
-func getTransitionProfiles(data interface{}) *C.serverProfiles {
-	if profiles, ok := data.(*server.ProfileInfo); ok {
-		return getCPtrProfiles(profiles)
-	}
-	return nil
-}
-
-func getTransitionServer(state *client.Client, data interface{}) *C.server {
-	if server, ok := data.(server.Server); ok {
-		base, baseErr := server.Base()
-		if baseErr != nil {
-			// TODO: LOG
-			return nil
-		}
-		return getCPtrServer(state, base)
-	}
-	return nil
-}
diff --git a/genexportsdoc.py b/genexportsdoc.py
new file mode 100755
index 00000000..ed538b2d
--- /dev/null
+++ b/genexportsdoc.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+
+import subprocess
+
+cmd=["go", "doc", "--all", "exports"]
+
+output=subprocess.check_output(cmd).decode("utf-8")
+
+section = [""]
+package_doc = ""
+
+in_section = True
+
+num = 0
+for out in output.splitlines():
+    line = out + "\n"
+    if line.startswith(" "):
+        line = line[4:]
+    if out.startswith("FUNCTIONS") or out.startswith("VARIABLES"):
+        in_section = False
+    if out.startswith("func"):
+        in_section = True
+        section.append(line)
+        num += 1
+        continue
+    if in_section:
+        section[num] += line
+
+def func_name(signature: str) -> str:
+    idx = signature.index("(")
+    return signature[len("func "):idx]
+
+def gen_toc(title: str) -> str:
+    id = "-".join(title.lower().split(" "))
+    return f"[{title}](#{id})"
+
+toc = ""
+first = True
+
+gen_sections = []
+for sec in section:
+    if first:
+        gen_sections.append(("About the API", sec))
+        first = False
+        continue
+    lines = sec.splitlines()
+    signature, doc = lines[0], "\n".join(lines[1:])
+    body = f"Signature:\n ```go\n{signature}\n```\n{doc}"
+    gen_sections.append((func_name(signature), body))
+
+first = True
+toc = "# Table of contents\n"
+for title, body in gen_sections:
+    if first:
+        toc += f"- {gen_toc(title)}\n"
+        func_toc = gen_toc("Functions")
+        toc += f"- {func_toc}\n"
+        first = False
+        continue
+    toc += f"    * {gen_toc(title)}\n"
+
+data = "This document was automatically generated from the exports/exports.go file\n\n"
+data += f"{toc}\n"
+first = True
+for title, body in gen_sections:
+    if first:
+        data += f"# {title}\n"
+        data += f"{body}\n"
+        data += "# Functions\n"
+        first = False
+        continue
+    data += f"## {title}\n"
+    data += f"{body}\n"
+
+with open("docs/src/api/functiondocs.md", "w+") as f:
+    f.write(data)
diff --git a/go.mod b/go.mod
index 71b28ef6..3a384515 100644
--- a/go.mod
+++ b/go.mod
@@ -4,12 +4,14 @@ go 1.18
 
 require (
 	github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267
+	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
+	golang.org/x/text v0.13.0
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
 )
 
 require (
 	github.com/go-errors/errors v1.4.2
-	golang.org/x/crypto v0.12.0 // indirect
-	golang.org/x/net v0.14.0
-	golang.org/x/sys v0.11.0 // indirect
+	golang.org/x/crypto v0.13.0 // indirect
+	golang.org/x/net v0.15.0
+	golang.org/x/sys v0.12.0 // indirect
 )
diff --git a/go.sum b/go.sum
index b762812c..4e790272 100644
--- a/go.sum
+++ b/go.sum
@@ -1,23 +1,18 @@
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/jedisct1/go-minisign v0.0.0-20230211184525-1f273d8dc776 h1:WXhZ7psl6HhDDW58rDWIJE6oB0ETjaQA4U6d8U7lMyg=
-github.com/jedisct1/go-minisign v0.0.0-20230211184525-1f273d8dc776/go.mod h1:09CTTv5TZgz94QHts03Xnuzy5LmxCE8BNqQRFigO5gA=
 github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY=
 github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde h1:ybF7AMzIUikL9x4LgwEmzhXtzRpKNqngme1VGDWz+Nk=
-golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
diff --git a/i18nerr/i18nerr.go b/i18nerr/i18nerr.go
new file mode 100644
index 00000000..d513fdd8
--- /dev/null
+++ b/i18nerr/i18nerr.go
@@ -0,0 +1,166 @@
+// package i18nerr implements errors with internationalization using gotext
+package i18nerr
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+
+	"github.com/eduvpn/eduvpn-common/internal/log"
+
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
+)
+
+var printers sync.Map
+var once sync.Once
+
+
+// TranslatedInner defines errors that are used as inner causes but are still translated because they can happen frequently
+func TranslatedInner(inner error) (string, bool) {
+	unwrapped := inner
+	for errors.Unwrap(unwrapped) != nil {
+		unwrapped = errors.Unwrap(unwrapped)
+	}
+
+	switch {
+	case errors.Is(inner, context.DeadlineExceeded):
+		return printerOrNew(language.English).Sprintf("timeout reached"), false
+	case errors.Is(inner, context.Canceled):
+		return unwrapped.Error(), true
+	}
+	return unwrapped.Error(), false
+}
+
+// Error wraps an actual error with the translation key
+// This translation key is later used to lookup translation
+// The inner error always consists of the translation key and some formatting
+type Error struct {
+	key message.Reference
+	args []interface{}
+	wrapped *Error
+	Misc bool
+}
+
+func (e *Error) translated(t language.Tag) string {
+	once.Do(func() {
+		inititializeLangs()
+	})
+	msg := printerOrNew(t).Sprintf(e.key, e.args...)
+	if e.wrapped != nil {
+		return msg + " " + printerOrNew(t).Sprintf("with cause:") + " " + e.wrapped.Error()
+	}
+	return msg
+}
+
+// Error gets the error string
+// it does this by simply forwarding the error method from the actual inner error
+func (e *Error) Error() string {
+	return e.translated(language.English)
+}
+
+// Translations returns all the translations for the error including the source translation (english)
+func (e *Error) Translations() map[string]string {
+	translations := make(map[string]string)
+	// add the source transltaion first
+	source := e.Error()
+	translations[language.English.String()] = source
+	for _, t := range message.DefaultCatalog.Languages() {
+		// already added
+		if t == language.English {
+			continue
+		}
+		// get the final translation string for the tag
+		// and add it if it's not equal to the english version
+		f := e.translated(t)
+		if f != source {
+			translations[t.String()] = f
+		}
+	}
+	return translations
+}
+
+
+// Unwrap returns the unwrapped error
+// it does this by unwrapping the inner error
+func (e *Error) Unwrap() error {
+	if e.wrapped == nil {
+		return nil
+	}
+	return e.wrapped.Unwrap()
+}
+
+// printerOrNew gets a message printer from the global printers map using the tag 'tag'
+// If the printer cannot be found in the sync map, we return a new printer
+func printerOrNew(tag language.Tag) *message.Printer {
+	v, ok := printers.Load(tag)
+	if !ok {
+		log.Logger.Debugf("i18n: could not load printer with tag: '%v' from map", tag)
+		return message.NewPrinter(tag)
+	}
+	p, ok := v.(*message.Printer)
+	if !ok {
+		log.Logger.Debugf("i18n: could not load printer with tag: '%v' as the type is not correct: '%T'", tag, p)
+		return message.NewPrinter(tag)
+	}
+	return p
+}
+
+// New creates a new i18n error using a message reference
+func New(key message.Reference) *Error {
+	_ = printerOrNew(language.English).Sprint(key)
+	return &Error{key: key}
+}
+
+// Newf creates a new i18n error using a message reference and arguments.
+// It formats this with fmt.Errorf
+func Newf(key message.Reference, args ...interface{}) *Error {
+	_ = printerOrNew(language.English).Sprintf(key, args...)
+	return &Error{key: key, args: args}
+}
+
+// Wrap creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key'.
+// It formats this with fmt.Errorf
+func Wrap(err error, key message.Reference) *Error {
+	_ = printerOrNew(language.English).Sprintf(key)
+	t, misc := TranslatedInner(err)
+	return &Error{key: key, wrapped: &Error{key: t, Misc: misc}, Misc: misc}
+}
+
+// Wrapf creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key' with format arguments 'args'.
+// It formats this with fmt.Errorf
+func Wrapf(err error, key message.Reference, args ...interface{}) *Error {
+	_ = printerOrNew(language.English).Sprintf(key, args...)
+	t, misc := TranslatedInner(err)
+	return &Error{key: key, args: args, wrapped: &Error{key: t, Misc: misc}, Misc: misc}
+}
+
+// NewInternalf creates an internal localised error from a display string
+func NewInternal(disp string) *Error {
+	return Wrap(errors.New(disp), "An internal error occurred")
+}
+
+// NewInternalf creates an internal localised error from a display string and arguments
+func NewInternalf(disp string, args...interface{}) *Error {
+	return NewInternal(fmt.Sprintf(disp, args...))
+}
+
+// WrapInternal wraps an error and a display string into a localised internal error
+func WrapInternal(err error, disp string) *Error {
+	return NewInternal(fmt.Sprintf("%s with internal cause: %v", disp, err))
+}
+
+// WrapInternalf wraps an error and a display string with args into a localised internal error
+func WrapInternalf(err error, disp string, args ...interface{}) *Error {
+	return WrapInternal(err, fmt.Sprintf(disp, args...))
+}
+
+// initializeLangs initializes the printers from the default catalog into the sync map
+// we cannot do this in init() because this is too early
+func inititializeLangs() {
+	log.Logger.Debugf("i18n: initializing languages...")
+	for _, t := range message.DefaultCatalog.Languages() {
+		printers.Store(t, message.NewPrinter(t))
+	}
+}
diff --git a/internal/discovery/.gitignore b/internal/discovery/.gitignore
new file mode 100644
index 00000000..2ff34c5e
--- /dev/null
+++ b/internal/discovery/.gitignore
@@ -0,0 +1,4 @@
+/organization_list.json
+/organization_list.json.minisig
+/server_list.json
+/server_list.json.minisig
diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go
index 788bca28..06548f90 100644
--- a/internal/discovery/discovery.go
+++ b/internal/discovery/discovery.go
@@ -2,13 +2,14 @@
 package discovery
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"time"
 
 	"github.com/eduvpn/eduvpn-common/internal/http"
 	"github.com/eduvpn/eduvpn-common/internal/verify"
-	"github.com/eduvpn/eduvpn-common/types"
+	discotypes "github.com/eduvpn/eduvpn-common/types/discovery"
 	"github.com/go-errors/errors"
 )
 
@@ -21,17 +22,17 @@ type Discovery struct {
 	httpClient *http.Client
 
 	// OrganizationList represents the organizations that are returned by the discovery server
-	OrganizationList types.DiscoveryOrganizations `json:"organizations"`
+	OrganizationList discotypes.Organizations `json:"organizations"`
 
 	// ServerList represents the servers that are returned by the discovery server
-	ServerList types.DiscoveryServers `json:"servers"`
+	ServerList discotypes.Servers `json:"servers"`
 }
 
 var DiscoURL = "https://disco.eduvpn.org/v2/"
 
 // file is a helper function that gets a disco JSON and fills the structure with it
 // If it was unsuccessful it returns an error.
-func (discovery *Discovery) file(jsonFile string, previousVersion uint64, structure interface{}) error {
+func (discovery *Discovery) file(ctx context.Context, jsonFile string, previousVersion uint64, structure interface{}) error {
 	// No HTTP client present, create one
 	if discovery.httpClient == nil {
 		discovery.httpClient = http.NewClient()
@@ -42,7 +43,7 @@ func (discovery *Discovery) file(jsonFile string, previousVersion uint64, struct
 	if err != nil {
 		return err
 	}
-	_, body, err := discovery.httpClient.Get(jsonURL)
+	_, body, err := discovery.httpClient.Get(ctx, jsonURL)
 	if err != nil {
 		return err
 	}
@@ -53,7 +54,7 @@ func (discovery *Discovery) file(jsonFile string, previousVersion uint64, struct
 	if err != nil {
 		return err
 	}
-	_, sigBody, err := discovery.httpClient.Get(sigURL)
+	_, sigBody, err := discovery.httpClient.Get(ctx, sigURL)
 	if err != nil {
 		return err
 	}
@@ -115,7 +116,7 @@ func (discovery *Discovery) SecureLocationList() []string {
 func (discovery *Discovery) ServerByURL(
 	baseURL string,
 	srvType string,
-) (*types.DiscoveryServer, error) {
+) (*discotypes.Server, error) {
 	for _, currentServer := range discovery.ServerList.List {
 		if currentServer.BaseURL == baseURL && currentServer.Type == srvType {
 			return &currentServer, nil
@@ -126,7 +127,7 @@ func (discovery *Discovery) ServerByURL(
 
 // ServerByCountryCode returns the discovery server by the country code
 // An error is returned if and only if nil is returned for the server.
-func (discovery *Discovery) ServerByCountryCode(countryCode string) (*types.DiscoveryServer, error) {
+func (discovery *Discovery) ServerByCountryCode(countryCode string) (*discotypes.Server, error) {
 	for _, srv := range discovery.ServerList.List {
 		if srv.CountryCode == countryCode && srv.Type == "secure_internet" {
 			return &srv, nil
@@ -137,7 +138,7 @@ func (discovery *Discovery) ServerByCountryCode(countryCode string) (*types.Disc
 
 // orgByID returns the discovery organization by the organization ID
 // An error is returned if and only if nil is returned for the organization.
-func (discovery *Discovery) orgByID(orgID string) (*types.DiscoveryOrganization, error) {
+func (discovery *Discovery) orgByID(orgID string) (*discotypes.Organization, error) {
 	for _, org := range discovery.OrganizationList.List {
 		if org.OrgID == orgID {
 			return &org, nil
@@ -150,7 +151,7 @@ func (discovery *Discovery) orgByID(orgID string) (*types.DiscoveryOrganization,
 // - The organization it belongs to
 // - The secure internet server itself
 // An error is returned if and only if nil is returned for the organization.
-func (discovery *Discovery) SecureHomeArgs(orgID string) (*types.DiscoveryOrganization, *types.DiscoveryServer, error) {
+func (discovery *Discovery) SecureHomeArgs(orgID string) (*discotypes.Organization, *discotypes.Server, error) {
 	org, err := discovery.orgByID(orgID)
 	if err != nil {
 		return nil, nil, err
@@ -178,7 +179,7 @@ func (discovery *Discovery) DetermineServersUpdate() bool {
 	return !time.Now().Before(upd)
 }
 
-func (discovery *Discovery) previousOrganizations() (*types.DiscoveryOrganizations, error) {
+func (discovery *Discovery) previousOrganizations() (*discotypes.Organizations, error) {
 	// If the version field is not zero then we have a cached struct
 	// We also immediately return this copy if we have no embedded JSON
 	if discovery.OrganizationList.Version != 0 || !HasCache {
@@ -186,7 +187,7 @@ func (discovery *Discovery) previousOrganizations() (*types.DiscoveryOrganizatio
 	}
 
 	// We do not have a cached struct, this we need to get it using the embedded JSON
-	var eo types.DiscoveryOrganizations
+	var eo discotypes.Organizations
 	if err := json.Unmarshal(eOrganizations, &eo); err != nil {
 		return nil, errors.WrapPrefix(err, "failed parsing discovery organizations from the embedded cache", 0)
 	}
@@ -194,7 +195,7 @@ func (discovery *Discovery) previousOrganizations() (*types.DiscoveryOrganizatio
 	return &eo, nil
 }
 
-func (discovery *Discovery) previousServers() (*types.DiscoveryServers, error) {
+func (discovery *Discovery) previousServers() (*discotypes.Servers, error) {
 	// If the version field is not zero then we have a cached struct
 	// We also immediately return this copy if we have no embedded JSON
 	if discovery.ServerList.Version != 0 || !HasCache {
@@ -202,7 +203,7 @@ func (discovery *Discovery) previousServers() (*types.DiscoveryServers, error) {
 	}
 
 	// We do not have a cached struct, this we need to get it using the embedded JSON
-	var es types.DiscoveryServers
+	var es discotypes.Servers
 	if err := json.Unmarshal(eServers, &es); err != nil {
 		return nil, errors.WrapPrefix(err, "failed parsing discovery servers from the embedded cache", 0)
 	}
@@ -212,12 +213,12 @@ func (discovery *Discovery) previousServers() (*types.DiscoveryServers, error) {
 
 // Organizations returns the discovery organizations
 // If there was an error, a cached copy is returned if available.
-func (discovery *Discovery) Organizations() (*types.DiscoveryOrganizations, error) {
+func (discovery *Discovery) Organizations(ctx context.Context) (*discotypes.Organizations, error) {
 	if !discovery.DetermineOrganizationsUpdate() {
 		return &discovery.OrganizationList, nil
 	}
 	file := "organization_list.json"
-	err := discovery.file(file, discovery.OrganizationList.Version, &discovery.OrganizationList)
+	err := discovery.file(ctx, file, discovery.OrganizationList.Version, &discovery.OrganizationList)
 	if err != nil {
 		// Return previous with an error
 		// TODO: Log here if we fail to get previous
@@ -230,12 +231,12 @@ func (discovery *Discovery) Organizations() (*types.DiscoveryOrganizations, erro
 
 // Servers returns the discovery servers
 // If there was an error, a cached copy is returned if available.
-func (discovery *Discovery) Servers() (*types.DiscoveryServers, error) {
+func (discovery *Discovery) Servers(ctx context.Context) (*discotypes.Servers, error) {
 	if !discovery.DetermineServersUpdate() {
 		return &discovery.ServerList, nil
 	}
 	file := "server_list.json"
-	err := discovery.file(file, discovery.ServerList.Version, &discovery.ServerList)
+	err := discovery.file(ctx, file, discovery.ServerList.Version, &discovery.ServerList)
 	if err != nil {
 		// Return previous with an error
 		// TODO: Log here if we fail to get previous
diff --git a/internal/failover/monitor.go b/internal/failover/monitor.go
index f143f940..2219fc4c 100644
--- a/internal/failover/monitor.go
+++ b/internal/failover/monitor.go
@@ -17,9 +17,6 @@ type DroppedConMon struct {
 	// The function that reads Rx bytes
 	// If this function returns an error, the monitor exits
 	readRxBytes func() (int64, error)
-	// The cancel context
-	// This is used to cancel the dropped connection monitor
-	cancel context.CancelFunc
 }
 
 func NewDroppedMonitor(pingInterval time.Duration, pDropped int, readRxBytes func() (int64, error)) *DroppedConMon {
@@ -40,18 +37,13 @@ func (m *DroppedConMon) dropped(startBytes int64) (bool, error) {
 // Start starts ticking every ping interval and check if the connection is dropped or alive
 // This does not check Rx bytes every tick, but rather when pAlive or pDropped is reached
 // It returns an error if there was an invalid input or a ping was failed to be sent
-func (m *DroppedConMon) Start(gateway string, mtuSize int) (bool, error) {
+func (m *DroppedConMon) Start(ctx context.Context, gateway string, mtuSize int) (bool, error) {
 	if mtuSize <= 0 {
 		return false, errors.New("invalid mtu size given")
 	}
 
-	// Create a context and save the cancel function
-	ctx, cancel := context.WithCancel(context.Background())
-	m.cancel = cancel
-	defer m.cancel()
-
 	// Create a ping struct with our mtu size
-	p, err := NewPinger(mtuSize)
+	p, err := NewPinger(gateway, mtuSize)
 	if err != nil {
 		return false, err
 	}
@@ -64,7 +56,7 @@ func (m *DroppedConMon) Start(gateway string, mtuSize int) (bool, error) {
 
 	// Send a ping and wait for max 2 seconds
 	// If we have then increased Rx bytes we return early
-	if err = p.Send(gateway, 1); err != nil {
+	if err = p.Send(1); err != nil {
 		log.Logger.Debugf("[Failover] First ping failed, exiting...")
 		return false, err
 	}
@@ -90,7 +82,7 @@ func (m *DroppedConMon) Start(gateway string, mtuSize int) (bool, error) {
 	for s := 2; s <= m.pDropped; s++ {
 		log.Logger.Debugf("[Failover] Sending ping: %d, with size: %d", s, mtuSize)
 		// Send a ping and return if an error occurs
-		if err := p.Send(gateway, s); err != nil {
+		if err := p.Send(s); err != nil {
 			log.Logger.Debugf("[Failover] A ping failed, exiting...")
 			return false, err
 		}
@@ -99,17 +91,10 @@ func (m *DroppedConMon) Start(gateway string, mtuSize int) (bool, error) {
 		case <-ticker.C:
 			continue
 		case <-ctx.Done():
-			return false, errors.New("failover was cancelled")
+			return false, errors.WrapPrefix(context.Canceled, "failover was stopped", 0)
 		}
 	}
 
 	// Dropped check if we have not returned early
 	return m.dropped(b)
 }
-
-// Cancel cancels the dropped connection failover monitor if there is one
-func (m *DroppedConMon) Cancel() {
-	if m.cancel != nil {
-		m.cancel()
-	}
-}
diff --git a/internal/failover/ping.go b/internal/failover/ping.go
index 15d4adbd..fb1df4e9 100644
--- a/internal/failover/ping.go
+++ b/internal/failover/ping.go
@@ -18,14 +18,7 @@ var mtuOverhead = 28
 type Pinger struct {
 	listener net.PacketConn
 	buffer   []byte
-}
-
-func NewPinger(size int) (*Pinger, error) {
-	l, err := icmp.ListenPacket("udp4", "0.0.0.0")
-	if err != nil {
-		return nil, errors.WrapPrefix(err, "failed creating ping", 0)
-	}
-	return &Pinger{listener: l, buffer: make([]byte, size-mtuOverhead)}, nil
+	gateway  net.Addr
 }
 
 func (p Pinger) Read(deadline time.Time) error {
@@ -52,7 +45,7 @@ func (p Pinger) Read(deadline time.Time) error {
 	}
 }
 
-func (p Pinger) Send(gateway string, seq int) error {
+func (p Pinger) Send(seq int) error {
 	errorMessage := fmt.Sprintf("failed sending ping, seq %d", seq)
 	// Make a new ICMP message
 	m := icmp.Message{
@@ -68,7 +61,7 @@ func (p Pinger) Send(gateway string, seq int) error {
 		return errors.WrapPrefix(err, errorMessage, 0)
 	}
 	// And send it to the gateway IP!
-	_, err = p.listener.WriteTo(b, &net.UDPAddr{IP: net.ParseIP(gateway)})
+	_, err = p.listener.WriteTo(b, p.gateway)
 	if err != nil {
 		return errors.WrapPrefix(err, errorMessage, 0)
 	}
diff --git a/internal/failover/ping_default.go b/internal/failover/ping_default.go
new file mode 100644
index 00000000..ef56d5ef
--- /dev/null
+++ b/internal/failover/ping_default.go
@@ -0,0 +1,22 @@
+//go:build !windows
+
+package failover
+
+import (
+	"net"
+
+	"github.com/go-errors/errors"
+	"golang.org/x/net/icmp"
+)
+
+func NewPinger(gateway string, size int) (*Pinger, error) {
+	l, err := icmp.ListenPacket("udp4", "0.0.0.0")
+	if err != nil {
+		return nil, errors.WrapPrefix(err, "failed creating ping", 0)
+	}
+	return &Pinger{
+		listener: l,
+		buffer:   make([]byte, size-mtuOverhead),
+		gateway:  &net.UDPAddr{IP: net.ParseIP(gateway)},
+	}, nil
+}
diff --git a/internal/failover/ping_windows.go b/internal/failover/ping_windows.go
new file mode 100644
index 00000000..0de1cdff
--- /dev/null
+++ b/internal/failover/ping_windows.go
@@ -0,0 +1,20 @@
+package failover
+
+import (
+	"net"
+
+	"github.com/go-errors/errors"
+	"golang.org/x/net/icmp"
+)
+
+func NewPinger(gateway string, size int) (*Pinger, error) {
+	l, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
+	if err != nil {
+		return nil, errors.WrapPrefix(err, "failed creating ping", 0)
+	}
+	return &Pinger{
+		listener: l,
+		buffer:   make([]byte, size-mtuOverhead),
+		gateway:  &net.IPAddr{IP: net.ParseIP(gateway)},
+	}, nil
+}
diff --git a/internal/fsm/fsm.go b/internal/fsm/fsm.go
index 84b5f1bd..c7ae1048 100644
--- a/internal/fsm/fsm.go
+++ b/internal/fsm/fsm.go
@@ -71,6 +71,9 @@ type FSM struct {
 
 	// GetStateName gets the name of a state as a string
 	GetStateName func(StateID) string
+
+	// initial is the initial state that we can always go back to
+	initial StateID
 }
 
 // Init initializes the state machine and sets it to the given current state.
@@ -88,6 +91,7 @@ func (fsm *FSM) Init(
 	fsm.Directory = directory
 	fsm.GetStateName = nameGen
 	fsm.Generate = generate
+	fsm.initial = current
 }
 
 // InState returns whether or not the state machine is in the given 'check' state.
@@ -96,6 +100,9 @@ func (fsm *FSM) InState(check StateID) bool {
 }
 
 func (fsm *FSM) CheckTransition(desired StateID) error {
+	if desired == fsm.initial {
+		return nil
+	}
 	for _, ts := range fsm.States[fsm.Current].Transitions {
 		if ts.To == desired {
 			return nil
@@ -131,17 +138,24 @@ func (fsm *FSM) writeGraph() {
 // If this transition is not handled by the client, it returns an error.
 func (fsm *FSM) GoTransitionRequired(newState StateID, data interface{}) error {
 	oldState := fsm.Current
-	if !fsm.GoTransitionWithData(newState, data) {
-		return errors.Errorf("fsm failed transition from '%v' to '%v'", oldState, newState)
+
+	handled, err := fsm.GoTransitionWithData(newState, data)
+	// transition ios not possible
+	if err != nil {
+		return err
+	}
+	// transition is not handled
+	if !handled {
+		return errors.Errorf("fsm failed transition from '%v' to '%v', is this required transition handled?", fsm.GetStateName(oldState), fsm.GetStateName(newState))
 	}
 	return nil
 }
 
 // GoTransitionWithData is a helper that transitions the state machine toward the 'newState' with associated state data 'data'
 // It returns whether or not the transition is handled by the client.
-func (fsm *FSM) GoTransitionWithData(newState StateID, data interface{}) bool {
-	if fsm.CheckTransition(newState) != nil {
-		return false
+func (fsm *FSM) GoTransitionWithData(newState StateID, data interface{}) (bool, error) {
+	if err := fsm.CheckTransition(newState); err != nil {
+		return false, err
 	}
 
 	prev := fsm.Current
@@ -150,11 +164,11 @@ func (fsm *FSM) GoTransitionWithData(newState StateID, data interface{}) bool {
 		fsm.writeGraph()
 	}
 
-	return fsm.StateCallback(prev, newState, data)
+	return fsm.StateCallback(prev, newState, data), nil
 }
 
 // GoTransition is an alias to call GoTransitionWithData but have an empty string as data.
-func (fsm *FSM) GoTransition(newState StateID) bool {
+func (fsm *FSM) GoTransition(newState StateID) (bool, error) {
 	// No data means the callback is never required
 	return fsm.GoTransitionWithData(newState, "")
 }
diff --git a/internal/http/http.go b/internal/http/http.go
index f3f7563b..58e8f05b 100644
--- a/internal/http/http.go
+++ b/internal/http/http.go
@@ -50,7 +50,7 @@ func cleanPath(u *url.URL, trailing bool) string {
 // - It 'cleans' up the path using path.Clean
 // - It makes sure that the URL ends with a /
 // It returns an error if the URL cannot be parsed.
-func EnsureValidURL(s string) (string, error) {
+func EnsureValidURL(s string, trailing bool) (string, error) {
 	u, err := url.Parse(s)
 	if err != nil {
 		return "", errors.WrapPrefix(err, "failed parsing url", 0)
@@ -60,7 +60,7 @@ func EnsureValidURL(s string) (string, error) {
 	if u.Scheme != "https" {
 		u.Scheme = "https"
 	}
-	return cleanPath(u, true), nil
+	return cleanPath(u, trailing), nil
 }
 
 // JoinURLPath joins url's path, in go 1.19 we can use url.JoinPath
@@ -155,18 +155,18 @@ func NewClient() *Client {
 }
 
 // Get creates a Get request and returns the headers, body and an error.
-func (c *Client) Get(url string) (http.Header, []byte, error) {
-	return c.Do(http.MethodGet, url, nil)
+func (c *Client) Get(ctx context.Context, url string) (http.Header, []byte, error) {
+	return c.Do(ctx, http.MethodGet, url, nil)
 }
 
 // PostWithOpts creates a Post request with optional parameters and returns the headers, body and an error.
-func (c *Client) PostWithOpts(url string, opts *OptionalParams) (http.Header, []byte, error) {
-	return c.Do(http.MethodPost, url, opts)
+func (c *Client) PostWithOpts(ctx context.Context, url string, opts *OptionalParams) (http.Header, []byte, error) {
+	return c.Do(ctx, http.MethodPost, url, opts)
 }
 
 // MethodWithOpts Do send a HTTP request using a method (e.g. GET, POST), an url and optional parameters
 // It returns the HTTP headers, the body and an error if there is one.
-func (c *Client) Do(method string, urlStr string, opts *OptionalParams) (http.Header, []byte, error) {
+func (c *Client) Do(ctx context.Context, method string, urlStr string, opts *OptionalParams) (http.Header, []byte, error) {
 	// Make sure the url contains all the parameters
 	// This can return an error,
 	// it already has the right error, so we don't wrap it further
@@ -182,7 +182,7 @@ func (c *Client) Do(method string, urlStr string, opts *OptionalParams) (http.He
 		timeout = opts.Timeout
 	}
 
-	ctx, cncl := context.WithTimeout(context.Background(), timeout)
+	ctx, cncl := context.WithTimeout(ctx, timeout)
 	defer cncl()
 
 	// Create request object with the body reader generated from the optional arguments
@@ -248,5 +248,5 @@ func (e *StatusError) Error() string {
 
 // RegisterAgent registers the user agent for client and version
 func RegisterAgent(client string, version string) {
-	UserAgent = fmt.Sprintf("%s/%s %s", client, version, "eduvpn-common/1.1.2")
+	UserAgent = fmt.Sprintf("%s/%s %s", client, version, "eduvpn-common/2.0.0")
 }
diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go
index f2e77197..4a873f1f 100644
--- a/internal/oauth/oauth.go
+++ b/internal/oauth/oauth.go
@@ -101,9 +101,6 @@ type OAuth struct {
 
 // exchangeSession is a structure that gets passed to the callback for easy access to the current state.
 type exchangeSession struct {
-	// ClientID is the ID of the OAuth client
-	ClientID string
-
 	// ISS indicates the issuer identifier
 	ISS string
 
@@ -123,12 +120,12 @@ type exchangeSession struct {
 // AccessToken gets the OAuth access token used for contacting the server API
 // It returns the access token as a string, possibly obtained fresh using the Refresh Token
 // If the token cannot be obtained, an error is returned and the token is an empty string.
-func (oauth *OAuth) AccessToken() (string, error) {
+func (oauth *OAuth) AccessToken(ctx context.Context) (string, error) {
 	tl := oauth.token
 	if tl == nil {
 		return "", errors.New("No token structure available")
 	}
-	return tl.Access()
+	return tl.Access(ctx)
 }
 
 // setupListener sets up an OAuth listener
@@ -147,7 +144,7 @@ func (oauth *OAuth) setupListener() error {
 
 // tokensWithCallback gets the OAuth tokens using a local web server
 // If it was unsuccessful it returns an error.
-func (oauth *OAuth) tokensWithCallback() error {
+func (oauth *OAuth) tokensWithCallback(ctx context.Context) error {
 	if oauth.session.Listener == nil {
 		return errors.New("failed getting tokens with callback: no listener")
 	}
@@ -159,7 +156,7 @@ func (oauth *OAuth) tokensWithCallback() error {
 		// A bit overkill maybe for a local server but good to define anyways
 		ReadHeaderTimeout: 60 * time.Second,
 	}
-	defer s.Shutdown(context.Background()) //nolint:errcheck
+	defer s.Shutdown(ctx) //nolint:errcheck
 
 	// Use a sync.Once to only handle one request up until we shutdown the server
 	var once sync.Once
@@ -174,7 +171,12 @@ func (oauth *OAuth) tokensWithCallback() error {
 			oauth.session.ErrChan <- errors.WrapPrefix(err, "failed getting tokens with callback", 0)
 		}
 	}()
-	return <-oauth.session.ErrChan
+	select {
+	case err := <-oauth.session.ErrChan:
+		return err
+	case <-ctx.Done():
+		return errors.WrapPrefix(context.Canceled, "stopped oauth server", 0)
+	}
 }
 
 // tokenResponse fills the OAuth token response structure by the response
@@ -221,7 +223,7 @@ func (oauth *OAuth) Token() Token {
 // Access tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.4
 // Refresh tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.2
 // If it was unsuccessful it returns an error.
-func (oauth *OAuth) tokensWithAuthCode(authCode string) error {
+func (oauth *OAuth) tokensWithAuthCode(ctx context.Context, authCode string) error {
 	// Make sure the verifier is set as the parameter
 	// so that the server can verify that we are the actual owner of the authorization code
 	u := oauth.TokenURL
@@ -232,7 +234,7 @@ func (oauth *OAuth) tokensWithAuthCode(authCode string) error {
 	}
 
 	data := url.Values{
-		"client_id":     {oauth.session.ClientID},
+		"client_id":     {oauth.ClientID},
 		"code":          {authCode},
 		"code_verifier": {oauth.session.Verifier},
 		"grant_type":    {"authorization_code"},
@@ -245,7 +247,7 @@ func (oauth *OAuth) tokensWithAuthCode(authCode string) error {
 	now := time.Now()
 
 	// We are sure that we have a http client because we have initialized it when starting the exchange
-	_, body, err := oauth.httpClient.PostWithOpts(u, opts)
+	_, body, err := oauth.httpClient.PostWithOpts(ctx, u, opts)
 	if err != nil {
 		return err
 	}
@@ -274,7 +276,7 @@ func (oauth *OAuth) UpdateTokens(t Token) {
 // Access tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.4
 // Refresh tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.2
 // If it was unsuccessful it returns an error.
-func (oauth *OAuth) refreshResponse(r string) (*TokenResponse, time.Time, error) {
+func (oauth *OAuth) refreshResponse(ctx context.Context, r string) (*TokenResponse, time.Time, error) {
 	u := oauth.TokenURL
 	if oauth.token == nil {
 		return nil, time.Time{}, errors.New("No oauth token structure in refresh")
@@ -298,7 +300,7 @@ func (oauth *OAuth) refreshResponse(r string) (*TokenResponse, time.Time, error)
 		oauth.httpClient = httpw.NewClient()
 	}
 
-	_, body, err := oauth.httpClient.PostWithOpts(u, opts)
+	_, body, err := oauth.httpClient.PostWithOpts(ctx, u, opts)
 	if err != nil {
 		return nil, time.Time{}, err
 	}
@@ -394,7 +396,7 @@ func (s *exchangeSession) Authcode(url *url.URL) (string, error) {
 
 // tokenHandler gets the tokens using the authorization code that is obtained through the url
 // This function is called by the http handler and returns an error if the tokens cannot be obtained
-func (oauth *OAuth) tokenHandler(url *url.URL) error {
+func (oauth *OAuth) tokenHandler(ctx context.Context, url *url.URL) error {
 	// Get the authorization code
 	c, err := oauth.session.Authcode(url)
 	if err != nil {
@@ -402,14 +404,15 @@ func (oauth *OAuth) tokenHandler(url *url.URL) error {
 	}
 	// Now that we have obtained the authorization code, we can move to the next step:
 	// Obtaining the access and refresh tokens
-	return oauth.tokensWithAuthCode(c)
+	return oauth.tokensWithAuthCode(ctx, c)
 }
 
 // Handler is the function used to get the OAuth tokens using an authorization code callback
 // The callback to retrieve the authorization code: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.1
 // It sends an error to the session channel (can be nil)
 func (oauth *OAuth) Handler(w http.ResponseWriter, req *http.Request) {
-	err := oauth.tokenHandler(req.URL)
+	// TODO: should this be something else than context background?
+	err := oauth.tokenHandler(context.Background(), req.URL)
 	if err != nil {
 		_ = writeResponseHTML(
 			w,
@@ -426,7 +429,8 @@ func (oauth *OAuth) Handler(w http.ResponseWriter, req *http.Request) {
 // - OAuth server issuer identification
 // - The URL used for authorization
 // - The URL to obtain new tokens.
-func (oauth *OAuth) Init(iss string, baseAuthorizationURL string, tokenURL string) {
+func (oauth *OAuth) Init(clientID string, iss string, baseAuthorizationURL string, tokenURL string) {
+	oauth.ClientID = clientID
 	oauth.ISS = iss
 	oauth.BaseAuthorizationURL = baseAuthorizationURL
 	oauth.TokenURL = tokenURL
@@ -463,7 +467,6 @@ func (oauth *OAuth) AuthURL(name string, postProcessAuth func(string) string) (s
 
 	// Fill the struct with the necessary fields filled for the next call to getting the HTTP client
 	oauth.session = exchangeSession{
-		ClientID: name,
 		ISS:      oauth.ISS,
 		State:    state,
 		Verifier: v,
@@ -495,6 +498,8 @@ func (oauth *OAuth) AuthURL(name string, postProcessAuth func(string) string) (s
 	if err != nil {
 		return "", errors.WrapPrefix(err, fmt.Sprintf("failed to parse OAuth base URL '%s'", oauth.BaseAuthorizationURL), 0)
 	}
+	// Make sure the scheme is HTTPS
+	p.Scheme = "https"
 
 	u, err := httpw.ConstructURL(p, params)
 	if err != nil {
@@ -507,17 +512,12 @@ func (oauth *OAuth) AuthURL(name string, postProcessAuth func(string) string) (s
 
 // Exchange starts the OAuth exchange by getting the tokens with the redirect callback
 // If it was unsuccessful it returns an error.
-func (oauth *OAuth) Exchange() error {
+func (oauth *OAuth) Exchange(ctx context.Context) error {
 	// If there is no HTTP client defined, create a new one
 	if oauth.httpClient == nil {
 		oauth.httpClient = httpw.NewClient()
 	}
-	return oauth.tokensWithCallback()
-}
-
-// Cancel cancels the existing OAuth server by sending a cancel error to the channel
-func (oauth *OAuth) Cancel() {
-	oauth.session.ErrChan <- errors.Wrap(&CancelledCallbackError{}, 0)
+	return oauth.tokensWithCallback(ctx)
 }
 
 type CancelledCallbackError struct{}
diff --git a/internal/oauth/token.go b/internal/oauth/token.go
index 58d61365..251b689f 100644
--- a/internal/oauth/token.go
+++ b/internal/oauth/token.go
@@ -1,11 +1,13 @@
 package oauth
 
 import (
+	"context"
 	"fmt"
 	"sync"
 	"time"
 
 	"github.com/eduvpn/eduvpn-common/internal/log"
+	"github.com/eduvpn/eduvpn-common/types/server"
 	"github.com/go-errors/errors"
 )
 
@@ -37,12 +39,20 @@ type Token struct {
 	ExpiredTimestamp time.Time
 }
 
+func (t *Token) Public() server.Tokens {
+	return server.Tokens{
+		Access:  t.Access,
+		Refresh: t.Refresh,
+		Expires: t.ExpiredTimestamp.Unix(),
+	}
+}
+
 // tokenRefresher is a structure that contains our access and refresh tokens and a timestamp when they expire.
 // Additionally, it contains the refresher to get new tokens
 type tokenRefresher struct {
 	Token
 	// Refresher is the function that refreshes the token
-	Refresher func(string) (*TokenResponse, time.Time, error)
+	Refresher func(context.Context, string) (*TokenResponse, time.Time, error)
 }
 
 // tokenLock is a wrapper around token that protects it with a lock
@@ -58,7 +68,7 @@ type tokenLock struct {
 // Access gets the OAuth access token used for contacting the server API
 // It returns the access token as a string, possibly obtained fresh using the refresher
 // If the token cannot be obtained, an error is returned and the token is an empty string.
-func (l *tokenLock) Access() (string, error) {
+func (l *tokenLock) Access(ctx context.Context) (string, error) {
 	log.Logger.Debugf("Getting access token")
 	l.mu.Lock()
 	defer l.mu.Unlock()
@@ -78,7 +88,7 @@ func (l *tokenLock) Access() (string, error) {
 	}
 
 	// Otherwise refresh and then later return the access token if we are successful
-	tr, s, err := l.t.Refresher(l.t.Refresh)
+	tr, s, err := l.t.Refresher(ctx, l.t.Refresh)
 	if err != nil {
 		log.Logger.Debugf("Got a refresh token error: %v", err)
 		// We have failed to ensure the tokens due to refresh not working
diff --git a/internal/server/api.go b/internal/server/api/api.go
similarity index 59%
rename from internal/server/api.go
rename to internal/server/api/api.go
index 546c02a1..9ad6f2d9 100644
--- a/internal/server/api.go
+++ b/internal/server/api/api.go
@@ -1,6 +1,7 @@
-package server
+package api
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -10,65 +11,43 @@ import (
 
 	httpw "github.com/eduvpn/eduvpn-common/internal/http"
 	"github.com/eduvpn/eduvpn-common/internal/log"
+	"github.com/eduvpn/eduvpn-common/internal/oauth"
+	"github.com/eduvpn/eduvpn-common/internal/server/base"
+	"github.com/eduvpn/eduvpn-common/internal/server/endpoints"
+	"github.com/eduvpn/eduvpn-common/internal/server/profile"
 	"github.com/go-errors/errors"
 )
 
-func validateEndpoints(endpoints Endpoints) error {
-	v3 := endpoints.API.V3
-	pAPI, err := url.Parse(v3.API)
+func Endpoints(ctx context.Context, b *base.Base) error {
+	uStr, err := httpw.JoinURLPath(b.URL, "/.well-known/vpn-user-portal")
 	if err != nil {
-		return errors.WrapPrefix(err, "failed to parse API endpoint", 0)
-	}
-	pAuth, err := url.Parse(v3.Authorization)
-	if err != nil {
-		return errors.WrapPrefix(err, "failed to parse API authorization endpoint", 0)
-	}
-	pToken, err := url.Parse(v3.Token)
-	if err != nil {
-		return errors.WrapPrefix(err, "failed to parse API token endpoint", 0)
-	}
-	if pAPI.Scheme != pAuth.Scheme {
-		return errors.Errorf("API scheme: '%v', is not equal to authorization scheme: '%v'", pAPI.Scheme, pAuth.Scheme)
-	}
-	if pAPI.Scheme != pToken.Scheme {
-		return errors.Errorf("API scheme: '%v', is not equal to token scheme: '%v'", pAPI.Scheme, pToken.Scheme)
-	}
-	if pAPI.Host != pAuth.Host {
-		return errors.Errorf("API host: '%v', is not equal to authorization host: '%v'", pAPI.Host, pAuth.Host)
-	}
-	if pAPI.Host != pToken.Host {
-		return errors.Errorf("API host: '%v', is not equal to token host: '%v'", pAPI.Host, pToken.Host)
-	}
-	return nil
-}
-
-func APIGetEndpoints(baseURL string, client *httpw.Client) (*Endpoints, error) {
-	uStr, err := httpw.JoinURLPath(baseURL, "/.well-known/vpn-user-portal")
-	if err != nil {
-		return nil, err
+		return err
 	}
-	if client == nil {
-		client = httpw.NewClient()
+	if b.HTTPClient == nil {
+		b.HTTPClient = httpw.NewClient()
 	}
-	_, body, err := client.Get(uStr)
+	_, body, err := b.HTTPClient.Get(ctx, uStr)
 	if err != nil {
-		return nil, errors.WrapPrefix(err, "failed getting server endpoints", 0)
+		return errors.WrapPrefix(err, "failed getting server endpoints", 0)
 	}
 
-	ep := Endpoints{}
+	ep := endpoints.Endpoints{}
 	if err = json.Unmarshal(body, &ep); err != nil {
-		return nil, errors.WrapPrefix(err, "failed getting server endpoints", 0)
+		return errors.WrapPrefix(err, "failed getting server endpoints", 0)
 	}
-	err = validateEndpoints(ep)
+	err = ep.Validate()
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	return &ep, nil
+	b.Endpoints = ep
+	return nil
 }
 
-func apiAuthorized(
-	srv Server,
+func authorized(
+	ctx context.Context,
+	b *base.Base,
+	oauth *oauth.OAuth,
 	method string,
 	endpoint string,
 	opts *httpw.OptionalParams,
@@ -78,10 +57,6 @@ func apiAuthorized(
 		opts = &httpw.OptionalParams{}
 	}
 	errorMessage := "failed API authorized"
-	b, err := srv.Base()
-	if err != nil {
-		return nil, nil, errors.WrapPrefix(err, errorMessage, 0)
-	}
 
 	// Join the paths
 	u, err := url.Parse(b.Endpoints.API.V3.API)
@@ -91,7 +66,7 @@ func apiAuthorized(
 	u.Path = path.Join(u.Path, endpoint)
 
 	// Make sure the tokens are valid, this will return an error if re-login is needed
-	t, err := HeaderToken(srv)
+	t, err := oauth.AccessToken(ctx)
 	if err != nil {
 		return nil, nil, errors.WrapPrefix(err, errorMessage, 0)
 	}
@@ -105,19 +80,21 @@ func apiAuthorized(
 	}
 
 	// Create a client if it doesn't exist
-	if b.httpClient == nil {
-		b.httpClient = httpw.NewClient()
+	if b.HTTPClient == nil {
+		b.HTTPClient = httpw.NewClient()
 	}
-	return b.httpClient.Do(method, u.String(), opts)
+	return b.HTTPClient.Do(ctx, method, u.String(), opts)
 }
 
-func apiAuthorizedRetry(
-	srv Server,
+func authorizedRetry(
+	ctx context.Context,
+	b *base.Base,
+	auth *oauth.OAuth,
 	method string,
 	endpoint string,
 	opts *httpw.OptionalParams,
 ) (http.Header, []byte, error) {
-	h, body, err := apiAuthorized(srv, method, endpoint, opts)
+	h, body, err := authorized(ctx, b, auth, method, endpoint, opts)
 	if err == nil {
 		return h, body, nil
 	}
@@ -127,27 +104,22 @@ func apiAuthorizedRetry(
 	if errors.As(err, &statErr) && statErr.Status == 401 {
 		log.Logger.Debugf("Got a 401 error after HTTP method: %s, endpoint: %s. Marking token as expired...", method, endpoint)
 		// Mark the token as expired and retry, so we trigger the refresh flow
-		MarkTokenExpired(srv)
-		h, body, err = apiAuthorized(srv, method, endpoint, opts)
+		auth.SetTokenExpired()
+		h, body, err = authorized(ctx, b, auth, method, endpoint, opts)
 	}
 	return h, body, err
 }
 
-func APIInfo(srv Server) error {
-	_, body, err := apiAuthorizedRetry(srv, http.MethodGet, "/info", nil)
+func Info(ctx context.Context, b *base.Base, auth *oauth.OAuth) error {
+	_, body, err := authorizedRetry(ctx, b, auth, http.MethodGet, "/info", nil)
 	if err != nil {
 		return err
 	}
-	profiles := ProfileInfo{}
+	profiles := profile.Info{}
 	if err = json.Unmarshal(body, &profiles); err != nil {
 		return errors.WrapPrefix(err, "failed API /info", 0)
 	}
 
-	b, err := srv.Base()
-	if err != nil {
-		return err
-	}
-
 	// Store the profiles and make sure that the current profile is not overwritten
 	prev := b.Profiles.Current
 	b.Profiles = profiles
@@ -163,8 +135,10 @@ func boolToYesNo(preferTCP bool) string {
 	return "no"
 }
 
-func APIConnectWireguard(
-	srv Server,
+func ConnectWireguard(
+	ctx context.Context,
+	b *base.Base,
+	auth *oauth.OAuth,
 	profileID string,
 	pubkey string,
 	preferTCP bool,
@@ -186,7 +160,7 @@ func APIConnectWireguard(
 		"public_key": {pubkey},
 		"prefer_tcp": {boolToYesNo(preferTCP)},
 	}
-	h, body, err := apiAuthorizedRetry(srv, http.MethodPost, "/connect",
+	h, body, err := authorizedRetry(ctx, b, auth, http.MethodPost, "/connect",
 		&httpw.OptionalParams{Headers: hdrs, Body: vals})
 	if err != nil {
 		return "", "", time.Time{}, err
@@ -208,7 +182,7 @@ func APIConnectWireguard(
 	return string(body), content, expTime, nil
 }
 
-func APIConnectOpenVPN(srv Server, profileID string, preferTCP bool) (string, time.Time, error) {
+func ConnectOpenVPN(ctx context.Context, b *base.Base, auth *oauth.OAuth, profileID string, preferTCP bool) (string, time.Time, error) {
 	hdrs := http.Header{
 		"content-type": {"application/x-www-form-urlencoded"},
 		"accept":       {"application/x-openvpn-profile"},
@@ -219,7 +193,7 @@ func APIConnectOpenVPN(srv Server, profileID string, preferTCP bool) (string, ti
 		"prefer_tcp": {boolToYesNo(preferTCP)},
 	}
 
-	h, body, err := apiAuthorizedRetry(srv, http.MethodPost, "/connect",
+	h, body, err := authorizedRetry(ctx, b, auth, http.MethodPost, "/connect",
 		&httpw.OptionalParams{Headers: hdrs, Body: vals})
 	if err != nil {
 		return "", time.Time{}, err
@@ -234,10 +208,10 @@ func APIConnectOpenVPN(srv Server, profileID string, preferTCP bool) (string, ti
 	return string(body), expT, nil
 }
 
-// APIDisconnect disconnects from the API.
-func APIDisconnect(server Server) error {
+// Disconnect disconnects the VPN using the API.
+func Disconnect(ctx context.Context, b *base.Base, auth *oauth.OAuth) error {
 	// The timeout is a bit lower here such that this does not take a too long time for disconnecting
 	// Clients may wish to retry this
-	_, _, err := apiAuthorized(server, http.MethodPost, "/disconnect", &httpw.OptionalParams{Timeout: 5 * time.Second})
+	_, _, err := authorized(ctx, b, auth, http.MethodPost, "/disconnect", &httpw.OptionalParams{Timeout: 5 * time.Second})
 	return err
 }
diff --git a/internal/server/base.go b/internal/server/base.go
deleted file mode 100644
index bfa0d73c..00000000
--- a/internal/server/base.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package server
-
-import (
-	"time"
-
-	"github.com/eduvpn/eduvpn-common/internal/http"
-)
-
-// Base is the base type for servers.
-type Base struct {
-	URL            string            `json:"base_url"`
-	DisplayName    map[string]string `json:"display_name"`
-	SupportContact []string          `json:"support_contact"`
-	Endpoints      Endpoints         `json:"endpoints"`
-	Profiles       ProfileInfo       `json:"profiles"`
-	StartTime      time.Time         `json:"start_time"`
-	EndTime        time.Time         `json:"expire_time"`
-	Type           string            `json:"server_type"`
-	httpClient     *http.Client
-}
-
-func (b *Base) InitializeEndpoints() error {
-	ep, err := APIGetEndpoints(b.URL, b.httpClient)
-	if err != nil {
-		return err
-	}
-	b.Endpoints = *ep
-	return nil
-}
-
-func (b *Base) ValidProfiles(wireguardSupport bool) ProfileInfo {
-	var valid []Profile
-	for _, p := range b.Profiles.Info.ProfileList {
-		// Not a valid profile because it does not support openvpn
-		// Also the client does not support wireguard
-		if !p.SupportsOpenVPN() && !wireguardSupport {
-			continue
-		}
-		valid = append(valid, p)
-	}
-	return ProfileInfo{
-		Current: b.Profiles.Current,
-		Info:    ProfileListInfo{ProfileList: valid},
-	}
-}
diff --git a/internal/server/base/base.go b/internal/server/base/base.go
new file mode 100644
index 00000000..d483dadc
--- /dev/null
+++ b/internal/server/base/base.go
@@ -0,0 +1,90 @@
+package base
+
+import (
+	"time"
+
+	"github.com/eduvpn/eduvpn-common/internal/http"
+	"github.com/eduvpn/eduvpn-common/internal/server/endpoints"
+	"github.com/eduvpn/eduvpn-common/internal/server/profile"
+	"github.com/eduvpn/eduvpn-common/types/server"
+)
+
+// Base is the base type for servers.
+type Base struct {
+	URL            string              `json:"base_url"`
+	DisplayName    map[string]string   `json:"display_name"`
+	SupportContact []string            `json:"support_contact"`
+	Endpoints      endpoints.Endpoints `json:"endpoints"`
+	Profiles       profile.Info        `json:"profiles"`
+	StartTime      time.Time           `json:"start_time"`
+	EndTime        time.Time           `json:"expire_time"`
+	Type           server.Type         `json:"server_type"`
+	HTTPClient     *http.Client        `json:"-"`
+}
+
+// RenewButtonTime returns the time when the renew button should be shown for the server
+// Implemented according to: https://github.com/eduvpn/documentation/blob/cdf4d054f7652d74e4192494e8bb0e21040e46ac/API.md#session-expiry
+func (b *Base) RenewButtonTime() int64 {
+	d := b.EndTime.Sub(b.StartTime)
+
+	// If the time is less than 24 hours (a day left), show it when 30 minutes have passed or on expired if less than 30 minutes
+	dayl := time.Duration(24 * time.Hour)
+	if d < dayl {
+		// Get the minimum time to add, 30 minutes or on expired
+		m := time.Duration(30 * time.Minute)
+		// The total delta time is larger, return that we should show the button after 30 minutes
+		if d > m {
+			return b.StartTime.Add(30 * time.Minute).Unix()
+		}
+		// Just show it on expired
+		return b.StartTime.Add(d).Unix()
+	}
+
+	// Else just show it when 24 hours is left
+	// This is the delta minus 24 hours left as that's how long it takes for a day to be left in the expiry
+	// We thus add this to the start time
+	tillDay := d - dayl
+	t := b.StartTime.Add(tillDay)
+	return t.Unix()
+}
+
+func (b *Base) CountdownTime() int64 {
+	d := b.EndTime.Sub(b.StartTime)
+
+	dayl := time.Duration(24 * time.Hour)
+
+	// This is just the last 24 hours
+	// if less than or equal to 24 hours, immediately
+	if d <= dayl {
+		return b.StartTime.Unix()
+	}
+
+	tillDay := d - dayl
+	t := b.StartTime.Add(tillDay)
+	return t.Unix()
+}
+
+func (b *Base) NotificationTimes() []int64 {
+	last := []time.Duration{
+		time.Duration(0),
+		time.Duration(1 * time.Hour),
+		time.Duration(2 * time.Hour),
+		time.Duration(4 * time.Hour),
+	}
+
+	var t []int64
+
+	d := b.EndTime.Sub(b.StartTime)
+	for _, l := range last {
+		// If the notification remaining time is more than the total delta, continue
+		if l > d {
+			continue
+		}
+		// calculating the time till a notification must happen
+		tillN := d - l
+		// Get absolute time when this notification must be shown by adding the delta
+		c := b.StartTime.Add(tillN)
+		t = append(t, c.Unix())
+	}
+	return t
+}
diff --git a/internal/server/custom.go b/internal/server/custom.go
deleted file mode 100644
index 6171e24c..00000000
--- a/internal/server/custom.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package server
-
-import (
-	"github.com/go-errors/errors"
-)
-
-func (ss *Servers) SetCustomServer(server Server) error {
-	b, err := server.Base()
-	if err != nil {
-		return err
-	}
-
-	if b.Type != "custom_server" {
-		return errors.New("not a custom server")
-	}
-
-	if _, ok := ss.CustomServers.Map[b.URL]; ok {
-		ss.CustomServers.CurrentURL = b.URL
-		ss.IsType = CustomServerType
-	} else {
-		return errors.Errorf("this server is not yet added as a custom server: %s", b.URL)
-	}
-	return nil
-}
-
-func (ss *Servers) GetCustomServer(url string) (*InstituteAccessServer, error) {
-	if srv, ok := ss.CustomServers.Map[url]; ok {
-		return srv, nil
-	}
-	return nil, errors.Errorf("failed to get institute access server - no custom server with URL '%s'", url)
-}
-
-func (ss *Servers) RemoveCustomServer(url string) {
-	ss.CustomServers.Remove(url)
-}
diff --git a/internal/server/custom/custom.go b/internal/server/custom/custom.go
new file mode 100644
index 00000000..af6ad673
--- /dev/null
+++ b/internal/server/custom/custom.go
@@ -0,0 +1,37 @@
+package custom
+
+import (
+	"context"
+	"net/url"
+
+	"github.com/eduvpn/eduvpn-common/internal/server/api"
+	"github.com/eduvpn/eduvpn-common/internal/server/base"
+	"github.com/eduvpn/eduvpn-common/internal/server/institute"
+	"github.com/eduvpn/eduvpn-common/types/server"
+	"github.com/go-errors/errors"
+)
+
+type (
+	Server  = institute.Server
+	Servers = institute.Servers
+)
+
+func New(ctx context.Context, clientID string, u string) (*Server, error) {
+	pu, err := url.Parse(u)
+	if err != nil {
+		return nil, errors.WrapPrefix(err, "failed to parse custom server URL", 0)
+	}
+	b := base.Base{
+		URL:         u,
+		DisplayName: map[string]string{"en": pu.Hostname()},
+		Type:        server.TypeCustom,
+	}
+	if err := api.Endpoints(ctx, &b); err != nil {
+		return nil, err
+	}
+	API := b.Endpoints.API.V3
+
+	s := &Server{Basic: b}
+	s.Auth.Init(clientID, u, API.Authorization, API.Token)
+	return s, nil
+}
diff --git a/internal/server/endpoints/endpoints.go b/internal/server/endpoints/endpoints.go
new file mode 100644
index 00000000..3e675e4e
--- /dev/null
+++ b/internal/server/endpoints/endpoints.go
@@ -0,0 +1,56 @@
+package endpoints
+
+import (
+	"net/url"
+
+	"github.com/go-errors/errors"
+)
+
+type List struct {
+	API           string `json:"api_endpoint"`
+	Authorization string `json:"authorization_endpoint"`
+	Token         string `json:"token_endpoint"`
+}
+
+type Versions struct {
+	V2 List `json:"http://eduvpn.org/api#2"`
+	V3 List `json:"http://eduvpn.org/api#3"`
+}
+
+// Endpoints defines the json format for /.well-known/vpn-user-portal".
+type Endpoints struct {
+	API Versions `json:"api"`
+	V   string   `json:"v"`
+}
+
+func (e Endpoints) Validate() error {
+	v3 := e.API.V3
+	pAPI, err := url.Parse(v3.API)
+	if err != nil {
+		return errors.WrapPrefix(err, "failed to parse API endpoint", 0)
+	}
+	pAuth, err := url.Parse(v3.Authorization)
+	if err != nil {
+		return errors.WrapPrefix(err, "failed to parse API authorization endpoint", 0)
+	}
+	pToken, err := url.Parse(v3.Token)
+	if err != nil {
+		return errors.WrapPrefix(err, "failed to parse API token endpoint", 0)
+	}
+	if pAPI.Scheme != "https" {
+		return errors.Errorf("API Scheme: '%v', is not equal to HTTPS", pAPI.Scheme)
+	}
+	if pAPI.Scheme != pAuth.Scheme {
+		return errors.Errorf("API scheme: '%v', is not equal to authorization scheme: '%v'", pAPI.Scheme, pAuth.Scheme)
+	}
+	if pAPI.Scheme != pToken.Scheme {
+		return errors.Errorf("API scheme: '%v', is not equal to token scheme: '%v'", pAPI.Scheme, pToken.Scheme)
+	}
+	if pAPI.Host != pAuth.Host {
+		return errors.Errorf("API host: '%v', is not equal to authorization host: '%v'", pAPI.Host, pAuth.Host)
+	}
+	if pAPI.Host != pToken.Host {
+		return errors.Errorf("API host: '%v', is not equal to token host: '%v'", pAPI.Host, pToken.Host)
+	}
+	return nil
+}
diff --git a/internal/server/institute/institute.go b/internal/server/institute/institute.go
new file mode 100644
index 00000000..46977ac8
--- /dev/null
+++ b/internal/server/institute/institute.go
@@ -0,0 +1,129 @@
+package institute
+
+import (
+	"context"
+
+	"github.com/eduvpn/eduvpn-common/internal/discovery"
+	"github.com/eduvpn/eduvpn-common/internal/oauth"
+	"github.com/eduvpn/eduvpn-common/internal/server/api"
+	"github.com/eduvpn/eduvpn-common/internal/server/base"
+	"github.com/eduvpn/eduvpn-common/types/server"
+	"github.com/go-errors/errors"
+)
+
+type Server struct {
+	// An instute access server has its own OAuth
+	Auth oauth.OAuth `json:"oauth"`
+
+	// Embed the server base
+	Basic base.Base `json:"base"`
+}
+
+type Servers struct {
+	Map        map[string]*Server `json:"map"`
+	CurrentURL string             `json:"current_url"`
+}
+
+func New(
+	ctx context.Context,
+	clientID string,
+	url string,
+	name map[string]string,
+	supportContact []string,
+) (*Server, error) {
+	b := base.Base{
+		URL:            url,
+		DisplayName:    name,
+		SupportContact: supportContact,
+		Type:           server.TypeInstituteAccess,
+	}
+	if err := api.Endpoints(ctx, &b); err != nil {
+		return nil, err
+	}
+	API := b.Endpoints.API.V3
+
+	s := &Server{Basic: b}
+	s.Auth.Init(clientID, url, API.Authorization, API.Token)
+	return s, nil
+}
+
+func (s *Servers) Current() (*Server, error) {
+	if s.Map == nil {
+		return nil, errors.Errorf("No map is found when getting the current server")
+	}
+
+	srv, ok := s.Map[s.CurrentURL]
+	if !ok || srv == nil {
+		return nil, errors.Errorf("server not found")
+	}
+	return srv, nil
+}
+
+func (s *Servers) Remove(url string) error {
+	// check if it is in the map to begin with
+	if _, ok := s.Map[url]; ok {
+		delete(s.Map, url)
+	} else {
+		return errors.Errorf("cannot remove URL: %v, not found in list", url)
+	}
+
+	// Reset the current url
+	if s.CurrentURL == url {
+		s.CurrentURL = ""
+	}
+	return nil
+}
+
+func (s *Servers) Add(srv *Server) {
+	if s.Map == nil {
+		s.Map = make(map[string]*Server)
+	}
+	s.Map[srv.Basic.URL] = srv
+}
+
+func (s *Server) TemplateAuth() func(string) string {
+	return func(authURL string) string {
+		return authURL
+	}
+}
+
+func (s *Server) Base() (*base.Base, error) {
+	return &s.Basic, nil
+}
+
+func (s *Server) OAuth() *oauth.OAuth {
+	return &s.Auth
+}
+
+func (s *Server) NeedsLocation() bool {
+	return false
+}
+
+func (s *Server) RefreshEndpoints(ctx context.Context, _ *discovery.Discovery) error {
+	// Re-initialize the endpoints
+	b, err := s.Base()
+	if err != nil {
+		return err
+	}
+
+	err = api.Endpoints(ctx, b)
+	if err != nil {
+		return err
+	}
+
+	// update OAuth
+	auth := s.OAuth()
+	if auth != nil {
+		auth.BaseAuthorizationURL = b.Endpoints.API.V3.Authorization
+		auth.TokenURL = b.Endpoints.API.V3.Token
+	}
+	return nil
+}
+
+func (s *Server) Public() (interface{}, error) {
+	return &server.Server{
+		DisplayName: s.Basic.DisplayName,
+		Identifier:  s.Basic.URL,
+		Profiles:    s.Basic.Profiles.Public(),
+	}, nil
+}
diff --git a/internal/server/instituteaccess.go b/internal/server/instituteaccess.go
deleted file mode 100644
index a51409f6..00000000
--- a/internal/server/instituteaccess.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package server
-
-import (
-	"github.com/eduvpn/eduvpn-common/internal/discovery"
-	"github.com/eduvpn/eduvpn-common/internal/oauth"
-	"github.com/go-errors/errors"
-)
-
-type InstituteAccessServer struct {
-	// An instute access server has its own OAuth
-	Auth oauth.OAuth `json:"oauth"`
-
-	// Embed the server base
-	Basic Base `json:"base"`
-}
-
-type InstituteAccessServers struct {
-	Map        map[string]*InstituteAccessServer `json:"map"`
-	CurrentURL string                            `json:"current_url"`
-}
-
-func (ss *Servers) SetInstituteAccess(srv Server) error {
-	b, err := srv.Base()
-	if err != nil {
-		return err
-	}
-
-	if b.Type != "institute_access" {
-		return errors.Errorf("not an institute access server, url: %s, type: %s", b.URL, b.Type)
-	}
-
-	if _, ok := ss.InstituteServers.Map[b.URL]; ok {
-		ss.InstituteServers.CurrentURL = b.URL
-		ss.IsType = InstituteAccessServerType
-	} else {
-		return errors.Errorf("institute access server with url: %s, is not yet configured", b.URL)
-	}
-	return nil
-}
-
-func (ss *Servers) GetInstituteAccess(url string) (*InstituteAccessServer, error) {
-	if srv, ok := ss.InstituteServers.Map[url]; ok {
-		return srv, nil
-	}
-	return nil, errors.Errorf("no institute access server with URL: %s", url)
-}
-
-func (ss *Servers) RemoveInstituteAccess(url string) {
-	ss.InstituteServers.Remove(url)
-}
-
-func (iass *InstituteAccessServers) Remove(url string) {
-	// Reset the current url
-	if iass.CurrentURL == url {
-		iass.CurrentURL = ""
-	}
-
-	// Delete the url from the map
-	delete(iass.Map, url)
-}
-
-func (ias *InstituteAccessServer) TemplateAuth() func(string) string {
-	return func(authURL string) string {
-		return authURL
-	}
-}
-
-func (ias *InstituteAccessServer) Base() (*Base, error) {
-	return &ias.Basic, nil
-}
-
-func (ias *InstituteAccessServer) OAuth() *oauth.OAuth {
-	return &ias.Auth
-}
-
-func (ias *InstituteAccessServer) RefreshEndpoints(_ *discovery.Discovery) error {
-	// Re-initialize the endpoints
-	b, err := ias.Base()
-	if err != nil {
-		return err
-	}
-
-	err = b.InitializeEndpoints()
-	if err != nil {
-		return err
-	}
-
-	// update OAuth
-	auth := ias.OAuth()
-	if auth != nil {
-		auth.BaseAuthorizationURL = b.Endpoints.API.V3.Authorization
-		auth.TokenURL = b.Endpoints.API.V3.Token
-	}
-	return nil
-}
-
-func (ias *InstituteAccessServer) init(
-	url string,
-	name map[string]string,
-	srvType string,
-	supportContact []string,
-) error {
-	ias.Basic.URL = url
-	ias.Basic.DisplayName = name
-	ias.Basic.SupportContact = supportContact
-	ias.Basic.Type = srvType
-	err := ias.Basic.InitializeEndpoints()
-	if err != nil {
-		return err
-	}
-	API := ias.Basic.Endpoints.API.V3
-	ias.Auth.Init(url, API.Authorization, API.Token)
-	return nil
-}
diff --git a/internal/server/list.go b/internal/server/list.go
new file mode 100644
index 00000000..f3ae4e8e
--- /dev/null
+++ b/internal/server/list.go
@@ -0,0 +1,180 @@
+package server
+
+import (
+	"context"
+
+	"github.com/eduvpn/eduvpn-common/internal/server/custom"
+	"github.com/eduvpn/eduvpn-common/internal/server/institute"
+	"github.com/eduvpn/eduvpn-common/internal/server/secure"
+	discotypes "github.com/eduvpn/eduvpn-common/types/discovery"
+	srvtypes "github.com/eduvpn/eduvpn-common/types/server"
+	"github.com/go-errors/errors"
+)
+
+type List struct {
+	CustomServers            custom.Servers    `json:"custom_servers"`
+	InstituteServers         institute.Servers `json:"institute_servers"`
+	SecureInternetHomeServer secure.Server     `json:"secure_internet_home"`
+	IsType                   srvtypes.Type     `json:"is_secure_internet"`
+}
+
+// HasSecureInternet returns whether or not we have a secure internet server added
+func (l *List) HasSecureInternet() bool {
+	return len(l.SecureInternetHomeServer.BaseMap) > 0
+}
+
+func (l *List) HasSecureLocation() bool {
+	return l.SecureInternetHomeServer.CurrentLocation != ""
+}
+
+func (l *List) Current() (Server, error) {
+	if l.IsType == srvtypes.TypeUnknown {
+		return nil, errors.New("no current server")
+	}
+	if l.IsType == srvtypes.TypeSecureInternet {
+		if !l.HasSecureLocation() {
+			return nil, errors.Errorf("Current server is secure internet but there is no secure internet location: %v", l.IsType)
+		}
+		return &l.SecureInternetHomeServer, nil
+	}
+
+	if l.IsType == srvtypes.TypeCustom {
+		return l.CustomServers.Current()
+	}
+	return l.InstituteServers.Current()
+}
+
+func (l *List) AddCustom(ctx context.Context, clientID string, url string) (Server, error) {
+	srv, err := custom.New(ctx, clientID, url)
+	if err != nil {
+		return nil, err
+	}
+	l.CustomServers.Add(srv)
+	return srv, nil
+}
+
+func (l *List) AddInstituteAccess(ctx context.Context, clientID string, discoServer *discotypes.Server) (Server, error) {
+	srv, err := institute.New(ctx, clientID, discoServer.BaseURL, discoServer.DisplayName, discoServer.SupportContact)
+	if err != nil {
+		return nil, err
+	}
+	l.InstituteServers.Add(srv)
+	return srv, nil
+}
+
+func (l *List) AddSecureInternet(
+	ctx context.Context,
+	clientID string,
+	secureOrg *discotypes.Organization,
+	secureServer *discotypes.Server,
+) (*secure.Server, error) {
+	// If we have specified an organization ID
+	// We also need to get an authorization template
+	err := l.SecureInternetHomeServer.Init(ctx, clientID, secureOrg, secureServer)
+	if err != nil {
+		return nil, err
+	}
+
+	l.IsType = srvtypes.TypeSecureInternet
+	return &l.SecureInternetHomeServer, nil
+}
+
+func (l *List) SecureInternet(identifier string) (*secure.Server, error) {
+	if l.SecureInternetHomeServer.HomeOrganizationID != identifier {
+		return nil, errors.Errorf("no secure internet home server with identifier: %s", identifier)
+	}
+	return &l.SecureInternetHomeServer, nil
+}
+
+func (l *List) SetSecureInternet(server Server) error {
+	b, err := server.Base()
+	if err != nil {
+		return err
+	}
+
+	if b.Type != srvtypes.TypeSecureInternet {
+		return errors.New("not a secure internet server")
+	}
+
+	// The location should already be configured
+	// TODO: check for location?
+	l.IsType = srvtypes.TypeSecureInternet
+	return nil
+}
+
+func (l *List) RemoveSecureInternet(identifier string) error {
+	oid := l.SecureInternetHomeServer.HomeOrganizationID
+	if identifier != oid {
+		return errors.Errorf("cannot remove secure internet server: identifier: %s, is not equal to the Org ID: %s", identifier, oid)
+	}
+	// Empty out the struct
+	l.SecureInternetHomeServer = secure.Server{}
+
+	// If the current server is secure internet, reset to unknown
+	if l.IsType == srvtypes.TypeSecureInternet {
+		l.IsType = srvtypes.TypeUnknown
+	}
+	return nil
+}
+
+func (l *List) SetInstituteAccess(srv Server) error {
+	b, err := srv.Base()
+	if err != nil {
+		return err
+	}
+
+	if b.Type != srvtypes.TypeInstituteAccess {
+		return errors.Errorf("not an institute access server, URL: %s, type: %v", b.URL, b.Type)
+	}
+
+	if _, ok := l.InstituteServers.Map[b.URL]; ok {
+		l.InstituteServers.CurrentURL = b.URL
+		l.IsType = srvtypes.TypeInstituteAccess
+	} else {
+		return errors.Errorf("institute access server with URL: %s, is not yet configured", b.URL)
+	}
+	return nil
+}
+
+func (l *List) InstituteAccess(url string) (*institute.Server, error) {
+	if srv, ok := l.InstituteServers.Map[url]; ok {
+		return srv, nil
+	}
+	return nil, errors.Errorf("no institute access server with URL: %s", url)
+}
+
+func (l *List) RemoveInstituteAccess(url string) error {
+	// TODO: Reset current to unknown?
+	return l.InstituteServers.Remove(url)
+}
+
+func (l *List) SetCustom(server Server) error {
+	b, err := server.Base()
+	if err != nil {
+		return err
+	}
+
+	if b.Type != srvtypes.TypeCustom {
+		return errors.New("not a custom server")
+	}
+
+	if _, ok := l.CustomServers.Map[b.URL]; ok {
+		l.CustomServers.CurrentURL = b.URL
+		l.IsType = srvtypes.TypeCustom
+	} else {
+		return errors.Errorf("this server is not yet added as a custom server: %s", b.URL)
+	}
+	return nil
+}
+
+func (l *List) CustomServer(url string) (*institute.Server, error) {
+	if srv, ok := l.CustomServers.Map[url]; ok {
+		return srv, nil
+	}
+	return nil, errors.Errorf("failed to get institute access server - no custom server with URL '%s'", url)
+}
+
+func (l *List) RemoveCustom(url string) error {
+	// TODO: Reset current to unknown?
+	return l.CustomServers.Remove(url)
+}
diff --git a/internal/server/profile.go b/internal/server/profile.go
deleted file mode 100644
index d9814212..00000000
--- a/internal/server/profile.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package server
-
-type Profile struct {
-	ID             string   `json:"profile_id"`
-	DisplayName    string   `json:"display_name"`
-	VPNProtoList   []string `json:"vpn_proto_list"`
-	DefaultGateway bool     `json:"default_gateway"`
-}
-
-type ProfileListInfo struct {
-	ProfileList []Profile `json:"profile_list"`
-}
-
-type ProfileInfo struct {
-	Current string          `json:"current_profile"`
-	Info    ProfileListInfo `json:"info"`
-}
-
-func (info ProfileInfo) CurrentProfileIndex() int {
-	for i, profile := range info.Info.ProfileList {
-		if profile.ID == info.Current {
-			return i
-		}
-	}
-	// Default is 'first' profile
-	return 0
-}
-
-func (profile *Profile) supportsProtocol(protocol string) bool {
-	for _, proto := range profile.VPNProtoList {
-		if proto == protocol {
-			return true
-		}
-	}
-	return false
-}
-
-func (profile *Profile) SupportsWireguard() bool {
-	return profile.supportsProtocol("wireguard")
-}
-
-func (profile *Profile) SupportsOpenVPN() bool {
-	return profile.supportsProtocol("openvpn")
-}
diff --git a/internal/server/profile/profile.go b/internal/server/profile/profile.go
new file mode 100644
index 00000000..7a196858
--- /dev/null
+++ b/internal/server/profile/profile.go
@@ -0,0 +1,88 @@
+package profile
+
+import (
+	"github.com/eduvpn/eduvpn-common/types/protocol"
+	"github.com/eduvpn/eduvpn-common/types/server"
+)
+
+type Profile struct {
+	ID             string   `json:"profile_id"`
+	DisplayName    string   `json:"display_name"`
+	VPNProtoList   []string `json:"vpn_proto_list"`
+	DefaultGateway bool     `json:"default_gateway"`
+}
+
+type ListInfo struct {
+	ProfileList []Profile `json:"profile_list"`
+}
+
+type Info struct {
+	Current string   `json:"current_profile"`
+	Info    ListInfo `json:"info"`
+}
+
+func (info Info) CurrentProfileIndex() int {
+	for i, profile := range info.Info.ProfileList {
+		if profile.ID == info.Current {
+			return i
+		}
+	}
+	// Default is 'first' profile
+	return 0
+}
+
+func (profile *Profile) supportsProtocol(protocol string) bool {
+	for _, proto := range profile.VPNProtoList {
+		if proto == protocol {
+			return true
+		}
+	}
+	return false
+}
+
+func (profile *Profile) SupportsWireguard() bool {
+	return profile.supportsProtocol("wireguard")
+}
+
+func (profile *Profile) SupportsOpenVPN() bool {
+	return profile.supportsProtocol("openvpn")
+}
+
+func (info Info) Supported(wireguardSupport bool) []Profile {
+	var valid []Profile
+	for _, p := range info.Info.ProfileList {
+		// Not a valid profile because it does not support openvpn
+		// Also the client does not support wireguard
+		if !p.SupportsOpenVPN() && !wireguardSupport {
+			continue
+		}
+		valid = append(valid, p)
+	}
+	return valid
+}
+
+func (info Info) Has(id string) bool {
+	for _, p := range info.Info.ProfileList {
+		if p.ID == id {
+			return true
+		}
+	}
+	return false
+}
+
+func (info Info) Public() server.Profiles {
+	m := make(map[string]server.Profile)
+	for _, p := range info.Info.ProfileList {
+		var protocols []protocol.Protocol
+		for _, ps := range p.VPNProtoList {
+			protocols = append(protocols, protocol.New(ps))
+		}
+		m[p.ID] = server.Profile{
+			DisplayName: map[string]string{
+				"en": p.DisplayName,
+			},
+			Protocols: protocols,
+		}
+	}
+	return server.Profiles{Map: m, Current: info.Current}
+}
diff --git a/internal/server/secureinternet.go b/internal/server/secure/secure.go
similarity index 57%
rename from internal/server/secureinternet.go
rename to internal/server/secure/secure.go
index 3c40253d..c60c38e4 100644
--- a/internal/server/secureinternet.go
+++ b/internal/server/secure/secure.go
@@ -1,21 +1,25 @@
-package server
+package secure
 
 import (
+	"context"
 	"github.com/eduvpn/eduvpn-common/internal/discovery"
 	"github.com/eduvpn/eduvpn-common/internal/oauth"
+	"github.com/eduvpn/eduvpn-common/internal/server/api"
+	"github.com/eduvpn/eduvpn-common/internal/server/base"
 	"github.com/eduvpn/eduvpn-common/internal/util"
-	"github.com/eduvpn/eduvpn-common/types"
+	discotypes "github.com/eduvpn/eduvpn-common/types/discovery"
+	"github.com/eduvpn/eduvpn-common/types/server"
 	"github.com/go-errors/errors"
 )
 
-// SecureInternetHomeServer secure internet server which has its own OAuth tokens
+// Server secure internet server which has its own OAuth tokens
 // It specifies the current location url it is connected to.
-type SecureInternetHomeServer struct {
+type Server struct {
 	Auth        oauth.OAuth       `json:"oauth"`
 	DisplayName map[string]string `json:"display_name"`
 
 	// The home server has a list of info for each configured server location
-	BaseMap map[string]*Base `json:"base_map"`
+	BaseMap map[string]*base.Base `json:"base_map"`
 
 	// We have the authorization URL template, the home organization ID and the current location
 	AuthorizationTemplate string `json:"authorization_template"`
@@ -23,46 +27,13 @@ type SecureInternetHomeServer struct {
 	CurrentLocation       string `json:"current_location"`
 }
 
-func (ss *Servers) GetSecureInternetHomeServer() (*SecureInternetHomeServer, error) {
-	if !ss.HasSecureLocation() {
-		return nil, errors.Errorf("no secure internet home server")
-	}
-	return &ss.SecureInternetHomeServer, nil
-}
-
-func (ss *Servers) SetSecureInternet(server Server) error {
-	b, err := server.Base()
-	if err != nil {
-		return err
-	}
-
-	if b.Type != "secure_internet" {
-		return errors.Errorf("not a secure internet server")
-	}
-
-	// The location should already be configured
-	// TODO: check for location?
-	ss.IsType = SecureInternetServerType
-	return nil
-}
-
-func (ss *Servers) RemoveSecureInternet() {
-	// Empty out the struct
-	ss.SecureInternetHomeServer = SecureInternetHomeServer{}
-
-	// If the current server is secure internet, default to custom server
-	if ss.IsType == SecureInternetServerType {
-		ss.IsType = CustomServerType
-	}
-}
-
-func (s *SecureInternetHomeServer) TemplateAuth() func(string) string {
+func (s *Server) TemplateAuth() func(string) string {
 	return func(authURL string) string {
 		return util.ReplaceWAYF(s.AuthorizationTemplate, authURL, s.HomeOrganizationID)
 	}
 }
 
-func (s *SecureInternetHomeServer) Base() (*Base, error) {
+func (s *Server) Base() (*base.Base, error) {
 	if s.BaseMap == nil {
 		return nil, errors.Errorf("secure internet map not found")
 	}
@@ -74,30 +45,72 @@ func (s *SecureInternetHomeServer) Base() (*Base, error) {
 	return b, nil
 }
 
-func (s *SecureInternetHomeServer) OAuth() *oauth.OAuth {
+func (s *Server) OAuth() *oauth.OAuth {
 	return &s.Auth
 }
 
-func (ss *Servers) HasSecureLocation() bool {
-	return ss.SecureInternetHomeServer.CurrentLocation != ""
+func (s *Server) NeedsLocation() bool {
+	if s.CurrentLocation == "" {
+		return true
+	}
+	if len(s.BaseMap) == 0 {
+		return true
+	}
+	return false
 }
 
-func (s *SecureInternetHomeServer) addLocation(locSrv *types.DiscoveryServer) (*Base, error) {
+func (s *Server) RefreshEndpoints(ctx context.Context, disco *discovery.Discovery) error {
+	// update OAuth for home server
+	auth := s.OAuth()
+	if auth != nil && s.HomeOrganizationID != "" {
+		_, srv, err := disco.SecureHomeArgs(s.HomeOrganizationID)
+		if err != nil {
+			return err
+		}
+		if hb, ok := s.BaseMap[srv.CountryCode]; ok && hb != nil {
+			err := api.Endpoints(ctx, hb)
+			if err != nil {
+				return err
+			}
+			auth.BaseAuthorizationURL = hb.Endpoints.API.V3.Authorization
+			auth.TokenURL = hb.Endpoints.API.V3.Token
+		}
+		// already updated, return
+		if srv.CountryCode == s.CurrentLocation {
+			return nil
+		}
+	}
+
+	// refresh the current location endpoints
+	// Re-initialize the endpoints
+	b, err := s.Base()
+	if err != nil {
+		return err
+	}
+
+	err = api.Endpoints(ctx, b)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *Server) addLocation(ctx context.Context, locSrv *discotypes.Server) (*base.Base, error) {
 	// Initialize the base map if it is non-nil
 	if s.BaseMap == nil {
-		s.BaseMap = make(map[string]*Base)
+		s.BaseMap = make(map[string]*base.Base)
 	}
 
 	// Add the location to the base map
 	b, ok := s.BaseMap[locSrv.CountryCode]
 	if !ok || b == nil {
 		// Create the base to be added to the map
-		b = &Base{}
+		b = &base.Base{}
 		b.URL = locSrv.BaseURL
 		b.DisplayName = s.DisplayName
 		b.SupportContact = locSrv.SupportContact
-		b.Type = "secure_internet"
-		if err := b.InitializeEndpoints(); err != nil {
+		b.Type = server.TypeSecureInternet
+		if err := api.Endpoints(ctx, b); err != nil {
 			return nil, err
 		}
 	}
@@ -107,13 +120,23 @@ func (s *SecureInternetHomeServer) addLocation(locSrv *types.DiscoveryServer) (*
 	return b, nil
 }
 
+func (s *Server) Location(ctx context.Context, locSrv *discotypes.Server) error {
+	if _, err := s.addLocation(ctx, locSrv); err != nil {
+		return err
+	}
+	s.CurrentLocation = locSrv.CountryCode
+	return nil
+}
+
 // Initializes the home server and adds its own location.
-func (s *SecureInternetHomeServer) init(
-	homeOrg *types.DiscoveryOrganization, homeLoc *types.DiscoveryServer,
+func (s *Server) Init(
+	ctx context.Context,
+	clientID string,
+	homeOrg *discotypes.Organization, homeLoc *discotypes.Server,
 ) error {
 	if s.HomeOrganizationID != homeOrg.OrgID {
 		// New home organisation, clear everything
-		*s = SecureInternetHomeServer{}
+		*s = Server{}
 	}
 
 	// Make sure to set the organization ID
@@ -123,53 +146,36 @@ func (s *SecureInternetHomeServer) init(
 	// Make sure to set the authorization URL template
 	s.AuthorizationTemplate = homeLoc.AuthenticationURLTemplate
 
-	b, err := s.addLocation(homeLoc)
+	b, err := s.addLocation(ctx, homeLoc)
 	if err != nil {
 		return err
 	}
 
-	// Set the current location to the home location if there is none
-	if s.CurrentLocation == "" {
-		s.CurrentLocation = homeLoc.CountryCode
+	// set the home location as the current
+	err = s.Location(ctx, homeLoc)
+	if err != nil {
+		return err
 	}
 
 	// Make sure oauth contains our endpoints
-	s.Auth.Init(b.URL, b.Endpoints.API.V3.Authorization, b.Endpoints.API.V3.Token)
+	s.Auth.Init(clientID, b.URL, b.Endpoints.API.V3.Authorization, b.Endpoints.API.V3.Token)
 	return nil
 }
 
-func (s *SecureInternetHomeServer) RefreshEndpoints(disco *discovery.Discovery) error {
-	// update OAuth for home server
-	auth := s.OAuth()
-	if auth != nil && s.HomeOrganizationID != "" {
-		_, srv, err := disco.SecureHomeArgs(s.HomeOrganizationID)
-		if err != nil {
-			return err
-		}
-		if hb, ok := s.BaseMap[srv.CountryCode]; ok && hb != nil {
-			err := hb.InitializeEndpoints()
-			if err != nil {
-				return err
-			}
-			auth.BaseAuthorizationURL = hb.Endpoints.API.V3.Authorization
-			auth.TokenURL = hb.Endpoints.API.V3.Token
-		}
-		// already updated, return
-		if srv.CountryCode == s.CurrentLocation {
-			return nil
-		}
-	}
-
-	// refresh the current location endpoints
-	// Re-initialize the endpoints
+func (s *Server) Public() (interface{}, error) {
 	b, err := s.Base()
-	if err != nil {
-		return err
+	var p server.Profiles
+	dn := s.DisplayName
+	if err == nil {
+		dn = b.DisplayName
+		p = b.Profiles.Public()
 	}
-
-	err = b.InitializeEndpoints()
-	if err != nil {
-		return err
-	}
-	return nil
+	return &server.SecureInternet{
+		Server: server.Server{
+			DisplayName: dn,
+			Identifier:  s.HomeOrganizationID,
+			Profiles:    p,
+		},
+		CountryCode: s.CurrentLocation,
+	}, nil
 }
diff --git a/internal/server/server.go b/internal/server/server.go
index 775095c1..b6f3b30e 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -1,81 +1,48 @@
 package server
 
 import (
+	"context"
 	"os"
 	"time"
 
 	"github.com/eduvpn/eduvpn-common/internal/discovery"
 	"github.com/eduvpn/eduvpn-common/internal/oauth"
+	"github.com/eduvpn/eduvpn-common/internal/server/api"
+	"github.com/eduvpn/eduvpn-common/internal/server/base"
+	"github.com/eduvpn/eduvpn-common/internal/server/profile"
 	"github.com/eduvpn/eduvpn-common/internal/wireguard"
+	"github.com/eduvpn/eduvpn-common/types/protocol"
+	srvtypes "github.com/eduvpn/eduvpn-common/types/server"
 	"github.com/go-errors/errors"
 )
 
-type Type int8
-
-const (
-	CustomServerType Type = iota
-	InstituteAccessServerType
-	SecureInternetServerType
-)
-
 type Server interface {
+	// OAuth returns the struct used for OAuth
 	OAuth() *oauth.OAuth
 
 	// TemplateAuth returns the authorization URL template function
 	TemplateAuth() func(string) string
 
 	// Base returns the server base
-	Base() (*Base, error)
+	Base() (*base.Base, error)
 
-	// RefreshEndpoints
-	RefreshEndpoints(*discovery.Discovery) error
-}
+	// NeedsLocation checks if the server needs a secure internet location
+	NeedsLocation() bool
 
-type EndpointList struct {
-	API           string `json:"api_endpoint"`
-	Authorization string `json:"authorization_endpoint"`
-	Token         string `json:"token_endpoint"`
-}
+	// Public returns the representation that will be passed over the CGO barrier
+	Public() (interface{}, error)
 
-type EndpointsVersions struct {
-	V2 EndpointList `json:"http://eduvpn.org/api#2"`
-	V3 EndpointList `json:"http://eduvpn.org/api#3"`
+	// RefreshEndpoints refreshes the endpoints for the server
+	RefreshEndpoints(context.Context, *discovery.Discovery) error
 }
 
-// Endpoints defines the json format for /.well-known/vpn-user-portal".
-type Endpoints struct {
-	API EndpointsVersions `json:"api"`
-	V   string            `json:"v"`
-}
-
-// ShouldRenewButton returns whether or not the renew button should be shown for the server
-// Implemented according to: https://github.com/eduvpn/documentation/blob/cdf4d054f7652d74e4192494e8bb0e21040e46ac/API.md#session-expiry
-func ShouldRenewButton(srv Server) bool {
-	b, err := srv.Base()
-	if err != nil {
-		// FIXME: Log error here?
-		return false
+// Name gets the name for the server and falls back to a default of "Unknown Server"
+func Name(srv Server) string {
+	n := "Unknown Server"
+	if b, err := srv.Base(); err == nil {
+		n = b.URL
 	}
-
-	// Get current time
-	now := time.Now()
-
-	// Session is expired
-	if !now.Before(b.EndTime) {
-		return true
-	}
-
-	// 30 minutes have not passed
-	if !now.After(b.StartTime.Add(30 * time.Minute)) {
-		return false
-	}
-
-	// Session will not expire today
-	if !now.Add(24 * time.Hour).After(b.EndTime) {
-		return false
-	}
-
-	return true
+	return n
 }
 
 func UpdateTokens(srv Server, t oauth.Token) {
@@ -86,12 +53,12 @@ func OAuthURL(srv Server, name string) (string, error) {
 	return srv.OAuth().AuthURL(name, srv.TemplateAuth())
 }
 
-func OAuthExchange(srv Server) error {
-	return srv.OAuth().Exchange()
+func OAuthExchange(ctx context.Context, srv Server) error {
+	return srv.OAuth().Exchange(ctx)
 }
 
-func HeaderToken(srv Server) (string, error) {
-	return srv.OAuth().AccessToken()
+func HeaderToken(ctx context.Context, srv Server) (string, error) {
+	return srv.OAuth().AccessToken(ctx)
 }
 
 func MarkTokenExpired(srv Server) {
@@ -102,16 +69,13 @@ func MarkTokensForRenew(srv Server) {
 	srv.OAuth().SetTokenRenew()
 }
 
-func NeedsRelogin(srv Server) bool {
-	_, err := HeaderToken(srv)
+func NeedsRelogin(ctx context.Context, srv Server) bool {
+	// TODO: this error can be a context cancel
+	_, err := HeaderToken(ctx, srv)
 	return err != nil
 }
 
-func CancelOAuth(srv Server) {
-	srv.OAuth().Cancel()
-}
-
-func CurrentProfile(srv Server) (*Profile, error) {
+func CurrentProfile(srv Server) (*profile.Profile, error) {
 	b, err := srv.Base()
 	if err != nil {
 		return nil, err
@@ -126,17 +90,34 @@ func CurrentProfile(srv Server) (*Profile, error) {
 	return nil, errors.Errorf("profile not found: " + pID)
 }
 
-func ValidProfiles(srv Server, wireguardSupport bool) (*ProfileInfo, error) {
+func ValidProfiles(srv Server, wireguardSupport bool) (*profile.Info, error) {
 	// No error wrapping here otherwise we wrap it too much
 	b, err := srv.Base()
 	if err != nil {
 		return nil, err
 	}
-	ps := b.ValidProfiles(wireguardSupport)
-	if len(ps.Info.ProfileList) == 0 {
+	ps := b.Profiles.Supported(wireguardSupport)
+	if len(ps) == 0 {
 		return nil, errors.Errorf("no profiles found with supported protocols")
 	}
-	return &ps, nil
+	return &profile.Info{
+		Current: b.Profiles.Current,
+		Info: profile.ListInfo{
+			ProfileList: ps,
+		},
+	}, nil
+}
+
+func Profile(srv Server, id string) error {
+	b, err := srv.Base()
+	if err != nil {
+		return err
+	}
+	if !b.Profiles.Has(id) {
+		return errors.Errorf("no profile available with id: %s", id)
+	}
+	b.Profiles.Current = id
+	return nil
 }
 
 type ConfigData struct {
@@ -145,12 +126,19 @@ type ConfigData struct {
 
 	// The type of configuration
 	Type string
+}
 
-	// The tokens
-	Tokens oauth.Token
+// Public gets the public data from the types package
+// dg specifies if this config is default gateway
+func (c *ConfigData) Public(dg bool) srvtypes.Configuration {
+	return srvtypes.Configuration{
+		VPNConfig:      c.Config,
+		Protocol:       protocol.New(c.Type),
+		DefaultGateway: dg,
+	}
 }
 
-func wireguardGetConfig(srv Server, preferTCP bool, openVPNSupport bool) (*ConfigData, error) {
+func wireguardGetConfig(ctx context.Context, srv Server, preferTCP bool, openVPNSupport bool) (*ConfigData, error) {
 	b, err := srv.Base()
 	if err != nil {
 		return nil, err
@@ -163,7 +151,7 @@ func wireguardGetConfig(srv Server, preferTCP bool, openVPNSupport bool) (*Confi
 	}
 
 	pub := key.PublicKey().String()
-	cfg, proto, exp, err := APIConnectWireguard(srv, pID, pub, preferTCP, openVPNSupport)
+	cfg, proto, exp, err := api.ConnectWireguard(ctx, b, srv.OAuth(), pID, pub, preferTCP, openVPNSupport)
 	if err != nil {
 		return nil, err
 	}
@@ -180,22 +168,16 @@ func wireguardGetConfig(srv Server, preferTCP bool, openVPNSupport bool) (*Confi
 		cfg = wireguard.ConfigAddKey(cfg, key)
 	}
 
-	t := oauth.Token{}
-	o := srv.OAuth()
-	if o != nil {
-		t = o.Token()
-	}
-
-	return &ConfigData{Config: cfg, Type: proto, Tokens: t}, nil
+	return &ConfigData{Config: cfg, Type: proto}, nil
 }
 
-func openVPNGetConfig(srv Server, preferTCP bool) (*ConfigData, error) {
+func openVPNGetConfig(ctx context.Context, srv Server, preferTCP bool) (*ConfigData, error) {
 	b, err := srv.Base()
 	if err != nil {
 		return nil, err
 	}
 	pid := b.Profiles.Current
-	cfg, exp, err := APIConnectOpenVPN(srv, pid, preferTCP)
+	cfg, exp, err := api.ConnectOpenVPN(ctx, b, srv.OAuth(), pid, preferTCP)
 	if err != nil {
 		return nil, err
 	}
@@ -204,25 +186,17 @@ func openVPNGetConfig(srv Server, preferTCP bool) (*ConfigData, error) {
 	b.StartTime = time.Now()
 	b.EndTime = exp
 
-	t := oauth.Token{}
-
-	o := srv.OAuth()
-	if o != nil {
-		t = o.Token()
-	}
-
-	return &ConfigData{Config: cfg, Type: "openvpn", Tokens: t}, nil
+	return &ConfigData{Config: cfg, Type: "openvpn"}, nil
 }
 
-func HasValidProfile(srv Server, wireguardSupport bool) (bool, error) {
-	// Get new profiles using the info call
-	// This does not override the current profile
-	err := APIInfo(srv)
+func HasValidProfile(ctx context.Context, srv Server, wireguardSupport bool) (bool, error) {
+	b, err := srv.Base()
 	if err != nil {
 		return false, err
 	}
-
-	b, err := srv.Base()
+	// Get new profiles using the info call
+	// This does not override the current profile
+	err = api.Info(ctx, b, srv.OAuth())
 	if err != nil {
 		return false, err
 	}
@@ -234,6 +208,7 @@ func HasValidProfile(srv Server, wireguardSupport bool) (bool, error) {
 		}
 	}
 
+	// there are multiple profiles and no selection has been made
 	if len(b.Profiles.Info.ProfileList) != 1 && b.Profiles.Current == "" {
 		return false, nil
 	}
@@ -255,7 +230,7 @@ func HasValidProfile(srv Server, wireguardSupport bool) (bool, error) {
 	return true, nil
 }
 
-func Config(server Server, wireguardSupport bool, preferTCP bool) (*ConfigData, error) {
+func Config(ctx context.Context, server Server, wireguardSupport bool, preferTCP bool) (*ConfigData, error) {
 	p, err := CurrentProfile(server)
 	if err != nil {
 		return nil, err
@@ -280,10 +255,10 @@ func Config(server Server, wireguardSupport bool, preferTCP bool) (*ConfigData,
 	case wg:
 		// A wireguard connect call needs to generate a wireguard key and add it to the config
 		// Also the server could send back an OpenVPN config if it supports OpenVPN
-		cfg, err = wireguardGetConfig(server, preferTCP, ovpn)
+		cfg, err = wireguardGetConfig(ctx, server, preferTCP, ovpn)
 	//  The config only supports OpenVPN
 	case ovpn:
-		cfg, err = openVPNGetConfig(server, preferTCP)
+		cfg, err = openVPNGetConfig(ctx, server, preferTCP)
 		// The config supports no available protocol because the profile only supports WireGuard but the client doesn't
 	default:
 		return nil, errors.New("no supported protocol found")
@@ -297,6 +272,10 @@ func Config(server Server, wireguardSupport bool, preferTCP bool) (*ConfigData,
 	return cfg, err
 }
 
-func Disconnect(server Server) error {
-	return APIDisconnect(server)
+func Disconnect(ctx context.Context, server Server) error {
+	b, err := server.Base()
+	if err != nil {
+		return err
+	}
+	return api.Disconnect(ctx, b, server.OAuth())
 }
diff --git a/internal/server/servers.go b/internal/server/servers.go
deleted file mode 100644
index 0fa81879..00000000
--- a/internal/server/servers.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package server
-
-import (
-	"github.com/eduvpn/eduvpn-common/types"
-	"github.com/go-errors/errors"
-)
-
-type Servers struct {
-	// A custom server is just an institute access server under the hood
-	CustomServers            InstituteAccessServers   `json:"custom_servers"`
-	InstituteServers         InstituteAccessServers   `json:"institute_servers"`
-	SecureInternetHomeServer SecureInternetHomeServer `json:"secure_internet_home"`
-	IsType                   Type                     `json:"is_secure_internet"`
-}
-
-// HasSecureInternet returns whether or not we have a secure internet server added
-func (ss *Servers) HasSecureInternet() bool {
-	return len(ss.SecureInternetHomeServer.BaseMap) > 0
-}
-
-func (ss *Servers) AddSecureInternet(
-	secureOrg *types.DiscoveryOrganization,
-	secureServer *types.DiscoveryServer,
-) (Server, error) {
-	// If we have specified an organization ID
-	// We also need to get an authorization template
-	err := ss.SecureInternetHomeServer.init(secureOrg, secureServer)
-	if err != nil {
-		return nil, err
-	}
-
-	ss.IsType = SecureInternetServerType
-	return &ss.SecureInternetHomeServer, nil
-}
-
-func (ss *Servers) GetCurrentServer() (Server, error) {
-	// TODO(jwijenbergh): Almost certainly the return type should be pointer (*Server)
-	if ss.IsType == SecureInternetServerType {
-		if !ss.HasSecureLocation() {
-			return nil, errors.Errorf("ss.IsType = %v; ss.HasSecureLocation() = false", ss.IsType)
-		}
-		return &ss.SecureInternetHomeServer, nil
-	}
-
-	srvs := &ss.InstituteServers
-
-	if ss.IsType == CustomServerType {
-		srvs = &ss.CustomServers
-	}
-	if srvs.Map == nil {
-		return nil, errors.Errorf("srvs.Map is nil")
-	}
-
-	srv, ok := srvs.Map[srvs.CurrentURL]
-	if !ok || srv == nil {
-		return nil, errors.Errorf("server not found")
-	}
-	return srv, nil
-}
-
-func (ss *Servers) addInstituteAndCustom(
-	discoServer *types.DiscoveryServer,
-	isCustom bool,
-) (Server, error) {
-	URL := discoServer.BaseURL
-	srvs := &ss.InstituteServers
-	srvType := InstituteAccessServerType
-
-	if isCustom {
-		srvs = &ss.CustomServers
-		srvType = CustomServerType
-	}
-
-	if srvs.Map == nil {
-		srvs.Map = make(map[string]*InstituteAccessServer)
-	}
-
-	srv, ok := srvs.Map[URL]
-
-	// initialize the server if it doesn't exist yet
-	if !ok {
-		srv = &InstituteAccessServer{}
-	}
-
-	if err := srv.init(URL, discoServer.DisplayName, discoServer.Type, discoServer.SupportContact); err != nil {
-		return nil, err
-	}
-	srvs.Map[URL] = srv
-	ss.IsType = srvType
-	return srv, nil
-}
-
-func (ss *Servers) AddInstituteAccessServer(
-	instituteServer *types.DiscoveryServer,
-) (Server, error) {
-	return ss.addInstituteAndCustom(instituteServer, false)
-}
-
-func (ss *Servers) AddCustomServer(
-	customServer *types.DiscoveryServer,
-) (Server, error) {
-	return ss.addInstituteAndCustom(customServer, true)
-}
-
-func (ss *Servers) GetSecureLocation() string {
-	return ss.SecureInternetHomeServer.CurrentLocation
-}
-
-func (ss *Servers) SetSecureLocation(
-	chosenLocationServer *types.DiscoveryServer,
-) error {
-	// Make sure to add the current location
-
-	if _, err := ss.SecureInternetHomeServer.addLocation(chosenLocationServer); err != nil {
-		return err
-	}
-
-	ss.SecureInternetHomeServer.CurrentLocation = chosenLocationServer.CountryCode
-	return nil
-}
diff --git a/internal/util/util.go b/internal/util/util.go
index 907f85fc..ebda183f 100644
--- a/internal/util/util.go
+++ b/internal/util/util.go
@@ -55,53 +55,3 @@ func ReplaceWAYF(template string, authURL string, orgID string) string {
 	template = strings.Replace(template, "@ORG_ID@", url.QueryEscape(orgID), 1)
 	return template
 }
-
-// GetLanguageMatched uses a map from language tags to strings to extract the right language given the tag
-// It implements it according to https://github.com/eduvpn/documentation/blob/dc4d53c47dd7a69e95d6650eec408e16eaa814a2/SERVER_DISCOVERY.md#language-matching
-func GetLanguageMatched(langMap map[string]string, langTag string) string {
-	// If no map is given, return the empty string
-	if len(langMap) == 0 {
-		return ""
-	}
-	// Try to find the exact match
-	if val, ok := langMap[langTag]; ok {
-		return val
-	}
-	// Try to find a key that starts with the OS language setting
-	for k := range langMap {
-		if strings.HasPrefix(k, langTag) {
-			return langMap[k]
-		}
-	}
-	// Try to find a key that starts with the first part of the OS language (e.g. de-)
-	pts := strings.Split(langTag, "-")
-	// We have a "-"
-	if len(pts) > 1 {
-		for k := range langMap {
-			if strings.HasPrefix(k, pts[0]+"-") {
-				return langMap[k]
-			}
-		}
-	}
-	// search for just the language (e.g. de)
-	for k := range langMap {
-		if k == pts[0] {
-			return langMap[k]
-		}
-	}
-
-	// Pick one that is deemed best, e.g. en-US or en, but note that not all languages are always available!
-	// We force an entry that is english exactly or with an english prefix
-	for k := range langMap {
-		if k == "en" || strings.HasPrefix(k, "en-") {
-			return langMap[k]
-		}
-	}
-
-	// Otherwise just return one
-	for k := range langMap {
-		return langMap[k]
-	}
-
-	return ""
-}
diff --git a/selenium_eduvpn.py b/selenium_eduvpn.py
index b3a7c5fa..2f452a0c 100644
--- a/selenium_eduvpn.py
+++ b/selenium_eduvpn.py
@@ -46,15 +46,15 @@ def login_oauth(driver, authURL):
     elem.send_keys(portal_pass)
 
     # Click login button
-    driver.find_element("css selector",
-        ".frm > fieldset:nth-child(2) > button:nth-child(2)"
+    driver.find_element(
+        "css selector", ".frm > fieldset:nth-child(2) > button:nth-child(2)"
     ).click()
 
     # Check that we have reached the approve application page
     assert "VPN Portal - Approve Application" in driver.title
     # Click approve
-    driver.find_element("css selector",
-        ".frm > fieldset:nth-child(1) > button:nth-child(1)"
+    driver.find_element(
+        "css selector", ".frm > fieldset:nth-child(1) > button:nth-child(1)"
     ).click()
 
 
diff --git a/sync_translations.sh b/sync_translations.sh
new file mode 100755
index 00000000..18f49c75
--- /dev/null
+++ b/sync_translations.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+# This should be run when the source code is updated
+
+go generate ./...
+files=$(find client/locales -iname "out.gotext.json")
+for f in $files
+do
+    dir=$(dirname "$f")
+    cp "$f" "$dir"/messages.gotext.json
+done
diff --git a/types/cookie/cookie.go b/types/cookie/cookie.go
new file mode 100644
index 00000000..e1cffca1
--- /dev/null
+++ b/types/cookie/cookie.go
@@ -0,0 +1,81 @@
+// package cookie implements a specialized version of a context
+// - It is cancellable
+// - It has a channel associated with it to reply to state callbacks
+// - It can be marshalled by having a cgo Handle attached to it
+package cookie
+
+import (
+	"context"
+	"encoding/json"
+	"runtime/cgo"
+
+	"github.com/go-errors/errors"
+)
+
+type Cookie struct {
+	c         chan string
+	ctx       context.Context
+	ctxCancel context.CancelFunc
+	H         cgo.Handle
+}
+
+// NewWithContext creates a new cookie with a context
+// It stores the cancel and channel inside of the struct
+func NewWithContext(ctx context.Context) Cookie {
+	ctx, cancel := context.WithCancel(ctx)
+	return Cookie{
+		c:         make(chan string),
+		ctx:       ctx,
+		ctxCancel: cancel,
+	}
+}
+
+// MarshalJSON marshals the cookie to JSON
+func (c *Cookie) MarshalJSON() ([]byte, error) {
+	if c.H == 0 {
+		return nil, errors.New("no associated handle found")
+	}
+	return json.Marshal(c.H)
+}
+
+// Receive receives a value from the cookie up until the context is done or errchan gets an error
+// This error chan is used for goroutines to signal errors that we have to exit early
+func (c *Cookie) Receive(errchan chan error) (string, error) {
+	select {
+	case r := <-c.c:
+		return r, nil
+	case e := <-errchan:
+		return "", e
+	case <-c.ctx.Done():
+		return "", errors.WrapPrefix(context.Canceled, "receive cookie", 0)
+	}
+}
+
+// Cancel cancels the cookie by calling the context cancel function
+// It returns an error if no such function exists
+func (c *Cookie) Cancel() error {
+	if c.ctxCancel == nil {
+		return errors.New("no cancel function found")
+	}
+	c.ctxCancel()
+	return nil
+}
+
+// Send sends data to the cookie channel if the context is not canceled
+func (c *Cookie) Send(data string) error {
+	select {
+	case <-c.ctx.Done():
+		return errors.WrapPrefix(context.Canceled, "send cookie", 0)
+	default:
+		if c.c == nil {
+			return errors.New("channel is nil")
+		}
+		c.c <- data
+		return nil
+	}
+}
+
+// Context gets the underlying context of the cookie
+func (c *Cookie) Context() context.Context {
+	return c.ctx
+}
diff --git a/types/discovery/discovery.go b/types/discovery/discovery.go
new file mode 100644
index 00000000..bc22eab5
--- /dev/null
+++ b/types/discovery/discovery.go
@@ -0,0 +1,94 @@
+// package discovery defines the public types that have to deal with discovery
+package discovery
+
+import (
+	"encoding/json"
+	"time"
+)
+
+// Organizations is the type that defines the upstream discovery format for the list of organizations
+// TODO: Discovery here is the same as the upstream discovery format, should we separate this as well?
+// Defined in URL: "https://disco.eduvpn.org/v2/organization_list.json"
+type Organizations struct {
+	// Version is the version field. The Go library internally already checks for rollbacks, you can use this for logging
+	Version uint64 `json:"v"`
+	// List is the list/slice of organizations. Omitted if none are there
+	List []Organization `json:"organization_list,omitempty"`
+	// Timestamp is a timestamp that is internally used by the Go library to keep track of when the organizations was last updated
+	// You can also use this for logging
+	Timestamp time.Time `json:"go_timestamp"`
+}
+
+// Organization is the type that defines the upstream discovery format for a single organization
+type Organization struct {
+	// DisplayName is the map of strings from language tags to display names
+	// Omitted if none is defined
+	DisplayName MapOrString `json:"display_name,omitempty"`
+	// OrgID is the organization ID for the server
+	OrgID string `json:"org_id"`
+	// SecureInternetHome is the secure internet home server that belongs to this organization
+	// Omitted if none is defined
+	SecureInternetHome string `json:"secure_internet_home,omitempty"`
+	// KeywordList is the list of keywords
+	// Omitted if none is defined
+	KeywordList MapOrString `json:"keyword_list,omitempty"`
+}
+
+// Servers is the type that defines the upstream discovery format for the list of servers
+// url: "https://disco.eduvpn.org/v2/server_list.json"
+type Servers struct {
+	// Version is the version field in discovery. The Go library already checks for rollbacks, use this for logging
+	Version uint64 `json:"v"`
+	// List is the actual list of servers, omitted from the JSON if empty
+	List []Server `json:"server_list,omitempty"`
+	// Timestamp is a timestamp that is internally used by the Go library to keep track of when the organizations was last updated
+	// You can also use this for logging
+	Timestamp time.Time `json:"go_timestamp"`
+}
+
+// Server is a signle discovery server
+type Server struct {
+	// AuthenticationURLTemplate is the template to be used for authentication to skip WAYF
+	AuthenticationURLTemplate string `json:"authentication_url_template,omitempty"`
+	// BaseURL is the base URL of the server which is used as an identifier for the server by the Go library
+	BaseURL string `json:"base_url"`
+	// CountryCode is the country code for the server in case of secure internet, e.g. NL
+	CountryCode string `json:"country_code,omitempty"`
+	// DisplayName is the display name of the server, omitted if empty
+	DisplayName MapOrString `json:"display_name,omitempty"`
+	// DisplayName are the keywords of the server, omitted if empty
+	KeywordList MapOrString `json:"keyword_list,omitempty"`
+	// PublicKeyList are the public keys of the server. Currently not used in this lib but returned by the upstream discovery server
+	PublicKeyList []string `json:"public_key_list,omitempty"`
+	// Type is the type of the server, "secure_internet" or "institute_access"
+	Type string `json:"server_type"`
+	// SupportContact is the list/slice of support contacts
+	SupportContact []string `json:"support_contact"`
+}
+
+// MapOrString is a custom type as the upstream discovery format is a map or a value.
+// This library always marshals the data as a map and then makes sure unmarshalling also gives a map
+type MapOrString map[string]string
+
+// The display name can either be a map or a string in the server list
+// Unmarshal it by first trying a string and then the map.
+func (displayName *MapOrString) UnmarshalJSON(data []byte) error {
+	var displayNameString string
+
+	err := json.Unmarshal(data, &displayNameString)
+
+	if err == nil {
+		*displayName = map[string]string{"en": displayNameString}
+		return nil
+	}
+
+	var resultingMap map[string]string
+
+	err = json.Unmarshal(data, &resultingMap)
+
+	if err == nil {
+		*displayName = resultingMap
+		return nil
+	}
+	return err
+}
diff --git a/types/error/error.go b/types/error/error.go
new file mode 100644
index 00000000..9697e9d1
--- /dev/null
+++ b/types/error/error.go
@@ -0,0 +1,24 @@
+package err
+
+// Translated defines the type for translated strings
+// It is a map from language tags to error messages
+type Translated map[string]string
+
+// Error is the struct that defines the public error types
+// This contains the error message with translations
+// And other info
+type Error struct {
+	// Message defines the error message
+	// It is a map from language tags to messages
+	// If a language is not translated, the whole language tag key is missing
+	// E.g. compare (english and french translations)
+	// {"en": "hello", "fr": "bonjour"}
+	// and
+	// {"en": "hello"}
+	// English is always present and should be used as a fallback
+	Message Translated `json:"message"`
+
+	// Misc indicates whether or not this error is only there for miscellaneous purposes
+	// If this is set to True, the client UI SHOULD NOT show this error
+	Misc bool `json:"misc"`
+}
diff --git a/types/protocol/protocol.go b/types/protocol/protocol.go
new file mode 100644
index 00000000..e35de2b7
--- /dev/null
+++ b/types/protocol/protocol.go
@@ -0,0 +1,26 @@
+// package protocol contains hte public type that have to do with VPN protocols
+package protocol
+
+// Protocol defines an 'enumeration' of protocols
+type Protocol int8
+
+const (
+	// Unknown indicates that the protocol is not known
+	Unknown Protocol = iota
+	// OpenVPN indicates that the protocol is OpenVPN
+	OpenVPN
+	// WireGuard indicates that the protocol is WireGuard
+	WireGuard
+)
+
+// New creates a new protocol type from a string
+func New(p string) Protocol {
+	switch p {
+	case "openvpn":
+		return OpenVPN
+	case "wireguard":
+		return WireGuard
+	default:
+		return Unknown
+	}
+}
diff --git a/types/server.go b/types/server.go
deleted file mode 100644
index 5cd7353a..00000000
--- a/types/server.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package types
-
-import (
-	"encoding/json"
-	"time"
-)
-
-// Shared server types
-
-// Structs that define the json format for
-// url: "https://disco.eduvpn.org/v2/organization_list.json"
-type DiscoveryOrganizations struct {
-	Version   uint64                  `json:"v"`
-	List      []DiscoveryOrganization `json:"organization_list"`
-	Timestamp time.Time               `json:"go_timestamp"`
-}
-
-type DiscoveryOrganization struct {
-	DisplayName        DiscoMapOrString `json:"display_name"`
-	OrgID              string           `json:"org_id"`
-	SecureInternetHome string           `json:"secure_internet_home"`
-	KeywordList        DiscoMapOrString `json:"keyword_list"`
-}
-
-// Structs that define the json format for
-// url: "https://disco.eduvpn.org/v2/server_list.json"
-type DiscoveryServers struct {
-	Version   uint64            `json:"v"`
-	List      []DiscoveryServer `json:"server_list"`
-	Timestamp time.Time         `json:"go_timestamp"`
-}
-
-type DiscoMapOrString map[string]string
-
-// The display name can either be a map or a string in the server list
-// Unmarshal it by first trying a string and then the map.
-func (displayName *DiscoMapOrString) UnmarshalJSON(data []byte) error {
-	var displayNameString string
-
-	err := json.Unmarshal(data, &displayNameString)
-
-	if err == nil {
-		*displayName = map[string]string{"en": displayNameString}
-		return nil
-	}
-
-	var resultingMap map[string]string
-
-	err = json.Unmarshal(data, &resultingMap)
-
-	if err == nil {
-		*displayName = resultingMap
-		return nil
-	}
-	return err
-}
-
-type DiscoveryServer struct {
-	AuthenticationURLTemplate string           `json:"authentication_url_template"`
-	BaseURL                   string           `json:"base_url"`
-	CountryCode               string           `json:"country_code"`
-	DisplayName               DiscoMapOrString `json:"display_name,omitempty"`
-	KeywordList               DiscoMapOrString `json:"keyword_list"`
-	PublicKeyList             []string         `json:"public_key_list"`
-	Type                      string           `json:"server_type"`
-	SupportContact            []string         `json:"support_contact"`
-}
diff --git a/types/server/server.go b/types/server/server.go
new file mode 100644
index 00000000..4198e86c
--- /dev/null
+++ b/types/server/server.go
@@ -0,0 +1,185 @@
+// package server defines public types that have to deal with the VPN server
+package server
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/eduvpn/eduvpn-common/types/cookie"
+	"github.com/eduvpn/eduvpn-common/types/protocol"
+)
+
+// Type gives the type of server
+type Type int8
+
+const (
+	// TypeUnknown means the server is unknown
+	TypeUnknown Type = iota
+	// TypeInstituteAccess means the server is of type Institute Access
+	TypeInstituteAccess
+	// TypeSecureInternet means the server is of type Secure Internet
+	TypeSecureInternet
+	// TypeCustom means the server is of type Custom Server
+	TypeCustom
+)
+
+// This is here to support V1 configs which had the server type as a string
+func (t *Type) UnmarshalJSON(data []byte) error {
+	// First try to just unmarshal the type
+	var num int8
+	if err := json.Unmarshal(data, &num); err == nil {
+		if num < int8(TypeUnknown) || num > int8(TypeCustom) {
+			return fmt.Errorf("invalid server type: %d", num)
+		}
+		*t = Type(num)
+		return nil
+	}
+
+	// unmarshal the old way, as a string
+	var str string
+	if err := json.Unmarshal(data, &str); err != nil {
+		return err
+	}
+	switch str {
+	case "secure_internet":
+		*t = TypeSecureInternet
+	case "institute_access":
+		*t = TypeInstituteAccess
+	case "custom_server":
+		*t = TypeCustom
+	default:
+		return fmt.Errorf("invalid server type: %s", str)
+	}
+	return nil
+}
+
+// RequiredAskTransition represents the data that is sent when a transition is required to be handled and the go library needs data from the client
+// This data can be a profile selection or secure internet location selection
+type RequiredAskTransition struct {
+	// C is the cookie which is needed for replying to the library, see CookieReply in exports/exports.go
+	// If this cookie is omitted, it is a protocol error
+	C *cookie.Cookie `json:"cookie,omitempty"`
+	// Data is the data associated to the transition, e.g. the list of profiles (Profiles struct)
+	// or the list of secure internet locations ([]string)
+	Data interface{} `json:"data"`
+}
+
+// Expiry is the struct that gives the time at which certain expiry elements should be shown
+type Expiry struct {
+	// StartTime is the start time of the VPN in Unix
+	StartTime int64 `json:"start_time"`
+	// EndTime is the end time of the VPN in Unix.
+	EndTime int64 `json:"end_time"`
+	// ButtonTime is the Unix time at which to start showing the renew button in the UI
+	ButtonTime int64 `json:"button_time"`
+	// CountdownTime is the Unix time at which to start showing more detailed countdown timer.
+	// E.g. first start with days (7 days left), and when the current time is after this time, show e.g. 9 minutes and 59 seconds
+	CountdownTime int64 `json:"countdown_time"`
+	// NotificationTimes is the slice/list of times at which to show a notification that the VPN is about to expire
+	NotificationTimes []int64 `json:"notification_times"`
+}
+
+// Profile is the profile for the VPN, to show in the UI where the user can switch to it to get a different VPN configuration
+type Profile struct {
+	// DisplayName is the display name of the profile as a map
+	// It is a map where country codes are mapped to names, this is to be consistent with the format of other display names
+	// E.g. {"en": "Default Profile"}
+	// If this is empty, the field is omitted from the JSON
+	DisplayName map[string]string `json:"display_name,omitempty"`
+	// Protocols is the list of protocols that this profile supports
+	Protocols []protocol.Protocol `json:"supported_protocols"`
+}
+
+// Profiles is the map of profiles with the current defined
+type Profiles struct {
+	// Map, the map of profiles from profile ID to the profile contents
+	// If this is empty, the field is omitted from the JSON
+	Map map[string]Profile `json:"map,omitempty"`
+	// Current is the current profile ID that is defined
+	Current string `json:"current"`
+}
+
+// Tokens are the OAuth tokens for the server
+type Tokens struct {
+	// Access is the access token
+	Access string `json:"access_token"`
+	// Refresh is the refresh token
+	Refresh string `json:"refresh_token"`
+	// Expires is the Unix timestamp when the token expires
+	Expires int64 `json:"expires_in"`
+}
+
+// Server is the basic type for a server. This is the base for secure internet and institute access. Custom servers are equal to this type
+type Server struct {
+	// DisplayName is the map from language tags to display name. If this is empty, the field is omitted from the JSON
+	DisplayName map[string]string `json:"display_name,omitempty"`
+	// Identifier is the Base URL for Institute Access and Custom Server. For Secure Internet this is the organization ID
+	// This identifier should be passed to the Go library for e.g. getting a config
+	Identifier string `json:"identifier"`
+	// Profiles is the profiles that this server has defined
+	// It could be that this is empty if the library has not discovered the profiles just yet
+	Profiles Profiles `json:"profiles"`
+}
+
+// Institute defines an institute access server
+type Institute struct {
+	// Server is the embedded server struct
+	Server
+	// SupportContacts are the list of support contacts
+	SupportContacts []string `json:"support_contacts"`
+	// Delisted is a boolean that indicates whether or not this server is delisted from discovery
+	// If it is, the UI should show a warning symbol or move the server to a new category, which is up to the client
+	Delisted bool `json:"delisted"`
+}
+
+// SecureInternet is a secure internet server
+type SecureInternet struct {
+	// Server is the embedded server struct
+	Server
+	// CountryCode is the country code of the currently configured location, e.g. "nl"
+	CountryCode string `json:"country_code"`
+	// Locations is the list of available secure internet locations
+	Locations []string `json:"locations"`
+	// SupportContacts are the list of support contacts
+	SupportContacts []string `json:"support_contacts"`
+	// Delisted is a boolean that indicates whether or not this server is delisted from discovery
+	// If it is, the UI should show a warning symbol or move the server to a new category, which is up to the client
+	Delisted bool `json:"delisted"`
+}
+
+// List is the list of servers
+type List struct {
+	// Institutes is the list/slice of institute access servers. If none are defined, this is omitted in the JSON
+	Institutes []Institute `json:"institute_access_servers,omitempty"`
+	// Secure Internet is the secure internet server if any. If none is there, it is omitted in the JSON
+	SecureInternet *SecureInternet `json:"secure_internet_server,omitempty"`
+	// Custom is the list/slice of custom servers. If none are defined, this is omitted in the JSON
+	Custom []Server `json:"custom_servers,omitempty"`
+}
+
+// Configuration is the configuration that you get back when you call the get config function
+type Configuration struct {
+	// VPNConfig is the VPN Configuration, a WireGuard or OpenVPN Configuration
+	// In case of OpenVPN, we append "script-security 0" to disable scripts from being run by default.
+	// A client may override this, e.g. for, very trusted, pre-provisioned VPNs
+	VPNConfig string `json:"config"`
+	// Protocol defines which protocol the configuration is for, OpenVPN or WireGuard
+	Protocol protocol.Protocol `json:"protocol"`
+	// DefaultGateway is a boolean that indicates whether or not this configuration should be configured as a default gateway
+	DefaultGateway bool `json:"default_gateway"`
+}
+
+// Current is the struct that defines the current server
+// It has different fields where only two are always filled in
+type Current struct {
+	// The following three are mutually exclusive
+
+	// Institute is the institute access server if any, if none is there this field is omitted in the JSON
+	Institute *Institute `json:"institute_access_server,omitempty"`
+	// Secure Internet is the secure internet server if any, if none is there this field is omitted in the JSON
+	SecureInternet *SecureInternet `json:"secure_internet_server,omitempty"`
+	// Custom is the custom server if any, if none is there this field is omitted in the JSON
+	Custom *Server `json:"custom_server,omitempty"`
+	// Type is the type of server that is there to check which of the three types should be non-nil
+	Type Type `json:"server_type"`
+}
diff --git a/update_translation.sh b/update_translation.sh
new file mode 100755
index 00000000..4eb51fa4
--- /dev/null
+++ b/update_translation.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+set -e
+
+git diff --quiet client/locales client/zgotext.go || { echo "There are local uncommited locale changes, exiting..."; exit 1; }
+
+tempdir=$(mktemp -d --tmpdir=.)
+trap 'rm -rf -- "$tempdir"' EXIT
+
+pushd "$tempdir" >/dev/null
+
+echo "Getting translations..."
+wget -q -O translation.zip "https://hosted.weblate.org/download/eduvpn-common/eduvpn-common/?format=zip"
+unzip -q translation.zip
+
+echo "Copying translations..."
+cp -r eduvpn-common/eduvpn-common/client ../
+
+popd >/dev/null
+
+echo "Checking for changes..."
+git diff --quiet client/locales && { echo "No locale changes, exiting..."; exit 1; }
+
+echo "Regenerating locales with go generate ./..."
+go generate ./...
+git add client/zgotext.go client/locales
+git commit -m "Client: Update translations from Weblate"
diff --git a/wrappers/python/docs/source/conf.py b/wrappers/python/docs/source/conf.py
index 52cf3b30..57638c5e 100644
--- a/wrappers/python/docs/source/conf.py
+++ b/wrappers/python/docs/source/conf.py
@@ -16,17 +16,16 @@
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 
 extensions = [
-    'sphinx.ext.autodoc',
-    'sphinx_rtd_theme',
+    "sphinx.ext.autodoc",
+    "sphinx_rtd_theme",
 ]
 
-templates_path = ['_templates']
+templates_path = ["_templates"]
 exclude_patterns = []
 
 
-
 # -- Options for HTML output -------------------------------------------------
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 
-html_theme = 'sphinx_rtd_theme'
+html_theme = "sphinx_rtd_theme"
 html_static_path = []
diff --git a/wrappers/python/eduvpn_common/__init__.py b/wrappers/python/eduvpn_common/__init__.py
index 72f26f59..8c0d5d5b 100644
--- a/wrappers/python/eduvpn_common/__init__.py
+++ b/wrappers/python/eduvpn_common/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "1.1.2"
+__version__ = "2.0.0"
diff --git a/wrappers/python/eduvpn_common/discovery.py b/wrappers/python/eduvpn_common/discovery.py
deleted file mode 100644
index 1d59c00c..00000000
--- a/wrappers/python/eduvpn_common/discovery.py
+++ /dev/null
@@ -1,213 +0,0 @@
-from ctypes import CDLL, POINTER, c_void_p, cast
-from typing import List, Optional
-
-from eduvpn_common.types import (
-    cDiscoveryOrganizations,
-    cDiscoveryServers,
-    get_ptr_list_strings,
-)
-
-
-class DiscoOrganization:
-    """The class that represents an organization from discovery
-
-    :param: display_name: str: The display name of the organizations
-    :param: org_id: str: The organization ID
-    :param: secure_internet_home: str: Indicating which server is the secure internet home server
-    :param: keyword_list: The list of strings that the users gets to search on to find the server
-    """
-
-    def __init__(
-        self,
-        display_name: str,
-        org_id: str,
-        secure_internet_home: str,
-        keyword_list: List[str],
-    ):
-        self.display_name = display_name
-        self.org_id = org_id
-        self.secure_internet_home = secure_internet_home
-        self.keyword_list = keyword_list
-
-    def __str__(self):
-        return self.display_name
-
-
-class DiscoOrganizations:
-    """The class that represents the list of disco organizations from discovery.
-    Additionally it has provided a version which indicates which exact 'version' was used from discovery
-
-    :param: version: int: The version of the list as returned by Discovery
-    :param: organizations: List[DiscoOrganization]: The actual list of discovery organizations
-    """
-
-    def __init__(self, version: int, organizations: List[DiscoOrganization]):
-        self.version = version
-        self.organizations = organizations
-
-
-class DiscoServer:
-    """The class that represents a discovery server, this can be an institute access or secure internet server
-
-    :param: authentication_url_template: str: The OAuth template to use to skip WAYF
-    :param: base_url: str: The base URL of the server
-    :param: country_code: str: The country code of the server
-    :param: display_name: str: The display name of the server
-    :param: keyword_list: List[str]: The list of keywords that the user can use to find the server
-    :param: public_keys: List[str]: The list of public keys
-    :param: server_type: str: The server type as a string
-    :param: support_contacts: List[str]: The list of support contacts
-    """
-
-    def __init__(
-        self,
-        authentication_url_template: str,
-        base_url: str,
-        country_code: str,
-        display_name: str,
-        keyword_list: List[str],
-        public_keys: List[str],
-        server_type: str,
-        support_contacts: List[str],
-    ):
-        self.authentication_url_template = authentication_url_template
-        self.base_url = base_url
-        self.country_code = country_code
-        self.display_name = display_name
-        self.keyword_list = keyword_list
-        self.public_keys = public_keys
-        self.server_type = server_type
-        self.support_contacts = support_contacts
-
-    def __str__(self):
-        return self.display_name
-
-
-class DiscoServers:
-    """This class represents the list of discovery servers.
-    The version indicates which exact 'version' from Discovery was used.
-
-    :param: version: int: The version of the list as returned by Discovery
-    :param: servers: List[DiscoServers]: The list of discovery servers
-    """
-
-    def __init__(self, version: int, servers: List[DiscoServer]):
-        self.version = version
-        self.servers = servers
-
-
-def get_disco_organization(ptr) -> Optional[DiscoOrganization]:
-    """Gets a discovery organization from the Go library in a C structure and returns a Python usable structure
-
-    :param ptr: The pointer returned by the go library that contains a discovery organization
-
-    :meta private:
-
-    :return: The Discovery Organization if there is one
-    :rtype: Optional[DiscoOrganization]
-    """
-    if not ptr:
-        return None
-
-    current_organization = ptr.contents
-    display_name = current_organization.display_name.decode("utf-8")
-    org_id = current_organization.org_id.decode("utf-8")
-    secure_internet_home = current_organization.secure_internet_home.decode("utf-8")
-    keyword_list = current_organization.keyword_list.decode("utf-8")
-    return DiscoOrganization(display_name, org_id, secure_internet_home, keyword_list)
-
-
-def get_disco_server(lib: CDLL, ptr) -> Optional[DiscoServer]:
-    """Gets a discovery server from the Go library in a C structure and returns a Python usable structure
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: The pointer to a discovery server returned by the Go library
-
-    :meta private:
-
-    :return: The Discovery Server if there is one
-    :rtype: Optional[DiscoServer]
-    """
-    if not ptr:
-        return None
-
-    current_server = ptr.contents
-    authentication_url_template = current_server.authentication_url_template.decode(
-        "utf-8"
-    )
-    base_url = current_server.base_url.decode("utf-8")
-    country_code = current_server.country_code.decode("utf-8")
-    display_name = current_server.display_name.decode("utf-8")
-    keyword_list = current_server.keyword_list.decode("utf-8")
-    public_keys = get_ptr_list_strings(
-        lib, current_server.public_key_list, current_server.total_public_keys
-    )
-    server_type = current_server.server_type.decode("utf-8")
-    support_contacts = get_ptr_list_strings(
-        lib, current_server.support_contact, current_server.total_support_contact
-    )
-    return DiscoServer(
-        authentication_url_template,
-        base_url,
-        country_code,
-        display_name,
-        keyword_list,
-        public_keys,
-        server_type,
-        support_contacts,
-    )
-
-
-def get_disco_servers(lib: CDLL, ptr: c_void_p) -> Optional[DiscoServers]:
-    """Gets servers from the Go library in a C structure and returns a Python usable structure
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The pointer returned by the Go library for the discovery servers
-
-    :meta private:
-
-    :return: The Discovery Servers if there are any
-    :rtype: Optional[DiscoServers]
-    """
-    if ptr:
-        svrs = cast(ptr, POINTER(cDiscoveryServers)).contents
-
-        servers = []
-
-        if svrs.servers:
-            for i in range(svrs.total_servers):
-                current = get_disco_server(lib, svrs.servers[i])
-
-                if current is None:
-                    continue
-                servers.append(current)
-        disco_version = svrs.version
-        lib.FreeDiscoServers(ptr)
-        return DiscoServers(disco_version, servers)
-    return None
-
-
-def get_disco_organizations(lib: CDLL, ptr: c_void_p) -> Optional[DiscoOrganizations]:
-    """Gets organizations from the Go library in a C structure and returns a Python usable structure
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The pointer returned by the Go library for the discovery organizations
-
-    :meta private:
-
-    :return: The Discovery Organizations if there are any
-    :rtype: Optional[DiscoOrganizations]
-    """
-    if ptr:
-        orgs = cast(ptr, POINTER(cDiscoveryOrganizations)).contents
-        organizations = []
-        if orgs.organizations:
-            for i in range(orgs.total_organizations):
-                current = get_disco_organization(orgs.organizations[i])
-                if current is None:
-                    continue
-                organizations.append(current)
-        disco_version = orgs.version
-        lib.FreeDiscoOrganizations(ptr)
-        return DiscoOrganizations(disco_version, organizations)
-    return None
diff --git a/wrappers/python/eduvpn_common/error.py b/wrappers/python/eduvpn_common/error.py
deleted file mode 100644
index 3d843311..00000000
--- a/wrappers/python/eduvpn_common/error.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from enum import Enum
-
-
-class WrappedError(Exception):
-    """An exception returned by the Go library
-
-    :param: traceback: str: The traceback of the error including newlines
-    :param: cause: str: The cause of the error as a message
-    """
-
-    def __init__(self, traceback: str, cause: str):
-        super(WrappedError, self).__init__(cause)
-        self.traceback = traceback
-        self.cause = cause
diff --git a/wrappers/python/eduvpn_common/event.py b/wrappers/python/eduvpn_common/event.py
index 746a0c9a..efe33103 100644
--- a/wrappers/python/eduvpn_common/event.py
+++ b/wrappers/python/eduvpn_common/event.py
@@ -1,34 +1,22 @@
-from ctypes import CDLL
+from enum import IntEnum
 from typing import Any, Callable, Dict, List, Tuple
 
-from eduvpn_common.server import (
-    get_locations,
-    get_servers,
-    get_transition_profiles,
-    get_transition_server,
-)
 from eduvpn_common.state import State, StateType
-from eduvpn_common.types import get_ptr_string
 
 # The attribute that callback functions get
 EDUVPN_CALLBACK_PROPERTY = "_eduvpn_property_callback"
 
-# A state transition decorator for classes
-# To use this, make sure to register the class with `register_class_callbacks`
+
 def class_state_transition(state: int, state_type: StateType) -> Callable:
     """A decorator to be internally by classes to register the event
-
     :param state: int: The state of the transition
     :param state_type: StateType: The type of transition
-
     :meta private:
     """
 
     def wrapper(func):
         """
-
         :param func: The function to set the internal attribute for
-
         """
         setattr(func, EDUVPN_CALLBACK_PROPERTY, (state, state_type))
         return func
@@ -36,47 +24,16 @@ def wrapper(func):
     return wrapper
 
 
-def convert_data(lib: CDLL, state: int, data: Any) -> None:
-    """The function that converts the C structure from the Go library to a Python structure
-
-    :param lib: CDLL: The Go shared library
-    :param state: int: The state to convert the data for
-    :param data: Any: The data itself that has to be converted
-
-    :meta private:
-    """
-    if not data:
-        return None
-    if state is State.NO_SERVER:
-        return get_servers(lib, data)
-    if state is State.OAUTH_STARTED:
-        return get_ptr_string(lib, data)
-    if state is State.ASK_LOCATION:
-        return get_locations(lib, data)
-    if state is State.ASK_PROFILE:
-        return get_transition_profiles(lib, data)
-    if state in [
-        State.DISCONNECTED,
-        State.DISCONNECTING,
-        State.CONNECTING,
-        State.CONNECTED,
-    ]:
-        return get_transition_server(lib, data)
-
-
 class EventHandler(object):
-    """The class that neatly handles event callbacks from the internal Go FSM"""
+    """The class that neatly handles event callbacks"""
 
-    def __init__(self, lib: CDLL):
+    def __init__(self):
         self.handlers: Dict[Tuple[int, StateType], List[Callable]] = {}
-        self.lib = lib
 
     def change_class_callbacks(self, cls: Any, add: bool = True) -> None:
         """The function that is used to change class callbacks
-
         :param cls: Any: The class to change the callbacks for
         :param add: bool:  (Default value = True): Whether or not to add or remove the event. If true the event gets added
-
         :meta private:
         """
         # Loop over method names
@@ -84,7 +41,7 @@ def change_class_callbacks(self, cls: Any, add: bool = True) -> None:
             try:
                 # Get the method
                 method = getattr(cls, method_name)
-            except:
+            except Exception as e:
                 # Unable to get a value, go to the next
                 continue
 
@@ -98,13 +55,11 @@ def change_class_callbacks(self, cls: Any, add: bool = True) -> None:
                 else:
                     self.remove_event(state, state_type, method)
 
-    def remove_event(self, state: int, state_type: StateType, func: Callable) -> None:
+    def remove_event(self, state: State, state_type: StateType, func: Callable) -> None:
         """Removes an event
-
-        :param state: int: The state to remove the event for
+        :param state: State: The state to remove the event for
         :param state_type: StateType: The state type to remove the event for
         :param func: Callable: The function that needs to be removed from the event
-
         :meta private:
         """
         for key, values in self.handlers.copy().items():
@@ -115,49 +70,25 @@ def remove_event(self, state: int, state_type: StateType, func: Callable) -> Non
                 else:
                     self.handlers[key] = values
 
-    def add_event(self, state: int, state_type: StateType, func: Callable) -> None:
+    def add_event(self, state: State, state_type: StateType, func: Callable) -> None:
         """Adds an event
-
-        :param state: int: The state to add the event for
+        :param state: State: The state to add the event for
         :param state_type: StateType: The state type to add the event for
         :param func: Callable: The function that needs to be added to the event
-
         :meta private:
         """
         if (state, state_type) not in self.handlers:
             self.handlers[(state, state_type)] = []
         self.handlers[(state, state_type)].append(func)
 
-    def on(self, state: int, state_type: StateType) -> Callable:
-        """The decorator for standalone functions
-
-        :param state: int: The state of the event
-        :param state_type: StateType: The state type of the event
-
-        :meta private:
-        """
-
-        def wrapped_f(func):
-            """
-
-            :param func: The function to add the event for
-
-            """
-            self.add_event(state, state_type, func)
-            return func
-
-        return wrapped_f
-
     def run_state(
-        self, state: int, other_state: int, state_type: StateType, data: str
+        self, state: State, other_state: State, state_type: StateType, data: str
     ) -> bool:
         """The function that runs the callback for a specific event
-
-        :param state: int: The state of the event
-        :param other_state: int: The other state of the event
+        :param state: State: The state of the event
+        :param other_state: State: The other state of the event
         :param state_type: StateType: The state type of the event
         :param data: str: The data that gets passed to the function callback when the event is ran
-
         :meta private:
         """
         if (state, state_type) not in self.handlers:
@@ -166,27 +97,16 @@ def run_state(
             func(other_state, data)
         return True
 
-    def run(
-        self, old_state: int, new_state: int, data: Any, convert: bool = True
-    ) -> bool:
+    def run(self, old_state: State, new_state: State, data: Any) -> bool:
         """Run a specific event.
         It converts the data and then runs the event for all state types
-
-        :param old_state: int: The previous state for running the event
-        :param new_state: int: The new state for running the event
+        :param old_state: State: The previous state for running the event
+        :param new_state: State: The new state for running the event
         :param data: Any: The data that gets passed to the event
         :param convert: bool:  (Default value = True): Whether or not to convert the data further
-
         """
         # First run leave transitions, then enter
-        # The state is done when the wait event finishes
-        converted = data
-        if convert:
-            converted = convert_data(self.lib, new_state, data)
-        self.run_state(old_state, new_state, StateType.LEAVE, converted)
+        self.run_state(old_state, new_state, StateType.LEAVE, data)
         # We decide handled based on enter transitions
-        handled = self.run_state(new_state, old_state, StateType.ENTER, converted)
-        # Only run wait transitions if the enter transition is handled
-        if handled:
-            self.run_state(new_state, old_state, StateType.WAIT, converted)
+        handled = self.run_state(new_state, old_state, StateType.ENTER, data)
         return handled
diff --git a/wrappers/python/eduvpn_common/loader.py b/wrappers/python/eduvpn_common/loader.py
index 673d1802..7da159ca 100644
--- a/wrappers/python/eduvpn_common/loader.py
+++ b/wrappers/python/eduvpn_common/loader.py
@@ -5,10 +5,11 @@
 
 from eduvpn_common import __version__
 from eduvpn_common.types import (
-    cToken,
+    BoolError,
     DataError,
     ReadRxBytes,
-    UpdateToken,
+    TokenGetter,
+    TokenSetter,
     VPNStateChange,
 )
 
@@ -63,111 +64,64 @@ def initialize_functions(lib: CDLL) -> None:
     # Exposed functions
     # We have to use c_void_p instead of c_char_p to free it properly
     # See https://stackoverflow.com/questions/13445568/python-ctypes-how-to-free-memory-getting-invalid-pointer-error
-    lib.CancelOAuth.argtypes, lib.CancelOAuth.restype = [c_char_p], c_void_p
-    lib.ChangeSecureLocation.argtypes, lib.ChangeSecureLocation.restype = [
-        c_char_p
-    ], c_void_p
-    lib.Deregister.argtypes, lib.Deregister.restype = [c_char_p], None
-    lib.FreeConfig.argtypes, lib.FreeConfig.restype = [c_void_p], None
-    lib.FreeTokens.argtypes, lib.FreeTokens.restype = [c_void_p], None
-    lib.FreeDiscoOrganizations.argtypes, lib.FreeDiscoOrganizations.restype = [
-        c_void_p
-    ], None
-    lib.FreeDiscoServers.argtypes, lib.FreeDiscoServers.restype = [c_void_p], None
-    lib.FreeError.argtypes, lib.FreeError.restype = [c_void_p], None
-    lib.FreeProfiles.argtypes, lib.FreeProfiles.restype = [c_void_p], None
-    lib.FreeSecureLocations.argtypes, lib.FreeSecureLocations.restype = [c_void_p], None
-    lib.FreeServer.argtypes, lib.FreeServer.restype = [c_void_p], None
-    lib.FreeServers.argtypes, lib.FreeServers.restype = [c_void_p], None
+    lib.Deregister.argtypes, lib.Deregister.restype = [], None
+    lib.ExpiryTimes.argtypes, lib.ExpiryTimes.restype = [], DataError
     lib.FreeString.argtypes, lib.FreeString.restype = [c_void_p], None
-    lib.GetConfigCustomServer.argtypes, lib.GetConfigCustomServer.restype = [
-        c_char_p,
-        c_char_p,
+    lib.DiscoOrganizations.argtypes, lib.DiscoOrganizations.restype = [c_int], DataError
+    lib.DiscoServers.argtypes, lib.DiscoServers.restype = [c_int], DataError
+    lib.GetConfig.argtypes, lib.GetConfig.restype = [
+        c_int,
         c_int,
-        cToken,
-    ], DataError
-    lib.GetConfigInstituteAccess.argtypes, lib.GetConfigInstituteAccess.restype = [
-        c_char_p,
         c_char_p,
         c_int,
-        cToken,
+        c_int,
     ], DataError
-    lib.GetConfigSecureInternet.argtypes, lib.GetConfigSecureInternet.restype = [
-        c_char_p,
+    lib.AddServer.argtypes, lib.AddServer.restype = [
+        c_int,
+        c_int,
         c_char_p,
         c_int,
-        cToken,
-    ], DataError
-    lib.GetDiscoOrganizations.argtypes, lib.GetDiscoOrganizations.restype = [
-        c_char_p
-    ], DataError
-    lib.GetDiscoServers.argtypes, lib.GetDiscoServers.restype = [c_char_p], DataError
-    lib.GetCurrentServer.argtypes, lib.GetCurrentServer.restype = [c_char_p], DataError
-    lib.GetSavedServers.argtypes, lib.GetSavedServers.restype = [c_char_p], DataError
-    lib.GoBack.argtypes, lib.GoBack.restype = [c_char_p], None
-    lib.InFSMState.argtypes, lib.InFSMState.restype = [c_void_p, c_int], int
-    lib.Register.argtypes, lib.Register.restype = [
+    ], c_char_p
+    lib.CurrentServer.argtypes, lib.CurrentServer.restype = [], DataError
+    lib.RemoveServer.argtypes, lib.RemoveServer.restype = [
+        c_int,
         c_char_p,
+    ], c_char_p
+    lib.ServerList.argtypes, lib.ServerList.restype = [], DataError
+    lib.Register.argtypes, lib.Register.restype = [
         c_char_p,
         c_char_p,
         c_char_p,
         VPNStateChange,
         c_int,
     ], c_void_p
-    lib.SetTokenUpdater.argtypes, lib.SetTokenUpdater.restype = [
-        c_char_p,
-        UpdateToken,
-    ], c_void_p
-    lib.RemoveCustomServer.argtypes, lib.RemoveCustomServer.restype = [
-        c_char_p,
-        c_char_p,
-    ], c_void_p
-    lib.AddInstituteAccess.argtypes, lib.AddInstituteAccess.restype = [
-        c_char_p,
-        c_char_p,
+    lib.RenewSession.argtypes, lib.RenewSession.restype = [c_int], c_void_p
+    lib.SetTokenHandler.argtypes, lib.SetTokenHandler.restype = [
+        TokenGetter,
+        TokenSetter,
     ], c_void_p
-    (
-        lib.AddSecureInternetHomeServer.argtypes,
-        lib.AddSecureInternetHomeServer.restype,
-    ) = [
-        c_char_p,
-        c_char_p,
-    ], c_void_p
-    lib.AddCustomServer.argtypes, lib.AddCustomServer.restype = [
-        c_char_p,
-        c_char_p,
-    ], c_void_p
-    lib.RemoveInstituteAccess.argtypes, lib.RemoveInstituteAccess.restype = [
-        c_char_p,
-        c_char_p,
-    ], c_void_p
-    lib.RemoveSecureInternet.argtypes, lib.RemoveSecureInternet.restype = [
-        c_char_p
-    ], c_void_p
-    lib.RenewSession.argtypes, lib.RenewSession.restype = [c_char_p], c_void_p
-    lib.SetConnected.argtypes, lib.SetConnected.restype = [c_char_p], c_void_p
-    lib.SetConnecting.argtypes, lib.SetConnecting.restype = [c_char_p], c_void_p
-    lib.Cleanup.argtypes, lib.Cleanup.restype = [
-        c_char_p,
-        cToken,
-    ], c_void_p
-    lib.SetDisconnected.argtypes, lib.SetDisconnected.restype = [c_char_p], c_void_p
-    lib.SetDisconnecting.argtypes, lib.SetDisconnecting.restype = [c_char_p], c_void_p
-    lib.SetProfileID.argtypes, lib.SetProfileID.restype = [c_char_p, c_char_p], c_void_p
-    lib.SetSearchServer.argtypes, lib.SetSearchServer.restype = [c_char_p], c_void_p
+    lib.Cleanup.argtypes, lib.Cleanup.restype = [c_int], c_void_p
+    lib.SetProfileID.argtypes, lib.SetProfileID.restype = [c_char_p], c_void_p
+    lib.CookieNew.argtypes, lib.CookieNew.restype = [], c_int
+    lib.CookieReply.argtypes, lib.CookieReply.restype = [c_int, c_char_p], c_void_p
+    lib.CookieCancel.argtypes, lib.CookieCancel.restype = [c_int], c_void_p
+    lib.CookieDelete.argtypes, lib.CookieDelete.restype = [c_int], c_void_p
     lib.SetSecureLocation.argtypes, lib.SetSecureLocation.restype = [
-        c_char_p,
+        c_int,
         c_char_p,
     ], c_void_p
     lib.SetSupportWireguard.argtypes, lib.SetSupportWireguard.restype = [
-        c_char_p,
         c_int,
     ], c_void_p
-    lib.ShouldRenewButton.argtypes, lib.ShouldRenewButton.restype = [], int
+    lib.SetState.argtypes, lib.SetState.restype = [
+        c_int,
+    ], c_void_p
+    lib.InState.argtypes, lib.InState.restype = [
+        c_int,
+    ], BoolError
     lib.StartFailover.argtypes, lib.StartFailover.restype = [
-        c_char_p,
+        c_int,
         c_char_p,
         c_int,
         ReadRxBytes,
-    ], DataError
-    lib.CancelFailover.argtypes, lib.CancelFailover.restype = [c_char_p], c_void_p
+    ], BoolError
diff --git a/wrappers/python/eduvpn_common/main.py b/wrappers/python/eduvpn_common/main.py
index 74ff52de..eb4897ba 100644
--- a/wrappers/python/eduvpn_common/main.py
+++ b/wrappers/python/eduvpn_common/main.py
@@ -1,463 +1,259 @@
-import threading
-from ctypes import POINTER, cast, c_void_p, c_int, pointer
-from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
-
-from eduvpn_common.discovery import (
-    DiscoOrganizations,
-    DiscoServers,
-    get_disco_organizations,
-    get_disco_servers,
-)
-from eduvpn_common.event import EventHandler
+import ctypes
+import json
+from enum import IntEnum
+from typing import Any, Callable, Iterator, Optional
+
 from eduvpn_common.loader import initialize_functions, load_lib
-from eduvpn_common.server import (
-    Profiles,
-    Config,
-    Token,
-    encode_tokens,
-    get_config,
-    Server,
-    get_tokens,
-    get_transition_server,
-    get_servers,
-)
-from eduvpn_common.state import State, StateType
 from eduvpn_common.types import (
-    VPNStateChange,
     ReadRxBytes,
-    UpdateToken,
+    TokenGetter,
+    TokenSetter,
+    VPNStateChange,
     decode_res,
     encode_args,
-    get_data_error,
-    get_bool,
 )
 
+from eduvpn_common.event import EventHandler
+from eduvpn_common.state import State
+
+
+class WrappedError(Exception):
+    def __init__(self, translations, language, misc):
+        self.translations = translations
+        self.language = language
+        self.misc = misc
+
+    def __str__(self) -> str:
+        return self.translations[self.language]
+
+
+def forwardError(error: str):
+    d = json.loads(error)
+    raise WrappedError(d["message"], "en", d["misc"])
+
+
+class ServerType(IntEnum):
+    UNKNOWN = 0
+    INSTITUTE_ACCESS = 1
+    SECURE_INTERNET = 2
+    CUSTOM = 3
+
+
+class Jar(object):
+    """A cookie jar"""
+
+    def __init__(self, canceller):
+        self.cookies = []
+        self.canceller = canceller
+
+    def add(self, cookie):
+        self.cookies.append(cookie)
+
+    def delete(self, cookie):
+        self.cookies.remove(cookie)
+
+    def cancel(self):
+        for cookie in self.cookies:
+            self.canceller(cookie)
+
 
 class EduVPN(object):
     """The main class used to communicate with the Go library.
     It registers the client with the library and then calls the needed appropriate functions
 
     :param name: str: The name of the client. For commonly used names, see https://git.sr.ht/~fkooman/vpn-user-portal/tree/v3/item/src/OAuth/ClientDb.php. E.g. org.eduvpn.app.linux, if this name has "letsconnect" in it, then it is a Let's Connect! variant
-    :param version: str: The version number of the client as a string, max 10 characters
+    :param version: str: The version number of the client as a string
     :param config_directory: str: The directory (absolute/relative) where to store the files
-    :param language: str: The language of the client, e.g. en
 
     """
 
-    def __init__(self, name: str, version: str, config_directory: str, language: str):
+    def __init__(self, name: str, version: str, config_directory: str):
         self.name = name
         self.version = version
         self.config_directory = config_directory
-        self.language = language
+        self.jar = Jar(lambda x: self.go_function(self.lib.CookieCancel, x))
+        self.token_setter = None
+        self.token_getter = None
+        self.event_handler = EventHandler()
 
         # Load the library
         self.lib = load_lib()
         initialize_functions(self.lib)
 
-        self.event_handler = EventHandler(self.lib)
-        self.token_callback = None
-
-        # Callbacks that need to wait for specific events
-
-        # The ask profile callback needs to wait for the UI thread to select a profile
-        # This is stored in the profile_event
-        self.profile_event: Optional[threading.Event] = None
-        self.location_event: Optional[threading.Event] = None
+    def register_class_callbacks(self, _class):
+        self.event_handler.change_class_callbacks(_class, add=True)
 
-        @self.event.on(State.ASK_PROFILE, StateType.WAIT)
-        def wait_profile_event(old_state: int, profiles: Profiles):
-            """This functions waits until the ask location thread event is finished
+    def deregister_class_callbacks(self, _class):
+        self.event_handler.change_class_callbacks(_class, add=False)
 
-            :param old_state: int: The old state of the profiles event
-            :param profiles: Profiles: The profiles
+    def go_cookie_function(self, func: Any, *args: Iterator) -> Any:
+        cookie = self.lib.CookieNew()
+        self.jar.add(cookie)
+        res = self.go_function(func, cookie, *args)
+        self.jar.delete(cookie)
+        self.lib.CookieDelete(cookie)
+        return res
 
-            """
-            if self.profile_event:
-                self.profile_event.wait()
-
-        @self.event.on(State.ASK_LOCATION, StateType.WAIT)
-        def wait_location_event(old_state: int, locations: List[str]):
-            """This functions waits until the location thread event is finished
-
-            :param old_state: int: The old state of the location event
-            :param locations: List[str]: The locations
-
-            """
-            if self.location_event:
-                self.location_event.wait()
-
-    def go_function(
-        self, func: Any, *args: Iterator, decode_func: Optional[Callable] = None
-    ) -> Any:
+    def go_function(self, func: Any, *args: Iterator) -> Any:
         """Call an internal go function and properly forward the arguments.
         Also handles decoding the result
 
         :param func: Any: The Go function to call from the shared library
         :param \*args: Iterator: The arguments to call the function with
-        :param decode_func: Optional[Callable]:  (Default value = None): The function to decode the result into a Python type
 
         :meta private:
         """
         # The functions all have at least one arg type which is the name of the client
-        args_gen = encode_args(list(args), func.argtypes[1:])
-        res = func(self.name.encode("utf-8"), *(args_gen))
-        if decode_func is None:
-            return decode_res(func.restype)(self.lib, res)
-        else:
-            return decode_func(self.lib, res)
-
-    def cancel_oauth(self) -> None:
-        """Cancel the OAuth process"""
-        cancel_oauth_err = self.go_function(self.lib.CancelOAuth)
-
-        if cancel_oauth_err:
-            raise cancel_oauth_err
+        args_gen = encode_args(list(args), func.argtypes)
+        res = func(*(args_gen))
+        return decode_res(func.restype)(self.lib, res)
 
     def deregister(self) -> None:
         """Deregister the Go shared library.
         This removes the object from internal bookkeeping and saves the configuration
         """
         self.go_function(self.lib.Deregister)
-        remove_as_global_object(self)
+        global global_object
+        global_object = None
 
     def register(self, debug: bool = False) -> None:
         """Register the Go shared library.
         This makes sure the FSM is initialized and that we can call Go functions
 
+        :param handler: Optional[Callable]:  (Default value = None): The handler that runs state transitions
         :param debug: bool:  (Default value = False): Whether or not we want to enable debug logging
 
         """
-        if not add_as_global_object(self):
+        global global_object
+        if global_object is not None:
             raise Exception("Already registered")
-
+        global_object = self
         register_err = self.go_function(
             self.lib.Register,
+            self.name,
             self.version,
             self.config_directory,
-            self.language,
             state_callback,
             debug,
         )
 
         if register_err:
-            raise register_err
-
-    def set_token_updater(self, updater: Callable):
-        self.token_callback = updater
-        updater_err = self.go_function(
-            self.lib.SetTokenUpdater,
-            token_callback,
-        )
-
-        if updater_err:
-            raise updater_err
-
-    def get_disco_servers(self) -> Optional[DiscoServers]:
-        """Get the discovery servers
-
-        :raises WrappedError: An error by the Go library
-
-        :return: The disco Servers if any
-        :rtype: Optional[DiscoServers]
-        """
-        servers, _ = self.go_function(
-            self.lib.GetDiscoServers,
-            decode_func=lambda lib, x: get_data_error(lib, x, get_disco_servers),
-        )
-
-        return servers
-
-    def get_disco_organizations(self) -> Optional[DiscoOrganizations]:
-        """Get the discovery organizations
-
-        :raises WrappedError: An error by the Go library
-
-        :return: The discovery Organizations if any
-        :rtype: Optional[DiscoOrganizations]
-        """
-        organizations, _ = self.go_function(
-            self.lib.GetDiscoOrganizations,
-            decode_func=lambda lib, x: get_data_error(lib, x, get_disco_organizations),
-        )
-
-        return organizations
+            forwardError(register_err)
 
-    def add_institute_access(self, url: str) -> None:
-        """Add an institute access server
+    def add_server(self, _type: ServerType, _id: str, ni: bool = False) -> None:
+        """Add a server
 
-        :param url: str: The URL for the institute access server. Use the exact base_url as returned by Discovery
+        :param _type: ServerType: The type of server e.g. SERVER.INSTITUTE_ACCESS
+        :param _id: str: The identifier of the server, e.g. "https://vpn.example.com/"
+        :param ni: bool: Whether the server should be added non interactively, meaning no callbacks
 
         :raises WrappedError: An error by the Go library
         """
-        add_err = self.go_function(self.lib.AddInstituteAccess, url)
+        add_err = self.go_cookie_function(self.lib.AddServer, int(_type), _id, ni)
 
         if add_err:
-            raise add_err
+            forwardError(add_err)
 
-    def add_secure_internet_home(self, org_id: str) -> None:
-        """Add a secure internet server
+    def get_expiry_times(self) -> str:
+        expiry, expiry_err = self.go_function(self.lib.ExpiryTimes)
+        if expiry_err:
+            forwardError(expiry_err)
+        return expiry
 
-        :param org_id: str: The organization ID of the secure internet server. Use the exact organization as returned by Discovery
-
-        :raises WrappedError: An error by the Go library
-        """
-        self.location_event = threading.Event()
-        add_err = self.go_function(self.lib.AddSecureInternetHomeServer, org_id)
-
-        if add_err:
-            raise add_err
-
-    def add_custom_server(self, url: str) -> None:
-        """Add a custom server
-
-        :param url: str: The base URL of the server
-
-        :raises WrappedError: An error by the Go library
-        """
-        add_err = self.go_function(self.lib.AddCustomServer, url)
-
-        if add_err:
-            raise add_err
+    def get_current_server(self) -> str:
+        server, server_err = self.go_function(self.lib.CurrentServer)
+        if server_err:
+            forwardError(server_err)
+        return server
 
-    def remove_secure_internet(self) -> None:
-        """Remove the secure internet server
+    def get_disco_organizations(self) -> str:
+        orgs, _ = self.go_cookie_function(self.lib.DiscoOrganizations)
+        # TODO: Log error
+        return orgs
 
-        :raises WrappedError: An error by the Go library
-        """
-        remove_err = self.go_function(self.lib.RemoveSecureInternet)
+    def get_disco_servers(self) -> str:
+        servers, _ = self.go_cookie_function(self.lib.DiscoServers)
+        # TODO: Log error
+        return servers
 
-        if remove_err:
-            raise remove_err
+    def get_servers(self) -> str:
+        servers, servers_err = self.go_function(self.lib.ServerList)
+        if servers_err:
+            forwardError(servers_err)
+        return servers
 
-    def remove_institute_access(self, url: str) -> None:
-        """Remove an institute access server
+    def remove_server(self, _type: ServerType, _id: str) -> None:
+        """Remove a server
 
-        :param url: str: The URL for the institute access server. Use the exact base_url as returned by Discovery
+        :param _type: ServerType: The type of server e.g. SERVER.INSTITUTE_ACCESS
+        :param _id: str: The identifier of the server, e.g. "https://vpn.example.com/"
 
         :raises WrappedError: An error by the Go library
         """
-        remove_err = self.go_function(self.lib.RemoveInstituteAccess, url)
+        remove_err = self.go_function(self.lib.RemoveServer, int(_type), _id)
 
         if remove_err:
-            raise remove_err
-
-    def remove_custom_server(self, url: str) -> None:
-        """Remove a custom server
-
-        :param url: str: The base URL of the server
+            forwardError(remove_err)
 
-        :raises WrappedError: An error by the Go library
-        """
-        remove_err = self.go_function(self.lib.RemoveCustomServer, url)
+    def set_state(self, state: State):
+        state_err = self.go_function(self.lib.SetState, state)
+        if state_err:
+            forwardError(state_err)
 
-        if remove_err:
-            raise remove_err
+    def in_state(self, state: State) -> bool:
+        yes, state_err = self.go_function(self.lib.InState, state)
+        if state_err:
+            forwardError(state_err)
+        return yes
 
     def get_config(
         self,
+        _type: ServerType,
         identifier: str,
-        func: Any,
         prefer_tcp: bool = False,
-        tokens: Optional[Token] = None,
-    ) -> Optional[Config]:
+        startup: bool = False,
+    ) -> str:
         """Get an OpenVPN/WireGuard configuration from the server
 
+        :param _type: ServerType: The type of server e.g. SERVER.INSTITUTE_ACCESS
         :param identifier: str: The identifier of the server, e.g. URL or ORG ID
-        :param func: Any: The Go function to call
         :param prefer_tcp: bool:  (Default value = False): Whether or not to prefer TCP
-        :param tokens: Optional[Token]  (Default value = None): The OAuth tokens if available
+        :param startup: bool:  (Default value = False): Whether or not the client is just starting up
 
         :meta private:
 
         :raises WrappedError: An error by the Go library
 
-        :return: The configuration and configuration type ('openvpn' or 'wireguard')
-        :rtype: Config
+        :return: The configuration and configuration type ('openvpn' or 'wireguard') as a JSON string
+        :rtype: str
         """
         # Because it could be the case that a profile callback is started, store a threading event
         # In the constructor, we have defined a wait event for Ask_Profile, this waits for this event to be set
         # The event is set in self.set_profile
-        self.profile_event = threading.Event()
-
-        config, config_err = self.go_function(
-            func,
+        config, config_err = self.go_cookie_function(
+            self.lib.GetConfig,
+            int(_type),
             identifier,
             prefer_tcp,
-            encode_tokens(tokens),
-            decode_func=lambda lib, x: get_data_error(lib, x, get_config),
+            startup,
         )
 
-        self.profile_event = None
-        self.location_event = None
-
         if config_err:
-            raise config_err
+            forwardError(config_err)
 
         return config
 
-    def get_config_custom_server(
-        self, url: str, prefer_tcp: bool = False, tokens: Optional[Token] = None
-    ) -> Optional[Config]:
-        """Get an OpenVPN/WireGuard configuration from a custom server
-
-        :param url: str: The URL of the custom server
-        :param prefer_tcp: bool:  (Default value = False): Whether or not to prefer TCP
-        :param tokens: Optional[Token]  (Default value = None): The OAuth tokens if available
-
-        :raises WrappedError: An error by the Go library
-
-        :return: The configuration and configuration type ('openvpn' or 'wireguard')
-        :rtype: Config
-        """
-        return self.get_config(url, self.lib.GetConfigCustomServer, prefer_tcp, tokens)
-
-    def get_config_institute_access(
-        self, url: str, prefer_tcp: bool = False, tokens: Optional[Token] = None
-    ) -> Optional[Config]:
-        """Get an OpenVPN/WireGuard configuration from an institute access server
-
-        :param url: str: The URL of the institute access server. Use the one from Discovery
-        :param prefer_tcp: bool:  (Default value = False): Whether or not to prefer TCP
-        :param tokens: Optional[Token]  (Default value = None): The OAuth tokens if available
-
-        :raises WrappedError: An error by the Go library
-
-        :return: The configuration and configuration type ('openvpn' or 'wireguard')
-        :rtype: Config
-        """
-        return self.get_config(
-            url, self.lib.GetConfigInstituteAccess, prefer_tcp, tokens
-        )
-
-    def get_config_secure_internet(
-        self, org_id: str, prefer_tcp: bool = False, tokens: Optional[Token] = None
-    ) -> Optional[Config]:
-        """Get an OpenVPN/WireGuard configuration from a secure internet server
-
-        :param org_id: str: The organization ID of the secure internet server. Use the one from Discovery
-        :param prefer_tcp: bool:  (Default value = False): Whether or not to prefer TCP
-        :param tokens: Optional[Token]  (Default value = None): The OAuth tokens if available
-
-        :raises WrappedError: An error by the Go library
-
-        :return: The configuration and configuration type ('openvpn' or 'wireguard')
-        :rtype: Config
-        """
-        return self.get_config(
-            org_id, self.lib.GetConfigSecureInternet, prefer_tcp, tokens
-        )
-
-    def go_back(self) -> None:
-        """Go back in the FSM"""
-        # Ignore the error
-        self.go_function(self.lib.GoBack)
-
-    def set_connected(self) -> None:
-        """Set the FSM to connected
-
-        :raises WrappedError: An error by the Go library
-        """
-        connect_err = self.go_function(self.lib.SetConnected)
-
-        if connect_err:
-            raise connect_err
-
-    def set_disconnecting(self) -> None:
-        """Set the FSM to disconnecting
-
-        :raises WrappedError: An error by the Go library
-        """
-        disconnecting_err = self.go_function(self.lib.SetDisconnecting)
-
-        if disconnecting_err:
-            raise disconnecting_err
-
-    def set_connecting(self) -> None:
-        """Set the FSM to connecting
-
-        :raises WrappedError: An error by the Go library
-        """
-        connecting_err = self.go_function(self.lib.SetConnecting)
-
-        if connecting_err:
-            raise connecting_err
-
-    def cleanup(self, tokens: Optional[Token] = None) -> None:
+    def cleanup(self) -> None:
         """Cleanup the vpn connection
 
-        :param tokens: Optional[Token]  (Default value = None): The OAuth tokens if available
+        :param tokens: str  (Default value = ""): The OAuth tokens if available
 
         :raises WrappedError: An error by the Go library
         """
-        cleanup_err = self.go_function(self.lib.Cleanup, encode_tokens(tokens))
+        cleanup_err = self.go_cookie_function(self.lib.Cleanup)
 
         if cleanup_err:
-            raise cleanup_err
-
-    def set_disconnected(
-        self,
-    ) -> None:
-        """Set the FSM to disconnected
-
-        :param cleanup: bool:  (Default value = True): Whether or not to call /disconnect to the server. This invalidates the OpenVPN/WireGuard configuration
-        :param tokens: Optional[Token]  (Default value = None): The OAuth tokens if available
-
-        :raises WrappedError: An error by the Go library
-        """
-        disconnect_err = self.go_function(self.lib.SetDisconnected)
-
-        if disconnect_err:
-            raise disconnect_err
-
-    def set_search_server(self) -> None:
-        """Set the FSM to search server
-
-        :raises WrappedError: An error by the Go library
-        """
-        search_err = self.go_function(self.lib.SetSearchServer)
-
-        if search_err:
-            raise search_err
-
-    def remove_class_callbacks(self, cls: Any) -> None:
-        """Remove class callbacks
-
-        :param cls: Any: The class to remove callbacks for
-
-        """
-        self.event_handler.change_class_callbacks(cls, add=False)
-
-    def register_class_callbacks(self, cls: Any) -> None:
-        """Register class callbacks
-
-        :param cls: Any: The class to register callbacks for
-
-        """
-        self.event_handler.change_class_callbacks(cls)
-
-    @property
-    def event(self) -> EventHandler:
-        """The property that gets the event handler
-
-        :return: The event handler
-        :rtype: EventHandler
-        """
-        return self.event_handler
-
-    def callback(self, old_state: State, new_state: State, data: Any) -> bool:
-        """Run an event callback
-
-        :param old_state: State: The previous state
-        :param new_state: State: The new state
-        :param data: Any: The data to pass to the event
-
-        """
-        return self.event.run(old_state, new_state, data)
-
-    def token_calback(self, srv: Server, tok: Token):
-        if self.token_callback is None:
-            return
-        self.token_callback(srv, tok)
+            forwardError(cleanup_err)
 
     def set_profile(self, profile_id: str) -> None:
         """Set the profile of the current server
@@ -471,23 +267,8 @@ def set_profile(self, profile_id: str) -> None:
 
         # If there is a profile event, set it so that the wait callback finishes
         # And so that the Go code can move to the next state
-        if self.profile_event:
-            self.profile_event.set()
-
         if profile_err:
-            raise profile_err
-
-    def change_secure_location(self) -> None:
-        """Change the secure location. This calls the necessary events
-
-        :raises WrappedError: An error by the Go library
-        """
-        # Set the location by country code
-        self.location_event = threading.Event()
-        location_err = self.go_function(self.lib.ChangeSecureLocation)
-
-        if location_err:
-            raise location_err
+            forwardError(profile_err)
 
     def set_secure_location(self, country_code: str) -> None:
         """Set the secure location
@@ -497,25 +278,38 @@ def set_secure_location(self, country_code: str) -> None:
         :raises WrappedError: An error by the Go library
         """
         # Set the location by country code
-        location_err = self.go_function(self.lib.SetSecureLocation, country_code)
+        location_err = self.go_cookie_function(self.lib.SetSecureLocation, country_code)
 
         # If there is a location event, set it so that the wait callback finishes
         # And so that the Go code can move to the next state
-        if self.location_event:
-            self.location_event.set()
-
         if location_err:
-            raise location_err
+            forwardError(location_err)
+
+    def set_token_handler(self, getter: Callable, setter: Callable) -> None:
+        self.token_setter = setter
+        self.token_getter = getter
+        handler_err = self.go_function(
+            self.lib.SetTokenHandler, token_getter, token_setter
+        )
+
+        if handler_err:
+            forwardError(handler_err)
+
+    def cookie_reply(self, cookie: int, data: str) -> None:
+        """Reply with the given cookie and data"""
+        cookie_err = self.go_function(self.lib.CookieReply, cookie, data)
+        if cookie_err:
+            forwardError(cookie_err)
 
     def renew_session(self) -> None:
         """Renew the session. This invalidates the tokens and runs the necessary callbacks to log back in
 
         :raises WrappedError: An error by the Go library
         """
-        renew_err = self.go_function(self.lib.RenewSession)
+        renew_err = self.go_cookie_function(self.lib.RenewSession)
 
         if renew_err:
-            raise renew_err
+            forwardError(renew_err)
 
     def set_support_wireguard(self, support: bool) -> None:
         """Indicates whether or not the OS supports WireGuard connections.
@@ -527,139 +321,69 @@ def set_support_wireguard(self, support: bool) -> None:
         support_err = self.go_function(self.lib.SetSupportWireguard, support)
 
         if support_err:
-            raise support_err
-
-    def should_renew_button(self) -> bool:
-        """Whether or not the UI should show the renew button
-
-        :return: Whether or not the return button should be shown
-        :rtype: bool
-        """
-        return self.go_function(self.lib.ShouldRenewButton)
-
-    def in_fsm_state(self, state_id: State) -> bool:
-        """Check whether or not the FSM is in the provided state
-
-        :param state_id: State: The state to check for
-
-        :return: Whether or not the FSM is in the provided state
-        :rtype: bool
-        """
-        return self.go_function(self.lib.InFSMState, state_id)
-
-    def get_current_server(self) -> Optional[Server]:
-        """Get the current server
-
-        :return: The current servers if there is any
-        :rtype: Optional[List[Servers]]
-        """
-        server, server_err = self.go_function(
-            self.lib.GetCurrentServer,
-            decode_func=lambda lib, x: get_data_error(lib, x, get_transition_server),
-        )
-
-        if server_err:
-            raise server_err
-
-        return server
-
-    def get_saved_servers(self) -> Optional[List[Server]]:
-        """Get a list of saved servers
-
-        :return: The list of Servers if there are any
-        :rtype: Optional[List[Servers]]
-        """
-        servers, servers_err = self.go_function(
-            self.lib.GetSavedServers,
-            decode_func=lambda lib, x: get_data_error(lib, x, get_servers),
-        )
-
-        if servers_err:
-            raise servers_err
-
-        return servers
+            forwardError(support_err)
 
     def start_failover(
         self, gateway: str, wg_mtu: int, readrxbytes: ReadRxBytes
     ) -> bool:
-        dropped, dropped_err = self.go_function(
+        dropped, dropped_err = self.go_cookie_function(
             self.lib.StartFailover,
             gateway,
             wg_mtu,
             readrxbytes,
-            decode_func=lambda lib, x: get_data_error(lib, x, get_bool),
         )
         if dropped_err:
-            raise dropped_err
+            forwardError(dropped_err)
         return dropped
 
-    def cancel_failover(self):
-        cancel_err = self.go_function(self.lib.CancelFailover)
-        if cancel_err:
-            raise cancel_err
+    def cancel(self):
+        self.jar.cancel()
 
 
-eduvpn_objects: Dict[str, EduVPN] = {}
+global_object: Optional[EduVPN] = None
 
 
-@UpdateToken
-def token_callback(name: bytes, srv, tok):
-    name_decoded = name.decode()
-    if name_decoded not in eduvpn_objects:
+@TokenSetter
+def token_setter(server: ctypes.c_char_p, tokens: ctypes.c_char_p):
+    global global_object
+    if global_object is None:
+        return
+    if global_object.token_setter is None:
         return 0
-    obj = eduvpn_objects[name_decoded]
-    srv_conv = get_transition_server(obj.lib, srv)
-    tok_conv = get_tokens(obj.lib, tok)
-    obj.token_callback(
-        srv_conv, tok_conv
-    )
+    global_object.token_setter(server.decode(), tokens.decode())
+
+
+@TokenGetter
+def token_getter(server: ctypes.c_char_p, buf: ctypes.c_char_p, size: ctypes.c_size_t):
+    global global_object
+    if global_object is None:
+        return
+    if global_object.token_getter is None:
+        return
+    got = global_object.token_getter(server.decode())
+    if got is None:
+        return
+
+    outbuf = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char * size))
+    outbuf.contents.value = got.encode("utf-8")
 
 
 @VPNStateChange
-def state_callback(name: bytes, old_state: int, new_state: int, data: Any) -> int:
+def state_callback(old_state: int, new_state: int, data: str) -> int:
     """The internal callback that is passed to the Go library
 
-    :param name: bytes: The name of the client
     :param old_state: int: The old state
     :param new_state: int: The new state
-    :param data: Any: The data that still needs to be converted
+    :param data: str: The data that still needs to be converted by parsing the JSON
 
     :meta private:
     """
-    name_decoded = name.decode()
-    if name_decoded not in eduvpn_objects:
+    global global_object
+    if global_object is None:
         return 0
-    handled = eduvpn_objects[name_decoded].callback(
-        State(old_state), State(new_state), data
+    handled = global_object.event_handler.run(
+        State(old_state), State(new_state), data.decode("utf-8")
     )
     if handled:
         return 1
     return 0
-
-
-def add_as_global_object(eduvpn: EduVPN) -> bool:
-    """Add the provided parameter to the global objects lists so we can call the callback
-
-    :param eduvpn: EduVPN: The class to add
-
-    :meta private:
-
-    :return: Whether or not the object was added
-    :rtype: bool
-    """
-    global eduvpn_objects
-    if eduvpn.name not in eduvpn_objects:
-        eduvpn_objects[eduvpn.name] = eduvpn
-        return True
-    return False
-
-
-def remove_as_global_object(eduvpn: EduVPN) -> None:
-    """Remove the provided parameter from the global objects list
-
-    :param eduvpn: EduVPN: The class to remove
-
-    :meta private:
-    """
-    global eduvpn_objects
-    eduvpn_objects.pop(eduvpn.name, None)
diff --git a/wrappers/python/eduvpn_common/server.py b/wrappers/python/eduvpn_common/server.py
deleted file mode 100644
index cd3ad15a..00000000
--- a/wrappers/python/eduvpn_common/server.py
+++ /dev/null
@@ -1,441 +0,0 @@
-from ctypes import CDLL, POINTER, c_void_p, cast
-from datetime import datetime
-from typing import List, Optional, Type
-
-from eduvpn_common.types import (
-    cConfig,
-    cServer,
-    cServerLocations,
-    cServerProfiles,
-    cServers,
-    cToken,
-)
-
-
-class Profile:
-    """The class that represents a server profile.
-
-    :param: identifier: str: The identifier (id) of the profile
-    :param: display_name: str: The display name of the profile
-    :param: default_gateway: str: Whether or not this profile should have the default gateway set
-    """
-
-    def __init__(self, identifier: str, display_name: str, default_gateway: bool):
-        self.identifier = identifier
-        self.display_name = display_name
-        self.default_gateway = default_gateway
-
-    def __str__(self):
-        return self.display_name
-
-
-class Token:
-    """The class that represents oauth Tokens
-
-    :param: access: str: The access token
-    :param: refresh: str: The refresh token
-    :param: expired: int: The expire unix time
-    """
-
-    def __init__(self, access: str, refresh: str, expired: int):
-        self.access = access
-        self.refresh = refresh
-        self.expires = expired
-
-
-class Config:
-    """The class that represents an OpenVPN/WireGuard config
-
-    :param: config: str: The config string
-    :param: config_type: str: The type of config, openvpn/wireguard
-    :param: tokens: Optional[Token]: The tokens
-    """
-
-    def __init__(self, config: str, config_type: str, tokens: Optional[Token]):
-        self.config = config
-        self.config_type = config_type
-        self.tokens = tokens
-
-    def __str__(self):
-        return self.config
-
-
-class Profiles:
-    """The class that represents a list of profiles
-
-    :param: profiles: List[Profile]: A list of profiles
-    :param: current: int: The current profile index
-    """
-
-    def __init__(self, profiles: List[Profile], current: int):
-        self.profiles = profiles
-        self.current_index = current
-
-    @property
-    def current(self) -> Optional[Profile]:
-        """Get the current profile if there is any
-
-        :return: The profile if there is a current one (meaning the index is valid)
-        :rtype: Optional[Profile]
-        """
-        if self.current_index < len(self.profiles):
-            return self.profiles[self.current_index]
-        return None
-
-
-class Server:
-    """The class that represents a server. Use this for a custom server
-
-    :param: url: str: The base URL of the server. In case of secure internet (supertype) this is the organisation ID URL
-    :param: display_name: str: The display name of the server
-    :param: profiles: Optional[Profiles]: The profiles if there are any already obtained, defaults to None
-    :param: expire_time: int: The expiry time in a Unix timestamp, defaults to 0
-    """
-
-    def __init__(
-        self,
-        url: str,
-        display_name: str,
-        profiles: Optional[Profiles] = None,
-        expire_time: int = 0,
-    ):
-        self.url = url
-        self.display_name = display_name
-        self.profiles = profiles
-        self.expire_time = datetime.fromtimestamp(expire_time)
-
-    def __str__(self):
-        return self.display_name
-
-    @property
-    def category(self) -> str:
-        """Return the category of the server as a string
-
-        :return: The category string, "Custom Server"
-        :rtype: str
-        """
-        return "Custom Server"
-
-
-class InstituteServer(Server):
-    """The class that represents an Institute Access Server
-
-    :param: url: str: The base URL of the Institute Access Server
-    :param: display_name: str: The display name of the Institute Access Server
-    :param: support_contact: List[str]: The list of support contacts
-    :param: profiles: Optional[Profiles]: The profiles of the server if there are any
-    :param: expire_time: int: The expiry time in a Unix timestamp
-    """
-
-    def __init__(
-        self,
-        url: str,
-        display_name: str,
-        support_contact: List[str],
-        profiles: Optional[Profiles],
-        expire_time: int,
-    ):
-        super().__init__(url, display_name, profiles, expire_time)
-        self.support_contact = support_contact
-
-    @property
-    def category(self) -> str:
-        """Return the category of the institute server as a string
-
-        :return: The category string, "Institute Access Server"
-        :rtype: str
-        """
-        return "Institute Access Server"
-
-
-class SecureInternetServer(Server):
-    """The class that represents a Secure Internet Server
-
-    :param: org_id: str: The organization ID of the Secure Internet Server as returned by Discovery
-    :param: display_name: str: The display name of the server
-    :param: support_contact: List[str]: The list of support contacts of the server
-    :param: locations: List[str]: The list of secure internet locations
-    :param: profiles: Optional[Profiles]: The list of profiles if there are any
-    :param: expire_time: int: The expiry time in a Unix timestamp
-    :param: country_code: str: The country code of the server
-    """
-
-    def __init__(
-        self,
-        org_id: str,
-        display_name: str,
-        support_contact: List[str],
-        locations: List[str],
-        profiles: Optional[Profiles],
-        expire_time: int,
-        country_code: str,
-    ):
-        super().__init__(org_id, display_name, profiles, expire_time)
-        self.org_id = org_id
-        self.support_contact = support_contact
-        self.locations = locations
-        self.country_code = country_code
-
-    @property
-    def category(self) -> str:
-        """Return the category of the secure internet server as a string
-
-        :return: The category string, "Secure Internet Server"
-        :rtype: str
-        """
-        return "Secure Internet Server"
-
-
-def get_type_for_str(type_str: str) -> Type[Server]:
-    """Get the right class type for a certain string input
-
-    :param type_str: str: The string that represents the type of server, one of secure_internet, institute_access, custom_server
-
-    :return: The server, defaults to Institute Server if an invalid input is given
-    :rtype: Type[Server]
-    """
-    if type_str == "secure_internet":
-        return SecureInternetServer
-    if type_str == "custom_server":
-        return Server
-    return InstituteServer
-
-
-def get_locations_from_ptr(ptr) -> List[str]:
-    """Get the locations from the Go shared library and convert it to a Python usable structure
-
-    :param ptr: The pointer to the List[str] locations as returned by the Go library
-
-    :meta private:
-
-    :return: Locations if there are any
-    :rtype: List[str]
-    """
-    if not ptr:
-        return []
-    locations = cast(ptr, POINTER(cServerLocations)).contents
-    location_list = []
-    for i in range(locations.total_locations):
-        location_list.append(locations.locations[i].decode("utf-8"))
-    return location_list
-
-
-def get_profiles(ptr) -> Optional[Profiles]:
-    """Get the profiles from the Go shared library and convert it to a Python usable structure
-
-    :param ptr: The pointer to the Profiles as returned by the Go library
-
-    :meta private:
-
-    :return: Profiles if there are any
-    :rtype: Optional[Profiles]
-    """
-    if not ptr:
-        return None
-    profiles = []
-    _profiles = ptr.contents
-    current_profile = _profiles.current
-    if not _profiles.profiles:
-        return None
-    for i in range(_profiles.total_profiles):
-        if not _profiles.profiles[i]:
-            continue
-        profile = _profiles.profiles[i].contents
-        profiles.append(
-            Profile(
-                profile.identifier.decode("utf-8"),
-                profile.display_name.decode("utf-8"),
-                profile.default_gateway == 1,
-            )
-        )
-    return Profiles(profiles, current_profile)
-
-
-def get_server(ptr, _type: Optional[str] = None) -> Optional[Server]:
-    """Get the server from the Go shared library and convert it to a Python usable structure
-
-    :param ptr: The pointer as returned by the Go library
-    :param _type:  (Default value = None): The optional parameter that represents whether or not the type is enforced to the input. If None it is automatically determined
-
-    :meta private:
-
-    :return: Server if there is any
-    :rtype: Optional[Server]
-    """
-    if not ptr:
-        return None
-
-    current_server = ptr.contents
-    if _type is None:
-        _type = get_type_for_str(current_server.server_type.decode("utf-8"))
-
-    identifier = current_server.identifier.decode("utf-8")
-    display_name = current_server.display_name.decode("utf-8")
-
-    if _type is not Server:
-        support_contact = []
-        for i in range(current_server.total_support_contact):
-            support_contact.append(current_server.support_contact[i].decode("utf-8"))
-    locations = get_locations_from_ptr(current_server.locations)
-    profiles = get_profiles(current_server.profiles)
-    if profiles is None:
-        profiles = Profiles([], 0)
-    if _type is SecureInternetServer:
-        return SecureInternetServer(
-            identifier,
-            display_name,
-            support_contact,
-            locations,
-            profiles,
-            current_server.expire_time,
-            current_server.country_code.decode("utf-8"),
-        )
-    if _type is InstituteServer:
-        return InstituteServer(
-            identifier,
-            display_name,
-            support_contact,
-            profiles,
-            current_server.expire_time,
-        )
-    return Server(identifier, display_name, profiles, current_server.expire_time)
-
-
-def get_transition_server(lib: CDLL, ptr: c_void_p) -> Optional[Server]:
-    """Get a server from a transition event
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The Go's returned C pointer that represents the Server
-
-    :meta private:
-
-    :return: The server if there is any
-    :rtype: Optional[Server]
-    """
-    if ptr:
-        server = get_server(cast(ptr, POINTER(cServer)))
-        lib.FreeServer(ptr)
-        return server
-    return None
-
-
-def get_transition_profiles(lib: CDLL, ptr: c_void_p) -> Optional[Profiles]:
-    """Get profiles from a transition event
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The Go's returned C pointer that represents the profiles
-
-    :meta private:
-
-    :return: The profiles if there is any
-    :rtype: Optional[Profiles]
-    """
-    if ptr:
-        profiles = get_profiles(cast(ptr, POINTER(cServerProfiles)))
-        lib.FreeProfiles(ptr)
-        return profiles
-    return None
-
-
-def get_servers(lib: CDLL, ptr: c_void_p) -> Optional[List[Server]]:
-    """Get servers from the Go library as a C structure and return a Python usable structure
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The C pointer to the servers structure
-
-    :meta private:
-
-    :return: The list of Servers if there is any
-    :rtype: Optional[List[Server]]
-    """
-    if ptr:
-        returned = []
-        servers = cast(ptr, POINTER(cServers)).contents
-        if servers.custom_servers:
-            for i in range(servers.total_custom):
-                current = get_server(servers.custom_servers[i], Server)
-                if current is None:
-                    continue
-                returned.append(current)
-
-        if servers.institute_servers:
-            for i in range(servers.total_institute):
-                current = get_server(servers.institute_servers[i], InstituteServer)
-                if current is None:
-                    continue
-                returned.append(current)
-
-        if servers.secure_internet:
-            current = get_server(servers.secure_internet, SecureInternetServer)
-            if current is not None:
-                returned.append(current)
-        lib.FreeServers(ptr)
-        return returned
-    return None
-
-
-def get_locations(lib: CDLL, ptr: c_void_p) -> Optional[List[str]]:
-    """Get locations from the Go library as a C structure and return a Python usable structure
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The C pointer to the locations structure
-
-    :meta private:
-
-    :return: The list of servers if there are any
-    :rtype: Optional[List[str]]
-    """
-    if ptr:
-        location_list = get_locations_from_ptr(ptr)
-        lib.FreeSecureLocations(ptr)
-        return location_list
-    return None
-
-
-def get_tokens(lib: CDLL, ptr: c_void_p) -> Optional[Token]:
-    if ptr:
-        toks = cast(ptr, POINTER(cToken)).contents
-        access = toks.access.decode("utf-8")
-        refresh = toks.refresh.decode("utf-8")
-        expired = toks.expired
-        lib.FreeTokens(ptr)
-        return Token(
-            access, refresh, expired
-        )
-    return None
-
-
-def get_config(lib: CDLL, ptr: c_void_p) -> Optional[Config]:
-    """Get the config from the Go library as a C structure and return a Python usable structure
-
-    :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The C pointer to the confg structure
-
-    :meta private:
-
-    :return: The configuration if there is any
-    :rtype: Optional[Config]
-    """
-    if ptr:
-        config = cast(ptr, POINTER(cConfig)).contents
-        cfg = config.config.decode("utf-8")
-        cfg_type = config.config_type.decode("utf-8")
-        tokens = None
-        if config.token:
-            token_struct = config.token.contents
-            tokens = Token(
-                token_struct.access.decode("utf-8"),
-                token_struct.refresh.decode("utf-8"),
-                token_struct.expired,
-            )
-
-        config_class = Config(cfg, cfg_type, tokens)
-        lib.FreeConfig(ptr)
-        return config_class
-    return None
-
-
-def encode_tokens(arg: Optional[Token]) -> cToken:
-    if arg is None:
-        return cToken("".encode("utf-8"), "".encode("utf-8"), 0)
-    return cToken(arg.access.encode("utf-8"), arg.refresh.encode("utf-8"), arg.expires)
diff --git a/wrappers/python/eduvpn_common/state.py b/wrappers/python/eduvpn_common/state.py
index 21573f76..bb6642b5 100644
--- a/wrappers/python/eduvpn_common/state.py
+++ b/wrappers/python/eduvpn_common/state.py
@@ -3,30 +3,30 @@
 
 class StateType(IntEnum):
     """
-    The State Type enum. Wait types are mostly used for internal code
+    The State Type enum.
     """
 
     ENTER = 1
     LEAVE = 2
-    WAIT = 3
 
 
-class State(IntEnum):
-    """
-    The State enum. Each state here also exists in the Go library
-    """
+StateEnum = IntEnum
+
 
-    DEREGISTERED = 0
-    NO_SERVER = 1
+class State(StateEnum):
+    # Go states
+    INITIAL = 0
+    MAIN = 1
     ASK_LOCATION = 2
-    SEARCH_SERVER = 3
+    CHOSEN_LOCATION = 3
     LOADING_SERVER = 4
     CHOSEN_SERVER = 5
     OAUTH_STARTED = 6
     AUTHORIZED = 7
     REQUEST_CONFIG = 8
     ASK_PROFILE = 9
-    DISCONNECTED = 10
-    DISCONNECTING = 11
+    CHOSEN_PROFILE = 10
+    GOT_CONFIG = 11
     CONNECTING = 12
-    CONNECTED = 13
+    DISCONNECTING = 13
+    CONNECTED = 14
diff --git a/wrappers/python/eduvpn_common/types.py b/wrappers/python/eduvpn_common/types.py
index 32a7a004..f1be42c3 100644
--- a/wrappers/python/eduvpn_common/types.py
+++ b/wrappers/python/eduvpn_common/types.py
@@ -3,199 +3,40 @@
     CFUNCTYPE,
     POINTER,
     Structure,
+    c_char,
     c_char_p,
     c_int,
-    c_size_t,
     c_ulonglong,
+    c_size_t,
     c_void_p,
     cast,
-    pointer,
 )
-from typing import Any, Callable, Iterator, List, Optional, Tuple
-
-from eduvpn_common.error import WrappedError
-
-
-class cToken(Structure):
-    """The C type that represents the Token as forwarded to the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("access", c_char_p),
-        ("refresh", c_char_p),
-        ("expired", c_ulonglong),
-    ]
-
-
-class cConfig(Structure):
-    """The C type that represents the data that gets by the Go library returned when a config is obtained
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("config", c_char_p),
-        ("config_type", c_char_p),
-        ("token", POINTER(cToken)),
-    ]
-
-
-class cError(Structure):
-    """The C type that represents the Error as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("traceback", c_char_p),
-        ("cause", c_char_p),
-    ]
-
-
-class cServerLocations(Structure):
-    """The C type that represents the Server Locations as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [("locations", POINTER(c_char_p)), ("total_locations", c_size_t)]
-
-
-class cDiscoveryOrganization(Structure):
-    """The C type that represents a Discovery Organization as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("display_name", c_char_p),
-        ("org_id", c_char_p),
-        ("secure_internet_home", c_char_p),
-        ("keyword_list", c_char_p),
-    ]
-
-
-class cDiscoveryOrganizations(Structure):
-    """The C type that represents Discovery Organizations as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("version", c_ulonglong),
-        ("organizations", POINTER(POINTER(cDiscoveryOrganization))),
-        ("total_organizations", c_size_t),
-    ]
-
-
-class cDiscoveryServer(Structure):
-    """The C type that represents a Discovery Server as returned by the Go library
-
-    :meta private:
-    """
+from typing import Any, Iterator, List, Tuple
 
-    _fields_ = [
-        ("authentication_url_template", c_char_p),
-        ("base_url", c_char_p),
-        ("country_code", c_char_p),
-        ("display_name", c_char_p),
-        ("keyword_list", c_char_p),
-        ("public_key_list", POINTER(c_char_p)),
-        ("total_public_keys", c_size_t),
-        ("server_type", c_char_p),
-        ("support_contact", POINTER(c_char_p)),
-        ("total_support_contact", c_size_t),
-    ]
 
-
-class cDiscoveryServers(Structure):
-    """The C type that represents Discovery Servers as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("version", c_ulonglong),
-        ("servers", POINTER(POINTER(cDiscoveryServer))),
-        ("total_servers", c_size_t),
-    ]
-
-
-class cServerProfile(Structure):
-    """The C type that represents a Server Profile as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("identifier", c_char_p),
-        ("display_name", c_char_p),
-        ("default_gateway", c_int),
-    ]
-
-
-class cServerProfiles(Structure):
-    """The C type that represents Server Profiles as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("current", c_int),
-        ("profiles", POINTER(POINTER(cServerProfile))),
-        ("total_profiles", c_size_t),
-    ]
-
-
-class cServer(Structure):
-    """The C type that represents a Server as returned by the Go library
-
-    :meta private:
-    """
-
-    _fields_ = [
-        ("identifier", c_char_p),
-        ("display_name", c_char_p),
-        ("server_type", c_char_p),
-        ("country_code", c_char_p),
-        ("support_contact", POINTER(c_char_p)),
-        ("total_support_contact", c_size_t),
-        ("locations", POINTER(cServerLocations)),
-        ("profiles", POINTER(cServerProfiles)),
-        ("expire_time", c_ulonglong),
-    ]
-
-
-class cServers(Structure):
-    """The C type that represents Servers as returned by the Go library
+class DataError(Structure):
+    """The C type that represents a tuple of data and error (both strings) as returned by the Go library
 
     :meta private:
     """
 
-    _fields_ = [
-        ("custom_servers", POINTER(POINTER(cServer))),
-        ("total_custom", c_size_t),
-        ("institute_servers", POINTER(POINTER(cServer))),
-        ("total_institute", c_size_t),
-        ("secure_internet", POINTER(cServer)),
-    ]
+    _fields_ = [("data", c_void_p), ("error", c_void_p)]
 
 
-class DataError(Structure):
-    """The C type that represents a tuple of data and error as returned by the Go library
+class BoolError(Structure):
+    """The C type that represents a tuple of boolean and error as returned by the Go library
 
     :meta private:
     """
 
-    _fields_ = [("data", c_void_p), ("error", c_void_p)]
+    _fields_ = [("boolean", c_int), ("error", c_void_p)]
 
 
 # The type for a Go state change callback
-VPNStateChange = CFUNCTYPE(c_int, c_char_p, c_int, c_int, c_void_p)
+VPNStateChange = CFUNCTYPE(c_int, c_int, c_int, c_char_p)
 ReadRxBytes = CFUNCTYPE(c_ulonglong)
-UpdateToken = CFUNCTYPE(None, c_char_p, c_void_p, c_void_p)
+TokenGetter = CFUNCTYPE(c_void_p, c_char_p, POINTER(c_char), c_size_t)
+TokenSetter = CFUNCTYPE(c_void_p, c_char_p, c_char_p)
 
 
 def encode_args(args: List[Any], types: List[Any]) -> Iterator[Any]:
@@ -230,9 +71,9 @@ def decode_res(res: Any) -> Any:
     :rtype: Any
     """
     decode_map = {
-        c_int: get_bool,
-        c_void_p: get_error,
+        c_void_p: get_ptr_string,
         DataError: get_data_error,
+        BoolError: get_bool_error,
     }
     return decode_map.get(res, lambda lib, x: x)
 
@@ -257,63 +98,36 @@ def get_ptr_string(lib: CDLL, ptr: c_void_p) -> str:
     return ""
 
 
-def get_ptr_list_strings(lib: CDLL, strings: pointer, total_strings: int) -> List[str]:
-    """Convert a list of C strings to a Python usable list of strings
-    This list is not freed here but is later freed in the Go library for convenience
-
-    :param lib: CDLL: The Go shared library
-    :param strings: pointer: The C pointer to the strings list
-    :param total_strings: int: The total strings in the list
-
-    :meta private:
-
-    :return: The list of strings converted to Python
-    :rtype: List[str]
-    """
-    if strings:
-        strings_list = []
-        for i in range(total_strings):
-            strings_list.append(strings[i].decode("utf-8"))
-        return strings_list
-    return []
-
-
-def get_error(lib: CDLL, ptr: c_void_p) -> Optional[WrappedError]:
-    """Convert a C error structure to a Python usable error structure
+def get_data_error(lib: CDLL, data_error: DataError) -> Tuple[str, str]:
+    """Convert a C data+error structure to a Python usable data+error structure
 
     :param lib: CDLL: The Go shared library
-    :param ptr: c_void_p: The pointer to the C error struct
+    :param data_error: DataError: The data error C structure
 
     :meta private:
 
-    :return: The error if there is one
-    :rtype: Optional[WrappedError]
+    :return: The data and error
+    :rtype: Tuple[str, str]
     """
-    if not ptr:
-        return None
-    err = cast(ptr, POINTER(cError)).contents
-    wrapped = WrappedError(err.traceback.decode("utf-8"), err.cause.decode("utf-8"))
-    lib.FreeError(ptr)
-    return wrapped
+    data = get_ptr_string(lib, data_error.data)
+    error = get_ptr_string(lib, data_error.error)
+    return data, error
 
 
-def get_data_error(
-    lib: CDLL, data_error: DataError, data_conv: Callable = get_ptr_string
-) -> Tuple[Any, Optional[WrappedError]]:
-    """Convert a C data+error structure to a Python usable data+error structure
+def get_bool_error(lib: CDLL, bool_error: BoolError) -> Tuple[bool, str]:
+    """Convert a C boolean (c int)+error structure to a Python usable boolean+error structure
 
     :param lib: CDLL: The Go shared library
-    :param data_error: DataError: The data error C structure
-    :param data_conv: Callable:  (Default value = get_ptr_string): The function that converts the data part
+    :param bool_error: BoolError: The boolean and error C structure
 
     :meta private:
 
-    :return: The data and optional error
-    :rtype: Tuple[Any, Optional[WrappedError]]
+    :return: The bool and error
+    :rtype: Tuple[bool, str]
     """
-    data = data_conv(lib, data_error.data)
-    error = get_error(lib, data_error.error)
-    return data, error
+    boolean = get_bool(lib, bool_error.boolean)
+    error = get_ptr_string(lib, bool_error.error)
+    return boolean, error
 
 
 def get_bool(lib: CDLL, boolInt: c_int) -> bool:
@@ -327,4 +141,4 @@ def get_bool(lib: CDLL, boolInt: c_int) -> bool:
     :return: The boolean converted to Python
     :rtype: bool
     """
-    return boolInt == 1
+    return int(boolInt) != 0
diff --git a/wrappers/python/example/main.py b/wrappers/python/example/main.py
index 6df3b404..ce5359f0 100644
--- a/wrappers/python/example/main.py
+++ b/wrappers/python/example/main.py
@@ -98,7 +98,7 @@ def do_secure_internet(edu: eduvpn.EduVPN) -> Optional[Config]:
 
 # The main entry point
 if __name__ == "__main__":
-    _eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "1.1.2-cli-py", "configs", "en")
+    _eduvpn = eduvpn.EduVPN("org.eduvpn.app.linux", "2.0.0-cli-py", "configs", "en")
     setup_callbacks(_eduvpn)
 
     # Register with the eduVPN-common library
diff --git a/wrappers/python/setup.py b/wrappers/python/setup.py
index 1bc565ad..f19be57b 100755
--- a/wrappers/python/setup.py
+++ b/wrappers/python/setup.py
@@ -10,7 +10,8 @@
 from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
 
 _libname = "eduvpn_common"
-__version__ = "1.1.2"
+__version__ = "2.0.0"
+
 
 def getlibpath(plat_name: str) -> typing.Union[str, None]:
     """Get library path for plat_name relative to exports/lib/ folder."""
@@ -65,8 +66,15 @@ def getlibpath(plat_name: str) -> typing.Union[str, None]:
 
     processed_os = os_map[plat_os]
     return (
-        processed_os + "/" + arch_map[plat_arch] + "/" + 
-        lib_prefixes[processed_os] + _libname + "-" + __version__ + lib_suffixes[processed_os]
+        processed_os
+        + "/"
+        + arch_map[plat_arch]
+        + "/"
+        + lib_prefixes[processed_os]
+        + _libname
+        + "-"
+        + __version__
+        + lib_suffixes[processed_os]
     )
 
 
@@ -91,7 +99,7 @@ def run(self):
             print("Unknown platform:", self.plat_name)
             sys.exit(1)
 
-        print("Building wheel for platform:",self.plat_name)
+        print("Building wheel for platform:", self.plat_name)
 
         # setuptools will only use paths inside the package for package_data, so we copy the library
         p = "eduvpn_common/lib"
@@ -100,7 +108,7 @@ def run(self):
         os.makedirs(p)
         shutil.copyfile(self.exports_lib_path + "/" + libpath, p+"/"+libpath.split("/")[-1])
         _bdist_wheel.run(self)
-        shutil.rmtree("eduvpn_common/lib/")
+        shutil.rmtree(p)
 
 
 setup(


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9930eb7e..0b48c272 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -29,8 +29,8 @@ jobs:
       - uses: actions/checkout@v3
       - name: Setup server
         run: |
-          git clone https://github.com/eduvpn/documentation
-          cd documentation
+          git clone https://codeberg.org/eduvpn/deploy
+          cd deploy
           sudo cp resources/repo+v3@eduvpn.org.asc /etc/apt/trusted.gpg.d/repo+v3@eduvpn.org.asc
           echo "deb https://repo.eduvpn.org/v3/deb jammy main" | sudo tee /etc/apt/sources.list.d/eduVPN_v3.list
           sudo apt-get -y update
diff --git a/ci/docker/gotest.docker b/ci/docker/gotest.docker
index ec98d0c2..b2e01130 100644
--- a/ci/docker/gotest.docker
+++ b/ci/docker/gotest.docker
@@ -10,10 +10,10 @@ RUN apt-get -y install openjdk-11-jre xvfb python3-selenium firefox-esr python3-
 
 # Install go from go.dev
 # Get
-RUN wget https://go.dev/dl/go1.19.4.linux-amd64.tar.gz -O go.tar.gz
+RUN wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz -O go.tar.gz
 
 # Verify hash
-RUN echo "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8  go.tar.gz" | sha256sum --check
+RUN echo "d0398903a16ba2232b389fb31032ddf57cac34efda306a0eebac34f0965a0742  go.tar.gz" | sha256sum --check
 
 # Untar
 RUN tar -C /usr/local -xzf go.tar.gz
diff --git a/client/client_test.go b/client/client_test.go
index b39b289b..7d070f37 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -1,9 +1,8 @@
 package client
 
 import (
+	"context"
 	"fmt"
-	"net/http"
-	"net/url"
 	"os"
 	"os/exec"
 	"strconv"
@@ -12,8 +11,9 @@ import (
 	"time"
 
 	httpw "github.com/eduvpn/eduvpn-common/internal/http"
-	"github.com/eduvpn/eduvpn-common/internal/oauth"
-	"github.com/go-errors/errors"
+	"github.com/eduvpn/eduvpn-common/types/cookie"
+	"github.com/eduvpn/eduvpn-common/types/protocol"
+	srvtypes "github.com/eduvpn/eduvpn-common/types/server"
 )
 
 func getServerURI(t *testing.T) string {
@@ -21,7 +21,7 @@ func getServerURI(t *testing.T) string {
 	if serverURI == "" {
 		t.Skip("Skipping server test as no SERVER_URI env var has been passed")
 	}
-	serverURI, parseErr := httpw.EnsureValidURL(serverURI)
+	serverURI, parseErr := httpw.EnsureValidURL(serverURI, true)
 	if parseErr != nil {
 		t.Skip("Skipping server test as the server uri is not valid")
 	}
@@ -40,13 +40,13 @@ func runCommand(errBuffer *strings.Builder, name string, args ...string) error {
 	return cmd.Wait()
 }
 
-func loginOAuthSelenium(url string, state *Client) {
+func loginOAuthSelenium(ck *cookie.Cookie, url string) {
 	// We could use the go selenium library
 	// But it does not support the latest selenium v4 just yet
 	var errBuffer strings.Builder
 	err := runCommand(&errBuffer, "python3", "../selenium_eduvpn.py", url)
 	if err != nil {
-		_ = state.CancelOAuth()
+		_ = ck.Cancel()
 		panic(fmt.Sprintf(
 			"Login OAuth with selenium script failed with error %v and stderr %s",
 			err,
@@ -57,10 +57,10 @@ func loginOAuthSelenium(url string, state *Client) {
 
 func stateCallback(
 	t *testing.T,
+	ck *cookie.Cookie,
 	_ FSMStateID,
 	newState FSMStateID,
 	data interface{},
-	state *Client,
 ) {
 	if newState == StateOAuthStarted {
 		url, ok := data.(string)
@@ -68,160 +68,43 @@ func stateCallback(
 		if !ok {
 			t.Fatalf("data is not a string for OAuth URL")
 		}
-		loginOAuthSelenium(url, state)
+		loginOAuthSelenium(ck, url)
 	}
 }
 
 func TestServer(t *testing.T) {
 	serverURI := getServerURI(t)
-	state := &Client{}
-
-	registerErr := state.Register(
+	ck := cookie.NewWithContext(context.Background())
+	defer ck.Cancel() //nolint:errcheck
+	dir := t.TempDir()
+	state, err := New(
 		"org.letsconnect-vpn.app.linux",
 		"0.1.0-test",
-		"configstest",
-		"en",
+		dir,
 		func(old FSMStateID, new FSMStateID, data interface{}) bool {
-			stateCallback(t, old, new, data, state)
+			stateCallback(t, &ck, old, new, data)
 			return true
 		},
 		false,
 	)
-	if registerErr != nil {
-		t.Fatalf("Register error: %v", registerErr)
+	if err != nil {
+		t.Fatalf("Creating client error: %v", err)
+	}
+	err = state.Register()
+	if err != nil {
+		t.Fatalf("Registering error: %v", err)
 	}
 
-	_, addErr := state.AddCustomServer(serverURI)
+	addErr := state.AddServer(&ck, serverURI, srvtypes.TypeCustom, false)
 	if addErr != nil {
 		t.Fatalf("Add error: %v", addErr)
 	}
-	_, configErr := state.GetConfigCustomServer(serverURI, false, oauth.Token{})
+	_, configErr := state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, false)
 	if configErr != nil {
 		t.Fatalf("Connect error: %v", configErr)
 	}
 }
 
-func testConnectOAuthParameter(
-	t *testing.T,
-	parameters httpw.URLParameters,
-	errPrefix string,
-) {
-	serverURI := getServerURI(t)
-	state := &Client{}
-	configDirectory := "test_oauth_parameters"
-
-	registerErr := state.Register(
-		"org.letsconnect-vpn.app.linux",
-		"0.1.0-test",
-		configDirectory,
-		"en",
-		func(oldState FSMStateID, newState FSMStateID, data interface{}) bool {
-			if newState == StateOAuthStarted {
-				server, serverErr := state.Servers.GetCustomServer(serverURI)
-				if serverErr != nil {
-					t.Fatalf("No server with error: %v", serverErr)
-				}
-				port, portErr := server.OAuth().ListenerPort()
-				if portErr != nil {
-					_ = state.CancelOAuth()
-					t.Fatalf("No port with error: %v", portErr)
-				}
-				baseURL := fmt.Sprintf("http://127.0.0.1:%d/callback", port)
-				p, err := url.Parse(baseURL)
-				if err != nil {
-					_ = state.CancelOAuth()
-					t.Fatalf("Failed to parse URL with error: %v", err)
-				}
-				url, err := httpw.ConstructURL(p, parameters)
-				if err != nil {
-					_ = state.CancelOAuth()
-					t.Fatalf(
-						"Error: Constructing url %s with parameters %s",
-						baseURL,
-						fmt.Sprint(parameters),
-					)
-				}
-				go func() {
-					_, getErr := http.Get(url)
-					if getErr != nil {
-						_ = state.CancelOAuth()
-						t.Logf("HTTP GET error: %v", getErr)
-					}
-				}()
-			}
-			return true
-		},
-		false,
-	)
-	if registerErr != nil {
-		t.Fatalf("Register error: %v", registerErr)
-	}
-
-	_, err := state.AddCustomServer(serverURI)
-
-	if errPrefix == "" {
-		if err != nil {
-			t.Fatalf("unexpected error %v", err)
-		}
-		return
-	}
-
-	if err == nil {
-		t.Fatalf("expected error with prefix '%s' but got nil", errPrefix)
-	}
-
-	err1, ok := err.(*errors.Error)
-	// We ensure the error is of a wrappedErrorMessage
-	if !ok {
-		t.Fatalf("error %T = %v, wantErr %T", err, err, &errors.Error{})
-	}
-
-	msg := err1.Error()
-	if err1.Err != nil {
-		msg = err1.Err.Error()
-	}
-
-	// Then we check if the cause is correct
-	if !strings.HasPrefix(msg, errPrefix) {
-		t.Fatalf("expected error with prefix '%s' but got '%s'", errPrefix, msg)
-	}
-}
-
-func TestConnectOAuthParameters(t *testing.T) {
-	const (
-		callbackParameterErrorPrefix  = "failed retrieving parameter '"
-		callbackStateMatchErrorPrefix = "failed matching state"
-		callbackISSMatchErrorPrefix   = "failed matching ISS"
-	)
-
-	serverURI := getServerURI(t)
-	// serverURI already ends with a / due to using the util EnsureValidURL function
-	iss := serverURI
-	tests := []struct {
-		errPrefix  string
-		parameters httpw.URLParameters
-	}{
-		// missing state and code
-		{callbackParameterErrorPrefix, httpw.URLParameters{"iss": iss}},
-		// missing state
-		{callbackParameterErrorPrefix, httpw.URLParameters{"iss": iss, "code": "42"}},
-		// invalid state
-		{
-			callbackStateMatchErrorPrefix,
-			httpw.URLParameters{"iss": iss, "code": "42", "state": "21"},
-		},
-		// invalid iss
-		{
-			callbackISSMatchErrorPrefix,
-			httpw.URLParameters{"iss": "37", "code": "42", "state": "21"},
-		},
-	}
-
-	for _, test := range tests {
-		testConnectOAuthParameter(t, test.parameters, test.errPrefix)
-	}
-}
-
 func TestTokenExpired(t *testing.T) {
 	serverURI := getServerURI(t)
 	expiredTTL := os.Getenv("OAUTH_EXPIRED_TTL")
@@ -239,42 +122,46 @@ func TestTokenExpired(t *testing.T) {
 	}
 
 	// Get a vpn state
-	state := &Client{}
-
-	registerErr := state.Register(
+	ck := cookie.NewWithContext(context.Background())
+	defer ck.Cancel() //nolint:errcheck
+	dir := t.TempDir()
+	state, err := New(
 		"org.letsconnect-vpn.app.linux",
 		"0.1.0-test",
-		"configsexpired",
-		"en",
+		dir,
 		func(old FSMStateID, new FSMStateID, data interface{}) bool {
-			stateCallback(t, old, new, data, state)
+			stateCallback(t, &ck, old, new, data)
 			return true
 		},
 		false,
 	)
-	if registerErr != nil {
-		t.Fatalf("Register error: %v", registerErr)
+	if err != nil {
+		t.Fatalf("Creating client error: %v", err)
+	}
+	err = state.Register()
+	if err != nil {
+		t.Fatalf("Registering error: %v", err)
 	}
 
-	_, addErr := state.AddCustomServer(serverURI)
+	addErr := state.AddServer(&ck, serverURI, srvtypes.TypeCustom, false)
 	if addErr != nil {
 		t.Fatalf("Add error: %v", addErr)
 	}
 
-	_, configErr := state.GetConfigCustomServer(serverURI, false, oauth.Token{})
+	_, configErr := state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, false)
 
 	if configErr != nil {
 		t.Fatalf("Connect error before expired: %v", configErr)
 	}
 
-	currentServer, serverErr := state.Servers.GetCurrentServer()
+	currentServer, serverErr := state.Servers.Current()
 	if serverErr != nil {
 		t.Fatalf("No server found")
 	}
 
 	serverOAuth := currentServer.OAuth()
 
-	accessToken, accessTokenErr := serverOAuth.AccessToken()
+	accessToken, accessTokenErr := serverOAuth.AccessToken(ck.Context())
 	if accessTokenErr != nil {
 		t.Fatalf("Failed to get token: %v", accessTokenErr)
 	}
@@ -282,14 +169,14 @@ func TestTokenExpired(t *testing.T) {
 	// Wait for TTL so that the tokens expire
 	time.Sleep(time.Duration(expiredInt) * time.Second)
 
-	_, configErr = state.GetConfigCustomServer(serverURI, false, oauth.Token{})
+	_, configErr = state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, false)
 
 	if configErr != nil {
 		t.Fatalf("Connect error after expiry: %v", configErr)
 	}
 
 	// Check if tokens have changed
-	accessTokenAfter, accessTokenAfterErr := serverOAuth.AccessToken()
+	accessTokenAfter, accessTokenAfterErr := serverOAuth.AccessToken(ck.Context())
 	if accessTokenAfterErr != nil {
 		t.Fatalf("Failed to get token: %v", accessTokenAfterErr)
 	}
@@ -301,36 +188,40 @@ func TestTokenExpired(t *testing.T) {
 
 // Test if an invalid profile will be corrected.
 func TestInvalidProfileCorrected(t *testing.T) {
+	dir := t.TempDir()
 	serverURI := getServerURI(t)
-	state := &Client{}
-
-	registerErr := state.Register(
+	ck := cookie.NewWithContext(context.Background())
+	defer ck.Cancel() //nolint:errcheck
+	state, err := New(
 		"org.letsconnect-vpn.app.linux",
 		"0.1.0-test",
-		"configscancelprofile",
-		"en",
+		dir,
 		func(old FSMStateID, new FSMStateID, data interface{}) bool {
-			stateCallback(t, old, new, data, state)
+			stateCallback(t, &ck, old, new, data)
 			return true
 		},
 		false,
 	)
-	if registerErr != nil {
-		t.Fatalf("Register error: %v", registerErr)
+	if err != nil {
+		t.Fatalf("Creating client error: %v", err)
+	}
+	err = state.Register()
+	if err != nil {
+		t.Fatalf("Registering error: %v", err)
 	}
 
-	_, addErr := state.AddCustomServer(serverURI)
+	addErr := state.AddServer(&ck, serverURI, srvtypes.TypeCustom, false)
 	if addErr != nil {
 		t.Fatalf("Add error: %v", addErr)
 	}
 
-	_, configErr := state.GetConfigCustomServer(serverURI, false, oauth.Token{})
+	_, configErr := state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, false)
 
 	if configErr != nil {
 		t.Fatalf("First connect error: %v", configErr)
 	}
 
-	currentServer, serverErr := state.Servers.GetCurrentServer()
+	currentServer, serverErr := state.Servers.Current()
 	if serverErr != nil {
 		t.Fatalf("No server found")
 	}
@@ -343,8 +234,7 @@ func TestInvalidProfileCorrected(t *testing.T) {
 	previousProfile := base.Profiles.Current
 	base.Profiles.Current = "IDONOTEXIST"
 
-	_, configErr = state.GetConfigCustomServer(serverURI, false, oauth.Token{})
-
+	_, configErr = state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, false)
 	if configErr != nil {
 		t.Fatalf("Second connect error: %v", configErr)
 	}
@@ -358,36 +248,100 @@ func TestInvalidProfileCorrected(t *testing.T) {
 	}
 }
 
+// TestConfigStartup tests if the 'startup' variable for getconfig behaves as expected
+func TestConfigStartup(t *testing.T) {
+	serverURI := getServerURI(t)
+	ck := cookie.NewWithContext(context.Background())
+	defer ck.Cancel() //nolint:errcheck
+	dir := t.TempDir()
+	state, err := New(
+		"org.letsconnect-vpn.app.linux",
+		"0.1.0-test",
+		dir,
+		func(old FSMStateID, new FSMStateID, data interface{}) bool {
+			stateCallback(t, &ck, old, new, data)
+			return true
+		},
+		false,
+	)
+	if err != nil {
+		t.Fatalf("Creating client error: %v", err)
+	}
+	err = state.Register()
+	if err != nil {
+		t.Fatalf("Failed to register with error: %v", err)
+	}
+	// we set true as last argument here such that no callbacks are ran
+	err = state.AddServer(&ck, serverURI, srvtypes.TypeCustom, true)
+	if err != nil {
+		t.Fatalf("Failed to add server for trying config startup: %v", err)
+	}
+	testTrue := func() {
+		// Now get config with setting startup to true
+		startup := true
+		_, err := state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, startup)
+		// this should fail as we have not authorized yet/chosen profile and startup=true does not do these callbacks
+		if err == nil {
+			t.Fatal("Got no error after getting config with startup true")
+		}
+		if !strings.HasPrefix(err.Error(), "The client tried to autoconnect to the VPN server") {
+			t.Fatalf("GetConfig error for GetConfig with startup=true is not what we expect: %v", err)
+		}
+	}
+	testFalse := func() {
+		startup := false
+		// This should succeed
+		_, err := state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, startup)
+		// this should fail as we have not authorized yet/chosen profile
+		if err != nil {
+			t.Fatalf("Got error after getting config with startup=false: %v", err)
+		}
+	}
+	testTrue()
+	testFalse()
+
+	// set invalid authorization and test again
+	// we cannot test by setting invalid profile because the server only has 1 profile
+	// TODO: support multiple profiles in the test server
+	state.Servers.CustomServers.Map[serverURI].OAuth().SetTokenRenew()
+	testTrue()
+	testFalse()
+}
+
 // Test if prefer tcp is handled correctly by checking the returned config and config type.
 func TestPreferTCP(t *testing.T) {
 	serverURI := getServerURI(t)
-	state := &Client{}
-
-	registerErr := state.Register(
+	ck := cookie.NewWithContext(context.Background())
+	defer ck.Cancel() //nolint:errcheck
+	dir := t.TempDir()
+	state, err := New(
 		"org.letsconnect-vpn.app.linux",
 		"0.1.0-test",
-		"configsprefertcp",
-		"en",
+		dir,
 		func(old FSMStateID, new FSMStateID, data interface{}) bool {
-			stateCallback(t, old, new, data, state)
+			stateCallback(t, &ck, old, new, data)
 			return true
 		},
 		false,
 	)
-	if registerErr != nil {
-		t.Fatalf("Register error: %v", registerErr)
+	if err != nil {
+		t.Fatalf("Creating client error: %v", err)
+	}
+	err = state.Register()
+	if err != nil {
+		t.Fatalf("Registering error: %v", err)
 	}
 
-	_, addErr := state.AddCustomServer(serverURI)
+	addErr := state.AddServer(&ck, serverURI, srvtypes.TypeCustom, false)
 	if addErr != nil {
 		t.Fatalf("Add error: %v", addErr)
 	}
 
 	// get a config with preferTCP set to true
-	config, configErr := state.GetConfigCustomServer(serverURI, true, oauth.Token{})
+	config, configErr := state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, true, false)
 
 	// Test server should accept prefer TCP!
-	if config.Type != "openvpn" {
+	if config.Protocol != protocol.OpenVPN {
 		t.Fatalf("Invalid protocol for prefer TCP, got: WireGuard, want: OpenVPN")
 	}
 
@@ -396,20 +350,20 @@ func TestPreferTCP(t *testing.T) {
 	}
 
 	// We also test for script security 0 here
-	if !strings.HasSuffix(config.Config, "udp\nscript-security 0") {
-		t.Fatalf("Suffix for prefer TCP is not in the right order for config: %s", config)
+	if !strings.HasSuffix(config.VPNConfig, "udp\nscript-security 0") {
+		t.Fatalf("Suffix for prefer TCP is not in the right order for config: %s", config.VPNConfig)
 	}
 
 	// get a config with preferTCP set to false
-	config, configErr = state.GetConfigCustomServer(serverURI, false, oauth.Token{})
+	config, configErr = state.GetConfig(&ck, serverURI, srvtypes.TypeCustom, false, false)
 	if configErr != nil {
 		t.Fatalf("Config error: %v", configErr)
 	}
 
 	// We also test for script security 0 here
-	if config.Type == "openvpn" &&
-		!strings.HasSuffix(config.Config, "tcp\nscript-security 0") {
-		t.Fatalf("Suffix for disable prefer TCP is not in the right order for config: %s", config.Config)
+	if config.Protocol == protocol.OpenVPN &&
+		!strings.HasSuffix(config.VPNConfig, "tcp\nscript-security 0") {
+		t.Fatalf("Suffix for disable prefer TCP is not in the right order for config: %s", config.VPNConfig)
 	}
 }
 
@@ -423,30 +377,28 @@ func TestInvalidClientID(t *testing.T) {
 		"org.letsconnect-vpn.app.macos": true,
 	}
 
+	dir := t.TempDir()
 	for k, v := range tests {
-		state := &Client{}
-		registerErr := state.Register(
+		_, err := New(
 			k,
 			"0.1.0-test",
-			"configsclientid",
-			"en",
+			dir,
 			func(old FSMStateID, new FSMStateID, data interface{}) bool {
-				stateCallback(t, old, new, data, state)
 				return true
 			},
 			false,
 		)
 		if v {
-			if registerErr != nil {
-				t.Fatalf("expected valid register with clientID: %v, got error: %v", k, registerErr)
+			if err != nil {
+				t.Fatalf("expected valid register with clientID: %v, got error: %v", k, err)
 			}
 			continue
 		}
-		if registerErr == nil {
+		if err == nil {
 			t.Fatalf("expected invalid register with clientID: %v, but got no error", k)
 		}
-		if !strings.HasPrefix(registerErr.Error(), "client ID is not allowed") {
-			t.Fatalf("register error has invalid prefix: %v", registerErr.Error())
+		if !strings.HasPrefix(err.Error(), "An internal error occurred with cause: The client registered with an invalid client ID") {
+			t.Fatalf("register error has invalid prefix: %v", err.Error())
 		}
 	}
 }
diff --git a/internal/discovery/discovery_test.go b/internal/discovery/discovery_test.go
index 2e6c38f4..317aa50d 100644
--- a/internal/discovery/discovery_test.go
+++ b/internal/discovery/discovery_test.go
@@ -1,13 +1,14 @@
 package discovery
 
 import (
+	"context"
 	"net/http"
 	"reflect"
 	"testing"
 	"time"
 
 	"github.com/eduvpn/eduvpn-common/internal/test"
-	"github.com/eduvpn/eduvpn-common/types"
+	discotypes "github.com/eduvpn/eduvpn-common/types/discovery"
 )
 
 // TestServers tests whether or not we can obtain discovery servers
@@ -22,7 +23,7 @@ func TestServers(t *testing.T) {
 	}
 	d := &Discovery{httpClient: c}
 	// get servers
-	s1, err := d.Servers()
+	s1, err := d.Servers(context.Background())
 	if err != nil {
 		t.Fatalf("Failed getting servers: %v", err)
 	}
@@ -30,7 +31,7 @@ func TestServers(t *testing.T) {
 	// Shutdown the server
 	s.Close()
 	// Test if we get the same cached copy
-	s2, err := d.Servers()
+	s2, err := d.Servers(context.Background())
 	// We should not get an error as the timestamp is not expired
 	if err != nil {
 		t.Fatalf("Got a servers error after shutting down server: %v", err)
@@ -42,7 +43,7 @@ func TestServers(t *testing.T) {
 	// Force expired, 1 hour in the past
 	d.ServerList.Timestamp = time.Now().Add(-1 * time.Hour)
 
-	s3, err := d.Servers()
+	s3, err := d.Servers(context.Background())
 	// Now we expect an error with the cached copy
 	if err == nil {
 		t.Fatalf("Got a servers nil error after shutting down file server and expired")
@@ -64,7 +65,7 @@ func TestOrganizations(t *testing.T) {
 	}
 	d := &Discovery{httpClient: c}
 	// get servers
-	s1, err := d.Organizations()
+	s1, err := d.Organizations(context.Background())
 	if err != nil {
 		t.Fatalf("Failed getting organizations: %v", err)
 	}
@@ -73,7 +74,7 @@ func TestOrganizations(t *testing.T) {
 	s.Close()
 	// Test if we get the same cached copy
 	// We should not get an error as the timestamp is not zero
-	s2, err := d.Organizations()
+	s2, err := d.Organizations(context.Background())
 	if err != nil {
 		t.Fatalf("Got an organizations error after shutting down file server: %v", err)
 	}
@@ -85,9 +86,9 @@ func TestOrganizations(t *testing.T) {
 // TestSecureLocationList tests the function for getting a list of secure internet servers
 func TestSecureLocationList(t *testing.T) {
 	d := Discovery{
-		ServerList: types.DiscoveryServers{
+		ServerList: discotypes.Servers{
 			Version: 1,
-			List: []types.DiscoveryServer{
+			List: []discotypes.Server{
 				// institute access server, this should not be found
 				{CountryCode: "", Type: "institute_access"},
 				// secure internet servers, these should be found
@@ -110,9 +111,9 @@ func TestSecureLocationList(t *testing.T) {
 // TestServerByURL tests the function for getting a server by the Base URL and type
 func TestServerByURL(t *testing.T) {
 	d := Discovery{
-		ServerList: types.DiscoveryServers{
+		ServerList: discotypes.Servers{
 			Version: 1,
-			List: []types.DiscoveryServer{
+			List: []discotypes.Server{
 				// institute access server
 				{BaseURL: "a", Type: "institute_access"},
 				// secure internet servers
@@ -149,11 +150,11 @@ func TestServerByURL(t *testing.T) {
 
 // TestServerByCountryCode tests the function for getting a server by the country code
 func TestServerByCountryCode(t *testing.T) {
-	s1 := types.DiscoveryServer{CountryCode: "a", Type: "secure_internet"}
+	s1 := discotypes.Server{CountryCode: "a", Type: "secure_internet"}
 	d := Discovery{
-		ServerList: types.DiscoveryServers{
+		ServerList: discotypes.Servers{
 			Version: 1,
-			List: []types.DiscoveryServer{
+			List: []discotypes.Server{
 				// secure internet server
 				s1,
 				// Unexpected types, these should not be found
@@ -184,11 +185,11 @@ func TestServerByCountryCode(t *testing.T) {
 
 // TestOrgByID tests the function for getting an organization by ID
 func TestOrgByID(t *testing.T) {
-	o1 := types.DiscoveryOrganization{OrgID: "a"}
+	o1 := discotypes.Organization{OrgID: "a"}
 	d := Discovery{
-		OrganizationList: types.DiscoveryOrganizations{
+		OrganizationList: discotypes.Organizations{
 			Version: 1,
-			List: []types.DiscoveryOrganization{
+			List: []discotypes.Organization{
 				o1,
 			},
 		},
@@ -208,19 +209,19 @@ func TestOrgByID(t *testing.T) {
 
 // TestSecureHomeArgs tests the function for getting an organization and matching secure internet server by organization ID
 func TestSecureHomeArgs(t *testing.T) {
-	o1 := types.DiscoveryOrganization{OrgID: "id", SecureInternetHome: "a"}
-	s1 := types.DiscoveryServer{BaseURL: "a", Type: "secure_internet"}
+	o1 := discotypes.Organization{OrgID: "id", SecureInternetHome: "a"}
+	s1 := discotypes.Server{BaseURL: "a", Type: "secure_internet"}
 	d := Discovery{
-		OrganizationList: types.DiscoveryOrganizations{
+		OrganizationList: discotypes.Organizations{
 			Version: 1,
-			List: []types.DiscoveryOrganization{
+			List: []discotypes.Organization{
 				{OrgID: "id2", SecureInternetHome: "c"},
 				o1,
 			},
 		},
-		ServerList: types.DiscoveryServers{
+		ServerList: discotypes.Servers{
 			Version: 1,
-			List: []types.DiscoveryServer{
+			List: []discotypes.Server{
 				s1,
 				{BaseURL: "b"},
 			},
diff --git a/internal/http/http_test.go b/internal/http/http_test.go
index 49df9318..8c2ae0f9 100644
--- a/internal/http/http_test.go
+++ b/internal/http/http_test.go
@@ -5,7 +5,7 @@ import (
 )
 
 func TestEnsureValidURL(t *testing.T) {
-	_, validErr := EnsureValidURL("%notvalid%")
+	_, validErr := EnsureValidURL("%notvalid%", true)
 
 	if validErr == nil {
 		t.Fatal("Got nil error, want: non-nil")
@@ -27,7 +27,7 @@ func TestEnsureValidURL(t *testing.T) {
 	}
 
 	for k, v := range testCases {
-		valid, validErr := EnsureValidURL(k)
+		valid, validErr := EnsureValidURL(k, true)
 		if validErr != nil {
 			t.Fatalf("Got: %v, want: nil", validErr)
 		}
diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go
index 8682e24b..4818f429 100644
--- a/internal/oauth/oauth_test.go
+++ b/internal/oauth/oauth_test.go
@@ -1,6 +1,7 @@
 package oauth
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"net/url"
@@ -60,7 +61,7 @@ func Test_challengergen(t *testing.T) {
 
 func Test_accessToken(t *testing.T) {
 	o := OAuth{}
-	_, err := o.AccessToken()
+	_, err := o.AccessToken(context.Background())
 	if err == nil {
 		t.Fatalf("No error when getting access token on empty structure")
 	}
@@ -69,7 +70,7 @@ func Test_accessToken(t *testing.T) {
 	want := "test"
 	expired := time.Now().Add(1 * time.Hour)
 	o = OAuth{token: &tokenLock{t: &tokenRefresher{Token: Token{Access: want, ExpiredTimestamp: expired}}}}
-	got, err := o.AccessToken()
+	got, err := o.AccessToken(context.Background())
 	if err != nil {
 		t.Fatalf("Got error when getting access token on non-empty structure: %v", err)
 	}
@@ -81,7 +82,7 @@ func Test_accessToken(t *testing.T) {
 	o.SetTokenExpired()
 
 	// We should get an error because expired and no refresh token
-	_, err = o.AccessToken()
+	_, err = o.AccessToken(context.Background())
 	if err == nil {
 		t.Fatal("Got no error when getting access token on non-empty structure and expired")
 	}
@@ -90,7 +91,7 @@ func Test_accessToken(t *testing.T) {
 	// Now we internally update the refresh function and refresh token, we should get new tokens
 	refresh := "refresh"
 	o.token.t.Refresh = refresh
-	o.token.t.Refresher = func(refreshToken string) (*TokenResponse, time.Time, error) {
+	o.token.t.Refresher = func(ctx context.Context, refreshToken string) (*TokenResponse, time.Time, error) {
 		if refreshToken != refresh {
 			t.Fatalf("Passed refresh token to refresher not equal to updated refresh token, got: %v, want: %v", refreshToken, refresh)
 		}
@@ -99,7 +100,7 @@ func Test_accessToken(t *testing.T) {
 		return r, expired, nil
 	}
 
-	got, err = o.AccessToken()
+	got, err = o.AccessToken(context.Background())
 	if err != nil {
 		t.Fatalf("Got error when getting access token on non-empty expired structure and with a 'valid' refresh token: %v", err)
 	}
@@ -115,7 +116,7 @@ func Test_accessToken(t *testing.T) {
 	// Now let's act like a 2.x server, we give no refresh token back. When we refresh the previous refresh token should be gotten
 	o.token.t.Refresh = refresh
 	prevRefresh := refresh
-	o.token.t.Refresher = func(refreshToken string) (*TokenResponse, time.Time, error) {
+	o.token.t.Refresher = func(ctx context.Context, refreshToken string) (*TokenResponse, time.Time, error) {
 		if refreshToken != refresh {
 			t.Fatalf("Passed refresh token to refresher not equal to updated refresh token, got: %v, want: %v", refreshToken, refresh)
 		}
@@ -124,7 +125,7 @@ func Test_accessToken(t *testing.T) {
 		return r, expired, nil
 	}
 
-	got, err = o.AccessToken()
+	got, err = o.AccessToken(context.Background())
 	if err != nil {
 		t.Fatalf("Got error when getting access token on non-empty expired structure and with an empty refresh response: %v", err)
 	}
@@ -174,8 +175,8 @@ func Test_AuthURL(t *testing.T) {
 	}
 
 	// Check if the OAuth session has valid values
-	if o.session.ClientID != id {
-		t.Fatalf("OAuth ClientID not equal, want: %v, got: %v", o.session.ClientID, id)
+	if o.ClientID != id {
+		t.Fatalf("OAuth ClientID not equal, want: %v, got: %v", o.ClientID, id)
 	}
 	if o.session.ISS != iss {
 		t.Fatalf("OAuth ISS not equal, want: %v, got: %v", o.session.ISS, iss)
diff --git a/internal/server/api_test.go b/internal/server/api/api_test.go
similarity index 67%
rename from internal/server/api_test.go
rename to internal/server/api/api_test.go
index b1e35503..2fea4c6f 100644
--- a/internal/server/api_test.go
+++ b/internal/server/api/api_test.go
@@ -1,11 +1,14 @@
-package server
+package api
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"net/http"
 	"testing"
 
+	"github.com/eduvpn/eduvpn-common/internal/server/base"
+	"github.com/eduvpn/eduvpn-common/internal/server/endpoints"
 	"github.com/eduvpn/eduvpn-common/internal/test"
 	"github.com/go-errors/errors"
 )
@@ -17,7 +20,7 @@ func getErrorMsg(err error) string {
 	return err.Error()
 }
 
-func compareEndpoints(ep1 Endpoints, ep2 Endpoints) bool {
+func compareEndpoints(ep1 endpoints.Endpoints, ep2 endpoints.Endpoints) bool {
 	v3_1 := ep1.API.V3
 	v3_2 := ep2.API.V3
 	return v3_1.API == v3_2.API && v3_1.Authorization == v3_2.Authorization && v3_1.Token == v3_2.Token
@@ -38,11 +41,11 @@ func Test_APIGetEndpoints(t *testing.T) {
 	}
 
 	testCases := []struct {
-		epl EndpointList
+		epl endpoints.List
 		err error
 	}{
 		{
-			epl: EndpointList{
+			epl: endpoints.List{
 				API:           "https://example.com/1",
 				Authorization: "https://example.com/2",
 				Token:         "https://example.com/3",
@@ -50,15 +53,15 @@ func Test_APIGetEndpoints(t *testing.T) {
 			err: nil,
 		},
 		{
-			epl: EndpointList{
-				API:           "http://example.com/1",
-				Authorization: "https://example.com/2",
-				Token:         "https://example.com/3",
+			epl: endpoints.List{
+				API:           "https://example.com/1",
+				Authorization: "http://example.com/2",
+				Token:         "http://example.com/3",
 			},
-			err: errors.New("API scheme: 'http', is not equal to authorization scheme: 'https'"),
+			err: errors.New("API scheme: 'https', is not equal to authorization scheme: 'http'"),
 		},
 		{
-			epl: EndpointList{
+			epl: endpoints.List{
 				API:           "https://example.com/1",
 				Authorization: "https://example.com/2",
 				Token:         "ftp://example.com/3",
@@ -66,7 +69,7 @@ func Test_APIGetEndpoints(t *testing.T) {
 			err: errors.New("API scheme: 'https', is not equal to token scheme: 'ftp'"),
 		},
 		{
-			epl: EndpointList{
+			epl: endpoints.List{
 				API:           "https://malicious.com/1",
 				Authorization: "https://example.com/2",
 				Token:         "https://example.com/3",
@@ -74,7 +77,7 @@ func Test_APIGetEndpoints(t *testing.T) {
 			err: errors.New("API host: 'malicious.com', is not equal to authorization host: 'example.com'"),
 		},
 		{
-			epl: EndpointList{
+			epl: endpoints.List{
 				API:           "https://example.com/1",
 				Authorization: "https://example.com/2",
 				Token:         "https://malicious.com/3",
@@ -82,18 +85,34 @@ func Test_APIGetEndpoints(t *testing.T) {
 			err: errors.New("API host: 'example.com', is not equal to token host: 'malicious.com'"),
 		},
 		{
-			epl: EndpointList{
+			epl: endpoints.List{
 				API:           "https://example.com/1",
 				Authorization: "https://malicious.com/2",
 				Token:         "https://example.com/3",
 			},
 			err: errors.New("API host: 'example.com', is not equal to authorization host: 'malicious.com'"),
 		},
+		{
+			epl: endpoints.List{
+				API:           "https://example.com/1",
+				Authorization: "https://example.com/2",
+				Token:         "ftp://example.com/3",
+			},
+			err: errors.New("API scheme: 'https', is not equal to token scheme: 'ftp'"),
+		},
+		{
+			epl: endpoints.List{
+				API:           "https://example.com/1",
+				Authorization: "ftp://example.com/2",
+				Token:         "https://example.com/3",
+			},
+			err: errors.New("API scheme: 'https', is not equal to authorization scheme: 'ftp'"),
+		},
 	}
 
 	for _, tc := range testCases {
-		ep := &Endpoints{
-			API: EndpointsVersions{
+		ep := &endpoints.Endpoints{
+			API: endpoints.Versions{
 				V3: tc.epl,
 			},
 		}
@@ -108,7 +127,11 @@ func Test_APIGetEndpoints(t *testing.T) {
 
 			fmt.Fprintln(w, string(jsonStr))
 		}))
-		gotEP, err := APIGetEndpoints(s.URL, c)
+		b := &base.Base{
+			URL:        s.URL,
+			HTTPClient: c,
+		}
+		err = Endpoints(context.Background(), b)
 		if getErrorMsg(err) != getErrorMsg(tc.err) {
 			t.Fatalf("Errors not equal, want err: %v, got: %v", tc.err, err)
 		}
@@ -119,12 +142,9 @@ func Test_APIGetEndpoints(t *testing.T) {
 		if ep == nil {
 			t.Fatalf("No test case endpoints")
 		}
-		if gotEP == nil {
-			t.Fatalf("Got no endpoints for nil error")
-		}
 		// if no error then the endpoints should be equal
-		if !compareEndpoints(*ep, *gotEP) {
-			t.Fatalf("Endpoints are not equal, got: %v, want: %v", gotEP, ep)
+		if !compareEndpoints(*ep, b.Endpoints) {
+			t.Fatalf("Endpoints are not equal, got: %v, want: %v", b.Endpoints, ep)
 		}
 	}
 }
diff --git a/internal/server/profile_test.go b/internal/server/profile/profile_test.go
similarity index 96%
rename from internal/server/profile_test.go
rename to internal/server/profile/profile_test.go
index d6a7e9db..e246b5c1 100644
--- a/internal/server/profile_test.go
+++ b/internal/server/profile/profile_test.go
@@ -1,4 +1,4 @@
-package server
+package profile
 
 import "testing"
 
@@ -86,9 +86,9 @@ func Test_CurrentProfileIndex(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		pri := &ProfileInfo{
+		pri := &Info{
 			Current: tc.current,
-			Info: ProfileListInfo{
+			Info: ListInfo{
 				ProfileList: tc.profiles,
 			},
 		}
diff --git a/internal/util/util_test.go b/internal/util/util_test.go
index c3d1cee1..5e19f571 100644
--- a/internal/util/util_test.go
+++ b/internal/util/util_test.go
@@ -65,47 +65,3 @@ func TestReplaceWAYF(t *testing.T) {
 		t.Fatalf("Got: %s, want: %s", replaced, wantReplaced)
 	}
 }
-
-func TestGetLanguageMatched(t *testing.T) {
-	// exact match
-	returned := GetLanguageMatched(map[string]string{"en": "test", "de": "test2"}, "en")
-	if returned != "test" {
-		t.Fatalf("Got: %s, want: %s", returned, "test")
-	}
-
-	// starts with language tag
-	returned = GetLanguageMatched(map[string]string{"en-US-test": "test", "de": "test2"}, "en-US")
-	if returned != "test" {
-		t.Fatalf("Got: %s, want: %s", returned, "test")
-	}
-
-	// starts with en-
-	returned = GetLanguageMatched(map[string]string{"en-UK": "test", "en": "test2"}, "en-US")
-	if returned != "test" {
-		t.Fatalf("Got: %s, want: %s", returned, "test")
-	}
-
-	// exact match for en
-	returned = GetLanguageMatched(map[string]string{"de": "test", "en": "test2"}, "en-US")
-	if returned != "test2" {
-		t.Fatalf("Got: %s, want: %s", returned, "test2")
-	}
-
-	// We default to english
-	returned = GetLanguageMatched(map[string]string{"es": "test", "en": "test2"}, "nl-NL")
-	if returned != "test2" {
-		t.Fatalf("Got: %s, want: %s", returned, "test2")
-	}
-
-	// We default to english with a - as well
-	returned = GetLanguageMatched(map[string]string{"est": "test", "en-": "test2"}, "en-US")
-	if returned != "test2" {
-		t.Fatalf("Got: %s, want: %s", returned, "test2")
-	}
-
-	// None found just return one
-	returned = GetLanguageMatched(map[string]string{"es": "test"}, "en-US")
-	if returned != "test" {
-		t.Fatalf("Got: %s, want: %s", returned, "test")
-	}
-}
diff --git a/internal/verify/verify_test.go b/internal/verify/verify_test.go
index cbc6cb9a..2c499b1f 100644
--- a/internal/verify/verify_test.go
+++ b/internal/verify/verify_test.go
@@ -359,24 +359,24 @@ func compareResults(
 			t.Errorf("error not expected but returned '%s', callstr '%s'", err.Error(), callStr())
 		}
 		if !ret {
-			t.Errorf("error is nil and result is false, callstr '%s'", callStr())
+			t.Errorf("error is nil and result is false, callstr: '%s'", callStr())
 		}
 		return
 	}
 
 	if err == nil {
 		// we expect an error but received nil
-		t.Errorf("expected error prefix '%s' but received nil, callstr '%s'", expectedErrPrefix, callStr())
+		t.Errorf("expected error prefix '%s' but received nil, callstr: '%s'", expectedErrPrefix, callStr())
 		return
 	}
 
 	if !strings.HasPrefix(err.Error(), expectedErrPrefix) {
 		// wrong error
-		t.Errorf("expected error prefix '%s' for error '%s', callStr '%s'", expectedErrPrefix, err.Error(), callStr())
+		t.Errorf("expected error prefix '%s' for error '%s', callstr: '%s'", expectedErrPrefix, err.Error(), callStr())
 		return
 	}
 
 	if ret {
-		t.Errorf("error is not nil and result is true, callStr '%s'", callStr())
+		t.Errorf("error is not nil and result is true, callstr: '%s'", callStr())
 	}
 }
diff --git a/types/server/server_test.go b/types/server/server_test.go
new file mode 100644
index 00000000..06a6851a
--- /dev/null
+++ b/types/server/server_test.go
@@ -0,0 +1,83 @@
+package server
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func errorString(err error) string {
+	if err == nil {
+		return ""
+	}
+	return err.Error()
+}
+
+func TestServerUnmarshal(t *testing.T) {
+	cases := []struct{
+		want Type
+		wantErr string
+		payload string
+	}{
+		{
+			want: TypeUnknown,
+			wantErr: "invalid server type: a",
+			payload: `{"value": "a"}`,
+		},
+		{
+			want: TypeInstituteAccess,
+			wantErr: "",
+			payload: `{"value": "institute_access"}`,
+		},
+		{
+			want: TypeCustom,
+			wantErr: "",
+			payload: `{"value": "custom_server"}`,
+		},
+		{
+			want: TypeSecureInternet,
+			wantErr: "",
+			payload: `{"value": "secure_internet"}`,
+		},
+		{
+			want: TypeUnknown,
+			wantErr: "",
+			payload: `{"value": 0}`,
+		},
+		{
+			want: TypeInstituteAccess,
+			wantErr: "",
+			payload: `{"value": 1}`,
+		},
+		{
+			want: TypeSecureInternet,
+			wantErr: "",
+			payload: `{"value": 2}`,
+		},
+		{
+			want: TypeCustom,
+			wantErr: "",
+			payload: `{"value": 3}`,
+		},
+		// Values that are outside the range will be error checked too
+		// This is thus even more strict than a regular type unmarshal/marshal
+		{
+			want: TypeUnknown,
+			wantErr: "invalid server type: 25",
+			payload: `{"value": 25}`,
+		},
+	}
+
+	for _, c := range cases {
+		var got struct{
+			Value Type `json:"value"`
+		}
+		err := json.Unmarshal([]byte(c.payload), &got)
+		if errorString(err) != c.wantErr {
+			t.Fatalf("server unmarshal error is not equal to want, got: %v, want: %v", err, c.want)
+		}
+		if got.Value != c.want {
+			t.Fatalf("server unmarshal value is not equal to want, got: %v, want: %v", got.Value, c.want)
+		}
+	}
+		
+}
diff --git a/wrappers/python/tests.py b/wrappers/python/tests.py
index bd8b4eb2..61d4ef77 100644
--- a/wrappers/python/tests.py
+++ b/wrappers/python/tests.py
@@ -2,11 +2,10 @@
 
 import unittest
 import eduvpn_common.main as eduvpn
+import eduvpn_common.event as event
 from eduvpn_common.state import State, StateType
-import webbrowser
 import sys
 import os
-import json
 
 # Import project root directory where the selenium python utility is
 sys.path.append(
@@ -16,15 +15,19 @@
 from selenium_eduvpn import login_eduvpn
 
 
+class Handler:
+    @event.class_state_transition(State.OAUTH_STARTED, StateType.ENTER)
+    def on_oauth(self, old_state: State, data: str):
+        login_eduvpn(data)
+
+
 class ConfigTests(unittest.TestCase):
     def testConfig(self):
-        _eduvpn = eduvpn.EduVPN("org.letsconnect-vpn.app.linux", "0.1.0", "testconfigs", "en")
+        _eduvpn = eduvpn.EduVPN("org.letsconnect-vpn.app.linux", "0.1.0", "testconfigs")
         # This can throw an exception
         _eduvpn.register()
-
-        @_eduvpn.event.on(State.OAUTH_STARTED, StateType.ENTER)
-        def oauth_initialized(old_state, url_json):
-            login_eduvpn(url_json)
+        handler = Handler()
+        _eduvpn.register_class_callbacks(handler)
 
         server_uri = os.getenv("SERVER_URI")
         if not server_uri:
@@ -33,16 +36,18 @@ def oauth_initialized(old_state, url_json):
             return
 
         # This can throw an exception
-        _eduvpn.add_custom_server(server_uri)
-        _eduvpn.get_config_custom_server(server_uri)
+        _eduvpn.add_server(eduvpn.ServerType.CUSTOM, server_uri)
+        _eduvpn.get_config(eduvpn.ServerType.CUSTOM, server_uri)
 
         # Deregister
         _eduvpn.deregister()
 
     def testDoubleRegister(self):
-        _eduvpn = eduvpn.EduVPN("org.letsconnect-vpn.app.linux", "0.1.0", "testconfigs", "en")
+        _eduvpn = eduvpn.EduVPN("org.letsconnect-vpn.app.linux", "0.1.0", "testconfigs")
         # This can throw an exception
         _eduvpn.register()
+        handler = Handler()
+        _eduvpn.register_class_callbacks(handler)
         # This should throw
         try:
             _eduvpn.register()