Repository: netbirdio/netbird

Issue:
ios-client cannot be in Connected state after switching to the background.
**Describe the problem**

Sorry for posting an issue here (the same issue has been posted in the ios-client repository).

After opening Netbird ios-client on my iPhone and switching to other apps, Netbird ios-client seems to automatically close and disconnect.

I don't find an option in the iPhone settings for the Netbird client to keep refreshing in the background. Sadly, this affects the network I use netbird on my iPhone.

I don’t know if anyone has encountered a similar situation. 

How to solve it?

**To Reproduce**

Steps to reproduce the behavior:
1. Open Netbird ios client on my iPhone
2. Click ‘Connect’ and see that the status shows Connected
3. Switch to other apps to use
4. After about 10 seconds, switch back to the Netbird ios client and it will display as `Disconnected`

**Expected behavior**

Netbird ios client can be in `Connected` state.

**Are you using NetBird Cloud?**

self-host.

**NetBird version**

`netbird version` 

ios-client 0.0.3
ios 17.4.1

**NetBird status -d output:**

Not applicable in ios-client

**Screenshots**

https://github.com/netbirdio/netbird/assets/27892476/ebe8dfc9-0e90-45a3-8fa6-4444b2bd641e

![image](https://github.com/netbirdio/netbird/assets/27892476/279c4d19-0ee3-442a-be7d-4994165eea4f)

**Additional context**

https://github.com/netbirdio/ios-client/issues/9

Hints from issue comments:
@jiangslee: EXC_RESOURCE -> NetbirdNetworkExtension[5634] exceeded mem limit: ActiveHard 50 MB (fatal)

I connected to my iPhone 12 and got the console log. It seems that my iPhone 12 has insufficient running memory, causing the kernel to kill the background process.

<img width="1232" alt="image" src="https://github.com/netbirdio/netbird/assets/27892476/50bf5e75-2633-44b1-88a9-64c551901fd3">
<img width="1234" alt="image" src="https://github.com/netbirdio/netbird/assets/27892476/14511f63-2fce-4e6b-b931-e4bd1b48482c">
@mlsmaycon: Hello @jiangslee, this shouldn't be the case. To help us understand better the case, can you update the app to the latest version, enable trace logs, and export them by accessing the Advanced menu of NetBird's app?@jiangslee: Hello @mlsmaycon, Thank you very much for paying attention to this problem of mine. 

I have updated to the latest version 0.0.4 and enabled Trace Logs, but I did not see any logs file.

![image](https://github.com/netbirdio/netbird/assets/27892476/49645883-d05c-41af-859e-51d1f3488662)
![image](https://github.com/netbirdio/netbird/assets/27892476/7a308387-fd5e-4de4-8ad4-df1fa011a694)

@mlsmaycon: You need to click on share logs, that will let you choose a path in your device to store the logs which then you can use to share them with us.@jiangslee: The console Logs show that there are some deny records, but I was not prompted for authorization.

Do I need to enable any settings on my iPhone?

<img width="1248" alt="image" src="https://github.com/netbirdio/netbird/assets/27892476/d83ea167-00c3-4844-83b1-ccbae4b618a2">
@jiangslee: EXC_RESOURCE -> NetbirdNetworkExtension[6591] exceeded mem limit: ActiveHard 50 MB (fatal)

<img width="1239" alt="image" src="https://github.com/netbirdio/netbird/assets/27892476/c7b98d06-f42f-4d85-933b-8d3aee7e9754">
@jiangslee: **choose a path in your device to store the logs **
sorry , I just got it@jiangslee: @mlsmaycon I’ve sent you the log file on Slack@KortanZ: Same problem. IOS app become quite unusable these days. I can't recall since when, maybe after upgrade IOS version to 17.4, connection will lost when netbird app are background. Repeat switch foreground reconnect and switch background auto disconnect 3 or 4 times, connection seems could stable for a while. Situation became worse recently, needs more retry.@jiangslee: @KortanZ How many peers you have?
The temporary solution is to modify the peers accessible to the iPhone in Access Control Prolicies. Currently, my iPhone only allows 7 peers to communicate with each other. After switching to the background, the normal connection can still be maintained.

I have sent the logs to the Netbird team via slack, hoping they can find a solution.

In addition, I searched for relevant information. The possible reason is that network extension (tunnel) has memory limitations in iOS.
https://developer.apple.com/forums/thread/73148?page=2
>> for tunnel providers it seems to be 50 MB now.
> Indeed. That’s a big bump!
>
> Anyway, here’s what I see on iOS 15.0:
>
> Provider Limit (MiB)
> packet tunnel 50
> app proxy 15
> filter control 12
> filter data 6
> DNS proxy 15
> app push 24
> And, just so we’re clear:
>
> IMPORTANT These limits have changed in the past and may well change in the future. I'm posting them to assist in your debugging. You should not hard code knowledge about these limits into your code. The only way to ensure that your provider can run within the system's memory limits is to thoroughly test it on a wide range of device and OS combinations.
>
> Share and Enjoy
> —
> Quinn “The Eskimo!” @ Developer Technical Support @ Apple
> let myEmail = "eskimo" + "1" + "@" + "apple.com"@KortanZ: Really thanks for help! @jiangslee 

> How many peers you have?

total peers are around over 20, active peers may around 4 to 10.

And i just upgrade ios to version 17.5, and situation is way much better. Connection will last a long period and only disconnet if app go background for quite a while. May be something wrong with my own server's reverse proxy configurition which cause gRPC timeout. I'm using Caddy with a really simple config. I'll use official nginx config to have a try to see if there any different happened.
@jiangslee: @pascal-fischer Thanks for your hard work.@KortanZ: @pascal-fischer hi, after #1981 , netbird ios client works well on my ios 17 device, but after update to ios 18.0 recently, the issue raise again, which connection will lost after i switch client to background.@jiangslee: @KortanZ  @pascal-fischer This bug seems to have not been fixed yet. I currently limit the number of peers that can connect to each other through policies to ensure that Netbird can be used temporarily under iOS.@nazarewk: Hello @jiangslee,

We're currently reviewing our open issues and would like to verify if this problem still exists in the [latest NetBird version](https://github.com/netbirdio/netbird/releases).

Could you please confirm if the issue is still there?

We may close this issue temporarily if we don't hear back from you within **2 weeks**, but feel free to reopen it with updated information.

Thanks for your contribution to improving the project!
@mlsmaycon: closing issue due to no recent feedback. Feel free to open a new one if the issue persist or reopen if this was a feature request.

Solution patch:
diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml
index 42f740e9..4259f1b3 100644
--- a/.github/workflows/golang-test-linux.yml
+++ b/.github/workflows/golang-test-linux.yml
@@ -15,7 +15,7 @@ jobs:
     strategy:
       matrix:
         arch: [ '386','amd64' ]
-        store: [ 'jsonfile', 'sqlite' ]
+        store: [ 'jsonfile', 'sqlite', 'postgres']
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
diff --git a/client/internal/connect.go b/client/internal/connect.go
index eb84940d..83909dfd 100644
--- a/client/internal/connect.go
+++ b/client/internal/connect.go
@@ -91,6 +91,9 @@ func (c *ConnectClient) RunOniOS(
 	networkChangeListener listener.NetworkChangeListener,
 	dnsManager dns.IosDnsManager,
 ) error {
+	// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
+	debug.SetGCPercent(5)
+
 	mobileDependency := MobileDependency{
 		FileDescriptor:        fileDescriptor,
 		NetworkChangeListener: networkChangeListener,
diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go
index cc31559f..e82c98fb 100644
--- a/client/internal/dns/upstream.go
+++ b/client/internal/dns/upstream.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"net"
-	"runtime"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -260,13 +259,10 @@ func (u *upstreamResolverBase) disable(err error) {
 		return
 	}
 
-	// todo test the deactivation logic, it seems to affect the client
-	if runtime.GOOS != "ios" {
-		log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod)
-		u.deactivate(err)
-		u.disabled = true
-		go u.waitUntilResponse()
-	}
+	log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod)
+	u.deactivate(err)
+	u.disabled = true
+	go u.waitUntilResponse()
 }
 
 func (u *upstreamResolverBase) testNameserver(server string) error {
diff --git a/client/internal/engine.go b/client/internal/engine.go
index c81b1321..351b21b2 100644
--- a/client/internal/engine.go
+++ b/client/internal/engine.go
@@ -133,7 +133,7 @@ type Engine struct {
 	// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
 	networkSerial uint64
 
-	networkWatcher *networkmonitor.NetworkWatcher
+	networkMonitor *networkmonitor.NetworkMonitor
 
 	sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error)
 	sshServer     nbssh.Server
@@ -212,7 +212,6 @@ func NewEngineWithProbes(
 		networkSerial:  0,
 		sshServerFunc:  nbssh.DefaultSSHServer,
 		statusRecorder: statusRecorder,
-		networkWatcher: networkmonitor.New(),
 		mgmProbe:       mgmProbe,
 		signalProbe:    signalProbe,
 		relayProbe:     relayProbe,
@@ -229,7 +228,10 @@ func (e *Engine) Stop() error {
 	}
 
 	// stopping network monitor first to avoid starting the engine again
-	e.networkWatcher.Stop()
+	if e.networkMonitor != nil {
+		e.networkMonitor.Stop()
+	}
+	log.Info("Network monitor: stopped")
 
 	err := e.removeAllPeers()
 	if err != nil {
@@ -259,7 +261,7 @@ func (e *Engine) Start() error {
 	}
 	e.ctx, e.cancel = context.WithCancel(e.clientCtx)
 
-	e.wgProxyFactory = wgproxy.NewFactory(e.clientCtx, e.config.WgPort)
+	e.wgProxyFactory = wgproxy.NewFactory(e.ctx, e.config.WgPort)
 
 	wgIface, err := e.newWgIface()
 	if err != nil {
@@ -344,20 +346,8 @@ func (e *Engine) Start() error {
 	e.receiveManagementEvents()
 	e.receiveProbeEvents()
 
-	if e.config.NetworkMonitor {
-		// starting network monitor at the very last to avoid disruptions
-		go e.networkWatcher.Start(e.ctx, func() {
-			log.Infof("Network monitor detected network change, restarting engine")
-			if err := e.Stop(); err != nil {
-				log.Errorf("Failed to stop engine: %v", err)
-			}
-			if err := e.Start(); err != nil {
-				log.Errorf("Failed to start engine: %v", err)
-			}
-		})
-	} else {
-		log.Infof("Network monitor is disabled, not starting")
-	}
+	// starting network monitor at the very last to avoid disruptions
+	e.startNetworkMonitor()
 
 	return nil
 }
@@ -1399,3 +1389,26 @@ func (e *Engine) probeSTUNs() []relay.ProbeResult {
 func (e *Engine) probeTURNs() []relay.ProbeResult {
 	return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs)
 }
+
+func (e *Engine) startNetworkMonitor() {
+	if !e.config.NetworkMonitor {
+		log.Infof("Network monitor is disabled, not starting")
+		return
+	}
+
+	e.networkMonitor = networkmonitor.New()
+	go func() {
+		err := e.networkMonitor.Start(e.ctx, func() {
+			log.Infof("Network monitor detected network change, restarting engine")
+			if err := e.Stop(); err != nil {
+				log.Errorf("Failed to stop engine: %v", err)
+			}
+			if err := e.Start(); err != nil {
+				log.Errorf("Failed to start engine: %v", err)
+			}
+		})
+		if err != nil && !errors.Is(err, networkmonitor.ErrStopped) {
+			log.Errorf("Network monitor: %v", err)
+		}
+	}()
+}
diff --git a/client/internal/networkmonitor/monitor.go b/client/internal/networkmonitor/monitor.go
index 71cf031b..5475455c 100644
--- a/client/internal/networkmonitor/monitor.go
+++ b/client/internal/networkmonitor/monitor.go
@@ -2,14 +2,20 @@ package networkmonitor
 
 import (
 	"context"
+	"errors"
+	"sync"
 )
 
-// NetworkWatcher watches for changes in network configuration.
-type NetworkWatcher struct {
+var ErrStopped = errors.New("monitor has been stopped")
+
+// NetworkMonitor watches for changes in network configuration.
+type NetworkMonitor struct {
 	cancel context.CancelFunc
+	wg     sync.WaitGroup
+	mu     sync.Mutex
 }
 
 // New creates a new network monitor.
-func New() *NetworkWatcher {
-	return &NetworkWatcher{}
+func New() *NetworkMonitor {
+	return &NetworkMonitor{}
 }
diff --git a/client/internal/networkmonitor/monitor_bsd.go b/client/internal/networkmonitor/monitor_bsd.go
index e15c08d7..de4209f5 100644
--- a/client/internal/networkmonitor/monitor_bsd.go
+++ b/client/internal/networkmonitor/monitor_bsd.go
@@ -31,7 +31,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac
 	for {
 		select {
 		case <-ctx.Done():
-			return ctx.Err()
+			return ErrStopped
 		default:
 			buf := make([]byte, 2048)
 			n, err := unix.Read(fd, buf)
@@ -63,7 +63,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac
 				}
 
 				log.Infof("Network monitor: monitored interface (%s) is down.", ifinfo.Name)
-				callback()
+				go callback()
 
 			// handle route changes
 			case unix.RTM_ADD, syscall.RTM_DELETE:
@@ -84,11 +84,11 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac
 				switch msg.Type {
 				case unix.RTM_ADD:
 					log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf)
-					callback()
+					go callback()
 				case unix.RTM_DELETE:
 					if intfv4 != nil && route.Gw.Compare(nexthopv4) == 0 || intfv6 != nil && route.Gw.Compare(nexthopv6) == 0 {
 						log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf)
-						callback()
+						go callback()
 					}
 				}
 			}
diff --git a/client/internal/networkmonitor/monitor_generic.go b/client/internal/networkmonitor/monitor_generic.go
index 329246c8..97cfbc2c 100644
--- a/client/internal/networkmonitor/monitor_generic.go
+++ b/client/internal/networkmonitor/monitor_generic.go
@@ -5,6 +5,7 @@ package networkmonitor
 import (
 	"context"
 	"errors"
+	"fmt"
 	"net"
 	"net/netip"
 	"runtime/debug"
@@ -15,20 +16,18 @@ import (
 	"github.com/netbirdio/netbird/client/internal/routemanager"
 )
 
-// Start begins watching for network changes and calls the callback function and stops when a change is detected.
-func (nw *NetworkWatcher) Start(ctx context.Context, callback func()) {
-	if nw.cancel != nil {
-		log.Warn("Network monitor: already running, stopping previous watcher")
-		nw.Stop()
-	}
-
+// Start begins monitoring network changes. When a change is detected, it calls the callback asynchronously and returns.
+func (nw *NetworkMonitor) Start(ctx context.Context, callback func()) (err error) {
 	if ctx.Err() != nil {
-		log.Info("Network monitor: not starting, context is already cancelled")
-		return
+		return ctx.Err()
 	}
 
+	nw.mu.Lock()
 	ctx, nw.cancel = context.WithCancel(ctx)
-	defer nw.Stop()
+	nw.mu.Unlock()
+
+	nw.wg.Add(1)
+	defer nw.wg.Done()
 
 	var nexthop4, nexthop6 netip.Addr
 	var intf4, intf6 *net.Interface
@@ -56,27 +55,30 @@ func (nw *NetworkWatcher) Start(ctx context.Context, callback func()) {
 	expBackOff := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
 
 	if err := backoff.Retry(operation, expBackOff); err != nil {
-		log.Errorf("Network monitor: failed to get default next hops: %v", err)
-		return
+		return fmt.Errorf("failed to get default next hops: %w", err)
 	}
 
 	// recover in case sys ops panic
 	defer func() {
 		if r := recover(); r != nil {
-			log.Errorf("Network monitor: panic occurred: %v, stack trace: %s", r, string(debug.Stack()))
+			err = fmt.Errorf("panic occurred: %v, stack trace: %s", r, string(debug.Stack()))
 		}
 	}()
 
-	if err := checkChange(ctx, nexthop4, intf4, nexthop6, intf6, callback); err != nil && !errors.Is(err, context.Canceled) {
-		log.Errorf("Network monitor: failed to start: %v", err)
+	if err := checkChange(ctx, nexthop4, intf4, nexthop6, intf6, callback); err != nil {
+		return fmt.Errorf("check change: %w", err)
 	}
+
+	return nil
 }
 
 // Stop stops the network monitor.
-func (nw *NetworkWatcher) Stop() {
+func (nw *NetworkMonitor) Stop() {
+	nw.mu.Lock()
+	defer nw.mu.Unlock()
+
 	if nw.cancel != nil {
 		nw.cancel()
-		nw.cancel = nil
-		log.Info("Network monitor: stopped")
+		nw.wg.Wait()
 	}
 }
diff --git a/client/internal/networkmonitor/monitor_linux.go b/client/internal/networkmonitor/monitor_linux.go
index f39f1235..3f93c6ac 100644
--- a/client/internal/networkmonitor/monitor_linux.go
+++ b/client/internal/networkmonitor/monitor_linux.go
@@ -36,7 +36,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac
 	for {
 		select {
 		case <-ctx.Done():
-			return ctx.Err()
+			return ErrStopped
 
 		// handle interface state changes
 		case update := <-linkChan:
@@ -47,12 +47,12 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac
 			switch update.Header.Type {
 			case syscall.RTM_DELLINK:
 				log.Infof("Network monitor: monitored interface (%s) is gone", update.Link.Attrs().Name)
-				callback()
+				go callback()
 				return nil
 			case syscall.RTM_NEWLINK:
 				if (update.IfInfomsg.Flags&syscall.IFF_RUNNING) == 0 && update.Link.Attrs().OperState == netlink.OperDown {
 					log.Infof("Network monitor: monitored interface (%s) is down.", update.Link.Attrs().Name)
-					callback()
+					go callback()
 					return nil
 				}
 			}
@@ -67,12 +67,12 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac
 			// triggered on added/replaced routes
 			case syscall.RTM_NEWROUTE:
 				log.Infof("Network monitor: default route changed: via %s, interface %d", route.Gw, route.LinkIndex)
-				callback()
+				go callback()
 				return nil
 			case syscall.RTM_DELROUTE:
 				if intfv4 != nil && route.Gw.Equal(nexthopv4.AsSlice()) || intfv6 != nil && route.Gw.Equal(nexthop6.AsSlice()) {
 					log.Infof("Network monitor: default route removed: via %s, interface %d", route.Gw, route.LinkIndex)
-					callback()
+					go callback()
 					return nil
 				}
 			}
diff --git a/client/internal/networkmonitor/monitor_mobile.go b/client/internal/networkmonitor/monitor_mobile.go
index 988f296b..c81fad16 100644
--- a/client/internal/networkmonitor/monitor_mobile.go
+++ b/client/internal/networkmonitor/monitor_mobile.go
@@ -4,8 +4,9 @@ package networkmonitor
 
 import "context"
 
-func (nw *NetworkWatcher) Start(context.Context, func()) {
+func (nw *NetworkMonitor) Start(context.Context, func()) error {
+	return nil
 }
 
-func (nw *NetworkWatcher) Stop() {
+func (nw *NetworkMonitor) Stop() {
 }
diff --git a/client/internal/networkmonitor/monitor_windows.go b/client/internal/networkmonitor/monitor_windows.go
index f6c5d963..b8d9c6de 100644
--- a/client/internal/networkmonitor/monitor_windows.go
+++ b/client/internal/networkmonitor/monitor_windows.go
@@ -48,10 +48,10 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac
 	for {
 		select {
 		case <-ctx.Done():
-			return ctx.Err()
+			return ErrStopped
 		case <-ticker.C:
 			if changed(nexthopv4, intfv4, neighborv4, nexthopv6, intfv6, neighborv6) {
-				callback()
+				go callback()
 				return nil
 			}
 		}
diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go
index 1ee8cdd7..3c4cfb13 100644
--- a/client/internal/peer/conn.go
+++ b/client/internal/peer/conn.go
@@ -487,6 +487,10 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int, rem
 		return nil, err
 	}
 
+	if runtime.GOOS == "ios" {
+		runtime.GC()
+	}
+
 	if conn.onConnected != nil {
 		conn.onConnected(conn.config.Key, remoteRosenpassPubKey, ipNet.IP.String(), remoteRosenpassAddr)
 	}
diff --git a/client/internal/wgproxy/proxy_ebpf.go b/client/internal/wgproxy/proxy_ebpf.go
index 01e8766e..bbd00d6e 100644
--- a/client/internal/wgproxy/proxy_ebpf.go
+++ b/client/internal/wgproxy/proxy_ebpf.go
@@ -109,7 +109,6 @@ func (p *WGEBPFProxy) AddTurnConn(turnConn net.Conn) (net.Addr, error) {
 
 // CloseConn doing nothing because this type of proxy implementation does not store the connection
 func (p *WGEBPFProxy) CloseConn() error {
-	p.cancel()
 	return nil
 }
 
diff --git a/go.mod b/go.mod
index 24af3c28..f75ddcb6 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ toolchain go1.21.0
 
 require (
 	cunicu.li/go-rosenpass v0.4.0
-	github.com/cenkalti/backoff/v4 v4.1.3
+	github.com/cenkalti/backoff/v4 v4.2.0
 	github.com/cloudflare/circl v1.3.3 // indirect
 	github.com/golang-jwt/jwt v3.2.2+incompatible
 	github.com/golang/protobuf v1.5.3
@@ -54,7 +54,7 @@ require (
 	github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
 	github.com/hashicorp/go-version v1.6.0
 	github.com/libp2p/go-netroute v0.2.1
-	github.com/magiconair/properties v1.8.5
+	github.com/magiconair/properties v1.8.7
 	github.com/mattn/go-sqlite3 v1.14.19
 	github.com/mdlayher/socket v0.4.1
 	github.com/miekg/dns v1.1.43
@@ -72,6 +72,8 @@ require (
 	github.com/rs/xid v1.3.0
 	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
 	github.com/stretchr/testify v1.8.4
+	github.com/testcontainers/testcontainers-go v0.20.0
+	github.com/testcontainers/testcontainers-go/modules/postgres v0.20.0
 	github.com/things-go/go-socks5 v0.0.4
 	github.com/yusufpapurcu/wmi v1.2.3
 	github.com/zcalusic/sysinfo v1.0.2
@@ -88,22 +90,31 @@ require (
 	golang.org/x/term v0.18.0
 	google.golang.org/api v0.126.0
 	gopkg.in/yaml.v3 v3.0.1
+	gorm.io/driver/postgres v1.5.7
 	gorm.io/driver/sqlite v1.5.3
-	gorm.io/gorm v1.25.4
+	gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
 )
 
 require (
 	cloud.google.com/go/compute v1.19.3 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
+	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/BurntSushi/toml v1.2.1 // indirect
+	github.com/Microsoft/go-winio v0.6.0 // indirect
 	github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/containerd/containerd v1.6.19 // indirect
+	github.com/cpuguy83/dockercfg v0.3.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dgraph-io/ristretto v0.1.1 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/docker/distribution v2.8.1+incompatible // indirect
+	github.com/docker/docker v23.0.5+incompatible // indirect
+	github.com/docker/go-connections v0.4.0 // indirect
+	github.com/docker/go-units v0.5.0 // indirect
 	github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
 	github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
 	github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
@@ -118,29 +129,43 @@ require (
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-redis/redis/v8 v8.11.5 // indirect
 	github.com/go-stack/stack v1.8.0 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/btree v1.0.1 // indirect
 	github.com/google/s2a-go v0.1.4 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/googleapis/gax-go/v2 v2.10.0 // indirect
-	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-uuid v1.0.2 // indirect
+	github.com/imdario/mergo v0.3.12 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+	github.com/jackc/pgx/v5 v5.4.3 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/josharian/native v1.1.0 // indirect
 	github.com/kelseyhightower/envconfig v1.4.0 // indirect
+	github.com/klauspost/compress v1.15.9 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/mdlayher/genetlink v1.3.2 // indirect
 	github.com/mdlayher/netlink v1.7.2 // indirect
+	github.com/moby/patternmatcher v0.5.0 // indirect
+	github.com/moby/sys/sequential v0.5.0 // indirect
+	github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f // indirect
+	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
+	github.com/opencontainers/go-digest v1.0.0 // indirect
+	github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
+	github.com/opencontainers/runc v1.1.5 // indirect
 	github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
 	github.com/pegasus-kv/thrift v0.13.0 // indirect
 	github.com/pion/dtls/v2 v2.2.10 // indirect
 	github.com/pion/mdns v0.0.12 // indirect
 	github.com/pion/randutil v0.1.0 // indirect
 	github.com/pion/transport/v2 v2.2.4 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_model v0.3.0 // indirect
 	github.com/prometheus/common v0.37.0 // indirect
@@ -154,12 +179,13 @@ require (
 	go.opentelemetry.io/otel/sdk v1.11.1 // indirect
 	go.opentelemetry.io/otel/trace v1.11.1 // indirect
 	golang.org/x/image v0.10.0 // indirect
+	golang.org/x/mod v0.12.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
+	golang.org/x/tools v0.13.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
-	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
diff --git a/go.sum b/go.sum
index 9b7b8952..49314e72 100644
--- a/go.sum
+++ b/go.sum
@@ -40,12 +40,18 @@ cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJpl
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 fyne.io/fyne/v2 v2.1.4 h1:bt1+28++kAzRzPB0GM2EuSV4cnl8rXNX4cjfd8G06Rc=
 fyne.io/fyne/v2 v2.1.4/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
 github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
+github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
+github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
+github.com/Microsoft/hcsshim v0.9.7 h1:mKNHW/Xvv1aFH87Jb6ERDzXTJTLPlmzfZ28VBFD/bfg=
+github.com/Microsoft/hcsshim v0.9.7/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
@@ -73,16 +79,18 @@ github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQ
 github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU=
 github.com/c-robinson/iplib v1.0.3/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
-github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
-github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
+github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
+github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
 github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y=
 github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -92,16 +100,25 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
 github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/containerd/containerd v1.6.19 h1:F0qgQPrG0P2JPgwpxWxYavrVeXAG0ezUIB9Z/4FTUAU=
+github.com/containerd/containerd v1.6.19/go.mod h1:HZCDMn4v/Xl2579/MvtOC2M206i+JJ6VxFWU/NetrGY=
+github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
+github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
 github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U=
 github.com/coocood/freecache v1.2.1/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
 github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
 github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
+github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:/DS5cDX3FJdl+XaN2D7XAwFpuanTxnp52DBLZAaJKx0=
 github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
+github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -111,6 +128,15 @@ github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkz
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
+github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v23.0.5+incompatible h1:DaxtlTJjFSnLOXVNUBU1+6kXGz2lpDoEAH6QoxaSg8k=
+github.com/docker/docker v23.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -127,6 +153,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
 github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
 github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
@@ -188,10 +215,13 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
 github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
@@ -290,8 +320,9 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 h1:Fkzd8ktnpOR9h47SXHe2AYPwelXLH2GjGsjlAloiWfo=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
@@ -305,8 +336,16 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
+github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
 github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -332,7 +371,10 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
 github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
 github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
+github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -345,11 +387,13 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
 github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
 github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
-github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
-github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
 github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
@@ -369,12 +413,22 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE9
 github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
 github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
 github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
+github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
+github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
+github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
+github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
+github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f h1:J/7hjLaHLD7epG0m6TBMGmp4NQ+ibBYLfeyJWdAIFLA=
+github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f/go.mod h1:15ce4BGCFxt7I5NQKT+HV0yEDxmf6fSysfEDiVo3zFM=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -414,6 +468,14 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
 github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
+github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
+github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
+github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
 github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
 github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
@@ -422,6 +484,7 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pegasus-kv/thrift v0.13.0 h1:4ESwaNoHImfbHa9RUGJiJZ4hrxorihZHk5aarYwY8d4=
 github.com/pegasus-kv/thrift v0.13.0/go.mod h1:Gl9NT/WHG6ABm6NsrbfE8LiJN0sAyneCrvB4qN4NPqQ=
+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
 github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
@@ -485,10 +548,12 @@ github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
 github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
@@ -526,13 +591,21 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/testcontainers/testcontainers-go v0.20.0 h1:ASrcJee7vcWNw43yUgL2n8KA5IOywrF031GawlrkVkE=
+github.com/testcontainers/testcontainers-go v0.20.0/go.mod h1:zb+NOlCQBkZ7RQp4QI+YMIHyO2CQ/qsXzNF5eLJ24SY=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.20.0 h1:skGd0Tv6USw6c9aJwea+Mb2WonLqf6N5npbS5WxbGQ0=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.20.0/go.mod h1:wtdaiIzG+DlZ/0DbNvrJ89TT7RUer8ZnRcv4y+xHcU8=
 github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0=
 github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
 github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So=
 github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
 github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
 github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
 github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
@@ -623,6 +696,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -658,6 +733,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
@@ -709,6 +785,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -718,6 +795,8 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -752,6 +831,9 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -834,14 +916,18 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -944,6 +1030,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
@@ -982,10 +1069,14 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
+gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
 gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
 gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
-gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
-gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
+gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
+gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
 gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
 gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/management/cmd/migration_down.go b/management/cmd/migration_down.go
index 6d136ec1..81ef93a6 100644
--- a/management/cmd/migration_down.go
+++ b/management/cmd/migration_down.go
@@ -7,10 +7,11 @@ import (
 	"os"
 	"path"
 
-	"github.com/netbirdio/netbird/management/server"
-	"github.com/netbirdio/netbird/util"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
+
+	"github.com/netbirdio/netbird/management/server"
+	"github.com/netbirdio/netbird/util"
 )
 
 var shortDown = "Rollback SQLite store to JSON file store. Please make a backup of the SQLite file before running this command."
@@ -39,16 +40,16 @@ var downCmd = &cobra.Command{
 			return fmt.Errorf("%s already exists, couldn't continue the operation", fileStorePath)
 		}
 
-		sqlstore, err := server.NewSqliteStore(mgmtDataDir, nil)
+		sqlStore, err := server.NewSqliteStore(mgmtDataDir, nil)
 		if err != nil {
 			return fmt.Errorf("failed creating file store: %s: %v", mgmtDataDir, err)
 		}
 
-		sqliteStoreAccounts := len(sqlstore.GetAllAccounts())
+		sqliteStoreAccounts := len(sqlStore.GetAllAccounts())
 		log.Infof("%d account will be migrated from sqlite store %s to file store %s",
 			sqliteStoreAccounts, sqliteStorePath, fileStorePath)
 
-		store, err := server.NewFilestoreFromSqliteStore(sqlstore, mgmtDataDir, nil)
+		store, err := server.NewFilestoreFromSqliteStore(sqlStore, mgmtDataDir, nil)
 		if err != nil {
 			return fmt.Errorf("failed creating file store: %s: %v", mgmtDataDir, err)
 		}
diff --git a/management/server/file_store.go b/management/server/file_store.go
index a6e29ec4..10f2240f 100644
--- a/management/server/file_store.go
+++ b/management/server/file_store.go
@@ -57,18 +57,18 @@ func NewFileStore(dataDir string, metrics telemetry.AppMetrics) (*FileStore, err
 }
 
 // NewFilestoreFromSqliteStore restores a store from Sqlite and stores to Filestore json in the file located in datadir
-func NewFilestoreFromSqliteStore(sqlitestore *SqliteStore, dataDir string, metrics telemetry.AppMetrics) (*FileStore, error) {
+func NewFilestoreFromSqliteStore(sqlStore *SqlStore, dataDir string, metrics telemetry.AppMetrics) (*FileStore, error) {
 	store, err := NewFileStore(dataDir, metrics)
 	if err != nil {
 		return nil, err
 	}
 
-	err = store.SaveInstallationID(sqlitestore.GetInstallationID())
+	err = store.SaveInstallationID(sqlStore.GetInstallationID())
 	if err != nil {
 		return nil, err
 	}
 
-	for _, account := range sqlitestore.GetAllAccounts() {
+	for _, account := range sqlStore.GetAllAccounts() {
 		store.Accounts[account.Id] = account
 	}
 
diff --git a/management/server/policy.go b/management/server/policy.go
index 704825ca..5206df9e 100644
--- a/management/server/policy.go
+++ b/management/server/policy.go
@@ -148,7 +148,7 @@ type Policy struct {
 	Enabled bool
 
 	// Rules of the policy
-	Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"`
+	Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id;constraint:OnDelete:CASCADE;"`
 
 	// SourcePostureChecks are ID references to Posture checks for policy source groups
 	SourcePostureChecks []string `gorm:"serializer:json"`
diff --git a/management/server/sqlite_store.go b/management/server/sql_store.go
similarity index 80%
rename from management/server/sqlite_store.go
rename to management/server/sql_store.go
index a206575a..8606267b 100644
--- a/management/server/sqlite_store.go
+++ b/management/server/sql_store.go
@@ -3,8 +3,6 @@ package server
 import (
 	"errors"
 	"fmt"
-	"net"
-	"net/netip"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -12,6 +10,7 @@ import (
 	"time"
 
 	log "github.com/sirupsen/logrus"
+	"gorm.io/driver/postgres"
 	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
@@ -20,7 +19,6 @@ import (
 	nbdns "github.com/netbirdio/netbird/dns"
 	"github.com/netbirdio/netbird/management/server/account"
 	nbgroup "github.com/netbirdio/netbird/management/server/group"
-	"github.com/netbirdio/netbird/management/server/migration"
 	nbpeer "github.com/netbirdio/netbird/management/server/peer"
 	"github.com/netbirdio/netbird/management/server/posture"
 	"github.com/netbirdio/netbird/management/server/status"
@@ -28,14 +26,14 @@ import (
 	"github.com/netbirdio/netbird/route"
 )
 
-// SqliteStore represents an account storage backed by a Sqlite DB persisted to disk
-type SqliteStore struct {
+// SqlStore represents an account storage backed by a Sql DB persisted to disk
+type SqlStore struct {
 	db                *gorm.DB
-	storeFile         string
 	accountLocks      sync.Map
 	globalAccountLock sync.Mutex
 	metrics           telemetry.AppMetrics
 	installationPK    int
+	storeEngine       StoreEngine
 }
 
 type installation struct {
@@ -45,24 +43,8 @@ type installation struct {
 
 type migrationFunc func(*gorm.DB) error
 
-// NewSqliteStore restores a store from the file located in the datadir
-func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, error) {
-	storeStr := "store.db?cache=shared"
-	if runtime.GOOS == "windows" {
-		// Vo avoid `The process cannot access the file because it is being used by another process` on Windows
-		storeStr = "store.db"
-	}
-
-	file := filepath.Join(dataDir, storeStr)
-	db, err := gorm.Open(sqlite.Open(file), &gorm.Config{
-		Logger:          logger.Default.LogMode(logger.Silent),
-		CreateBatchSize: 400,
-		PrepareStmt:     true,
-	})
-	if err != nil {
-		return nil, err
-	}
-
+// NewSqlStore creates a new SqlStore instance.
+func NewSqlStore(db *gorm.DB, storeEngine StoreEngine, metrics telemetry.AppMetrics) (*SqlStore, error) {
 	sql, err := db.DB()
 	if err != nil {
 		return nil, err
@@ -82,33 +64,11 @@ func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore,
 		return nil, fmt.Errorf("auto migrate: %w", err)
 	}
 
-	return &SqliteStore{db: db, storeFile: file, metrics: metrics, installationPK: 1}, nil
-}
-
-// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir
-func NewSqliteStoreFromFileStore(filestore *FileStore, dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, error) {
-	store, err := NewSqliteStore(dataDir, metrics)
-	if err != nil {
-		return nil, err
-	}
-
-	err = store.SaveInstallationID(filestore.InstallationID)
-	if err != nil {
-		return nil, err
-	}
-
-	for _, account := range filestore.GetAllAccounts() {
-		err := store.SaveAccount(account)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return store, nil
+	return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1}, nil
 }
 
 // AcquireGlobalLock acquires global lock across all the accounts and returns a function that releases the lock
-func (s *SqliteStore) AcquireGlobalLock() (unlock func()) {
+func (s *SqlStore) AcquireGlobalLock() (unlock func()) {
 	log.Tracef("acquiring global lock")
 	start := time.Now()
 	s.globalAccountLock.Lock()
@@ -127,7 +87,7 @@ func (s *SqliteStore) AcquireGlobalLock() (unlock func()) {
 	return unlock
 }
 
-func (s *SqliteStore) AcquireAccountWriteLock(accountID string) (unlock func()) {
+func (s *SqlStore) AcquireAccountWriteLock(accountID string) (unlock func()) {
 	log.Tracef("acquiring write lock for account %s", accountID)
 
 	start := time.Now()
@@ -143,7 +103,7 @@ func (s *SqliteStore) AcquireAccountWriteLock(accountID string) (unlock func())
 	return unlock
 }
 
-func (s *SqliteStore) AcquireAccountReadLock(accountID string) (unlock func()) {
+func (s *SqlStore) AcquireAccountReadLock(accountID string) (unlock func()) {
 	log.Tracef("acquiring read lock for account %s", accountID)
 
 	start := time.Now()
@@ -159,7 +119,7 @@ func (s *SqliteStore) AcquireAccountReadLock(accountID string) (unlock func()) {
 	return unlock
 }
 
-func (s *SqliteStore) SaveAccount(account *Account) error {
+func (s *SqlStore) SaveAccount(account *Account) error {
 	start := time.Now()
 
 	for _, key := range account.SetupKeys {
@@ -225,12 +185,12 @@ func (s *SqliteStore) SaveAccount(account *Account) error {
 	if s.metrics != nil {
 		s.metrics.StoreMetrics().CountPersistenceDuration(took)
 	}
-	log.Debugf("took %d ms to persist an account to the SQLite", took.Milliseconds())
+	log.Debugf("took %d ms to persist an account to the store", took.Milliseconds())
 
 	return err
 }
 
-func (s *SqliteStore) DeleteAccount(account *Account) error {
+func (s *SqlStore) DeleteAccount(account *Account) error {
 	start := time.Now()
 
 	err := s.db.Transaction(func(tx *gorm.DB) error {
@@ -256,19 +216,19 @@ func (s *SqliteStore) DeleteAccount(account *Account) error {
 	if s.metrics != nil {
 		s.metrics.StoreMetrics().CountPersistenceDuration(took)
 	}
-	log.Debugf("took %d ms to delete an account to the SQLite", took.Milliseconds())
+	log.Debugf("took %d ms to delete an account to the store", took.Milliseconds())
 
 	return err
 }
 
-func (s *SqliteStore) SaveInstallationID(ID string) error {
+func (s *SqlStore) SaveInstallationID(ID string) error {
 	installation := installation{InstallationIDValue: ID}
 	installation.ID = uint(s.installationPK)
 
 	return s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&installation).Error
 }
 
-func (s *SqliteStore) GetInstallationID() string {
+func (s *SqlStore) GetInstallationID() string {
 	var installation installation
 
 	if result := s.db.First(&installation, "id = ?", s.installationPK); result.Error != nil {
@@ -278,7 +238,7 @@ func (s *SqliteStore) GetInstallationID() string {
 	return installation.InstallationIDValue
 }
 
-func (s *SqliteStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.PeerStatus) error {
+func (s *SqlStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.PeerStatus) error {
 	var peerCopy nbpeer.Peer
 	peerCopy.Status = &peerStatus
 	result := s.db.Model(&nbpeer.Peer{}).
@@ -296,7 +256,7 @@ func (s *SqliteStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer
 	return nil
 }
 
-func (s *SqliteStore) SavePeerLocation(accountID string, peerWithLocation *nbpeer.Peer) error {
+func (s *SqlStore) SavePeerLocation(accountID string, peerWithLocation *nbpeer.Peer) error {
 	// To maintain data integrity, we create a copy of the peer's location to prevent unintended updates to other fields.
 	var peerCopy nbpeer.Peer
 	// Since the location field has been migrated to JSON serialization,
@@ -318,17 +278,17 @@ func (s *SqliteStore) SavePeerLocation(accountID string, peerWithLocation *nbpee
 	return nil
 }
 
-// DeleteHashedPAT2TokenIDIndex is noop in Sqlite
-func (s *SqliteStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error {
+// DeleteHashedPAT2TokenIDIndex is noop in SqlStore
+func (s *SqlStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error {
 	return nil
 }
 
-// DeleteTokenID2UserIDIndex is noop in Sqlite
-func (s *SqliteStore) DeleteTokenID2UserIDIndex(tokenID string) error {
+// DeleteTokenID2UserIDIndex is noop in SqlStore
+func (s *SqlStore) DeleteTokenID2UserIDIndex(tokenID string) error {
 	return nil
 }
 
-func (s *SqliteStore) GetAccountByPrivateDomain(domain string) (*Account, error) {
+func (s *SqlStore) GetAccountByPrivateDomain(domain string) (*Account, error) {
 	var account Account
 
 	result := s.db.First(&account, "domain = ? and is_domain_primary_account = ? and domain_category = ?",
@@ -345,7 +305,7 @@ func (s *SqliteStore) GetAccountByPrivateDomain(domain string) (*Account, error)
 	return s.GetAccount(account.Id)
 }
 
-func (s *SqliteStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
+func (s *SqlStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
 	var key SetupKey
 	result := s.db.Select("account_id").First(&key, "key = ?", strings.ToUpper(setupKey))
 	if result.Error != nil {
@@ -363,7 +323,7 @@ func (s *SqliteStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
 	return s.GetAccount(key.AccountID)
 }
 
-func (s *SqliteStore) GetTokenIDByHashedToken(hashedToken string) (string, error) {
+func (s *SqlStore) GetTokenIDByHashedToken(hashedToken string) (string, error) {
 	var token PersonalAccessToken
 	result := s.db.First(&token, "hashed_token = ?", hashedToken)
 	if result.Error != nil {
@@ -377,7 +337,7 @@ func (s *SqliteStore) GetTokenIDByHashedToken(hashedToken string) (string, error
 	return token.ID, nil
 }
 
-func (s *SqliteStore) GetUserByTokenID(tokenID string) (*User, error) {
+func (s *SqlStore) GetUserByTokenID(tokenID string) (*User, error) {
 	var token PersonalAccessToken
 	result := s.db.First(&token, "id = ?", tokenID)
 	if result.Error != nil {
@@ -406,7 +366,7 @@ func (s *SqliteStore) GetUserByTokenID(tokenID string) (*User, error) {
 	return &user, nil
 }
 
-func (s *SqliteStore) GetAllAccounts() (all []*Account) {
+func (s *SqlStore) GetAllAccounts() (all []*Account) {
 	var accounts []Account
 	result := s.db.Find(&accounts)
 	if result.Error != nil {
@@ -422,7 +382,7 @@ func (s *SqliteStore) GetAllAccounts() (all []*Account) {
 	return all
 }
 
-func (s *SqliteStore) GetAccount(accountID string) (*Account, error) {
+func (s *SqlStore) GetAccount(accountID string) (*Account, error) {
 
 	var account Account
 	result := s.db.Model(&account).
@@ -490,7 +450,7 @@ func (s *SqliteStore) GetAccount(accountID string) (*Account, error) {
 	return &account, nil
 }
 
-func (s *SqliteStore) GetAccountByUser(userID string) (*Account, error) {
+func (s *SqlStore) GetAccountByUser(userID string) (*Account, error) {
 	var user User
 	result := s.db.Select("account_id").First(&user, "id = ?", userID)
 	if result.Error != nil {
@@ -508,7 +468,7 @@ func (s *SqliteStore) GetAccountByUser(userID string) (*Account, error) {
 	return s.GetAccount(user.AccountID)
 }
 
-func (s *SqliteStore) GetAccountByPeerID(peerID string) (*Account, error) {
+func (s *SqlStore) GetAccountByPeerID(peerID string) (*Account, error) {
 	var peer nbpeer.Peer
 	result := s.db.Select("account_id").First(&peer, "id = ?", peerID)
 	if result.Error != nil {
@@ -526,7 +486,7 @@ func (s *SqliteStore) GetAccountByPeerID(peerID string) (*Account, error) {
 	return s.GetAccount(peer.AccountID)
 }
 
-func (s *SqliteStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) {
+func (s *SqlStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) {
 	var peer nbpeer.Peer
 
 	result := s.db.Select("account_id").First(&peer, "key = ?", peerKey)
@@ -545,7 +505,7 @@ func (s *SqliteStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) {
 	return s.GetAccount(peer.AccountID)
 }
 
-func (s *SqliteStore) GetAccountIDByPeerPubKey(peerKey string) (string, error) {
+func (s *SqlStore) GetAccountIDByPeerPubKey(peerKey string) (string, error) {
 	var peer nbpeer.Peer
 	var accountID string
 	result := s.db.Model(&peer).Select("account_id").Where("key = ?", peerKey).First(&accountID)
@@ -561,7 +521,7 @@ func (s *SqliteStore) GetAccountIDByPeerPubKey(peerKey string) (string, error) {
 }
 
 // SaveUserLastLogin stores the last login time for a user in DB.
-func (s *SqliteStore) SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error {
+func (s *SqlStore) SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error {
 	var user User
 
 	result := s.db.First(&user, "account_id = ? and id = ?", accountID, userID)
@@ -579,7 +539,7 @@ func (s *SqliteStore) SaveUserLastLogin(accountID, userID string, lastLogin time
 }
 
 // Close closes the underlying DB connection
-func (s *SqliteStore) Close() error {
+func (s *SqlStore) Close() error {
 	sql, err := s.db.DB()
 	if err != nil {
 		return fmt.Errorf("get db: %w", err)
@@ -587,40 +547,85 @@ func (s *SqliteStore) Close() error {
 	return sql.Close()
 }
 
-// GetStoreEngine returns SqliteStoreEngine
-func (s *SqliteStore) GetStoreEngine() StoreEngine {
-	return SqliteStoreEngine
+// GetStoreEngine returns underlying store engine
+func (s *SqlStore) GetStoreEngine() StoreEngine {
+	return s.storeEngine
+}
+
+// NewSqliteStore creates a new SQLite store.
+func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+	storeStr := "store.db?cache=shared"
+	if runtime.GOOS == "windows" {
+		// Vo avoid `The process cannot access the file because it is being used by another process` on Windows
+		storeStr = "store.db"
+	}
+
+	file := filepath.Join(dataDir, storeStr)
+	db, err := gorm.Open(sqlite.Open(file), &gorm.Config{
+		Logger:          logger.Default.LogMode(logger.Silent),
+		CreateBatchSize: 400,
+		PrepareStmt:     true,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return NewSqlStore(db, SqliteStoreEngine, metrics)
+}
+
+// NewPostgresqlStore creates a new Postgres store.
+func NewPostgresqlStore(dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
+		Logger:      logger.Default.LogMode(logger.Silent),
+		PrepareStmt: true,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return NewSqlStore(db, PostgresStoreEngine, metrics)
 }
 
-// migrate migrates the SQLite database to the latest schema
-func migrate(db *gorm.DB) error {
-	migrations := getMigrations()
+// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir.
+func NewSqliteStoreFromFileStore(fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+	store, err := NewSqliteStore(dataDir, metrics)
+	if err != nil {
+		return nil, err
+	}
 
-	for _, m := range migrations {
-		if err := m(db); err != nil {
-			return err
+	err = store.SaveInstallationID(fileStore.InstallationID)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, account := range fileStore.GetAllAccounts() {
+		err := store.SaveAccount(account)
+		if err != nil {
+			return nil, err
 		}
 	}
 
-	return nil
+	return store, nil
 }
 
-func getMigrations() []migrationFunc {
-	return []migrationFunc{
-		func(db *gorm.DB) error {
-			return migration.MigrateFieldFromGobToJSON[Account, net.IPNet](db, "network_net")
-		},
-		func(db *gorm.DB) error {
-			return migration.MigrateFieldFromGobToJSON[route.Route, netip.Prefix](db, "network")
-		},
-		func(db *gorm.DB) error {
-			return migration.MigrateFieldFromGobToJSON[route.Route, []string](db, "peer_groups")
-		},
-		func(db *gorm.DB) error {
-			return migration.MigrateNetIPFieldFromBlobToJSON[nbpeer.Peer](db, "location_connection_ip", "")
-		},
-		func(db *gorm.DB) error {
-			return migration.MigrateNetIPFieldFromBlobToJSON[nbpeer.Peer](db, "ip", "idx_peers_account_id_ip")
-		},
+// NewPostgresqlStoreFromFileStore restores a store from FileStore and stores Postgres DB.
+func NewPostgresqlStoreFromFileStore(fileStore *FileStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+	store, err := NewPostgresqlStore(dsn, metrics)
+	if err != nil {
+		return nil, err
 	}
+
+	err = store.SaveInstallationID(fileStore.InstallationID)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, account := range fileStore.GetAllAccounts() {
+		err := store.SaveAccount(account)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return store, nil
 }
diff --git a/management/server/store.go b/management/server/store.go
index 8674f1cf..0f0fcfb0 100644
--- a/management/server/store.go
+++ b/management/server/store.go
@@ -1,16 +1,25 @@
 package server
 
 import (
+	"context"
 	"fmt"
+	"net"
+	"net/netip"
 	"os"
 	"path/filepath"
 	"strings"
 	"time"
 
 	log "github.com/sirupsen/logrus"
+	"github.com/testcontainers/testcontainers-go"
+	"github.com/testcontainers/testcontainers-go/modules/postgres"
+	"github.com/testcontainers/testcontainers-go/wait"
+	"gorm.io/gorm"
 
+	"github.com/netbirdio/netbird/management/server/migration"
 	nbpeer "github.com/netbirdio/netbird/management/server/peer"
 	"github.com/netbirdio/netbird/management/server/telemetry"
+	"github.com/netbirdio/netbird/route"
 )
 
 type Store interface {
@@ -49,8 +58,11 @@ type Store interface {
 type StoreEngine string
 
 const (
-	FileStoreEngine   StoreEngine = "jsonfile"
-	SqliteStoreEngine StoreEngine = "sqlite"
+	FileStoreEngine     StoreEngine = "jsonfile"
+	SqliteStoreEngine   StoreEngine = "sqlite"
+	PostgresStoreEngine StoreEngine = "postgres"
+
+	postgresDsnEnv = "NETBIRD_STORE_ENGINE_POSTGRES_DSN"
 )
 
 func getStoreEngineFromEnv() StoreEngine {
@@ -61,8 +73,7 @@ func getStoreEngineFromEnv() StoreEngine {
 	}
 
 	value := StoreEngine(strings.ToLower(kind))
-
-	if value == FileStoreEngine || value == SqliteStoreEngine {
+	if value == FileStoreEngine || value == SqliteStoreEngine || value == PostgresStoreEngine {
 		return value
 	}
 
@@ -94,18 +105,60 @@ func NewStore(kind StoreEngine, dataDir string, metrics telemetry.AppMetrics) (S
 	case SqliteStoreEngine:
 		log.Info("using SQLite store engine")
 		return NewSqliteStore(dataDir, metrics)
+	case PostgresStoreEngine:
+		log.Info("using Postgres store engine")
+		dsn, ok := os.LookupEnv(postgresDsnEnv)
+		if !ok {
+			return nil, fmt.Errorf("%s is not set", postgresDsnEnv)
+		}
+		return NewPostgresqlStore(dsn, metrics)
 	default:
 		return nil, fmt.Errorf("unsupported kind of store %s", kind)
 	}
 }
 
-// NewStoreFromJson is only used in tests
-func NewStoreFromJson(dataDir string, metrics telemetry.AppMetrics) (Store, error) {
+// migrate migrates the SQLite database to the latest schema
+func migrate(db *gorm.DB) error {
+	migrations := getMigrations()
+
+	for _, m := range migrations {
+		if err := m(db); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func getMigrations() []migrationFunc {
+	return []migrationFunc{
+		func(db *gorm.DB) error {
+			return migration.MigrateFieldFromGobToJSON[Account, net.IPNet](db, "network_net")
+		},
+		func(db *gorm.DB) error {
+			return migration.MigrateFieldFromGobToJSON[route.Route, netip.Prefix](db, "network")
+		},
+		func(db *gorm.DB) error {
+			return migration.MigrateFieldFromGobToJSON[route.Route, []string](db, "peer_groups")
+		},
+		func(db *gorm.DB) error {
+			return migration.MigrateNetIPFieldFromBlobToJSON[nbpeer.Peer](db, "location_connection_ip", "")
+		},
+		func(db *gorm.DB) error {
+			return migration.MigrateNetIPFieldFromBlobToJSON[nbpeer.Peer](db, "ip", "idx_peers_account_id_ip")
+		},
+	}
+}
+
+// NewTestStoreFromJson is only used in tests
+func NewTestStoreFromJson(dataDir string) (Store, func(), error) {
 	fstore, err := NewFileStore(dataDir, nil)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
+	cleanUp := func() {}
+
 	// if store engine is not set in the config we first try to evaluate NETBIRD_STORE_ENGINE
 	kind := getStoreEngineFromEnv()
 	if kind == "" {
@@ -115,10 +168,64 @@ func NewStoreFromJson(dataDir string, metrics telemetry.AppMetrics) (Store, erro
 
 	switch kind {
 	case FileStoreEngine:
-		return fstore, nil
+		return fstore, cleanUp, nil
 	case SqliteStoreEngine:
-		return NewSqliteStoreFromFileStore(fstore, dataDir, metrics)
+		store, err := NewSqliteStoreFromFileStore(fstore, dataDir, nil)
+		if err != nil {
+			return nil, nil, err
+		}
+		return store, cleanUp, nil
+	case PostgresStoreEngine:
+		cleanUp, err = createPGDB()
+		if err != nil {
+			return nil, nil, err
+		}
+
+		dsn, ok := os.LookupEnv(postgresDsnEnv)
+		if !ok {
+			return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv)
+		}
+
+		store, err := NewPostgresqlStoreFromFileStore(fstore, dsn, nil)
+		if err != nil {
+			return nil, nil, err
+		}
+		return store, cleanUp, nil
 	default:
-		return NewSqliteStoreFromFileStore(fstore, dataDir, metrics)
+		store, err := NewSqliteStoreFromFileStore(fstore, dataDir, nil)
+		if err != nil {
+			return nil, nil, err
+		}
+		return store, cleanUp, nil
+	}
+}
+
+func createPGDB() (func(), error) {
+	ctx := context.Background()
+	c, err := postgres.RunContainer(ctx,
+		testcontainers.WithImage("postgres:alpine"),
+		postgres.WithDatabase("test"),
+		postgres.WithUsername("postgres"),
+		postgres.WithPassword("postgres"),
+		testcontainers.WithWaitStrategy(
+			wait.ForLog("database system is ready to accept connections").
+				WithOccurrence(2).WithStartupTimeout(15*time.Second)),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	cleanup := func() {
+		timeout := 10 * time.Second
+		err = c.Stop(ctx, &timeout)
+		if err != nil {
+			log.Warnf("failed to stop container: %s", err)
+		}
+	}
+
+	talksConn, err := c.ConnectionString(ctx)
+	if err != nil {
+		return cleanup, err
 	}
+	return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN", talksConn)
 }
diff --git a/release_files/install.sh b/release_files/install.sh
index a0a9abf9..198d7442 100755
--- a/release_files/install.sh
+++ b/release_files/install.sh
@@ -349,39 +349,55 @@ fi
 if type uname >/dev/null 2>&1; then
 	case "$(uname)" in
         Linux)
-            OS_NAME="$(. /etc/os-release && echo "$ID")"
-            OS_TYPE="linux"
-            INSTALL_DIR="/usr/bin"
-
-            # Allow netbird UI installation for x64 arch only
-            if [ "$ARCH" != "amd64" ] && [ "$ARCH" != "arm64" ] \
-                && [ "$ARCH" != "x86_64" ];then
-                SKIP_UI_APP=true
-                echo "NetBird UI installation will be omitted as $ARCH is not a compatible architecture"
+          OS_TYPE="linux"
+          UNAME_OUTPUT="$(uname -a)"
+          if echo "$UNAME_OUTPUT" | grep -qi "synology"; then
+            OS_NAME="synology"
+            INSTALL_DIR="/usr/local/bin"
+            PACKAGE_MANAGER="bin"
+            SKIP_UI_APP=true
+          else
+            if [ -f /etc/os-release ]; then
+              OS_NAME="$(. /etc/os-release && echo "$ID")"
+              INSTALL_DIR="/usr/bin"
+
+              # Allow netbird UI installation for x64 arch only
+              if [ "$ARCH" != "amd64" ] && [ "$ARCH" != "arm64" ] \
+                  && [ "$ARCH" != "x86_64" ];then
+                  SKIP_UI_APP=true
+                  echo "NetBird UI installation will be omitted as $ARCH is not a compatible architecture"
+              fi
+
+              # Allow netbird UI installation for linux running desktop environment
+              if [ -z "$XDG_CURRENT_DESKTOP" ];then
+                  SKIP_UI_APP=true
+                  echo "NetBird UI installation will be omitted as Linux does not run desktop environment"
+              fi
+
+              # Check the availability of a compatible package manager
+              if check_use_bin_variable; then
+                  PACKAGE_MANAGER="bin"
+              elif [ -x "$(command -v apt-get)" ]; then
+                  PACKAGE_MANAGER="apt"
+                  echo "The installation will be performed using apt package manager"
+              elif [ -x "$(command -v dnf)" ]; then
+                  PACKAGE_MANAGER="dnf"
+                  echo "The installation will be performed using dnf package manager"
+              elif [ -x "$(command -v yum)" ]; then
+                  PACKAGE_MANAGER="yum"
+                  echo "The installation will be performed using yum package manager"
+              elif [ -x "$(command -v pacman)" ]; then
+                  PACKAGE_MANAGER="pacman"
+                  echo "The installation will be performed using pacman package manager"
+              fi
+
+            else
+              echo "Unable to determine OS type from /etc/os-release"
+              exit 1
             fi
+          fi
 
-            # Allow netbird UI installation for linux running desktop environment
-            if [ -z "$XDG_CURRENT_DESKTOP" ];then
-                SKIP_UI_APP=true
-                echo "NetBird UI installation will be omitted as Linux does not run desktop environment"
-            fi
 
-            # Check the availability of a compatible package manager
-            if check_use_bin_variable; then
-                PACKAGE_MANAGER="bin"
-            elif [ -x "$(command -v apt)" ]; then
-                PACKAGE_MANAGER="apt"
-                echo "The installation will be performed using apt package manager"
-            elif [ -x "$(command -v dnf)" ]; then
-                PACKAGE_MANAGER="dnf"
-                echo "The installation will be performed using dnf package manager"
-            elif [ -x "$(command -v yum)" ]; then
-                PACKAGE_MANAGER="yum"
-                echo "The installation will be performed using yum package manager"
-            elif [ -x "$(command -v pacman)" ]; then
-                PACKAGE_MANAGER="pacman"
-                echo "The installation will be performed using pacman package manager"
-            fi
 		;;
 		Darwin)
             OS_NAME="macos"



Test patch:
diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go
index 2f92e1c0..35fd7c53 100644
--- a/client/cmd/testutil.go
+++ b/client/cmd/testutil.go
@@ -14,6 +14,7 @@ import (
 	"google.golang.org/grpc"
 
 	"github.com/netbirdio/management-integrations/integrations"
+
 	clientProto "github.com/netbirdio/netbird/client/proto"
 	client "github.com/netbirdio/netbird/client/server"
 	mgmtProto "github.com/netbirdio/netbird/management/proto"
@@ -69,10 +70,11 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
 		t.Fatal(err)
 	}
 	s := grpc.NewServer()
-	store, err := mgmt.NewStoreFromJson(config.Datadir, nil)
+	store, cleanUp, err := mgmt.NewTestStoreFromJson(config.Datadir)
 	if err != nil {
 		t.Fatal(err)
 	}
+	t.Cleanup(cleanUp)
 
 	peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
 	eventStore := &activity.InMemoryEventStore{}
diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go
index 1bac4145..0239ae58 100644
--- a/client/internal/engine_test.go
+++ b/client/internal/engine_test.go
@@ -1042,7 +1042,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) {
 		return nil, "", err
 	}
 	s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
-	store, err := server.NewStoreFromJson(config.Datadir, nil)
+	store, _, err := server.NewTestStoreFromJson(config.Datadir)
 	if err != nil {
 		return nil, "", err
 	}
diff --git a/client/server/server_test.go b/client/server/server_test.go
index 8ac8fd95..a9f23ce7 100644
--- a/client/server/server_test.go
+++ b/client/server/server_test.go
@@ -106,10 +106,11 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
 		return nil, "", err
 	}
 	s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
-	store, err := server.NewStoreFromJson(config.Datadir, nil)
+	store, cleanUp, err := server.NewTestStoreFromJson(config.Datadir)
 	if err != nil {
 		return nil, "", err
 	}
+	t.Cleanup(cleanUp)
 
 	peersUpdateManager := server.NewPeersUpdateManager(nil)
 	eventStore := &activity.InMemoryEventStore{}
diff --git a/management/client/client_test.go b/management/client/client_test.go
index 5d04fa59..32ad8fce 100644
--- a/management/client/client_test.go
+++ b/management/client/client_test.go
@@ -62,10 +62,11 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
 		t.Fatal(err)
 	}
 	s := grpc.NewServer()
-	store, err := mgmt.NewStoreFromJson(config.Datadir, nil)
+	store, cleanUp, err := mgmt.NewTestStoreFromJson(config.Datadir)
 	if err != nil {
 		t.Fatal(err)
 	}
+	t.Cleanup(cleanUp)
 
 	peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
 	eventStore := &activity.InMemoryEventStore{}
diff --git a/management/server/account_test.go b/management/server/account_test.go
index 0b299276..8c4219da 100644
--- a/management/server/account_test.go
+++ b/management/server/account_test.go
@@ -1294,6 +1294,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
 		t.Fatal(err)
 		return
 	}
+
 	userID := "account_creator"
 	account, err := createAccount(manager, "test_account", userID, "netbird.cloud")
 	if err != nil {
@@ -1655,6 +1656,7 @@ func TestDefaultAccountManager_DefaultAccountSettings(t *testing.T) {
 func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) {
 	manager, err := createManager(t)
 	require.NoError(t, err, "unable to create account manager")
+
 	_, err = manager.GetAccountByUserOrAccountID(userID, "", "")
 	require.NoError(t, err, "unable to create an account")
 
@@ -1707,6 +1709,7 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) {
 func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing.T) {
 	manager, err := createManager(t)
 	require.NoError(t, err, "unable to create account manager")
+
 	account, err := manager.GetAccountByUserOrAccountID(userID, "", "")
 	require.NoError(t, err, "unable to create an account")
 
@@ -1750,6 +1753,7 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing.
 func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *testing.T) {
 	manager, err := createManager(t)
 	require.NoError(t, err, "unable to create account manager")
+
 	_, err = manager.GetAccountByUserOrAccountID(userID, "", "")
 	require.NoError(t, err, "unable to create an account")
 
@@ -2267,21 +2271,29 @@ func TestAccount_UserGroupsRemoveFromPeers(t *testing.T) {
 
 func createManager(t *testing.T) (*DefaultAccountManager, error) {
 	t.Helper()
+
 	store, err := createStore(t)
 	if err != nil {
 		return nil, err
 	}
 	eventStore := &activity.InMemoryEventStore{}
-	return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{})
+
+	manager, err := BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{})
+	if err != nil {
+		return nil, err
+	}
+
+	return manager, nil
 }
 
 func createStore(t *testing.T) (Store, error) {
 	t.Helper()
 	dataDir := t.TempDir()
-	store, err := NewStoreFromJson(dataDir, nil)
+	store, cleanUp, err := NewTestStoreFromJson(dataDir)
 	if err != nil {
 		return nil, err
 	}
+	t.Cleanup(cleanUp)
 
 	return store, nil
 }
diff --git a/management/server/dns_test.go b/management/server/dns_test.go
index 18f942e6..b5074e50 100644
--- a/management/server/dns_test.go
+++ b/management/server/dns_test.go
@@ -32,7 +32,7 @@ func TestGetDNSSettings(t *testing.T) {
 
 	account, err := initTestDNSAccount(t, am)
 	if err != nil {
-		t.Error("failed to init testing account")
+		t.Fatal("failed to init testing account")
 	}
 
 	dnsSettings, err := am.GetDNSSettings(account.Id, dnsAdminUserID)
@@ -200,10 +200,11 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) {
 func createDNSStore(t *testing.T) (Store, error) {
 	t.Helper()
 	dataDir := t.TempDir()
-	store, err := NewStoreFromJson(dataDir, nil)
+	store, cleanUp, err := NewTestStoreFromJson(dataDir)
 	if err != nil {
 		return nil, err
 	}
+	t.Cleanup(cleanUp)
 
 	return store, nil
 }
diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go
index d53298d8..11571b0b 100644
--- a/management/server/file_store_test.go
+++ b/management/server/file_store_test.go
@@ -59,6 +59,7 @@ func TestStalePeerIndices(t *testing.T) {
 
 func TestNewStore(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 
 	if store.Accounts == nil || len(store.Accounts) != 0 {
 		t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
@@ -87,6 +88,7 @@ func TestNewStore(t *testing.T) {
 
 func TestSaveAccount(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 
 	account := newAccountWithId("account_id", "testuser", "")
 	setupKey := GenerateDefaultSetupKey()
@@ -135,6 +137,8 @@ func TestDeleteAccount(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	defer store.Close()
+
 	var account *Account
 	for _, a := range store.Accounts {
 		account = a
@@ -179,6 +183,7 @@ func TestDeleteAccount(t *testing.T) {
 
 func TestStore(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 
 	account := newAccountWithId("account_id", "testuser", "")
 	account.Peers["testpeer"] = &nbpeer.Peer{
@@ -436,6 +441,7 @@ func TestFileStore_GetTokenIDByHashedToken(t *testing.T) {
 
 func TestFileStore_DeleteHashedPAT2TokenIDIndex(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	store.HashedPAT2TokenID["someHashedToken"] = "someTokenId"
 
 	err := store.DeleteHashedPAT2TokenIDIndex("someHashedToken")
diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go
index 98ad0de0..c2672b1e 100644
--- a/management/server/management_proto_test.go
+++ b/management/server/management_proto_test.go
@@ -405,10 +405,12 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error)
 		return nil, "", err
 	}
 	s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
-	store, err := NewStoreFromJson(config.Datadir, nil)
+	store, cleanUp, err := NewTestStoreFromJson(config.Datadir)
 	if err != nil {
 		return nil, "", err
 	}
+	t.Cleanup(cleanUp)
+
 	peersUpdateManager := NewPeersUpdateManager(nil)
 	eventStore := &activity.InMemoryEventStore{}
 	accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted",
diff --git a/management/server/management_test.go b/management/server/management_test.go
index 13db5ae9..f7eaeaf1 100644
--- a/management/server/management_test.go
+++ b/management/server/management_test.go
@@ -532,10 +532,11 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
 	Expect(err).NotTo(HaveOccurred())
 	s := grpc.NewServer()
 
-	store, err := server.NewStoreFromJson(config.Datadir, nil)
+	store, _, err := server.NewTestStoreFromJson(config.Datadir)
 	if err != nil {
 		log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
 	}
+
 	peersUpdateManager := server.NewPeersUpdateManager(nil)
 	eventStore := &activity.InMemoryEventStore{}
 	accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted",
diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go
index b10f9387..f2921532 100644
--- a/management/server/nameserver_test.go
+++ b/management/server/nameserver_test.go
@@ -766,10 +766,11 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
 func createNSStore(t *testing.T) (Store, error) {
 	t.Helper()
 	dataDir := t.TempDir()
-	store, err := NewStoreFromJson(dataDir, nil)
+	store, cleanUp, err := NewTestStoreFromJson(dataDir)
 	if err != nil {
 		return nil, err
 	}
+	t.Cleanup(cleanUp)
 
 	return store, nil
 }
diff --git a/management/server/posture_checks_test.go b/management/server/posture_checks_test.go
index a65cb8c5..dd92fe8b 100644
--- a/management/server/posture_checks_test.go
+++ b/management/server/posture_checks_test.go
@@ -3,8 +3,9 @@ package server
 import (
 	"testing"
 
-	"github.com/netbirdio/netbird/management/server/posture"
 	"github.com/stretchr/testify/assert"
+
+	"github.com/netbirdio/netbird/management/server/posture"
 )
 
 const (
diff --git a/management/server/route_test.go b/management/server/route_test.go
index e06ac650..d28b40d4 100644
--- a/management/server/route_test.go
+++ b/management/server/route_test.go
@@ -1021,10 +1021,11 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) {
 func createRouterStore(t *testing.T) (Store, error) {
 	t.Helper()
 	dataDir := t.TempDir()
-	store, err := NewStoreFromJson(dataDir, nil)
+	store, cleanUp, err := NewTestStoreFromJson(dataDir)
 	if err != nil {
 		return nil, err
 	}
+	t.Cleanup(cleanUp)
 
 	return store, nil
 }
diff --git a/management/server/sqlite_store_test.go b/management/server/sql_store_test.go
similarity index 66%
rename from management/server/sqlite_store_test.go
rename to management/server/sql_store_test.go
index 8758e3b0..b07d5042 100644
--- a/management/server/sqlite_store_test.go
+++ b/management/server/sql_store_test.go
@@ -5,6 +5,7 @@ import (
 	"math/rand"
 	"net"
 	"net/netip"
+	"os"
 	"path/filepath"
 	"runtime"
 	"testing"
@@ -569,7 +570,7 @@ func TestMigrate(t *testing.T) {
 	require.NoError(t, err, "Migration should not fail on migrated db")
 }
 
-func newSqliteStore(t *testing.T) *SqliteStore {
+func newSqliteStore(t *testing.T) *SqlStore {
 	t.Helper()
 
 	store, err := NewSqliteStore(t.TempDir(), nil)
@@ -579,7 +580,7 @@ func newSqliteStore(t *testing.T) *SqliteStore {
 	return store
 }
 
-func newSqliteStoreFromFile(t *testing.T, filename string) *SqliteStore {
+func newSqliteStoreFromFile(t *testing.T, filename string) *SqlStore {
 	t.Helper()
 
 	storeDir := t.TempDir()
@@ -613,3 +614,298 @@ func newAccount(store Store, id int) error {
 
 	return store.SaveAccount(account)
 }
+
+func newPostgresqlStore(t *testing.T) *SqlStore {
+	t.Helper()
+
+	cleanUp, err := createPGDB()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(cleanUp)
+
+	postgresDsn, ok := os.LookupEnv(postgresDsnEnv)
+	if !ok {
+		t.Fatalf("could not initialize postgresql store: %s is not set", postgresDsnEnv)
+	}
+
+	store, err := NewPostgresqlStore(postgresDsn, nil)
+	if err != nil {
+		t.Fatalf("could not initialize postgresql store: %s", err)
+	}
+	require.NoError(t, err)
+	require.NotNil(t, store)
+
+	return store
+}
+
+func newPostgresqlStoreFromFile(t *testing.T, filename string) *SqlStore {
+	t.Helper()
+
+	storeDir := t.TempDir()
+	err := util.CopyFileContents(filename, filepath.Join(storeDir, "store.json"))
+	require.NoError(t, err)
+
+	fStore, err := NewFileStore(storeDir, nil)
+	require.NoError(t, err)
+
+	cleanUp, err := createPGDB()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(cleanUp)
+
+	postgresDsn, ok := os.LookupEnv(postgresDsnEnv)
+	if !ok {
+		t.Fatalf("could not initialize postgresql store: %s is not set", postgresDsnEnv)
+	}
+
+	store, err := NewPostgresqlStoreFromFileStore(fStore, postgresDsn, nil)
+	require.NoError(t, err)
+	require.NotNil(t, store)
+
+	return store
+}
+
+func TestPostgresql_NewStore(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("The PostgreSQL store is not properly supported by %s yet", runtime.GOOS)
+	}
+
+	store := newPostgresqlStore(t)
+
+	if len(store.GetAllAccounts()) != 0 {
+		t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
+	}
+}
+
+func TestPostgresql_SaveAccount(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("The PostgreSQL store is not properly supported by %s yet", runtime.GOOS)
+	}
+
+	store := newPostgresqlStore(t)
+
+	account := newAccountWithId("account_id", "testuser", "")
+	setupKey := GenerateDefaultSetupKey()
+	account.SetupKeys[setupKey.Key] = setupKey
+	account.Peers["testpeer"] = &nbpeer.Peer{
+		Key:      "peerkey",
+		SetupKey: "peerkeysetupkey",
+		IP:       net.IP{127, 0, 0, 1},
+		Meta:     nbpeer.PeerSystemMeta{},
+		Name:     "peer name",
+		Status:   &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
+	}
+
+	err := store.SaveAccount(account)
+	require.NoError(t, err)
+
+	account2 := newAccountWithId("account_id2", "testuser2", "")
+	setupKey = GenerateDefaultSetupKey()
+	account2.SetupKeys[setupKey.Key] = setupKey
+	account2.Peers["testpeer2"] = &nbpeer.Peer{
+		Key:      "peerkey2",
+		SetupKey: "peerkeysetupkey2",
+		IP:       net.IP{127, 0, 0, 2},
+		Meta:     nbpeer.PeerSystemMeta{},
+		Name:     "peer name 2",
+		Status:   &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
+	}
+
+	err = store.SaveAccount(account2)
+	require.NoError(t, err)
+
+	if len(store.GetAllAccounts()) != 2 {
+		t.Errorf("expecting 2 Accounts to be stored after SaveAccount()")
+	}
+
+	a, err := store.GetAccount(account.Id)
+	if a == nil {
+		t.Errorf("expecting Account to be stored after SaveAccount(): %v", err)
+	}
+
+	if a != nil && len(a.Policies) != 1 {
+		t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies))
+	}
+
+	if a != nil && len(a.Policies[0].Rules) != 1 {
+		t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules))
+		return
+	}
+
+	if a, err := store.GetAccountByPeerPubKey("peerkey"); a == nil {
+		t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err)
+	}
+
+	if a, err := store.GetAccountByUser("testuser"); a == nil {
+		t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err)
+	}
+
+	if a, err := store.GetAccountByPeerID("testpeer"); a == nil {
+		t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err)
+	}
+
+	if a, err := store.GetAccountBySetupKey(setupKey.Key); a == nil {
+		t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err)
+	}
+}
+
+func TestPostgresql_DeleteAccount(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("The PostgreSQL store is not properly supported by %s yet", runtime.GOOS)
+	}
+
+	store := newPostgresqlStore(t)
+
+	testUserID := "testuser"
+	user := NewAdminUser(testUserID)
+	user.PATs = map[string]*PersonalAccessToken{"testtoken": {
+		ID:   "testtoken",
+		Name: "test token",
+	}}
+
+	account := newAccountWithId("account_id", testUserID, "")
+	setupKey := GenerateDefaultSetupKey()
+	account.SetupKeys[setupKey.Key] = setupKey
+	account.Peers["testpeer"] = &nbpeer.Peer{
+		Key:      "peerkey",
+		SetupKey: "peerkeysetupkey",
+		IP:       net.IP{127, 0, 0, 1},
+		Meta:     nbpeer.PeerSystemMeta{},
+		Name:     "peer name",
+		Status:   &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
+	}
+	account.Users[testUserID] = user
+
+	err := store.SaveAccount(account)
+	require.NoError(t, err)
+
+	if len(store.GetAllAccounts()) != 1 {
+		t.Errorf("expecting 1 Accounts to be stored after SaveAccount()")
+	}
+
+	err = store.DeleteAccount(account)
+	require.NoError(t, err)
+
+	if len(store.GetAllAccounts()) != 0 {
+		t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()")
+	}
+
+	_, err = store.GetAccountByPeerPubKey("peerkey")
+	require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer public key")
+
+	_, err = store.GetAccountByUser("testuser")
+	require.Error(t, err, "expecting error after removing DeleteAccount when getting account by user")
+
+	_, err = store.GetAccountByPeerID("testpeer")
+	require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer id")
+
+	_, err = store.GetAccountBySetupKey(setupKey.Key)
+	require.Error(t, err, "expecting error after removing DeleteAccount when getting account by setup key")
+
+	_, err = store.GetAccount(account.Id)
+	require.Error(t, err, "expecting error after removing DeleteAccount when getting account by id")
+
+	for _, policy := range account.Policies {
+		var rules []*PolicyRule
+		err = store.db.Model(&PolicyRule{}).Find(&rules, "policy_id = ?", policy.ID).Error
+		require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for policy rules")
+		require.Len(t, rules, 0, "expecting no policy rules to be found after removing DeleteAccount")
+
+	}
+
+	for _, accountUser := range account.Users {
+		var pats []*PersonalAccessToken
+		err = store.db.Model(&PersonalAccessToken{}).Find(&pats, "user_id = ?", accountUser.Id).Error
+		require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for personal access token")
+		require.Len(t, pats, 0, "expecting no personal access token to be found after removing DeleteAccount")
+
+	}
+
+}
+
+func TestPostgresql_SavePeerStatus(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("The PostgreSQL store is not properly supported by %s yet", runtime.GOOS)
+	}
+
+	store := newPostgresqlStoreFromFile(t, "testdata/store.json")
+
+	account, err := store.GetAccount("bf1c8084-ba50-4ce7-9439-34653001fc3b")
+	require.NoError(t, err)
+
+	// save status of non-existing peer
+	newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}
+	err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus)
+	assert.Error(t, err)
+
+	// save new status of existing peer
+	account.Peers["testpeer"] = &nbpeer.Peer{
+		Key:      "peerkey",
+		ID:       "testpeer",
+		SetupKey: "peerkeysetupkey",
+		IP:       net.IP{127, 0, 0, 1},
+		Meta:     nbpeer.PeerSystemMeta{},
+		Name:     "peer name",
+		Status:   &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()},
+	}
+
+	err = store.SaveAccount(account)
+	require.NoError(t, err)
+
+	err = store.SavePeerStatus(account.Id, "testpeer", newStatus)
+	require.NoError(t, err)
+
+	account, err = store.GetAccount(account.Id)
+	require.NoError(t, err)
+
+	actual := account.Peers["testpeer"].Status
+	assert.Equal(t, newStatus.Connected, actual.Connected)
+}
+
+func TestPostgresql_TestGetAccountByPrivateDomain(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("The PostgreSQL store is not properly supported by %s yet", runtime.GOOS)
+	}
+
+	store := newPostgresqlStoreFromFile(t, "testdata/store.json")
+
+	existingDomain := "test.com"
+
+	account, err := store.GetAccountByPrivateDomain(existingDomain)
+	require.NoError(t, err, "should found account")
+	require.Equal(t, existingDomain, account.Domain, "domains should match")
+
+	_, err = store.GetAccountByPrivateDomain("missing-domain.com")
+	require.Error(t, err, "should return error on domain lookup")
+}
+
+func TestPostgresql_GetTokenIDByHashedToken(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("The PostgreSQL store is not properly supported by %s yet", runtime.GOOS)
+	}
+
+	store := newPostgresqlStoreFromFile(t, "testdata/store.json")
+
+	hashed := "SoMeHaShEdToKeN"
+	id := "9dj38s35-63fb-11ec-90d6-0242ac120003"
+
+	token, err := store.GetTokenIDByHashedToken(hashed)
+	require.NoError(t, err)
+	require.Equal(t, id, token)
+}
+
+func TestPostgresql_GetUserByTokenID(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skipf("The PostgreSQL store is not properly supported by %s yet", runtime.GOOS)
+	}
+
+	store := newPostgresqlStoreFromFile(t, "testdata/store.json")
+
+	id := "9dj38s35-63fb-11ec-90d6-0242ac120003"
+
+	user, err := store.GetUserByTokenID(id)
+	require.NoError(t, err)
+	require.Equal(t, id, user.PATs[id].ID)
+}
diff --git a/management/server/user_test.go b/management/server/user_test.go
index c92f87e6..5edb811c 100644
--- a/management/server/user_test.go
+++ b/management/server/user_test.go
@@ -39,6 +39,7 @@ const (
 
 func TestUser_CreatePAT_ForSameUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -76,6 +77,7 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) {
 
 func TestUser_CreatePAT_ForDifferentUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users[mockTargetUserId] = &User{
 		Id:            mockTargetUserId,
@@ -97,6 +99,7 @@ func TestUser_CreatePAT_ForDifferentUser(t *testing.T) {
 
 func TestUser_CreatePAT_ForServiceUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users[mockTargetUserId] = &User{
 		Id:            mockTargetUserId,
@@ -122,6 +125,7 @@ func TestUser_CreatePAT_ForServiceUser(t *testing.T) {
 
 func TestUser_CreatePAT_WithWrongExpiration(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -140,6 +144,7 @@ func TestUser_CreatePAT_WithWrongExpiration(t *testing.T) {
 
 func TestUser_CreatePAT_WithEmptyName(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -158,6 +163,7 @@ func TestUser_CreatePAT_WithEmptyName(t *testing.T) {
 
 func TestUser_DeletePAT(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users[mockUserID] = &User{
 		Id: mockUserID,
@@ -190,6 +196,7 @@ func TestUser_DeletePAT(t *testing.T) {
 
 func TestUser_GetPAT(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users[mockUserID] = &User{
 		Id: mockUserID,
@@ -221,6 +228,7 @@ func TestUser_GetPAT(t *testing.T) {
 
 func TestUser_GetAllPATs(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users[mockUserID] = &User{
 		Id: mockUserID,
@@ -322,6 +330,7 @@ func validateStruct(s interface{}) (err error) {
 
 func TestUser_CreateServiceUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -359,6 +368,7 @@ func TestUser_CreateServiceUser(t *testing.T) {
 
 func TestUser_CreateUser_ServiceUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -397,6 +407,7 @@ func TestUser_CreateUser_ServiceUser(t *testing.T) {
 
 func TestUser_CreateUser_RegularUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -421,6 +432,7 @@ func TestUser_CreateUser_RegularUser(t *testing.T) {
 
 func TestUser_InviteNewUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -549,6 +561,7 @@ func TestUser_DeleteUser_ServiceUser(t *testing.T) {
 
 func TestUser_DeleteUser_SelfDelete(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -569,6 +582,7 @@ func TestUser_DeleteUser_SelfDelete(t *testing.T) {
 
 func TestUser_DeleteUser_regularUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	targetId := "user2"
@@ -650,6 +664,7 @@ func TestUser_DeleteUser_regularUser(t *testing.T) {
 
 func TestDefaultAccountManager_GetUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 
 	err := store.SaveAccount(account)
@@ -678,6 +693,7 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
 
 func TestDefaultAccountManager_ListUsers(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users["normal_user1"] = NewRegularUser("normal_user1")
 	account.Users["normal_user2"] = NewRegularUser("normal_user2")
@@ -790,6 +806,7 @@ func TestDefaultAccountManager_ListUsers_DashboardPermissions(t *testing.T) {
 
 func TestDefaultAccountManager_ExternalCache(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	externalUser := &User{
 		Id:     "externalUser",
@@ -853,6 +870,7 @@ func TestUser_IsAdmin(t *testing.T) {
 
 func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users[mockServiceUserID] = &User{
 		Id:            mockServiceUserID,
@@ -880,6 +898,8 @@ func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) {
 
 func TestUser_GetUsersFromAccount_ForUser(t *testing.T) {
 	store := newStore(t)
+	defer store.Close()
+
 	account := newAccountWithId(mockAccountID, mockUserID, "")
 	account.Users[mockServiceUserID] = &User{
 		Id:            mockServiceUserID,