diff --git a/README.md b/README.md
index bad370a8126a80dafe6a1b88d2a1f5f21aa0d27e..f1bc5572fc4bec71937a685c1bc39bdc03dc244d 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ The goal is to run Wg Gen Web in a container and WireGuard on host system.
  * Generation of `wg0.conf` after any modification
  * Dockerized
  * Pretty cool look
+
 ![Screenshot](wg-gen-web_screenshot.png)
 
 ## Running
diff --git a/api/api.go b/api/api.go
index 3aa4d6ce262a533b493d3a8176b4195ccb95351c..f1b7097d03061e48b4a5a2c94e25379d913ce2b7 100644
--- a/api/api.go
+++ b/api/api.go
@@ -4,8 +4,8 @@ import (
 	"github.com/gin-gonic/gin"
 	log "github.com/sirupsen/logrus"
 	"github.com/skip2/go-qrcode"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
-	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/repository"
 	"net/http"
 )
 
@@ -41,7 +41,7 @@ func createClient(c *gin.Context) {
 		return
 	}
 
-	client, err := repository.CreateClient(&data)
+	client, err := core.CreateClient(&data)
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -56,7 +56,7 @@ func createClient(c *gin.Context) {
 func readClient(c *gin.Context) {
 	id := c.Param("id")
 
-	client, err := repository.ReadClient(id)
+	client, err := core.ReadClient(id)
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -80,7 +80,7 @@ func updateClient(c *gin.Context) {
 		return
 	}
 
-	client, err := repository.UpdateClient(id, &data)
+	client, err := core.UpdateClient(id, &data)
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -95,7 +95,7 @@ func updateClient(c *gin.Context) {
 func deleteClient(c *gin.Context) {
 	id := c.Param("id")
 
-	err := repository.DeleteClient(id)
+	err := core.DeleteClient(id)
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -108,7 +108,7 @@ func deleteClient(c *gin.Context) {
 }
 
 func readClients(c *gin.Context) {
-	clients, err := repository.ReadClients()
+	clients, err := core.ReadClients()
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -121,7 +121,7 @@ func readClients(c *gin.Context) {
 }
 
 func configClient(c *gin.Context) {
-	configData, err := repository.ReadClientConfig(c.Param("id"))
+	configData, err := core.ReadClientConfig(c.Param("id"))
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -153,7 +153,7 @@ func configClient(c *gin.Context) {
 func emailClient(c *gin.Context) {
 	id := c.Param("id")
 
-	err := repository.EmailClient(id)
+	err := core.EmailClient(id)
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -166,7 +166,7 @@ func emailClient(c *gin.Context) {
 }
 
 func readServer(c *gin.Context) {
-	client, err := repository.ReadServer()
+	client, err := core.ReadServer()
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
@@ -189,7 +189,7 @@ func updateServer(c *gin.Context) {
 		return
 	}
 
-	client, err := repository.UpdateServer(&data)
+	client, err := core.UpdateServer(&data)
 	if err != nil {
 		log.WithFields(log.Fields{
 			"err": err,
diff --git a/repository/repository.go b/core/client.go
similarity index 58%
rename from repository/repository.go
rename to core/client.go
index 7e64a00d22cf958503804413b85cf2d102ab73c0..8d49c703c0fc5c465fc3641931d8fb97f2bff07c 100644
--- a/repository/repository.go
+++ b/core/client.go
@@ -1,12 +1,13 @@
-package repository
+package core
 
 import (
-	"encoding/json"
 	"errors"
 	uuid "github.com/satori/go.uuid"
 	log "github.com/sirupsen/logrus"
 	"github.com/skip2/go-qrcode"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/storage"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"gopkg.in/gomail.v2"
@@ -68,27 +69,27 @@ func CreateClient(client *model.Client) (*model.Client, error) {
 		ips = append(ips, ip)
 	}
 	client.Address = strings.Join(ips, ",")
-
 	client.Created = time.Now().UTC()
 	client.Updated = client.Created
 
-	err = serialize(client.Id, client)
+	err = storage.Serialize(client.Id, client)
 	if err != nil {
 		return nil, err
 	}
 
-	v, err := deserialize(client.Id)
+	v, err := storage.Deserialize(client.Id)
 	if err != nil {
 		return nil, err
 	}
 	client = v.(*model.Client)
 
-	return client, nil
+	// data modified, dump new config
+	return client, UpdateServerConfigWg()
 }
 
 // ReadClient client by id
 func ReadClient(id string) (*model.Client, error) {
-	v, err := deserialize(id)
+	v, err := storage.Deserialize(id)
 	if err != nil {
 		return nil, err
 	}
@@ -97,29 +98,9 @@ func ReadClient(id string) (*model.Client, error) {
 	return client, nil
 }
 
-// ReadClientConfig in wg format
-func ReadClientConfig(id string) ([]byte, error) {
-	client, err := ReadClient(id)
-	if err != nil {
-		return nil, err
-	}
-
-	server, err := ReadServer()
-	if err != nil {
-		return nil, err
-	}
-
-	configDataWg, err := util.DumpClient(client, server)
-	if err != nil {
-		return nil, err
-	}
-
-	return configDataWg.Bytes(), nil
-}
-
 // UpdateClient preserve keys
 func UpdateClient(Id string, client *model.Client) (*model.Client, error) {
-	v, err := deserialize(Id)
+	v, err := storage.Deserialize(Id)
 	if err != nil {
 		return nil, err
 	}
@@ -131,21 +112,21 @@ func UpdateClient(Id string, client *model.Client) (*model.Client, error) {
 	// keep keys
 	client.PrivateKey = current.PrivateKey
 	client.PublicKey = current.PublicKey
-
 	client.Updated = time.Now().UTC()
 
-	err = serialize(client.Id, client)
+	err = storage.Serialize(client.Id, client)
 	if err != nil {
 		return nil, err
 	}
 
-	v, err = deserialize(Id)
+	v, err = storage.Deserialize(Id)
 	if err != nil {
 		return nil, err
 	}
 	client = v.(*model.Client)
 
-	return client, nil
+	// data modified, dump new config
+	return client, UpdateServerConfigWg()
 }
 
 // DeleteClient from disk
@@ -157,83 +138,7 @@ func DeleteClient(id string) error {
 	}
 
 	// data modified, dump new config
-	return generateWgConfig()
-}
-
-// SendEmail to client
-func EmailClient(id string) error {
-	client, err := ReadClient(id)
-	if err != nil {
-		return err
-	}
-
-	configData, err := ReadClientConfig(id)
-	if err != nil {
-		return err
-	}
-
-	// conf as .conf file
-	tmpfileCfg, err := ioutil.TempFile("", "wireguard-vpn-*.conf")
-	if err != nil {
-		return err
-	}
-	if _, err := tmpfileCfg.Write(configData); err != nil {
-		return err
-	}
-	if err := tmpfileCfg.Close(); err != nil {
-		return err
-	}
-	defer os.Remove(tmpfileCfg.Name()) // clean up
-
-	// conf as png image
-	png, err := qrcode.Encode(string(configData), qrcode.Medium, 280)
-	if err != nil {
-		return err
-	}
-	tmpfilePng, err := ioutil.TempFile("", "qrcode-*.png")
-	if err != nil {
-		return err
-	}
-	if _, err := tmpfilePng.Write(png); err != nil {
-		return err
-	}
-	if err := tmpfilePng.Close(); err != nil {
-		return err
-	}
-	defer os.Remove(tmpfilePng.Name()) // clean up
-
-	// get email body
-	emailBody, err := util.DumpEmail(client, filepath.Base(tmpfilePng.Name()))
-	if err != nil {
-		return err
-	}
-
-	// port to int
-	port, err := strconv.Atoi(os.Getenv("SMTP_PORT"))
-	if err != nil {
-		return err
-	}
-
-	d := gomail.NewDialer(os.Getenv("SMTP_HOST"), port, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD"))
-	s, err := d.Dial()
-	if err != nil {
-		return err
-	}
-	m := gomail.NewMessage()
-
-	m.SetHeader("From", os.Getenv("SMTP_FROM"))
-	m.SetAddressHeader("To", client.Email, client.Name)
-	m.SetHeader("Subject", "WireGuard VPN Configuration")
-	m.SetBody("text/html", emailBody.String())
-	m.Attach(tmpfileCfg.Name())
-	m.Embed(tmpfilePng.Name())
-
-	err = gomail.Send(s, m)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return UpdateServerConfigWg()
 }
 
 // ReadClients all clients
@@ -249,12 +154,12 @@ func ReadClients() ([]*model.Client, error) {
 		// clients file name is an uuid
 		_, err := uuid.FromString(f.Name())
 		if err == nil {
-			c, err := deserialize(f.Name())
+			c, err := storage.Deserialize(f.Name())
 			if err != nil {
 				log.WithFields(log.Fields{
 					"err":  err,
 					"path": f.Name(),
-				}).Error("failed to deserialize client")
+				}).Error("failed to storage.Destorage.Serialize client")
 			} else {
 				clients = append(clients, c.(*model.Client))
 			}
@@ -268,142 +173,95 @@ func ReadClients() ([]*model.Client, error) {
 	return clients, nil
 }
 
-// ReadServer object, create default one
-func ReadServer() (*model.Server, error) {
-	if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) {
-		server := &model.Server{}
-
-		key, err := wgtypes.GeneratePrivateKey()
-		if err != nil {
-			return nil, err
-		}
-		server.PrivateKey = key.String()
-		server.PublicKey = key.PublicKey().String()
-
-		presharedKey, err := wgtypes.GenerateKey()
-		if err != nil {
-			return nil, err
-		}
-		server.PresharedKey = presharedKey.String()
-
-		server.Name = "Created with default values"
-		server.Endpoint = "wireguard.example.com:123"
-		server.ListenPort = 51820
-		server.Address = "fd9f:6666::10:6:6:1/112, 10.6.6.1/24"
-		server.Dns = "fd9f::10:0:0:2, 10.0.0.2"
-		server.PersistentKeepalive = 16
-		server.Created = time.Now().UTC()
-		server.Updated = server.Created
-
-		err = serialize("server.json", server)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	c, err := deserialize("server.json")
-	if err != nil {
-		return nil, err
-	}
-
-	return c.(*model.Server), nil
-}
-
-// UpdateServer keep private values from existing one
-func UpdateServer(server *model.Server) (*model.Server, error) {
-	current, err := deserialize("server.json")
+// ReadClientConfig in wg format
+func ReadClientConfig(id string) ([]byte, error) {
+	client, err := ReadClient(id)
 	if err != nil {
 		return nil, err
 	}
-	server.PrivateKey = current.(*model.Server).PrivateKey
-	server.PublicKey = current.(*model.Server).PublicKey
-	server.PresharedKey = current.(*model.Server).PresharedKey
-
-	server.Updated = time.Now().UTC()
 
-	err = serialize("server.json", server)
+	server, err := ReadServer()
 	if err != nil {
 		return nil, err
 	}
 
-	v, err := deserialize("server.json")
+	configDataWg, err := template.DumpClientWg(client, server)
 	if err != nil {
 		return nil, err
 	}
-	server = v.(*model.Server)
 
-	return server, nil
+	return configDataWg, nil
 }
 
-// Write object to disk
-func serialize(id string, c interface{}) error {
-	b, err := json.MarshalIndent(c, "", "  ")
+// SendEmail to client
+func EmailClient(id string) error {
+	client, err := ReadClient(id)
 	if err != nil {
 		return err
 	}
 
-	err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), id), b)
+	configData, err := ReadClientConfig(id)
 	if err != nil {
 		return err
 	}
 
-	// data modified, dump new config
-	return generateWgConfig()
-}
-
-// Read client from disc
-func deserializeClient(data []byte) (*model.Client, error) {
-	var c *model.Client
-	err := json.Unmarshal(data, &c)
+	// conf as .conf file
+	tmpfileCfg, err := ioutil.TempFile("", "wireguard-vpn-*.conf")
 	if err != nil {
-		return nil, err
+		return err
 	}
+	if _, err := tmpfileCfg.Write(configData); err != nil {
+		return err
+	}
+	if err := tmpfileCfg.Close(); err != nil {
+		return err
+	}
+	defer os.Remove(tmpfileCfg.Name()) // clean up
 
-	return c, nil
-}
-
-// Read server from disc
-func deserializeServer(data []byte) (*model.Server, error) {
-	var c *model.Server
-	err := json.Unmarshal(data, &c)
+	// conf as png image
+	png, err := qrcode.Encode(string(configData), qrcode.Medium, 280)
 	if err != nil {
-		return nil, err
+		return err
 	}
-
-	return c, nil
-}
-func deserialize(id string) (interface{}, error) {
-	path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
-
-	b, err := util.ReadFile(path)
+	tmpfilePng, err := ioutil.TempFile("", "qrcode-*.png")
 	if err != nil {
-		return nil, err
+		return err
+	}
+	if _, err := tmpfilePng.Write(png); err != nil {
+		return err
 	}
-	if id == "server.json" {
-		return deserializeServer(b)
+	if err := tmpfilePng.Close(); err != nil {
+		return err
 	}
+	defer os.Remove(tmpfilePng.Name()) // clean up
 
-	return deserializeClient(b)
-}
-
-// Generate Wireguard interface configuration
-func generateWgConfig() error {
-	clients, err := ReadClients()
+	// get email body
+	emailBody, err := template.DumpEmail(client, filepath.Base(tmpfilePng.Name()))
 	if err != nil {
 		return err
 	}
 
-	server, err := ReadServer()
+	// port to int
+	port, err := strconv.Atoi(os.Getenv("SMTP_PORT"))
 	if err != nil {
 		return err
 	}
 
-	configDataWg, err := util.DumpServerWg(clients, server)
+	d := gomail.NewDialer(os.Getenv("SMTP_HOST"), port, os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD"))
+	s, err := d.Dial()
 	if err != nil {
 		return err
 	}
+	m := gomail.NewMessage()
 
-	err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), os.Getenv("WG_INTERFACE_NAME")), configDataWg.Bytes())
+	m.SetHeader("From", os.Getenv("SMTP_FROM"))
+	m.SetAddressHeader("To", client.Email, client.Name)
+	m.SetHeader("Subject", "WireGuard VPN Configuration")
+	m.SetBody("text/html", string(emailBody))
+	m.Attach(tmpfileCfg.Name())
+	m.Embed(tmpfilePng.Name())
+
+	err = gomail.Send(s, m)
 	if err != nil {
 		return err
 	}
diff --git a/core/server.go b/core/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..f1664ff020edc0dc37573615a87e216d079d2fc6
--- /dev/null
+++ b/core/server.go
@@ -0,0 +1,98 @@
+package core
+
+import (
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/storage"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+// ReadServer object, create default one
+func ReadServer() (*model.Server, error) {
+	if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) {
+		server := &model.Server{}
+
+		key, err := wgtypes.GeneratePrivateKey()
+		if err != nil {
+			return nil, err
+		}
+		server.PrivateKey = key.String()
+		server.PublicKey = key.PublicKey().String()
+
+		presharedKey, err := wgtypes.GenerateKey()
+		if err != nil {
+			return nil, err
+		}
+		server.PresharedKey = presharedKey.String()
+
+		server.Name = "Created with default values"
+		server.Endpoint = "wireguard.example.com:123"
+		server.ListenPort = 51820
+		server.Address = "fd9f:6666::10:6:6:1/112, 10.6.6.1/24"
+		server.Dns = "fd9f::10:0:0:2, 10.0.0.2"
+		server.PersistentKeepalive = 16
+		server.Created = time.Now().UTC()
+		server.Updated = server.Created
+
+		err = storage.Serialize("server.json", server)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	c, err := storage.Deserialize("server.json")
+	if err != nil {
+		return nil, err
+	}
+
+	return c.(*model.Server), nil
+}
+
+// UpdateServer keep private values from existing one
+func UpdateServer(server *model.Server) (*model.Server, error) {
+	current, err := storage.Deserialize("server.json")
+	if err != nil {
+		return nil, err
+	}
+	server.PrivateKey = current.(*model.Server).PrivateKey
+	server.PublicKey = current.(*model.Server).PublicKey
+	server.PresharedKey = current.(*model.Server).PresharedKey
+	server.Updated = time.Now().UTC()
+
+	err = storage.Serialize("server.json", server)
+	if err != nil {
+		return nil, err
+	}
+
+	v, err := storage.Deserialize("server.json")
+	if err != nil {
+		return nil, err
+	}
+	server = v.(*model.Server)
+
+	return server, UpdateServerConfigWg()
+}
+
+// UpdateServerConfigWg in wg format
+func UpdateServerConfigWg() error {
+	clients, err := ReadClients()
+	if err != nil {
+		return err
+	}
+
+	server, err := ReadServer()
+	if err != nil {
+		return err
+	}
+
+	_, err = template.DumpServerWg(clients, server)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/storage/file.go b/storage/file.go
new file mode 100644
index 0000000000000000000000000000000000000000..e259efe04772a0d4e4fa484ed965be669cece517
--- /dev/null
+++ b/storage/file.go
@@ -0,0 +1,47 @@
+package storage
+
+import (
+	"encoding/json"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
+	"os"
+	"path/filepath"
+)
+
+// Serialize write interface to disk
+func Serialize(id string, c interface{}) error {
+	b, err := json.MarshalIndent(c, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	return util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), id), b)
+}
+
+// Deserialize read interface from disk
+func Deserialize(id string) (interface{}, error) {
+	path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
+
+	data, err := util.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	if id == "server.json" {
+		var s *model.Server
+		err = json.Unmarshal(data, &s)
+		if err != nil {
+			return nil, err
+		}
+		return s, nil
+	}
+
+	// if not the server, must be client
+	var c *model.Client
+	err = json.Unmarshal(data, &c)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
diff --git a/util/tpl.go b/template/template.go
similarity index 94%
rename from util/tpl.go
rename to template/template.go
index 87338399952fda1b6bd8f0a1d27e275384ab2cdb..7469d6a3bc8a17a99f84451fb052d1ef95c789b0 100644
--- a/util/tpl.go
+++ b/template/template.go
@@ -1,8 +1,11 @@
-package util
+package template
 
 import (
 	"bytes"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
+	"os"
+	"path/filepath"
 	"strings"
 	"text/template"
 )
@@ -226,13 +229,11 @@ AllowedIPs = {{.Address}}
 	{{end}}`
 )
 
-// DumpClient dump client wg config with go template
-func DumpClient(client *model.Client, server *model.Server) (bytes.Buffer, error) {
-	var tplBuff bytes.Buffer
-
+// DumpClientWg dump client wg config with go template
+func DumpClientWg(client *model.Client, server *model.Server) ([]byte, error) {
 	t, err := template.New("client").Parse(clientTpl)
 	if err != nil {
-		return tplBuff, err
+		return nil, err
 	}
 
 	return dump(t, struct {
@@ -244,16 +245,14 @@ func DumpClient(client *model.Client, server *model.Server) (bytes.Buffer, error
 	})
 }
 
-// DumpServerWg dump server wg config with go template
-func DumpServerWg(clients []*model.Client, server *model.Server) (bytes.Buffer, error) {
-	var tplBuff bytes.Buffer
-
+// DumpServerWg dump server wg config with go template, write it to file and return bytes
+func DumpServerWg(clients []*model.Client, server *model.Server) ([]byte, error) {
 	t, err := template.New("server").Parse(wgTpl)
 	if err != nil {
-		return tplBuff, err
+		return nil, err
 	}
 
-	return dump(t, struct {
+	configDataWg, err := dump(t, struct {
 		Clients        []*model.Client
 		Server         *model.Server
 		ServerAdresses []string
@@ -262,15 +261,23 @@ func DumpServerWg(clients []*model.Client, server *model.Server) (bytes.Buffer,
 		Clients:        clients,
 		Server:         server,
 	})
+	if err != nil {
+		return nil, err
+	}
+
+	err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), os.Getenv("WG_INTERFACE_NAME")), configDataWg)
+	if err != nil {
+		return nil, err
+	}
+
+	return configDataWg, nil
 }
 
 // DumpEmail dump server wg config with go template
-func DumpEmail(client *model.Client, qrcodePngName string) (bytes.Buffer, error) {
-	var tplBuff bytes.Buffer
-
+func DumpEmail(client *model.Client, qrcodePngName string) ([]byte, error) {
 	t, err := template.New("email").Parse(emailTpl)
 	if err != nil {
-		return tplBuff, err
+		return nil, err
 	}
 
 	return dump(t, struct {
@@ -282,13 +289,13 @@ func DumpEmail(client *model.Client, qrcodePngName string) (bytes.Buffer, error)
 	})
 }
 
-func dump(tpl *template.Template, data interface{}) (bytes.Buffer, error) {
+func dump(tpl *template.Template, data interface{}) ([]byte, error) {
 	var tplBuff bytes.Buffer
 
 	err := tpl.Execute(&tplBuff, data)
 	if err != nil {
-		return tplBuff, err
+		return nil, err
 	}
 
-	return tplBuff, nil
+	return tplBuff.Bytes(), nil
 }
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 495f1fea9a2aa3702ab9321cbcf57fff3eb60d08..6a3bf90cb066a417153b064600fec466be0812f9 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -1453,6 +1453,14 @@
         "tslib": "^1.9.0"
       }
     },
+    "cidr-regex": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-2.0.10.tgz",
+      "integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==",
+      "requires": {
+        "ip-regex": "^2.1.0"
+      }
+    },
     "cipher-base": {
       "version": "1.0.4",
       "resolved": "https://registry.npm.taobao.org/cipher-base/download/cipher-base-1.0.4.tgz",
@@ -3553,14 +3561,12 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -3575,20 +3581,17 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -3705,8 +3708,7 @@
         "inherits": {
           "version": "2.0.4",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -3718,7 +3720,6 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -3733,7 +3734,6 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -3741,14 +3741,12 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.9.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -3767,7 +3765,6 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -3857,8 +3854,7 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -3870,7 +3866,6 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -3992,7 +3987,6 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -4762,8 +4756,7 @@
     "ip-regex": {
       "version": "2.1.0",
       "resolved": "https://registry.npm.taobao.org/ip-regex/download/ip-regex-2.1.0.tgz",
-      "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
-      "dev": true
+      "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk="
     },
     "ipaddr.js": {
       "version": "1.9.0",
@@ -4830,6 +4823,14 @@
       "integrity": "sha1-9+RrWWiQRW23Tn9ul2yzJz0G+qs=",
       "dev": true
     },
+    "is-cidr": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-3.1.0.tgz",
+      "integrity": "sha512-3kxTForpuj8O4iHn0ocsn1jxRm5VYm60GDghK6HXmpn4IyZOoRy9/GmdjFA2yEMqw91TB1/K3bFTuI7FlFNR1g==",
+      "requires": {
+        "cidr-regex": "^2.0.10"
+      }
+    },
     "is-color-stop": {
       "version": "1.1.0",
       "resolved": "https://registry.npm.taobao.org/is-color-stop/download/is-color-stop-1.1.0.tgz",
diff --git a/ui/package.json b/ui/package.json
index 09a15f12b58a0f2ccf624bf4943264f52b627a14..82eb32803489b1a2f93196adc3df5be00dfdfbd0 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -8,6 +8,7 @@
   },
   "dependencies": {
     "axios": "^0.19.2",
+    "is-cidr": "^3.1.0",
     "moment": "^2.24.0",
     "vue": "^2.6.10",
     "vue-moment": "^4.1.0",
diff --git a/ui/src/main.js b/ui/src/main.js
index b242f4a8ad23c498089a4826d816af5f39dd537d..6ba418755b8dbfecc00f77f704b303d12966f1e1 100644
--- a/ui/src/main.js
+++ b/ui/src/main.js
@@ -4,6 +4,7 @@ import router from './router'
 import vuetify from './plugins/vuetify';
 import './plugins/axios';
 import './plugins/moment';
+import './plugins/cidr'
 
 Vue.config.productionTip = false
 
diff --git a/ui/src/plugins/cidr.js b/ui/src/plugins/cidr.js
new file mode 100644
index 0000000000000000000000000000000000000000..78f33889adfe27f0c1205b7eba998fe4bfe1a642
--- /dev/null
+++ b/ui/src/plugins/cidr.js
@@ -0,0 +1,11 @@
+import Vue from 'vue'
+const isCidr = require('is-cidr');
+
+const plugin = {
+  install () {
+    Vue.isCidr = isCidr;
+    Vue.prototype.$isCidr = isCidr
+  }
+};
+
+Vue.use(plugin);
diff --git a/ui/src/views/Home.vue b/ui/src/views/Home.vue
index b2f9424260dccc6ea7a85f957bd56382b025a855..a6b70d103c3e25935e68942f886154c1e4bf8230 100644
--- a/ui/src/views/Home.vue
+++ b/ui/src/views/Home.vue
@@ -35,14 +35,26 @@
                         ]"
                       required
               />
-              <v-text-field
+              <v-combobox
                       v-model="server.address"
+                      chips
+                      hint="Write IPv4 or IPv6 CIDR and hit enter"
                       label="Server interface addresses"
-                      :rules="[
-                          v => !!v || 'Server interface address is required',
-                        ]"
-                      required
-              />
+                      multiple
+                      dark
+              >
+                <template v-slot:selection="{ attrs, item, select, selected }">
+                  <v-chip
+                          v-bind="attrs"
+                          :input-value="selected"
+                          close
+                          @click="select"
+                          @click:close="server.address.splice(server.address.indexOf(item), 1)"
+                  >
+                    <strong>{{ item }}</strong>&nbsp;
+                  </v-chip>
+                </template>
+              </v-combobox>
             </v-col>
             <v-col cols="6">
               <v-text-field
@@ -55,14 +67,6 @@
                       label="Preshared key"
                       disabled
               />
-              <v-text-field
-                      v-model="server.dns"
-                      label="DNS servers for clients"
-                      :rules="[
-                          v => !!v || 'DNS server is required',
-                        ]"
-                      required
-              />
               <v-text-field
                       v-model="server.listenPort"
                       type="number"
@@ -72,6 +76,26 @@
                       label="Listen port"
                       required
               />
+              <v-combobox
+                      v-model="server.dns"
+                      chips
+                      hint="Write IPv4 or IPv6 address and hit enter"
+                      label="DNS servers for clients"
+                      multiple
+                      dark
+              >
+                <template v-slot:selection="{ attrs, item, select, selected }">
+                  <v-chip
+                          v-bind="attrs"
+                          :input-value="selected"
+                          close
+                          @click="select"
+                          @click:close="server.dns.splice(server.dns.indexOf(item), 1)"
+                  >
+                    <strong>{{ item }}</strong>&nbsp;
+                  </v-chip>
+                </template>
+              </v-combobox>
             </v-col>
           </div>
 
@@ -99,7 +123,7 @@
             </v-list-item-content>
             <v-btn
                     color="success"
-                    @click.stop="dialogAddClient = true"
+                    @click.stop="startAddClient"
             >
               Add new client
               <v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
@@ -112,7 +136,7 @@
                     cols="6"
             >
               <v-card
-                      color="#1F7087"
+                      :color="client.enable ? '#1F7087' : 'warning'"
                       class="mx-auto"
                       raised
                       shaped
@@ -154,7 +178,7 @@
                   </v-btn>
                   <v-btn
                           text
-                          @click.stop="dialogEditClient = true; clientToEdit = client"
+                          @click.stop="editClient(client.id)"
                   >
                     Edit
                     <v-icon right dark>mdi-square-edit-outline</v-icon>
@@ -181,7 +205,7 @@
                               v-on="on"
                               color="success"
                               v-model="client.enable"
-                              v-on:change="updateClient(client)"
+                              v-on:change="disableClient(client)"
                       />
                     </template>
                     <span> {{client.enable ? 'Disable' : 'Enable'}} this client</span>
@@ -195,6 +219,7 @@
       </v-col>
     </v-row>
     <v-dialog
+            v-if="client"
             v-model="dialogAddClient"
             max-width="550"
     >
@@ -238,6 +263,27 @@
                         persistent-hint
                         required
                 />
+                <v-combobox
+                        v-model="client.allowedIPs"
+                        chips
+                        hint="Write IPv4 or IPv6 CIDR and hit enter"
+                        label="Allowed IPs"
+                        multiple
+                        dark
+                >
+                  <template v-slot:selection="{ attrs, item, select, selected }">
+                    <v-chip
+                            v-bind="attrs"
+                            :input-value="selected"
+                            close
+                            @click="select"
+                            @click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
+                    >
+                      <strong>{{ item }}</strong>&nbsp;
+                    </v-chip>
+                  </template>
+                </v-combobox>
+
                 <v-switch
                         v-model="client.enable"
                         color="red"
@@ -253,7 +299,7 @@
           <v-btn
                   :disabled="!valid"
                   color="success"
-                  @click="addClient()"
+                  @click="addClient(client)"
           >
             Submit
           </v-btn>
@@ -267,7 +313,7 @@
       </v-card>
     </v-dialog>
     <v-dialog
-            v-if="clientToEdit"
+            v-if="client"
             v-model="dialogEditClient"
             max-width="550"
     >
@@ -283,22 +329,42 @@
                       v-model="valid"
               >
                 <v-text-field
-                        v-model="clientToEdit.name"
-                        label="Client friendly name"
+                        v-model="client.name"
+                        label="Friendly name"
                         :rules="[
                           v => !!v || 'Client name is required',
                         ]"
                         required
                 />
                 <v-text-field
-                        v-model="clientToEdit.email"
-                        label="Client email"
+                        v-model="client.email"
+                        label="Email"
                         :rules="[
-                        v => !!v || 'E-mail is required',
-                        v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
-                      ]"
+                        v => !!v || 'Email is required',
+                        v => /.+@.+\..+/.test(v) || 'Email must be valid',
+                        ]"
                         required
                 />
+                <v-combobox
+                        v-model="client.allowedIPs"
+                        chips
+                        hint="Write IPv4 or IPv6 CIDR and hit enter"
+                        label="Allowed IPs"
+                        multiple
+                        dark
+                >
+                  <template v-slot:selection="{ attrs, item, select, selected }">
+                    <v-chip
+                            v-bind="attrs"
+                            :input-value="selected"
+                            close
+                            @click="select"
+                            @click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
+                    >
+                      <strong>{{ item }}</strong>&nbsp;
+                    </v-chip>
+                  </template>
+                </v-combobox>
               </v-form>
             </v-col>
           </v-row>
@@ -308,7 +374,7 @@
           <v-btn
                   :disabled="!valid"
                   color="success"
-                  @click="updateClient(clientToEdit)"
+                  @click="updateClient(client)"
           >
             Submit
           </v-btn>
@@ -361,22 +427,40 @@
       serverAddress: [],
       dialogAddClient: false,
       dialogEditClient: false,
-      clientToEdit: null,
-      client: {
-        name: "",
-        email: "",
-        enable: true,
-        allowedIPs: "0.0.0.0/0,::/0",
-        address: "",
-      }
+      client: null,
     }),
-
     methods: {
+      startAddClient() {
+        this.dialogAddClient = true;
+        this.client = {
+          name: "",
+          email: "",
+          enable: true,
+          allowedIPs: ["0.0.0.0/0", "::/0"],
+          address: "",
+        }
+      },
+      editClient(id) {
+        this.$get(`/client/${id}`).then((res) => {
+          this.dialogEditClient = true;
+          res.allowedIPs = res.allowedIPs.split(',');
+          this.client = res
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+      disableClient(client) {
+        client.allowedIPs = client.allowedIPs.split(',');
+        this.updateClient(client)
+      },
       getData() {
         this.$get('/server').then((res) => {
+          res.address = res.address.split(',');
+          res.dns = res.dns.split(',');
           this.server = res;
-          this.clientAddress = this.serverAddress = this.server.address.split(',')
+          this.clientAddress = this.serverAddress = this.server.address
         }).catch((e) => {
+          console.log(e)
           this.notify('error', e.response.status + ' ' + e.response.statusText);
         });
 
@@ -390,6 +474,20 @@
         // convert int values
         this.server.listenPort = parseInt(this.server.listenPort, 10);
         this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
+        // check server addresses
+        if (this.server.address.length < 1) {
+          this.notify('error', 'Please provide at least one valid CIDR address for server interface');
+          return;
+        }
+        for (let i = 0; i < this.server.address.length; i++){
+          if (this.$isCidr(this.server.address[i]) === 0) {
+            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
+            return
+          }
+        }
+        this.server.address = this.server.address.join(',');
+        this.server.dns = this.server.dns.join(',');
+
         this.$patch('/server', this.server).then((res) => {
           this.notify('success', "Server successfully updated");
           this.getData()
@@ -397,10 +495,23 @@
           this.notify('error', e.response.status + ' ' + e.response.statusText);
         });
       },
-      addClient () {
+      addClient(client) {
+        if (client.allowedIPs.length < 1) {
+          this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
+          return;
+        }
+        for (let i = 0; i < client.allowedIPs.length; i++){
+          if (this.$isCidr(client.allowedIPs[i]) === 0) {
+            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
+            return
+          }
+        }
+
         this.dialogAddClient = false;
-        this.client.address = this.clientAddress.join(',');
-        this.$post('/client', this.client).then((res) => {
+        client.address = this.clientAddress.join(',');
+        client.allowedIPs = this.client.allowedIPs.join(',');
+
+        this.$post('/client', client).then((res) => {
           this.notify('success', "Client successfully added");
           this.getData()
         }).catch((e) => {
@@ -437,7 +548,20 @@
         }
       },
       updateClient(client) {
+        if (client.allowedIPs.length < 1) {
+          this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
+          return;
+        }
+        for (let i = 0; i < client.allowedIPs.length; i++){
+          if (this.$isCidr(client.allowedIPs[i]) === 0) {
+            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
+            return
+          }
+        }
+
         this.dialogEditClient = false;
+        client.allowedIPs = client.allowedIPs.join(',');
+
         this.$patch(`/client/${client.id}`, client).then((res) => {
           this.notify('success', "Client successfully updated");
           this.getData()
diff --git a/util/util.go b/util/util.go
index 0b005ea2792744260a14cef4a9413d66391cd38f..cced1a9b01ec72b374b6edff1e68c77d72122a84 100644
--- a/util/util.go
+++ b/util/util.go
@@ -84,6 +84,11 @@ func GetAllAddressesFromCidr(cidr string) ([]string, error) {
 	return ips[2 : len(ips)-1], nil
 }
 
+// IsIPv6 check if given ip is IPv6
+func IsIPv6(address string) bool {
+	return strings.Count(address, ":") >= 2
+}
+
 //  http://play.golang.org/p/m8TNTtygK0
 func inc(ip net.IP) {
 	for j := len(ip) - 1; j >= 0; j-- {
@@ -93,8 +98,3 @@ func inc(ip net.IP) {
 		}
 	}
 }
-
-// IsIPv6 check if given ip is IPv6
-func IsIPv6(address string) bool {
-	return strings.Count(address, ":") >= 2
-}
diff --git a/wg-gen-web_screenshot.png b/wg-gen-web_screenshot.png
index 667c7a150072ba838bf4eba4c5810ee389dd7391..fb5407b94d639e6dbeb4a4fb566194270605166e 100644
Binary files a/wg-gen-web_screenshot.png and b/wg-gen-web_screenshot.png differ